##// END OF EJS Templates
Adds buit-in groups to give specific permissions to anonymous and non members users per project (#17976)....
Jean-Philippe Lang -
r13053:7e7ac5340a28
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,30
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class GroupAnonymous < GroupBuiltin
19 def name
20 l(:label_group_anonymous)
21 end
22
23 def builtin_type
24 "anonymous"
25 end
26
27 def self.instance_id
28 @@instance_id ||= load_instance.id
29 end
30 end
@@ -0,0 +1,56
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class GroupBuiltin < Group
19 validate :validate_uniqueness, :on => :create
20
21 def validate_uniqueness
22 errors.add :base, 'The builtin group already exists.' if self.class.exists?
23 end
24
25 def builtin?
26 true
27 end
28
29 def destroy
30 false
31 end
32
33 def user_added(user)
34 raise 'Cannot add users to a builtin group'
35 end
36
37 class << self
38 def load_instance
39 return nil if self == GroupBuiltin
40 instance = first(:order => 'id') || create_instance
41 end
42
43 def create_instance
44 raise 'The builtin group already exists.' if exists?
45 instance = new
46 instance.lastname = name
47 instance.save :validate => false
48 raise 'Unable to create builtin group.' if instance.new_record?
49 instance
50 end
51 private :create_instance
52 end
53 end
54
55 require_dependency "group_anonymous"
56 require_dependency "group_non_member"
@@ -0,0 +1,30
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class GroupNonMember < GroupBuiltin
19 def name
20 l(:label_group_non_member)
21 end
22
23 def builtin_type
24 "non_member"
25 end
26
27 def self.instance_id
28 @@instance_id ||= load_instance.id
29 end
30 end
@@ -0,0 +1,21
1 class InsertBuiltinGroups < ActiveRecord::Migration
2 def up
3 Group.reset_column_information
4
5 unless GroupAnonymous.any?
6 g = GroupAnonymous.new(:lastname => 'Anonymous users')
7 g.status = 1
8 g.save :validate => false
9 end
10 unless GroupNonMember.any?
11 g = GroupNonMember.new(:lastname => 'Non member users')
12 g.status = 1
13 g.save :validate => false
14 end
15 end
16
17 def down
18 GroupAnonymous.delete_all
19 GroupNonMember.delete_all
20 end
21 end
@@ -1,150 +1,154
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class GroupsController < ApplicationController
18 class GroupsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin
21 before_filter :require_admin
22 before_filter :find_group, :except => [:index, :new, :create]
22 before_filter :find_group, :except => [:index, :new, :create]
23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
24
24
25 helper :custom_fields
25 helper :custom_fields
26
26
27 def index
27 def index
28 @groups = Group.sorted.all
29 respond_to do |format|
28 respond_to do |format|
30 format.html {
29 format.html {
30 @groups = Group.sorted.all
31 @user_count_by_group_id = user_count_by_group_id
31 @user_count_by_group_id = user_count_by_group_id
32 }
32 }
33 format.api
33 format.api {
34 scope = Group.sorted
35 scope = scope.givable unless params[:builtin] == '1'
36 @groups = scope.all
37 }
34 end
38 end
35 end
39 end
36
40
37 def show
41 def show
38 respond_to do |format|
42 respond_to do |format|
39 format.html
43 format.html
40 format.api
44 format.api
41 end
45 end
42 end
46 end
43
47
44 def new
48 def new
45 @group = Group.new
49 @group = Group.new
46 end
50 end
47
51
48 def create
52 def create
49 @group = Group.new
53 @group = Group.new
50 @group.safe_attributes = params[:group]
54 @group.safe_attributes = params[:group]
51
55
52 respond_to do |format|
56 respond_to do |format|
53 if @group.save
57 if @group.save
54 format.html {
58 format.html {
55 flash[:notice] = l(:notice_successful_create)
59 flash[:notice] = l(:notice_successful_create)
56 redirect_to(params[:continue] ? new_group_path : groups_path)
60 redirect_to(params[:continue] ? new_group_path : groups_path)
57 }
61 }
58 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
62 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
59 else
63 else
60 format.html { render :action => "new" }
64 format.html { render :action => "new" }
61 format.api { render_validation_errors(@group) }
65 format.api { render_validation_errors(@group) }
62 end
66 end
63 end
67 end
64 end
68 end
65
69
66 def edit
70 def edit
67 end
71 end
68
72
69 def update
73 def update
70 @group.safe_attributes = params[:group]
74 @group.safe_attributes = params[:group]
71
75
72 respond_to do |format|
76 respond_to do |format|
73 if @group.save
77 if @group.save
74 flash[:notice] = l(:notice_successful_update)
78 flash[:notice] = l(:notice_successful_update)
75 format.html { redirect_to(groups_path) }
79 format.html { redirect_to(groups_path) }
76 format.api { render_api_ok }
80 format.api { render_api_ok }
77 else
81 else
78 format.html { render :action => "edit" }
82 format.html { render :action => "edit" }
79 format.api { render_validation_errors(@group) }
83 format.api { render_validation_errors(@group) }
80 end
84 end
81 end
85 end
82 end
86 end
83
87
84 def destroy
88 def destroy
85 @group.destroy
89 @group.destroy
86
90
87 respond_to do |format|
91 respond_to do |format|
88 format.html { redirect_to(groups_path) }
92 format.html { redirect_to(groups_path) }
89 format.api { render_api_ok }
93 format.api { render_api_ok }
90 end
94 end
91 end
95 end
92
96
93 def add_users
97 def add_users
94 @users = User.where(:id => (params[:user_id] || params[:user_ids])).all
98 @users = User.where(:id => (params[:user_id] || params[:user_ids])).all
95 @group.users << @users if request.post?
99 @group.users << @users if request.post?
96 respond_to do |format|
100 respond_to do |format|
97 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
101 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
98 format.js
102 format.js
99 format.api { render_api_ok }
103 format.api { render_api_ok }
100 end
104 end
101 end
105 end
102
106
103 def remove_user
107 def remove_user
104 @group.users.delete(User.find(params[:user_id])) if request.delete?
108 @group.users.delete(User.find(params[:user_id])) if request.delete?
105 respond_to do |format|
109 respond_to do |format|
106 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
110 format.html { redirect_to edit_group_path(@group, :tab => 'users') }
107 format.js
111 format.js
108 format.api { render_api_ok }
112 format.api { render_api_ok }
109 end
113 end
110 end
114 end
111
115
112 def autocomplete_for_user
116 def autocomplete_for_user
113 respond_to do |format|
117 respond_to do |format|
114 format.js
118 format.js
115 end
119 end
116 end
120 end
117
121
118 def edit_membership
122 def edit_membership
119 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
123 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
120 @membership.save if request.post?
124 @membership.save if request.post?
121 respond_to do |format|
125 respond_to do |format|
122 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
126 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
123 format.js
127 format.js
124 end
128 end
125 end
129 end
126
130
127 def destroy_membership
131 def destroy_membership
128 Member.find(params[:membership_id]).destroy if request.post?
132 Member.find(params[:membership_id]).destroy if request.post?
129 respond_to do |format|
133 respond_to do |format|
130 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
134 format.html { redirect_to edit_group_path(@group, :tab => 'memberships') }
131 format.js
135 format.js
132 end
136 end
133 end
137 end
134
138
135 private
139 private
136
140
137 def find_group
141 def find_group
138 @group = Group.find(params[:id])
142 @group = Group.find(params[:id])
139 rescue ActiveRecord::RecordNotFound
143 rescue ActiveRecord::RecordNotFound
140 render_404
144 render_404
141 end
145 end
142
146
143 def user_count_by_group_id
147 def user_count_by_group_id
144 h = User.joins(:groups).group('group_id').count
148 h = User.joins(:groups).group('group_id').count
145 h.keys.each do |key|
149 h.keys.each do |key|
146 h[key.to_i] = h.delete(key)
150 h[key.to_i] = h.delete(key)
147 end
151 end
148 h
152 h
149 end
153 end
150 end
154 end
@@ -1,42 +1,43
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module GroupsHelper
20 module GroupsHelper
21 def group_settings_tabs
21 def group_settings_tabs(group)
22 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
22 tabs = []
23 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
23 tabs << {:name => 'general', :partial => 'groups/general', :label => :label_general}
24 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
24 tabs << {:name => 'users', :partial => 'groups/users', :label => :label_user_plural} if group.givable?
25 ]
25 tabs << {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
26 tabs
26 end
27 end
27
28
28 def render_principals_for_new_group_users(group)
29 def render_principals_for_new_group_users(group)
29 scope = User.active.sorted.not_in_group(group).like(params[:q])
30 scope = User.active.sorted.not_in_group(group).like(params[:q])
30 principal_count = scope.count
31 principal_count = scope.count
31 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
32 principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
32 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
33 principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all
33
34
34 s = content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals')
35 s = content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals')
35
36
36 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
37 links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
37 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
38 link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
38 }
39 }
39
40
40 s + content_tag('p', links, :class => 'pagination')
41 s + content_tag('p', links, :class => 'pagination')
41 end
42 end
42 end
43 end
@@ -1,54 +1,54
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module UsersHelper
20 module UsersHelper
21 def users_status_options_for_select(selected)
21 def users_status_options_for_select(selected)
22 user_count_by_status = User.group('status').count.to_hash
22 user_count_by_status = User.group('status').count.to_hash
23 options_for_select([[l(:label_all), ''],
23 options_for_select([[l(:label_all), ''],
24 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
24 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
25 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
25 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
26 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
26 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
27 end
27 end
28
28
29 def user_mail_notification_options(user)
29 def user_mail_notification_options(user)
30 user.valid_notification_options.collect {|o| [l(o.last), o.first]}
30 user.valid_notification_options.collect {|o| [l(o.last), o.first]}
31 end
31 end
32
32
33 def change_status_link(user)
33 def change_status_link(user)
34 url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
34 url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
35
35
36 if user.locked?
36 if user.locked?
37 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
37 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
38 elsif user.registered?
38 elsif user.registered?
39 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
39 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
40 elsif user != User.current
40 elsif user != User.current
41 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
41 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
42 end
42 end
43 end
43 end
44
44
45 def user_settings_tabs
45 def user_settings_tabs
46 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
46 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
47 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
47 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
48 ]
48 ]
49 if Group.all.any?
49 if Group.givable.any?
50 tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
50 tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
51 end
51 end
52 tabs
52 tabs
53 end
53 end
54 end
54 end
@@ -1,91 +1,120
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Group < Principal
18 class Group < Principal
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 has_and_belongs_to_many :users,
21 has_and_belongs_to_many :users,
22 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
22 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
23 :after_add => :user_added,
23 :after_add => :user_added,
24 :after_remove => :user_removed
24 :after_remove => :user_removed
25
25
26 acts_as_customizable
26 acts_as_customizable
27
27
28 validates_presence_of :lastname
28 validates_presence_of :lastname
29 validates_uniqueness_of :lastname, :case_sensitive => false
29 validates_uniqueness_of :lastname, :case_sensitive => false
30 validates_length_of :lastname, :maximum => 255
30 validates_length_of :lastname, :maximum => 255
31
31
32 before_destroy :remove_references_before_destroy
32 before_destroy :remove_references_before_destroy
33
33
34 scope :sorted, lambda { order("#{table_name}.lastname ASC") }
34 scope :sorted, lambda { order("#{table_name}.type, #{table_name}.lastname ASC") }
35 scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
35 scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
36 scope :givable, lambda {where(:type => 'Group')}
36
37
37 safe_attributes 'name',
38 safe_attributes 'name',
38 'user_ids',
39 'user_ids',
39 'custom_field_values',
40 'custom_field_values',
40 'custom_fields',
41 'custom_fields',
41 :if => lambda {|group, user| user.admin?}
42 :if => lambda {|group, user| user.admin? && !group.builtin?}
42
43
43 def to_s
44 def to_s
44 lastname.to_s
45 name.to_s
45 end
46 end
46
47
47 def name
48 def name
48 lastname
49 lastname
49 end
50 end
50
51
51 def name=(arg)
52 def name=(arg)
52 self.lastname = arg
53 self.lastname = arg
53 end
54 end
54
55
56 def builtin_type
57 nil
58 end
59
60 # Return true if the group is a builtin group
61 def builtin?
62 false
63 end
64
65 # Returns true if the group can be given to a user
66 def givable?
67 !builtin?
68 end
69
55 def user_added(user)
70 def user_added(user)
56 members.each do |member|
71 members.each do |member|
57 next if member.project.nil?
72 next if member.project.nil?
58 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
73 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
59 member.member_roles.each do |member_role|
74 member.member_roles.each do |member_role|
60 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
75 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
61 end
76 end
62 user_member.save!
77 user_member.save!
63 end
78 end
64 end
79 end
65
80
66 def user_removed(user)
81 def user_removed(user)
67 members.each do |member|
82 members.each do |member|
68 MemberRole.
83 MemberRole.
69 includes(:member).
84 includes(:member).
70 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
85 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
71 each(&:destroy)
86 each(&:destroy)
72 end
87 end
73 end
88 end
74
89
75 def self.human_attribute_name(attribute_key_name, *args)
90 def self.human_attribute_name(attribute_key_name, *args)
76 attr_name = attribute_key_name.to_s
91 attr_name = attribute_key_name.to_s
77 if attr_name == 'lastname'
92 if attr_name == 'lastname'
78 attr_name = "name"
93 attr_name = "name"
79 end
94 end
80 super(attr_name, *args)
95 super(attr_name, *args)
81 end
96 end
82
97
98 def self.builtin_id(arg)
99 (arg.anonymous? ? GroupAnonymous : GroupNonMember).instance_id
100 end
101
102 def self.anonymous
103 GroupAnonymous.load_instance
104 end
105
106 def self.non_member
107 GroupNonMember.load_instance
108 end
109
83 private
110 private
84
111
85 # Removes references that are not handled by associations
112 # Removes references that are not handled by associations
86 def remove_references_before_destroy
113 def remove_references_before_destroy
87 return if self.id.nil?
114 return if self.id.nil?
88
115
89 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
116 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
90 end
117 end
91 end
118 end
119
120 require_dependency "group_builtin"
@@ -1,484 +1,485
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
42 QueryColumn.new(:description, :inline => false)
42 QueryColumn.new(:description, :inline => false)
43 ]
43 ]
44
44
45 scope :visible, lambda {|*args|
45 scope :visible, lambda {|*args|
46 user = args.shift || User.current
46 user = args.shift || User.current
47 base = Project.allowed_to_condition(user, :view_issues, *args)
47 base = Project.allowed_to_condition(user, :view_issues, *args)
48 scope = includes(:project).where("#{table_name}.project_id IS NULL OR (#{base})")
48 scope = includes(:project).where("#{table_name}.project_id IS NULL OR (#{base})")
49
49
50 if user.admin?
50 if user.admin?
51 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
51 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
52 elsif user.memberships.any?
52 elsif user.memberships.any?
53 scope.where("#{table_name}.visibility = ?" +
53 scope.where("#{table_name}.visibility = ?" +
54 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
54 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
55 "SELECT DISTINCT q.id FROM #{table_name} q" +
55 "SELECT DISTINCT q.id FROM #{table_name} q" +
56 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
56 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
57 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
57 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
58 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
58 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
59 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
59 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
60 " OR #{table_name}.user_id = ?",
60 " OR #{table_name}.user_id = ?",
61 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
61 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
62 elsif user.logged?
62 elsif user.logged?
63 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
63 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
64 else
64 else
65 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
65 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
66 end
66 end
67 }
67 }
68
68
69 def initialize(attributes=nil, *args)
69 def initialize(attributes=nil, *args)
70 super attributes
70 super attributes
71 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
71 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
72 end
72 end
73
73
74 # Returns true if the query is visible to +user+ or the current user.
74 # Returns true if the query is visible to +user+ or the current user.
75 def visible?(user=User.current)
75 def visible?(user=User.current)
76 return true if user.admin?
76 return true if user.admin?
77 return false unless project.nil? || user.allowed_to?(:view_issues, project)
77 return false unless project.nil? || user.allowed_to?(:view_issues, project)
78 case visibility
78 case visibility
79 when VISIBILITY_PUBLIC
79 when VISIBILITY_PUBLIC
80 true
80 true
81 when VISIBILITY_ROLES
81 when VISIBILITY_ROLES
82 if project
82 if project
83 (user.roles_for_project(project) & roles).any?
83 (user.roles_for_project(project) & roles).any?
84 else
84 else
85 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
85 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
86 end
86 end
87 else
87 else
88 user == self.user
88 user == self.user
89 end
89 end
90 end
90 end
91
91
92 def is_private?
92 def is_private?
93 visibility == VISIBILITY_PRIVATE
93 visibility == VISIBILITY_PRIVATE
94 end
94 end
95
95
96 def is_public?
96 def is_public?
97 !is_private?
97 !is_private?
98 end
98 end
99
99
100 def draw_relations
100 def draw_relations
101 r = options[:draw_relations]
101 r = options[:draw_relations]
102 r.nil? || r == '1'
102 r.nil? || r == '1'
103 end
103 end
104
104
105 def draw_relations=(arg)
105 def draw_relations=(arg)
106 options[:draw_relations] = (arg == '0' ? '0' : nil)
106 options[:draw_relations] = (arg == '0' ? '0' : nil)
107 end
107 end
108
108
109 def draw_progress_line
109 def draw_progress_line
110 r = options[:draw_progress_line]
110 r = options[:draw_progress_line]
111 r == '1'
111 r == '1'
112 end
112 end
113
113
114 def draw_progress_line=(arg)
114 def draw_progress_line=(arg)
115 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
115 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
116 end
116 end
117
117
118 def build_from_params(params)
118 def build_from_params(params)
119 super
119 super
120 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
120 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
121 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
121 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
122 self
122 self
123 end
123 end
124
124
125 def initialize_available_filters
125 def initialize_available_filters
126 principals = []
126 principals = []
127 subprojects = []
127 subprojects = []
128 versions = []
128 versions = []
129 categories = []
129 categories = []
130 issue_custom_fields = []
130 issue_custom_fields = []
131
131
132 if project
132 if project
133 principals += project.principals.sort
133 principals += project.principals.sort
134 unless project.leaf?
134 unless project.leaf?
135 subprojects = project.descendants.visible.all
135 subprojects = project.descendants.visible.all
136 principals += Principal.member_of(subprojects)
136 principals += Principal.member_of(subprojects)
137 end
137 end
138 versions = project.shared_versions.all
138 versions = project.shared_versions.all
139 categories = project.issue_categories.all
139 categories = project.issue_categories.all
140 issue_custom_fields = project.all_issue_custom_fields
140 issue_custom_fields = project.all_issue_custom_fields
141 else
141 else
142 if all_projects.any?
142 if all_projects.any?
143 principals += Principal.member_of(all_projects)
143 principals += Principal.member_of(all_projects)
144 end
144 end
145 versions = Version.visible.where(:sharing => 'system').all
145 versions = Version.visible.where(:sharing => 'system').all
146 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
146 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
147 end
147 end
148 principals.uniq!
148 principals.uniq!
149 principals.sort!
149 principals.sort!
150 principals.reject! {|p| p.is_a?(GroupBuiltin)}
150 users = principals.select {|p| p.is_a?(User)}
151 users = principals.select {|p| p.is_a?(User)}
151
152
152 add_available_filter "status_id",
153 add_available_filter "status_id",
153 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
154 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
154
155
155 if project.nil?
156 if project.nil?
156 project_values = []
157 project_values = []
157 if User.current.logged? && User.current.memberships.any?
158 if User.current.logged? && User.current.memberships.any?
158 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
159 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
159 end
160 end
160 project_values += all_projects_values
161 project_values += all_projects_values
161 add_available_filter("project_id",
162 add_available_filter("project_id",
162 :type => :list, :values => project_values
163 :type => :list, :values => project_values
163 ) unless project_values.empty?
164 ) unless project_values.empty?
164 end
165 end
165
166
166 add_available_filter "tracker_id",
167 add_available_filter "tracker_id",
167 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
168 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
168 add_available_filter "priority_id",
169 add_available_filter "priority_id",
169 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
170 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
170
171
171 author_values = []
172 author_values = []
172 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
173 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
173 author_values += users.collect{|s| [s.name, s.id.to_s] }
174 author_values += users.collect{|s| [s.name, s.id.to_s] }
174 add_available_filter("author_id",
175 add_available_filter("author_id",
175 :type => :list, :values => author_values
176 :type => :list, :values => author_values
176 ) unless author_values.empty?
177 ) unless author_values.empty?
177
178
178 assigned_to_values = []
179 assigned_to_values = []
179 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
180 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
180 assigned_to_values += (Setting.issue_group_assignment? ?
181 assigned_to_values += (Setting.issue_group_assignment? ?
181 principals : users).collect{|s| [s.name, s.id.to_s] }
182 principals : users).collect{|s| [s.name, s.id.to_s] }
182 add_available_filter("assigned_to_id",
183 add_available_filter("assigned_to_id",
183 :type => :list_optional, :values => assigned_to_values
184 :type => :list_optional, :values => assigned_to_values
184 ) unless assigned_to_values.empty?
185 ) unless assigned_to_values.empty?
185
186
186 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
187 group_values = Group.givable.collect {|g| [g.name, g.id.to_s] }
187 add_available_filter("member_of_group",
188 add_available_filter("member_of_group",
188 :type => :list_optional, :values => group_values
189 :type => :list_optional, :values => group_values
189 ) unless group_values.empty?
190 ) unless group_values.empty?
190
191
191 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
192 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
192 add_available_filter("assigned_to_role",
193 add_available_filter("assigned_to_role",
193 :type => :list_optional, :values => role_values
194 :type => :list_optional, :values => role_values
194 ) unless role_values.empty?
195 ) unless role_values.empty?
195
196
196 if versions.any?
197 if versions.any?
197 add_available_filter "fixed_version_id",
198 add_available_filter "fixed_version_id",
198 :type => :list_optional,
199 :type => :list_optional,
199 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
200 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
200 end
201 end
201
202
202 if categories.any?
203 if categories.any?
203 add_available_filter "category_id",
204 add_available_filter "category_id",
204 :type => :list_optional,
205 :type => :list_optional,
205 :values => categories.collect{|s| [s.name, s.id.to_s] }
206 :values => categories.collect{|s| [s.name, s.id.to_s] }
206 end
207 end
207
208
208 add_available_filter "subject", :type => :text
209 add_available_filter "subject", :type => :text
209 add_available_filter "created_on", :type => :date_past
210 add_available_filter "created_on", :type => :date_past
210 add_available_filter "updated_on", :type => :date_past
211 add_available_filter "updated_on", :type => :date_past
211 add_available_filter "closed_on", :type => :date_past
212 add_available_filter "closed_on", :type => :date_past
212 add_available_filter "start_date", :type => :date
213 add_available_filter "start_date", :type => :date
213 add_available_filter "due_date", :type => :date
214 add_available_filter "due_date", :type => :date
214 add_available_filter "estimated_hours", :type => :float
215 add_available_filter "estimated_hours", :type => :float
215 add_available_filter "done_ratio", :type => :integer
216 add_available_filter "done_ratio", :type => :integer
216
217
217 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
218 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
218 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
219 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
219 add_available_filter "is_private",
220 add_available_filter "is_private",
220 :type => :list,
221 :type => :list,
221 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
222 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
222 end
223 end
223
224
224 if User.current.logged?
225 if User.current.logged?
225 add_available_filter "watcher_id",
226 add_available_filter "watcher_id",
226 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
227 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
227 end
228 end
228
229
229 if subprojects.any?
230 if subprojects.any?
230 add_available_filter "subproject_id",
231 add_available_filter "subproject_id",
231 :type => :list_subprojects,
232 :type => :list_subprojects,
232 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
233 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
233 end
234 end
234
235
235 add_custom_fields_filters(issue_custom_fields)
236 add_custom_fields_filters(issue_custom_fields)
236
237
237 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
238 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
238
239
239 IssueRelation::TYPES.each do |relation_type, options|
240 IssueRelation::TYPES.each do |relation_type, options|
240 add_available_filter relation_type, :type => :relation, :label => options[:name]
241 add_available_filter relation_type, :type => :relation, :label => options[:name]
241 end
242 end
242
243
243 Tracker.disabled_core_fields(trackers).each {|field|
244 Tracker.disabled_core_fields(trackers).each {|field|
244 delete_available_filter field
245 delete_available_filter field
245 }
246 }
246 end
247 end
247
248
248 def available_columns
249 def available_columns
249 return @available_columns if @available_columns
250 return @available_columns if @available_columns
250 @available_columns = self.class.available_columns.dup
251 @available_columns = self.class.available_columns.dup
251 @available_columns += (project ?
252 @available_columns += (project ?
252 project.all_issue_custom_fields :
253 project.all_issue_custom_fields :
253 IssueCustomField
254 IssueCustomField
254 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
255 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
255
256
256 if User.current.allowed_to?(:view_time_entries, project, :global => true)
257 if User.current.allowed_to?(:view_time_entries, project, :global => true)
257 index = nil
258 index = nil
258 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
259 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
259 index = (index ? index + 1 : -1)
260 index = (index ? index + 1 : -1)
260 # insert the column after estimated_hours or at the end
261 # insert the column after estimated_hours or at the end
261 @available_columns.insert index, QueryColumn.new(:spent_hours,
262 @available_columns.insert index, QueryColumn.new(:spent_hours,
262 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
263 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
263 :default_order => 'desc',
264 :default_order => 'desc',
264 :caption => :label_spent_time
265 :caption => :label_spent_time
265 )
266 )
266 end
267 end
267
268
268 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
269 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
269 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
270 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
270 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
271 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
271 end
272 end
272
273
273 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
274 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
274 @available_columns.reject! {|column|
275 @available_columns.reject! {|column|
275 disabled_fields.include?(column.name.to_s)
276 disabled_fields.include?(column.name.to_s)
276 }
277 }
277
278
278 @available_columns
279 @available_columns
279 end
280 end
280
281
281 def default_columns_names
282 def default_columns_names
282 @default_columns_names ||= begin
283 @default_columns_names ||= begin
283 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
284 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
284
285
285 project.present? ? default_columns : [:project] | default_columns
286 project.present? ? default_columns : [:project] | default_columns
286 end
287 end
287 end
288 end
288
289
289 # Returns the issue count
290 # Returns the issue count
290 def issue_count
291 def issue_count
291 Issue.visible.joins(:status, :project).where(statement).count
292 Issue.visible.joins(:status, :project).where(statement).count
292 rescue ::ActiveRecord::StatementInvalid => e
293 rescue ::ActiveRecord::StatementInvalid => e
293 raise StatementInvalid.new(e.message)
294 raise StatementInvalid.new(e.message)
294 end
295 end
295
296
296 # Returns the issue count by group or nil if query is not grouped
297 # Returns the issue count by group or nil if query is not grouped
297 def issue_count_by_group
298 def issue_count_by_group
298 r = nil
299 r = nil
299 if grouped?
300 if grouped?
300 begin
301 begin
301 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
302 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
302 r = Issue.visible.
303 r = Issue.visible.
303 joins(:status, :project).
304 joins(:status, :project).
304 where(statement).
305 where(statement).
305 joins(joins_for_order_statement(group_by_statement)).
306 joins(joins_for_order_statement(group_by_statement)).
306 group(group_by_statement).
307 group(group_by_statement).
307 count
308 count
308 rescue ActiveRecord::RecordNotFound
309 rescue ActiveRecord::RecordNotFound
309 r = {nil => issue_count}
310 r = {nil => issue_count}
310 end
311 end
311 c = group_by_column
312 c = group_by_column
312 if c.is_a?(QueryCustomFieldColumn)
313 if c.is_a?(QueryCustomFieldColumn)
313 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
314 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
314 end
315 end
315 end
316 end
316 r
317 r
317 rescue ::ActiveRecord::StatementInvalid => e
318 rescue ::ActiveRecord::StatementInvalid => e
318 raise StatementInvalid.new(e.message)
319 raise StatementInvalid.new(e.message)
319 end
320 end
320
321
321 # Returns the issues
322 # Returns the issues
322 # Valid options are :order, :offset, :limit, :include, :conditions
323 # Valid options are :order, :offset, :limit, :include, :conditions
323 def issues(options={})
324 def issues(options={})
324 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
325 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
325
326
326 scope = Issue.visible.
327 scope = Issue.visible.
327 joins(:status, :project).
328 joins(:status, :project).
328 where(statement).
329 where(statement).
329 includes(([:status, :project] + (options[:include] || [])).uniq).
330 includes(([:status, :project] + (options[:include] || [])).uniq).
330 where(options[:conditions]).
331 where(options[:conditions]).
331 order(order_option).
332 order(order_option).
332 joins(joins_for_order_statement(order_option.join(','))).
333 joins(joins_for_order_statement(order_option.join(','))).
333 limit(options[:limit]).
334 limit(options[:limit]).
334 offset(options[:offset])
335 offset(options[:offset])
335
336
336 scope = scope.preload(:custom_values)
337 scope = scope.preload(:custom_values)
337 if has_column?(:author)
338 if has_column?(:author)
338 scope = scope.preload(:author)
339 scope = scope.preload(:author)
339 end
340 end
340
341
341 issues = scope.all
342 issues = scope.all
342
343
343 if has_column?(:spent_hours)
344 if has_column?(:spent_hours)
344 Issue.load_visible_spent_hours(issues)
345 Issue.load_visible_spent_hours(issues)
345 end
346 end
346 if has_column?(:relations)
347 if has_column?(:relations)
347 Issue.load_visible_relations(issues)
348 Issue.load_visible_relations(issues)
348 end
349 end
349 issues
350 issues
350 rescue ::ActiveRecord::StatementInvalid => e
351 rescue ::ActiveRecord::StatementInvalid => e
351 raise StatementInvalid.new(e.message)
352 raise StatementInvalid.new(e.message)
352 end
353 end
353
354
354 # Returns the issues ids
355 # Returns the issues ids
355 def issue_ids(options={})
356 def issue_ids(options={})
356 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
357 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
357
358
358 Issue.visible.
359 Issue.visible.
359 joins(:status, :project).
360 joins(:status, :project).
360 where(statement).
361 where(statement).
361 includes(([:status, :project] + (options[:include] || [])).uniq).
362 includes(([:status, :project] + (options[:include] || [])).uniq).
362 where(options[:conditions]).
363 where(options[:conditions]).
363 order(order_option).
364 order(order_option).
364 joins(joins_for_order_statement(order_option.join(','))).
365 joins(joins_for_order_statement(order_option.join(','))).
365 limit(options[:limit]).
366 limit(options[:limit]).
366 offset(options[:offset]).
367 offset(options[:offset]).
367 find_ids
368 find_ids
368 rescue ::ActiveRecord::StatementInvalid => e
369 rescue ::ActiveRecord::StatementInvalid => e
369 raise StatementInvalid.new(e.message)
370 raise StatementInvalid.new(e.message)
370 end
371 end
371
372
372 # Returns the journals
373 # Returns the journals
373 # Valid options are :order, :offset, :limit
374 # Valid options are :order, :offset, :limit
374 def journals(options={})
375 def journals(options={})
375 Journal.visible.
376 Journal.visible.
376 joins(:issue => [:project, :status]).
377 joins(:issue => [:project, :status]).
377 where(statement).
378 where(statement).
378 order(options[:order]).
379 order(options[:order]).
379 limit(options[:limit]).
380 limit(options[:limit]).
380 offset(options[:offset]).
381 offset(options[:offset]).
381 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
382 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
382 all
383 all
383 rescue ::ActiveRecord::StatementInvalid => e
384 rescue ::ActiveRecord::StatementInvalid => e
384 raise StatementInvalid.new(e.message)
385 raise StatementInvalid.new(e.message)
385 end
386 end
386
387
387 # Returns the versions
388 # Returns the versions
388 # Valid options are :conditions
389 # Valid options are :conditions
389 def versions(options={})
390 def versions(options={})
390 Version.visible.
391 Version.visible.
391 where(project_statement).
392 where(project_statement).
392 where(options[:conditions]).
393 where(options[:conditions]).
393 includes(:project).
394 includes(:project).
394 all
395 all
395 rescue ::ActiveRecord::StatementInvalid => e
396 rescue ::ActiveRecord::StatementInvalid => e
396 raise StatementInvalid.new(e.message)
397 raise StatementInvalid.new(e.message)
397 end
398 end
398
399
399 def sql_for_watcher_id_field(field, operator, value)
400 def sql_for_watcher_id_field(field, operator, value)
400 db_table = Watcher.table_name
401 db_table = Watcher.table_name
401 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
402 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
402 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
403 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
403 end
404 end
404
405
405 def sql_for_member_of_group_field(field, operator, value)
406 def sql_for_member_of_group_field(field, operator, value)
406 if operator == '*' # Any group
407 if operator == '*' # Any group
407 groups = Group.all
408 groups = Group.givable
408 operator = '=' # Override the operator since we want to find by assigned_to
409 operator = '=' # Override the operator since we want to find by assigned_to
409 elsif operator == "!*"
410 elsif operator == "!*"
410 groups = Group.all
411 groups = Group.givable
411 operator = '!' # Override the operator since we want to find by assigned_to
412 operator = '!' # Override the operator since we want to find by assigned_to
412 else
413 else
413 groups = Group.where(:id => value).all
414 groups = Group.where(:id => value).all
414 end
415 end
415 groups ||= []
416 groups ||= []
416
417
417 members_of_groups = groups.inject([]) {|user_ids, group|
418 members_of_groups = groups.inject([]) {|user_ids, group|
418 user_ids + group.user_ids + [group.id]
419 user_ids + group.user_ids + [group.id]
419 }.uniq.compact.sort.collect(&:to_s)
420 }.uniq.compact.sort.collect(&:to_s)
420
421
421 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
422 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
422 end
423 end
423
424
424 def sql_for_assigned_to_role_field(field, operator, value)
425 def sql_for_assigned_to_role_field(field, operator, value)
425 case operator
426 case operator
426 when "*", "!*" # Member / Not member
427 when "*", "!*" # Member / Not member
427 sw = operator == "!*" ? 'NOT' : ''
428 sw = operator == "!*" ? 'NOT' : ''
428 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
429 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
429 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
430 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
430 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
431 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
431 when "=", "!"
432 when "=", "!"
432 role_cond = value.any? ?
433 role_cond = value.any? ?
433 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
434 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
434 "1=0"
435 "1=0"
435
436
436 sw = operator == "!" ? 'NOT' : ''
437 sw = operator == "!" ? 'NOT' : ''
437 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
438 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
438 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
439 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
439 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
440 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
440 end
441 end
441 end
442 end
442
443
443 def sql_for_is_private_field(field, operator, value)
444 def sql_for_is_private_field(field, operator, value)
444 op = (operator == "=" ? 'IN' : 'NOT IN')
445 op = (operator == "=" ? 'IN' : 'NOT IN')
445 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
446 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
446
447
447 "#{Issue.table_name}.is_private #{op} (#{va})"
448 "#{Issue.table_name}.is_private #{op} (#{va})"
448 end
449 end
449
450
450 def sql_for_relations(field, operator, value, options={})
451 def sql_for_relations(field, operator, value, options={})
451 relation_options = IssueRelation::TYPES[field]
452 relation_options = IssueRelation::TYPES[field]
452 return relation_options unless relation_options
453 return relation_options unless relation_options
453
454
454 relation_type = field
455 relation_type = field
455 join_column, target_join_column = "issue_from_id", "issue_to_id"
456 join_column, target_join_column = "issue_from_id", "issue_to_id"
456 if relation_options[:reverse] || options[:reverse]
457 if relation_options[:reverse] || options[:reverse]
457 relation_type = relation_options[:reverse] || relation_type
458 relation_type = relation_options[:reverse] || relation_type
458 join_column, target_join_column = target_join_column, join_column
459 join_column, target_join_column = target_join_column, join_column
459 end
460 end
460
461
461 sql = case operator
462 sql = case operator
462 when "*", "!*"
463 when "*", "!*"
463 op = (operator == "*" ? 'IN' : 'NOT IN')
464 op = (operator == "*" ? 'IN' : 'NOT IN')
464 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
465 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
465 when "=", "!"
466 when "=", "!"
466 op = (operator == "=" ? 'IN' : 'NOT IN')
467 op = (operator == "=" ? 'IN' : 'NOT IN')
467 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
468 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
468 when "=p", "=!p", "!p"
469 when "=p", "=!p", "!p"
469 op = (operator == "!p" ? 'NOT IN' : 'IN')
470 op = (operator == "!p" ? 'NOT IN' : 'IN')
470 comp = (operator == "=!p" ? '<>' : '=')
471 comp = (operator == "=!p" ? '<>' : '=')
471 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
472 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
472 end
473 end
473
474
474 if relation_options[:sym] == field && !options[:reverse]
475 if relation_options[:sym] == field && !options[:reverse]
475 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
476 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
476 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
477 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
477 end
478 end
478 "(#{sql})"
479 "(#{sql})"
479 end
480 end
480
481
481 IssueRelation::TYPES.keys.each do |relation_type|
482 IssueRelation::TYPES.keys.each do |relation_type|
482 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
483 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
483 end
484 end
484 end
485 end
@@ -1,116 +1,119
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Principal < ActiveRecord::Base
18 class Principal < ActiveRecord::Base
19 self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
19 self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
20
20
21 # Account statuses
21 # Account statuses
22 STATUS_ANONYMOUS = 0
22 STATUS_ANONYMOUS = 0
23 STATUS_ACTIVE = 1
23 STATUS_ACTIVE = 1
24 STATUS_REGISTERED = 2
24 STATUS_REGISTERED = 2
25 STATUS_LOCKED = 3
25 STATUS_LOCKED = 3
26
26
27 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
27 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
28 has_many :memberships, :class_name => 'Member',
28 has_many :memberships, :class_name => 'Member',
29 :foreign_key => 'user_id',
29 :foreign_key => 'user_id',
30 :include => [:project, :roles],
30 :include => [:project, :roles],
31 :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}",
31 :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}",
32 :order => "#{Project.table_name}.name"
32 :order => "#{Project.table_name}.name"
33 has_many :projects, :through => :memberships
33 has_many :projects, :through => :memberships
34 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
34 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
35
35
36 # Groups and active users
36 # Groups and active users
37 scope :active, lambda { where(:status => STATUS_ACTIVE) }
37 scope :active, lambda { where(:status => STATUS_ACTIVE) }
38
38
39 scope :like, lambda {|q|
39 scope :like, lambda {|q|
40 q = q.to_s
40 q = q.to_s
41 if q.blank?
41 if q.blank?
42 where({})
42 where({})
43 else
43 else
44 pattern = "%#{q}%"
44 pattern = "%#{q}%"
45 sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
45 sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
46 params = {:p => pattern}
46 params = {:p => pattern}
47 if q =~ /^(.+)\s+(.+)$/
47 if q =~ /^(.+)\s+(.+)$/
48 a, b = "#{$1}%", "#{$2}%"
48 a, b = "#{$1}%", "#{$2}%"
49 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
49 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
50 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
50 sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
51 params.merge!(:a => a, :b => b)
51 params.merge!(:a => a, :b => b)
52 end
52 end
53 where(sql, params)
53 where(sql, params)
54 end
54 end
55 }
55 }
56
56
57 # Principals that are members of a collection of projects
57 # Principals that are members of a collection of projects
58 scope :member_of, lambda {|projects|
58 scope :member_of, lambda {|projects|
59 projects = [projects] unless projects.is_a?(Array)
59 projects = [projects] unless projects.is_a?(Array)
60 if projects.empty?
60 if projects.empty?
61 where("1=0")
61 where("1=0")
62 else
62 else
63 ids = projects.map(&:id)
63 ids = projects.map(&:id)
64 active.where("#{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
64 active.where("#{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
65 end
65 end
66 }
66 }
67 # Principals that are not members of projects
67 # Principals that are not members of projects
68 scope :not_member_of, lambda {|projects|
68 scope :not_member_of, lambda {|projects|
69 projects = [projects] unless projects.is_a?(Array)
69 projects = [projects] unless projects.is_a?(Array)
70 if projects.empty?
70 if projects.empty?
71 where("1=0")
71 where("1=0")
72 else
72 else
73 ids = projects.map(&:id)
73 ids = projects.map(&:id)
74 where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
74 where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
75 end
75 end
76 }
76 }
77 scope :sorted, lambda { order(*Principal.fields_for_order_statement)}
77 scope :sorted, lambda { order(*Principal.fields_for_order_statement)}
78
78
79 before_create :set_default_empty_values
79 before_create :set_default_empty_values
80
80
81 def name(formatter = nil)
81 def name(formatter = nil)
82 to_s
82 to_s
83 end
83 end
84
84
85 def <=>(principal)
85 def <=>(principal)
86 if principal.nil?
86 if principal.nil?
87 -1
87 -1
88 elsif self.class.name == principal.class.name
88 elsif self.class.name == principal.class.name
89 self.to_s.downcase <=> principal.to_s.downcase
89 self.to_s.downcase <=> principal.to_s.downcase
90 else
90 else
91 # groups after users
91 # groups after users
92 principal.class.name <=> self.class.name
92 principal.class.name <=> self.class.name
93 end
93 end
94 end
94 end
95
95
96 # Returns an array of fields names than can be used to make an order statement for principals.
96 # Returns an array of fields names than can be used to make an order statement for principals.
97 # Users are sorted before Groups.
97 # Users are sorted before Groups.
98 # Examples:
98 # Examples:
99 def self.fields_for_order_statement(table=nil)
99 def self.fields_for_order_statement(table=nil)
100 table ||= table_name
100 table ||= table_name
101 columns = ['type DESC'] + (User.name_formatter[:order] - ['id']) + ['lastname', 'id']
101 columns = ['type DESC'] + (User.name_formatter[:order] - ['id']) + ['lastname', 'id']
102 columns.uniq.map {|field| "#{table}.#{field}"}
102 columns.uniq.map {|field| "#{table}.#{field}"}
103 end
103 end
104
104
105 protected
105 protected
106
106
107 # Make sure we don't try to insert NULL values (see #4632)
107 # Make sure we don't try to insert NULL values (see #4632)
108 def set_default_empty_values
108 def set_default_empty_values
109 self.login ||= ''
109 self.login ||= ''
110 self.hashed_password ||= ''
110 self.hashed_password ||= ''
111 self.firstname ||= ''
111 self.firstname ||= ''
112 self.lastname ||= ''
112 self.lastname ||= ''
113 self.mail ||= ''
113 self.mail ||= ''
114 true
114 true
115 end
115 end
116 end
116 end
117
118 require_dependency "user"
119 require_dependency "group"
@@ -1,1054 +1,1064
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overridden Activities
29 # Specific overridden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
36
36
37 has_many :enabled_modules, :dependent => :delete_all
37 has_many :enabled_modules, :dependent => :delete_all
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :time_entries, :dependent => :destroy
42 has_many :time_entries, :dependent => :destroy
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, :dependent => :destroy, :include => :author
45 has_many :news, :dependent => :destroy, :include => :author
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_one :repository, :conditions => ["is_default = ?", true]
48 has_one :repository, :conditions => ["is_default = ?", true]
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :dependent => :destroy
59 acts_as_nested_set :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # downcase letters, digits, dashes but not digits only
77 # downcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
84 before_destroy :delete_all_members
84 before_destroy :delete_all_members
85
85
86 scope :has_module, lambda {|mod|
86 scope :has_module, lambda {|mod|
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 }
88 }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :all_public, lambda { where(:is_public => true) }
91 scope :all_public, lambda { where(:is_public => true) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :allowed_to, lambda {|*args|
93 scope :allowed_to, lambda {|*args|
94 user = User.current
94 user = User.current
95 permission = nil
95 permission = nil
96 if args.first.is_a?(Symbol)
96 if args.first.is_a?(Symbol)
97 permission = args.shift
97 permission = args.shift
98 else
98 else
99 user = args.shift
99 user = args.shift
100 permission = args.shift
100 permission = args.shift
101 end
101 end
102 where(Project.allowed_to_condition(user, permission, *args))
102 where(Project.allowed_to_condition(user, permission, *args))
103 }
103 }
104 scope :like, lambda {|arg|
104 scope :like, lambda {|arg|
105 if arg.blank?
105 if arg.blank?
106 where(nil)
106 where(nil)
107 else
107 else
108 pattern = "%#{arg.to_s.strip.downcase}%"
108 pattern = "%#{arg.to_s.strip.downcase}%"
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 end
110 end
111 }
111 }
112
112
113 def initialize(attributes=nil, *args)
113 def initialize(attributes=nil, *args)
114 super
114 super
115
115
116 initialized = (attributes || {}).stringify_keys
116 initialized = (attributes || {}).stringify_keys
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 self.identifier = Project.next_identifier
118 self.identifier = Project.next_identifier
119 end
119 end
120 if !initialized.key?('is_public')
120 if !initialized.key?('is_public')
121 self.is_public = Setting.default_projects_public?
121 self.is_public = Setting.default_projects_public?
122 end
122 end
123 if !initialized.key?('enabled_module_names')
123 if !initialized.key?('enabled_module_names')
124 self.enabled_module_names = Setting.default_projects_modules
124 self.enabled_module_names = Setting.default_projects_modules
125 end
125 end
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 default = Setting.default_projects_tracker_ids
127 default = Setting.default_projects_tracker_ids
128 if default.is_a?(Array)
128 if default.is_a?(Array)
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
130 else
130 else
131 self.trackers = Tracker.sorted.all
131 self.trackers = Tracker.sorted.all
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def identifier=(identifier)
136 def identifier=(identifier)
137 super unless identifier_frozen?
137 super unless identifier_frozen?
138 end
138 end
139
139
140 def identifier_frozen?
140 def identifier_frozen?
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
142 end
142 end
143
143
144 # returns latest created projects
144 # returns latest created projects
145 # non public projects will be returned only if user is a member of those
145 # non public projects will be returned only if user is a member of those
146 def self.latest(user=nil, count=5)
146 def self.latest(user=nil, count=5)
147 visible(user).limit(count).order("created_on DESC").all
147 visible(user).limit(count).order("created_on DESC").all
148 end
148 end
149
149
150 # Returns true if the project is visible to +user+ or to the current user.
150 # Returns true if the project is visible to +user+ or to the current user.
151 def visible?(user=User.current)
151 def visible?(user=User.current)
152 user.allowed_to?(:view_project, self)
152 user.allowed_to?(:view_project, self)
153 end
153 end
154
154
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
156 #
156 #
157 # Examples:
157 # Examples:
158 # Project.visible_condition(admin) => "projects.status = 1"
158 # Project.visible_condition(admin) => "projects.status = 1"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
161 def self.visible_condition(user, options={})
161 def self.visible_condition(user, options={})
162 allowed_to_condition(user, :view_project, options)
162 allowed_to_condition(user, :view_project, options)
163 end
163 end
164
164
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
166 #
166 #
167 # Valid options:
167 # Valid options:
168 # * :project => limit the condition to project
168 # * :project => limit the condition to project
169 # * :with_subprojects => limit the condition to project and its subprojects
169 # * :with_subprojects => limit the condition to project and its subprojects
170 # * :member => limit the condition to the user projects
170 # * :member => limit the condition to the user projects
171 def self.allowed_to_condition(user, permission, options={})
171 def self.allowed_to_condition(user, permission, options={})
172 perm = Redmine::AccessControl.permission(permission)
172 perm = Redmine::AccessControl.permission(permission)
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
174 if perm && perm.project_module
174 if perm && perm.project_module
175 # If the permission belongs to a project module, make sure the module is enabled
175 # If the permission belongs to a project module, make sure the module is enabled
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
177 end
177 end
178 if options[:project]
178 if options[:project]
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 end
182 end
183
183
184 if user.admin?
184 if user.admin?
185 base_statement
185 base_statement
186 else
186 else
187 statement_by_role = {}
187 statement_by_role = {}
188 unless options[:member]
188 unless options[:member]
189 role = user.builtin_role
189 role = user.builtin_role
190 if role.allowed_to?(permission)
190 if role.allowed_to?(permission)
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 end
192 end
193 end
193 end
194 if user.logged?
194 user.projects_by_role.each do |role, projects|
195 user.projects_by_role.each do |role, projects|
195 if role.allowed_to?(permission) && projects.any?
196 if role.allowed_to?(permission) && projects.any?
196 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 end
199 end
197 end
200 end
198 end
201 if statement_by_role.empty?
199 if statement_by_role.empty?
202 "1=0"
200 "1=0"
203 else
201 else
204 if block_given?
202 if block_given?
205 statement_by_role.each do |role, statement|
203 statement_by_role.each do |role, statement|
206 if s = yield(role, user)
204 if s = yield(role, user)
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
205 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 end
206 end
209 end
207 end
210 end
208 end
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
209 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 end
210 end
213 end
211 end
214 end
212 end
215
213
214 def override_roles(role)
215 @override_members ||= memberships.where(:user_id => [GroupAnonymous.instance_id, GroupNonMember.instance_id]).all
216 member = @override_members.detect {|m| role.anonymous? ^ (m.user_id == GroupNonMember.instance_id)}
217 member ? member.roles : [role]
218 end
219
216 def principals
220 def principals
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
221 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
218 end
222 end
219
223
220 def users
224 def users
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
225 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
222 end
226 end
223
227
224 # Returns the Systemwide and project specific activities
228 # Returns the Systemwide and project specific activities
225 def activities(include_inactive=false)
229 def activities(include_inactive=false)
226 if include_inactive
230 if include_inactive
227 return all_activities
231 return all_activities
228 else
232 else
229 return active_activities
233 return active_activities
230 end
234 end
231 end
235 end
232
236
233 # Will create a new Project specific Activity or update an existing one
237 # Will create a new Project specific Activity or update an existing one
234 #
238 #
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
239 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 # does not successfully save.
240 # does not successfully save.
237 def update_or_create_time_entry_activity(id, activity_hash)
241 def update_or_create_time_entry_activity(id, activity_hash)
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
242 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
239 self.create_time_entry_activity_if_needed(activity_hash)
243 self.create_time_entry_activity_if_needed(activity_hash)
240 else
244 else
241 activity = project.time_entry_activities.find_by_id(id.to_i)
245 activity = project.time_entry_activities.find_by_id(id.to_i)
242 activity.update_attributes(activity_hash) if activity
246 activity.update_attributes(activity_hash) if activity
243 end
247 end
244 end
248 end
245
249
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
250 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
247 #
251 #
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
252 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
249 # does not successfully save.
253 # does not successfully save.
250 def create_time_entry_activity_if_needed(activity)
254 def create_time_entry_activity_if_needed(activity)
251 if activity['parent_id']
255 if activity['parent_id']
252 parent_activity = TimeEntryActivity.find(activity['parent_id'])
256 parent_activity = TimeEntryActivity.find(activity['parent_id'])
253 activity['name'] = parent_activity.name
257 activity['name'] = parent_activity.name
254 activity['position'] = parent_activity.position
258 activity['position'] = parent_activity.position
255 if Enumeration.overriding_change?(activity, parent_activity)
259 if Enumeration.overriding_change?(activity, parent_activity)
256 project_activity = self.time_entry_activities.create(activity)
260 project_activity = self.time_entry_activities.create(activity)
257 if project_activity.new_record?
261 if project_activity.new_record?
258 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
262 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
259 else
263 else
260 self.time_entries.
264 self.time_entries.
261 where(["activity_id = ?", parent_activity.id]).
265 where(["activity_id = ?", parent_activity.id]).
262 update_all("activity_id = #{project_activity.id}")
266 update_all("activity_id = #{project_activity.id}")
263 end
267 end
264 end
268 end
265 end
269 end
266 end
270 end
267
271
268 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
272 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
269 #
273 #
270 # Examples:
274 # Examples:
271 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
275 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
272 # project.project_condition(false) => "projects.id = 1"
276 # project.project_condition(false) => "projects.id = 1"
273 def project_condition(with_subprojects)
277 def project_condition(with_subprojects)
274 cond = "#{Project.table_name}.id = #{id}"
278 cond = "#{Project.table_name}.id = #{id}"
275 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
279 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
276 cond
280 cond
277 end
281 end
278
282
279 def self.find(*args)
283 def self.find(*args)
280 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
284 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
281 project = find_by_identifier(*args)
285 project = find_by_identifier(*args)
282 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
286 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
283 project
287 project
284 else
288 else
285 super
289 super
286 end
290 end
287 end
291 end
288
292
289 def self.find_by_param(*args)
293 def self.find_by_param(*args)
290 self.find(*args)
294 self.find(*args)
291 end
295 end
292
296
293 alias :base_reload :reload
297 alias :base_reload :reload
294 def reload(*args)
298 def reload(*args)
295 @principals = nil
299 @principals = nil
296 @users = nil
300 @users = nil
297 @shared_versions = nil
301 @shared_versions = nil
298 @rolled_up_versions = nil
302 @rolled_up_versions = nil
299 @rolled_up_trackers = nil
303 @rolled_up_trackers = nil
300 @all_issue_custom_fields = nil
304 @all_issue_custom_fields = nil
301 @all_time_entry_custom_fields = nil
305 @all_time_entry_custom_fields = nil
302 @to_param = nil
306 @to_param = nil
303 @allowed_parents = nil
307 @allowed_parents = nil
304 @allowed_permissions = nil
308 @allowed_permissions = nil
305 @actions_allowed = nil
309 @actions_allowed = nil
306 @start_date = nil
310 @start_date = nil
307 @due_date = nil
311 @due_date = nil
312 @override_members = nil
308 base_reload(*args)
313 base_reload(*args)
309 end
314 end
310
315
311 def to_param
316 def to_param
312 # id is used for projects with a numeric identifier (compatibility)
317 # id is used for projects with a numeric identifier (compatibility)
313 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
318 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
314 end
319 end
315
320
316 def active?
321 def active?
317 self.status == STATUS_ACTIVE
322 self.status == STATUS_ACTIVE
318 end
323 end
319
324
320 def archived?
325 def archived?
321 self.status == STATUS_ARCHIVED
326 self.status == STATUS_ARCHIVED
322 end
327 end
323
328
324 # Archives the project and its descendants
329 # Archives the project and its descendants
325 def archive
330 def archive
326 # Check that there is no issue of a non descendant project that is assigned
331 # Check that there is no issue of a non descendant project that is assigned
327 # to one of the project or descendant versions
332 # to one of the project or descendant versions
328 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
333 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
329 if v_ids.any? &&
334 if v_ids.any? &&
330 Issue.
335 Issue.
331 includes(:project).
336 includes(:project).
332 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
337 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
333 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
338 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
334 exists?
339 exists?
335 return false
340 return false
336 end
341 end
337 Project.transaction do
342 Project.transaction do
338 archive!
343 archive!
339 end
344 end
340 true
345 true
341 end
346 end
342
347
343 # Unarchives the project
348 # Unarchives the project
344 # All its ancestors must be active
349 # All its ancestors must be active
345 def unarchive
350 def unarchive
346 return false if ancestors.detect {|a| !a.active?}
351 return false if ancestors.detect {|a| !a.active?}
347 update_attribute :status, STATUS_ACTIVE
352 update_attribute :status, STATUS_ACTIVE
348 end
353 end
349
354
350 def close
355 def close
351 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
356 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
352 end
357 end
353
358
354 def reopen
359 def reopen
355 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
360 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
356 end
361 end
357
362
358 # Returns an array of projects the project can be moved to
363 # Returns an array of projects the project can be moved to
359 # by the current user
364 # by the current user
360 def allowed_parents
365 def allowed_parents
361 return @allowed_parents if @allowed_parents
366 return @allowed_parents if @allowed_parents
362 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
367 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
363 @allowed_parents = @allowed_parents - self_and_descendants
368 @allowed_parents = @allowed_parents - self_and_descendants
364 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
369 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
365 @allowed_parents << nil
370 @allowed_parents << nil
366 end
371 end
367 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
372 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
368 @allowed_parents << parent
373 @allowed_parents << parent
369 end
374 end
370 @allowed_parents
375 @allowed_parents
371 end
376 end
372
377
373 # Sets the parent of the project with authorization check
378 # Sets the parent of the project with authorization check
374 def set_allowed_parent!(p)
379 def set_allowed_parent!(p)
375 unless p.nil? || p.is_a?(Project)
380 unless p.nil? || p.is_a?(Project)
376 if p.to_s.blank?
381 if p.to_s.blank?
377 p = nil
382 p = nil
378 else
383 else
379 p = Project.find_by_id(p)
384 p = Project.find_by_id(p)
380 return false unless p
385 return false unless p
381 end
386 end
382 end
387 end
383 if p.nil?
388 if p.nil?
384 if !new_record? && allowed_parents.empty?
389 if !new_record? && allowed_parents.empty?
385 return false
390 return false
386 end
391 end
387 elsif !allowed_parents.include?(p)
392 elsif !allowed_parents.include?(p)
388 return false
393 return false
389 end
394 end
390 set_parent!(p)
395 set_parent!(p)
391 end
396 end
392
397
393 # Sets the parent of the project
398 # Sets the parent of the project
394 # Argument can be either a Project, a String, a Fixnum or nil
399 # Argument can be either a Project, a String, a Fixnum or nil
395 def set_parent!(p)
400 def set_parent!(p)
396 unless p.nil? || p.is_a?(Project)
401 unless p.nil? || p.is_a?(Project)
397 if p.to_s.blank?
402 if p.to_s.blank?
398 p = nil
403 p = nil
399 else
404 else
400 p = Project.find_by_id(p)
405 p = Project.find_by_id(p)
401 return false unless p
406 return false unless p
402 end
407 end
403 end
408 end
404 if p == parent && !p.nil?
409 if p == parent && !p.nil?
405 # Nothing to do
410 # Nothing to do
406 true
411 true
407 elsif p.nil? || (p.active? && move_possible?(p))
412 elsif p.nil? || (p.active? && move_possible?(p))
408 set_or_update_position_under(p)
413 set_or_update_position_under(p)
409 Issue.update_versions_from_hierarchy_change(self)
414 Issue.update_versions_from_hierarchy_change(self)
410 true
415 true
411 else
416 else
412 # Can not move to the given target
417 # Can not move to the given target
413 false
418 false
414 end
419 end
415 end
420 end
416
421
417 # Recalculates all lft and rgt values based on project names
422 # Recalculates all lft and rgt values based on project names
418 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
423 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
419 # Used in BuildProjectsTree migration
424 # Used in BuildProjectsTree migration
420 def self.rebuild_tree!
425 def self.rebuild_tree!
421 transaction do
426 transaction do
422 update_all "lft = NULL, rgt = NULL"
427 update_all "lft = NULL, rgt = NULL"
423 rebuild!(false)
428 rebuild!(false)
424 all.each { |p| p.set_or_update_position_under(p.parent) }
429 all.each { |p| p.set_or_update_position_under(p.parent) }
425 end
430 end
426 end
431 end
427
432
428 # Returns an array of the trackers used by the project and its active sub projects
433 # Returns an array of the trackers used by the project and its active sub projects
429 def rolled_up_trackers
434 def rolled_up_trackers
430 @rolled_up_trackers ||=
435 @rolled_up_trackers ||=
431 Tracker.
436 Tracker.
432 joins(:projects).
437 joins(:projects).
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
438 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 select("DISTINCT #{Tracker.table_name}.*").
439 select("DISTINCT #{Tracker.table_name}.*").
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
440 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
436 sorted.
441 sorted.
437 all
442 all
438 end
443 end
439
444
440 # Closes open and locked project versions that are completed
445 # Closes open and locked project versions that are completed
441 def close_completed_versions
446 def close_completed_versions
442 Version.transaction do
447 Version.transaction do
443 versions.where(:status => %w(open locked)).each do |version|
448 versions.where(:status => %w(open locked)).each do |version|
444 if version.completed?
449 if version.completed?
445 version.update_attribute(:status, 'closed')
450 version.update_attribute(:status, 'closed')
446 end
451 end
447 end
452 end
448 end
453 end
449 end
454 end
450
455
451 # Returns a scope of the Versions on subprojects
456 # Returns a scope of the Versions on subprojects
452 def rolled_up_versions
457 def rolled_up_versions
453 @rolled_up_versions ||=
458 @rolled_up_versions ||=
454 Version.
459 Version.
455 includes(:project).
460 includes(:project).
456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
461 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
457 end
462 end
458
463
459 # Returns a scope of the Versions used by the project
464 # Returns a scope of the Versions used by the project
460 def shared_versions
465 def shared_versions
461 if new_record?
466 if new_record?
462 Version.
467 Version.
463 includes(:project).
468 includes(:project).
464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
469 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
465 else
470 else
466 @shared_versions ||= begin
471 @shared_versions ||= begin
467 r = root? ? self : root
472 r = root? ? self : root
468 Version.
473 Version.
469 includes(:project).
474 includes(:project).
470 where("#{Project.table_name}.id = #{id}" +
475 where("#{Project.table_name}.id = #{id}" +
471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
476 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
472 " #{Version.table_name}.sharing = 'system'" +
477 " #{Version.table_name}.sharing = 'system'" +
473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
478 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
479 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
480 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
476 "))")
481 "))")
477 end
482 end
478 end
483 end
479 end
484 end
480
485
481 # Returns a hash of project users grouped by role
486 # Returns a hash of project users grouped by role
482 def users_by_role
487 def users_by_role
483 members.includes(:user, :roles).inject({}) do |h, m|
488 members.includes(:user, :roles).inject({}) do |h, m|
484 m.roles.each do |r|
489 m.roles.each do |r|
485 h[r] ||= []
490 h[r] ||= []
486 h[r] << m.user
491 h[r] << m.user
487 end
492 end
488 h
493 h
489 end
494 end
490 end
495 end
491
496
492 # Deletes all project's members
497 # Deletes all project's members
493 def delete_all_members
498 def delete_all_members
494 me, mr = Member.table_name, MemberRole.table_name
499 me, mr = Member.table_name, MemberRole.table_name
495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
500 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
496 Member.delete_all(['project_id = ?', id])
501 Member.delete_all(['project_id = ?', id])
497 end
502 end
498
503
499 # Users/groups issues can be assigned to
504 # Users/groups issues can be assigned to
500 def assignable_users
505 def assignable_users
501 assignable = Setting.issue_group_assignment? ? member_principals : members
506 types = ['User']
502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
507 types << 'Group' if Setting.issue_group_assignment?
508
509 member_principals.
510 select {|m| types.include?(m.principal.type) && m.roles.detect(&:assignable?)}.
511 map(&:principal).
512 sort
503 end
513 end
504
514
505 # Returns the mail addresses of users that should be always notified on project events
515 # Returns the mail addresses of users that should be always notified on project events
506 def recipients
516 def recipients
507 notified_users.collect {|user| user.mail}
517 notified_users.collect {|user| user.mail}
508 end
518 end
509
519
510 # Returns the users that should be notified on project events
520 # Returns the users that should be notified on project events
511 def notified_users
521 def notified_users
512 # TODO: User part should be extracted to User#notify_about?
522 # TODO: User part should be extracted to User#notify_about?
513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
523 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
514 end
524 end
515
525
516 # Returns a scope of all custom fields enabled for project issues
526 # Returns a scope of all custom fields enabled for project issues
517 # (explicitly associated custom fields and custom fields enabled for all projects)
527 # (explicitly associated custom fields and custom fields enabled for all projects)
518 def all_issue_custom_fields
528 def all_issue_custom_fields
519 @all_issue_custom_fields ||= IssueCustomField.
529 @all_issue_custom_fields ||= IssueCustomField.
520 sorted.
530 sorted.
521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
531 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
532 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
523 " WHERE cfp.project_id = ?)", true, id)
533 " WHERE cfp.project_id = ?)", true, id)
524 end
534 end
525
535
526 # Returns an array of all custom fields enabled for project time entries
536 # Returns an array of all custom fields enabled for project time entries
527 # (explictly associated custom fields and custom fields enabled for all projects)
537 # (explictly associated custom fields and custom fields enabled for all projects)
528 def all_time_entry_custom_fields
538 def all_time_entry_custom_fields
529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
539 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
530 end
540 end
531
541
532 def project
542 def project
533 self
543 self
534 end
544 end
535
545
536 def <=>(project)
546 def <=>(project)
537 name.downcase <=> project.name.downcase
547 name.downcase <=> project.name.downcase
538 end
548 end
539
549
540 def to_s
550 def to_s
541 name
551 name
542 end
552 end
543
553
544 # Returns a short description of the projects (first lines)
554 # Returns a short description of the projects (first lines)
545 def short_description(length = 255)
555 def short_description(length = 255)
546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
556 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
547 end
557 end
548
558
549 def css_classes
559 def css_classes
550 s = 'project'
560 s = 'project'
551 s << ' root' if root?
561 s << ' root' if root?
552 s << ' child' if child?
562 s << ' child' if child?
553 s << (leaf? ? ' leaf' : ' parent')
563 s << (leaf? ? ' leaf' : ' parent')
554 unless active?
564 unless active?
555 if archived?
565 if archived?
556 s << ' archived'
566 s << ' archived'
557 else
567 else
558 s << ' closed'
568 s << ' closed'
559 end
569 end
560 end
570 end
561 s
571 s
562 end
572 end
563
573
564 # The earliest start date of a project, based on it's issues and versions
574 # The earliest start date of a project, based on it's issues and versions
565 def start_date
575 def start_date
566 @start_date ||= [
576 @start_date ||= [
567 issues.minimum('start_date'),
577 issues.minimum('start_date'),
568 shared_versions.minimum('effective_date'),
578 shared_versions.minimum('effective_date'),
569 Issue.fixed_version(shared_versions).minimum('start_date')
579 Issue.fixed_version(shared_versions).minimum('start_date')
570 ].compact.min
580 ].compact.min
571 end
581 end
572
582
573 # The latest due date of an issue or version
583 # The latest due date of an issue or version
574 def due_date
584 def due_date
575 @due_date ||= [
585 @due_date ||= [
576 issues.maximum('due_date'),
586 issues.maximum('due_date'),
577 shared_versions.maximum('effective_date'),
587 shared_versions.maximum('effective_date'),
578 Issue.fixed_version(shared_versions).maximum('due_date')
588 Issue.fixed_version(shared_versions).maximum('due_date')
579 ].compact.max
589 ].compact.max
580 end
590 end
581
591
582 def overdue?
592 def overdue?
583 active? && !due_date.nil? && (due_date < Date.today)
593 active? && !due_date.nil? && (due_date < Date.today)
584 end
594 end
585
595
586 # Returns the percent completed for this project, based on the
596 # Returns the percent completed for this project, based on the
587 # progress on it's versions.
597 # progress on it's versions.
588 def completed_percent(options={:include_subprojects => false})
598 def completed_percent(options={:include_subprojects => false})
589 if options.delete(:include_subprojects)
599 if options.delete(:include_subprojects)
590 total = self_and_descendants.collect(&:completed_percent).sum
600 total = self_and_descendants.collect(&:completed_percent).sum
591
601
592 total / self_and_descendants.count
602 total / self_and_descendants.count
593 else
603 else
594 if versions.count > 0
604 if versions.count > 0
595 total = versions.collect(&:completed_percent).sum
605 total = versions.collect(&:completed_percent).sum
596
606
597 total / versions.count
607 total / versions.count
598 else
608 else
599 100
609 100
600 end
610 end
601 end
611 end
602 end
612 end
603
613
604 # Return true if this project allows to do the specified action.
614 # Return true if this project allows to do the specified action.
605 # action can be:
615 # action can be:
606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
616 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
607 # * a permission Symbol (eg. :edit_project)
617 # * a permission Symbol (eg. :edit_project)
608 def allows_to?(action)
618 def allows_to?(action)
609 if archived?
619 if archived?
610 # No action allowed on archived projects
620 # No action allowed on archived projects
611 return false
621 return false
612 end
622 end
613 unless active? || Redmine::AccessControl.read_action?(action)
623 unless active? || Redmine::AccessControl.read_action?(action)
614 # No write action allowed on closed projects
624 # No write action allowed on closed projects
615 return false
625 return false
616 end
626 end
617 # No action allowed on disabled modules
627 # No action allowed on disabled modules
618 if action.is_a? Hash
628 if action.is_a? Hash
619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
629 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
620 else
630 else
621 allowed_permissions.include? action
631 allowed_permissions.include? action
622 end
632 end
623 end
633 end
624
634
625 # Return the enabled module with the given name
635 # Return the enabled module with the given name
626 # or nil if the module is not enabled for the project
636 # or nil if the module is not enabled for the project
627 def enabled_module(name)
637 def enabled_module(name)
628 name = name.to_s
638 name = name.to_s
629 enabled_modules.detect {|m| m.name == name}
639 enabled_modules.detect {|m| m.name == name}
630 end
640 end
631
641
632 # Return true if the module with the given name is enabled
642 # Return true if the module with the given name is enabled
633 def module_enabled?(name)
643 def module_enabled?(name)
634 enabled_module(name).present?
644 enabled_module(name).present?
635 end
645 end
636
646
637 def enabled_module_names=(module_names)
647 def enabled_module_names=(module_names)
638 if module_names && module_names.is_a?(Array)
648 if module_names && module_names.is_a?(Array)
639 module_names = module_names.collect(&:to_s).reject(&:blank?)
649 module_names = module_names.collect(&:to_s).reject(&:blank?)
640 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
650 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
641 else
651 else
642 enabled_modules.clear
652 enabled_modules.clear
643 end
653 end
644 end
654 end
645
655
646 # Returns an array of the enabled modules names
656 # Returns an array of the enabled modules names
647 def enabled_module_names
657 def enabled_module_names
648 enabled_modules.collect(&:name)
658 enabled_modules.collect(&:name)
649 end
659 end
650
660
651 # Enable a specific module
661 # Enable a specific module
652 #
662 #
653 # Examples:
663 # Examples:
654 # project.enable_module!(:issue_tracking)
664 # project.enable_module!(:issue_tracking)
655 # project.enable_module!("issue_tracking")
665 # project.enable_module!("issue_tracking")
656 def enable_module!(name)
666 def enable_module!(name)
657 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
667 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
658 end
668 end
659
669
660 # Disable a module if it exists
670 # Disable a module if it exists
661 #
671 #
662 # Examples:
672 # Examples:
663 # project.disable_module!(:issue_tracking)
673 # project.disable_module!(:issue_tracking)
664 # project.disable_module!("issue_tracking")
674 # project.disable_module!("issue_tracking")
665 # project.disable_module!(project.enabled_modules.first)
675 # project.disable_module!(project.enabled_modules.first)
666 def disable_module!(target)
676 def disable_module!(target)
667 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
677 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
668 target.destroy unless target.blank?
678 target.destroy unless target.blank?
669 end
679 end
670
680
671 safe_attributes 'name',
681 safe_attributes 'name',
672 'description',
682 'description',
673 'homepage',
683 'homepage',
674 'is_public',
684 'is_public',
675 'identifier',
685 'identifier',
676 'custom_field_values',
686 'custom_field_values',
677 'custom_fields',
687 'custom_fields',
678 'tracker_ids',
688 'tracker_ids',
679 'issue_custom_field_ids'
689 'issue_custom_field_ids'
680
690
681 safe_attributes 'enabled_module_names',
691 safe_attributes 'enabled_module_names',
682 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
692 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
683
693
684 safe_attributes 'inherit_members',
694 safe_attributes 'inherit_members',
685 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
695 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
686
696
687 # Returns an array of projects that are in this project's hierarchy
697 # Returns an array of projects that are in this project's hierarchy
688 #
698 #
689 # Example: parents, children, siblings
699 # Example: parents, children, siblings
690 def hierarchy
700 def hierarchy
691 parents = project.self_and_ancestors || []
701 parents = project.self_and_ancestors || []
692 descendants = project.descendants || []
702 descendants = project.descendants || []
693 project_hierarchy = parents | descendants # Set union
703 project_hierarchy = parents | descendants # Set union
694 end
704 end
695
705
696 # Returns an auto-generated project identifier based on the last identifier used
706 # Returns an auto-generated project identifier based on the last identifier used
697 def self.next_identifier
707 def self.next_identifier
698 p = Project.order('id DESC').first
708 p = Project.order('id DESC').first
699 p.nil? ? nil : p.identifier.to_s.succ
709 p.nil? ? nil : p.identifier.to_s.succ
700 end
710 end
701
711
702 # Copies and saves the Project instance based on the +project+.
712 # Copies and saves the Project instance based on the +project+.
703 # Duplicates the source project's:
713 # Duplicates the source project's:
704 # * Wiki
714 # * Wiki
705 # * Versions
715 # * Versions
706 # * Categories
716 # * Categories
707 # * Issues
717 # * Issues
708 # * Members
718 # * Members
709 # * Queries
719 # * Queries
710 #
720 #
711 # Accepts an +options+ argument to specify what to copy
721 # Accepts an +options+ argument to specify what to copy
712 #
722 #
713 # Examples:
723 # Examples:
714 # project.copy(1) # => copies everything
724 # project.copy(1) # => copies everything
715 # project.copy(1, :only => 'members') # => copies members only
725 # project.copy(1, :only => 'members') # => copies members only
716 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
726 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
717 def copy(project, options={})
727 def copy(project, options={})
718 project = project.is_a?(Project) ? project : Project.find(project)
728 project = project.is_a?(Project) ? project : Project.find(project)
719
729
720 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
730 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
721 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
731 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
722
732
723 Project.transaction do
733 Project.transaction do
724 if save
734 if save
725 reload
735 reload
726 to_be_copied.each do |name|
736 to_be_copied.each do |name|
727 send "copy_#{name}", project
737 send "copy_#{name}", project
728 end
738 end
729 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
739 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
730 save
740 save
731 end
741 end
732 end
742 end
733 end
743 end
734
744
735 # Returns a new unsaved Project instance with attributes copied from +project+
745 # Returns a new unsaved Project instance with attributes copied from +project+
736 def self.copy_from(project)
746 def self.copy_from(project)
737 project = project.is_a?(Project) ? project : Project.find(project)
747 project = project.is_a?(Project) ? project : Project.find(project)
738 # clear unique attributes
748 # clear unique attributes
739 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
749 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
740 copy = Project.new(attributes)
750 copy = Project.new(attributes)
741 copy.enabled_modules = project.enabled_modules
751 copy.enabled_modules = project.enabled_modules
742 copy.trackers = project.trackers
752 copy.trackers = project.trackers
743 copy.custom_values = project.custom_values.collect {|v| v.clone}
753 copy.custom_values = project.custom_values.collect {|v| v.clone}
744 copy.issue_custom_fields = project.issue_custom_fields
754 copy.issue_custom_fields = project.issue_custom_fields
745 copy
755 copy
746 end
756 end
747
757
748 # Yields the given block for each project with its level in the tree
758 # Yields the given block for each project with its level in the tree
749 def self.project_tree(projects, &block)
759 def self.project_tree(projects, &block)
750 ancestors = []
760 ancestors = []
751 projects.sort_by(&:lft).each do |project|
761 projects.sort_by(&:lft).each do |project|
752 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
762 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
753 ancestors.pop
763 ancestors.pop
754 end
764 end
755 yield project, ancestors.size
765 yield project, ancestors.size
756 ancestors << project
766 ancestors << project
757 end
767 end
758 end
768 end
759
769
760 private
770 private
761
771
762 def after_parent_changed(parent_was)
772 def after_parent_changed(parent_was)
763 remove_inherited_member_roles
773 remove_inherited_member_roles
764 add_inherited_member_roles
774 add_inherited_member_roles
765 end
775 end
766
776
767 def update_inherited_members
777 def update_inherited_members
768 if parent
778 if parent
769 if inherit_members? && !inherit_members_was
779 if inherit_members? && !inherit_members_was
770 remove_inherited_member_roles
780 remove_inherited_member_roles
771 add_inherited_member_roles
781 add_inherited_member_roles
772 elsif !inherit_members? && inherit_members_was
782 elsif !inherit_members? && inherit_members_was
773 remove_inherited_member_roles
783 remove_inherited_member_roles
774 end
784 end
775 end
785 end
776 end
786 end
777
787
778 def remove_inherited_member_roles
788 def remove_inherited_member_roles
779 member_roles = memberships.map(&:member_roles).flatten
789 member_roles = memberships.map(&:member_roles).flatten
780 member_role_ids = member_roles.map(&:id)
790 member_role_ids = member_roles.map(&:id)
781 member_roles.each do |member_role|
791 member_roles.each do |member_role|
782 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
792 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
783 member_role.destroy
793 member_role.destroy
784 end
794 end
785 end
795 end
786 end
796 end
787
797
788 def add_inherited_member_roles
798 def add_inherited_member_roles
789 if inherit_members? && parent
799 if inherit_members? && parent
790 parent.memberships.each do |parent_member|
800 parent.memberships.each do |parent_member|
791 member = Member.find_or_new(self.id, parent_member.user_id)
801 member = Member.find_or_new(self.id, parent_member.user_id)
792 parent_member.member_roles.each do |parent_member_role|
802 parent_member.member_roles.each do |parent_member_role|
793 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
803 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
794 end
804 end
795 member.save!
805 member.save!
796 end
806 end
797 end
807 end
798 end
808 end
799
809
800 # Copies wiki from +project+
810 # Copies wiki from +project+
801 def copy_wiki(project)
811 def copy_wiki(project)
802 # Check that the source project has a wiki first
812 # Check that the source project has a wiki first
803 unless project.wiki.nil?
813 unless project.wiki.nil?
804 wiki = self.wiki || Wiki.new
814 wiki = self.wiki || Wiki.new
805 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
815 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
806 wiki_pages_map = {}
816 wiki_pages_map = {}
807 project.wiki.pages.each do |page|
817 project.wiki.pages.each do |page|
808 # Skip pages without content
818 # Skip pages without content
809 next if page.content.nil?
819 next if page.content.nil?
810 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
820 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
811 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
821 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
812 new_wiki_page.content = new_wiki_content
822 new_wiki_page.content = new_wiki_content
813 wiki.pages << new_wiki_page
823 wiki.pages << new_wiki_page
814 wiki_pages_map[page.id] = new_wiki_page
824 wiki_pages_map[page.id] = new_wiki_page
815 end
825 end
816
826
817 self.wiki = wiki
827 self.wiki = wiki
818 wiki.save
828 wiki.save
819 # Reproduce page hierarchy
829 # Reproduce page hierarchy
820 project.wiki.pages.each do |page|
830 project.wiki.pages.each do |page|
821 if page.parent_id && wiki_pages_map[page.id]
831 if page.parent_id && wiki_pages_map[page.id]
822 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
832 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
823 wiki_pages_map[page.id].save
833 wiki_pages_map[page.id].save
824 end
834 end
825 end
835 end
826 end
836 end
827 end
837 end
828
838
829 # Copies versions from +project+
839 # Copies versions from +project+
830 def copy_versions(project)
840 def copy_versions(project)
831 project.versions.each do |version|
841 project.versions.each do |version|
832 new_version = Version.new
842 new_version = Version.new
833 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
843 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
834 self.versions << new_version
844 self.versions << new_version
835 end
845 end
836 end
846 end
837
847
838 # Copies issue categories from +project+
848 # Copies issue categories from +project+
839 def copy_issue_categories(project)
849 def copy_issue_categories(project)
840 project.issue_categories.each do |issue_category|
850 project.issue_categories.each do |issue_category|
841 new_issue_category = IssueCategory.new
851 new_issue_category = IssueCategory.new
842 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
852 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
843 self.issue_categories << new_issue_category
853 self.issue_categories << new_issue_category
844 end
854 end
845 end
855 end
846
856
847 # Copies issues from +project+
857 # Copies issues from +project+
848 def copy_issues(project)
858 def copy_issues(project)
849 # Stores the source issue id as a key and the copied issues as the
859 # Stores the source issue id as a key and the copied issues as the
850 # value. Used to map the two together for issue relations.
860 # value. Used to map the two together for issue relations.
851 issues_map = {}
861 issues_map = {}
852
862
853 # Store status and reopen locked/closed versions
863 # Store status and reopen locked/closed versions
854 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
864 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
855 version_statuses.each do |version, status|
865 version_statuses.each do |version, status|
856 version.update_attribute :status, 'open'
866 version.update_attribute :status, 'open'
857 end
867 end
858
868
859 # Get issues sorted by root_id, lft so that parent issues
869 # Get issues sorted by root_id, lft so that parent issues
860 # get copied before their children
870 # get copied before their children
861 project.issues.reorder('root_id, lft').each do |issue|
871 project.issues.reorder('root_id, lft').each do |issue|
862 new_issue = Issue.new
872 new_issue = Issue.new
863 new_issue.copy_from(issue, :subtasks => false, :link => false)
873 new_issue.copy_from(issue, :subtasks => false, :link => false)
864 new_issue.project = self
874 new_issue.project = self
865 # Changing project resets the custom field values
875 # Changing project resets the custom field values
866 # TODO: handle this in Issue#project=
876 # TODO: handle this in Issue#project=
867 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
877 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
868 # Reassign fixed_versions by name, since names are unique per project
878 # Reassign fixed_versions by name, since names are unique per project
869 if issue.fixed_version && issue.fixed_version.project == project
879 if issue.fixed_version && issue.fixed_version.project == project
870 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
880 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
871 end
881 end
872 # Reassign the category by name, since names are unique per project
882 # Reassign the category by name, since names are unique per project
873 if issue.category
883 if issue.category
874 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
884 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
875 end
885 end
876 # Parent issue
886 # Parent issue
877 if issue.parent_id
887 if issue.parent_id
878 if copied_parent = issues_map[issue.parent_id]
888 if copied_parent = issues_map[issue.parent_id]
879 new_issue.parent_issue_id = copied_parent.id
889 new_issue.parent_issue_id = copied_parent.id
880 end
890 end
881 end
891 end
882
892
883 self.issues << new_issue
893 self.issues << new_issue
884 if new_issue.new_record?
894 if new_issue.new_record?
885 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
895 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
886 else
896 else
887 issues_map[issue.id] = new_issue unless new_issue.new_record?
897 issues_map[issue.id] = new_issue unless new_issue.new_record?
888 end
898 end
889 end
899 end
890
900
891 # Restore locked/closed version statuses
901 # Restore locked/closed version statuses
892 version_statuses.each do |version, status|
902 version_statuses.each do |version, status|
893 version.update_attribute :status, status
903 version.update_attribute :status, status
894 end
904 end
895
905
896 # Relations after in case issues related each other
906 # Relations after in case issues related each other
897 project.issues.each do |issue|
907 project.issues.each do |issue|
898 new_issue = issues_map[issue.id]
908 new_issue = issues_map[issue.id]
899 unless new_issue
909 unless new_issue
900 # Issue was not copied
910 # Issue was not copied
901 next
911 next
902 end
912 end
903
913
904 # Relations
914 # Relations
905 issue.relations_from.each do |source_relation|
915 issue.relations_from.each do |source_relation|
906 new_issue_relation = IssueRelation.new
916 new_issue_relation = IssueRelation.new
907 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
917 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
908 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
918 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
909 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
919 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
910 new_issue_relation.issue_to = source_relation.issue_to
920 new_issue_relation.issue_to = source_relation.issue_to
911 end
921 end
912 new_issue.relations_from << new_issue_relation
922 new_issue.relations_from << new_issue_relation
913 end
923 end
914
924
915 issue.relations_to.each do |source_relation|
925 issue.relations_to.each do |source_relation|
916 new_issue_relation = IssueRelation.new
926 new_issue_relation = IssueRelation.new
917 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
927 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
918 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
928 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
919 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
929 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
920 new_issue_relation.issue_from = source_relation.issue_from
930 new_issue_relation.issue_from = source_relation.issue_from
921 end
931 end
922 new_issue.relations_to << new_issue_relation
932 new_issue.relations_to << new_issue_relation
923 end
933 end
924 end
934 end
925 end
935 end
926
936
927 # Copies members from +project+
937 # Copies members from +project+
928 def copy_members(project)
938 def copy_members(project)
929 # Copy users first, then groups to handle members with inherited and given roles
939 # Copy users first, then groups to handle members with inherited and given roles
930 members_to_copy = []
940 members_to_copy = []
931 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
941 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
932 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
942 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
933
943
934 members_to_copy.each do |member|
944 members_to_copy.each do |member|
935 new_member = Member.new
945 new_member = Member.new
936 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
946 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
937 # only copy non inherited roles
947 # only copy non inherited roles
938 # inherited roles will be added when copying the group membership
948 # inherited roles will be added when copying the group membership
939 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
949 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
940 next if role_ids.empty?
950 next if role_ids.empty?
941 new_member.role_ids = role_ids
951 new_member.role_ids = role_ids
942 new_member.project = self
952 new_member.project = self
943 self.members << new_member
953 self.members << new_member
944 end
954 end
945 end
955 end
946
956
947 # Copies queries from +project+
957 # Copies queries from +project+
948 def copy_queries(project)
958 def copy_queries(project)
949 project.queries.each do |query|
959 project.queries.each do |query|
950 new_query = IssueQuery.new
960 new_query = IssueQuery.new
951 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
961 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
952 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
962 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
953 new_query.project = self
963 new_query.project = self
954 new_query.user_id = query.user_id
964 new_query.user_id = query.user_id
955 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
965 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
956 self.queries << new_query
966 self.queries << new_query
957 end
967 end
958 end
968 end
959
969
960 # Copies boards from +project+
970 # Copies boards from +project+
961 def copy_boards(project)
971 def copy_boards(project)
962 project.boards.each do |board|
972 project.boards.each do |board|
963 new_board = Board.new
973 new_board = Board.new
964 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
974 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
965 new_board.project = self
975 new_board.project = self
966 self.boards << new_board
976 self.boards << new_board
967 end
977 end
968 end
978 end
969
979
970 def allowed_permissions
980 def allowed_permissions
971 @allowed_permissions ||= begin
981 @allowed_permissions ||= begin
972 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
982 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
973 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
983 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
974 end
984 end
975 end
985 end
976
986
977 def allowed_actions
987 def allowed_actions
978 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
988 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
979 end
989 end
980
990
981 # Returns all the active Systemwide and project specific activities
991 # Returns all the active Systemwide and project specific activities
982 def active_activities
992 def active_activities
983 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
993 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
984
994
985 if overridden_activity_ids.empty?
995 if overridden_activity_ids.empty?
986 return TimeEntryActivity.shared.active
996 return TimeEntryActivity.shared.active
987 else
997 else
988 return system_activities_and_project_overrides
998 return system_activities_and_project_overrides
989 end
999 end
990 end
1000 end
991
1001
992 # Returns all the Systemwide and project specific activities
1002 # Returns all the Systemwide and project specific activities
993 # (inactive and active)
1003 # (inactive and active)
994 def all_activities
1004 def all_activities
995 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
1005 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
996
1006
997 if overridden_activity_ids.empty?
1007 if overridden_activity_ids.empty?
998 return TimeEntryActivity.shared
1008 return TimeEntryActivity.shared
999 else
1009 else
1000 return system_activities_and_project_overrides(true)
1010 return system_activities_and_project_overrides(true)
1001 end
1011 end
1002 end
1012 end
1003
1013
1004 # Returns the systemwide active activities merged with the project specific overrides
1014 # Returns the systemwide active activities merged with the project specific overrides
1005 def system_activities_and_project_overrides(include_inactive=false)
1015 def system_activities_and_project_overrides(include_inactive=false)
1006 t = TimeEntryActivity.table_name
1016 t = TimeEntryActivity.table_name
1007 scope = TimeEntryActivity.where(
1017 scope = TimeEntryActivity.where(
1008 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1018 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1009 time_entry_activities.map(&:parent_id), id
1019 time_entry_activities.map(&:parent_id), id
1010 )
1020 )
1011 unless include_inactive
1021 unless include_inactive
1012 scope = scope.active
1022 scope = scope.active
1013 end
1023 end
1014 scope
1024 scope
1015 end
1025 end
1016
1026
1017 # Archives subprojects recursively
1027 # Archives subprojects recursively
1018 def archive!
1028 def archive!
1019 children.each do |subproject|
1029 children.each do |subproject|
1020 subproject.send :archive!
1030 subproject.send :archive!
1021 end
1031 end
1022 update_attribute :status, STATUS_ARCHIVED
1032 update_attribute :status, STATUS_ARCHIVED
1023 end
1033 end
1024
1034
1025 def update_position_under_parent
1035 def update_position_under_parent
1026 set_or_update_position_under(parent)
1036 set_or_update_position_under(parent)
1027 end
1037 end
1028
1038
1029 public
1039 public
1030
1040
1031 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1041 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1032 def set_or_update_position_under(target_parent)
1042 def set_or_update_position_under(target_parent)
1033 parent_was = parent
1043 parent_was = parent
1034 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1044 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1035 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1045 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1036
1046
1037 if to_be_inserted_before
1047 if to_be_inserted_before
1038 move_to_left_of(to_be_inserted_before)
1048 move_to_left_of(to_be_inserted_before)
1039 elsif target_parent.nil?
1049 elsif target_parent.nil?
1040 if sibs.empty?
1050 if sibs.empty?
1041 # move_to_root adds the project in first (ie. left) position
1051 # move_to_root adds the project in first (ie. left) position
1042 move_to_root
1052 move_to_root
1043 else
1053 else
1044 move_to_right_of(sibs.last) unless self == sibs.last
1054 move_to_right_of(sibs.last) unless self == sibs.last
1045 end
1055 end
1046 else
1056 else
1047 # move_to_child_of adds the project in last (ie.right) position
1057 # move_to_child_of adds the project in last (ie.right) position
1048 move_to_child_of(target_parent)
1058 move_to_child_of(target_parent)
1049 end
1059 end
1050 if parent_was != target_parent
1060 if parent_was != target_parent
1051 after_parent_changed(parent_was)
1061 after_parent_changed(parent_was)
1052 end
1062 end
1053 end
1063 end
1054 end
1064 end
@@ -1,781 +1,789
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstinitial_lastname => {
35 :firstinitial_lastname => {
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 :order => %w(firstname lastname id),
37 :order => %w(firstname lastname id),
38 :setting_order => 2
38 :setting_order => 2
39 },
39 },
40 :firstname => {
40 :firstname => {
41 :string => '#{firstname}',
41 :string => '#{firstname}',
42 :order => %w(firstname id),
42 :order => %w(firstname id),
43 :setting_order => 3
43 :setting_order => 3
44 },
44 },
45 :lastname_firstname => {
45 :lastname_firstname => {
46 :string => '#{lastname} #{firstname}',
46 :string => '#{lastname} #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 4
48 :setting_order => 4
49 },
49 },
50 :lastname_coma_firstname => {
50 :lastname_coma_firstname => {
51 :string => '#{lastname}, #{firstname}',
51 :string => '#{lastname}, #{firstname}',
52 :order => %w(lastname firstname id),
52 :order => %w(lastname firstname id),
53 :setting_order => 5
53 :setting_order => 5
54 },
54 },
55 :lastname => {
55 :lastname => {
56 :string => '#{lastname}',
56 :string => '#{lastname}',
57 :order => %w(lastname id),
57 :order => %w(lastname id),
58 :setting_order => 6
58 :setting_order => 6
59 },
59 },
60 :username => {
60 :username => {
61 :string => '#{login}',
61 :string => '#{login}',
62 :order => %w(login id),
62 :order => %w(login id),
63 :setting_order => 7
63 :setting_order => 7
64 },
64 },
65 }
65 }
66
66
67 MAIL_NOTIFICATION_OPTIONS = [
67 MAIL_NOTIFICATION_OPTIONS = [
68 ['all', :label_user_mail_option_all],
68 ['all', :label_user_mail_option_all],
69 ['selected', :label_user_mail_option_selected],
69 ['selected', :label_user_mail_option_selected],
70 ['only_my_events', :label_user_mail_option_only_my_events],
70 ['only_my_events', :label_user_mail_option_only_my_events],
71 ['only_assigned', :label_user_mail_option_only_assigned],
71 ['only_assigned', :label_user_mail_option_only_assigned],
72 ['only_owner', :label_user_mail_option_only_owner],
72 ['only_owner', :label_user_mail_option_only_owner],
73 ['none', :label_user_mail_option_none]
73 ['none', :label_user_mail_option_none]
74 ]
74 ]
75
75
76 has_and_belongs_to_many :groups,
76 has_and_belongs_to_many :groups,
77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
78 :after_add => Proc.new {|user, group| group.user_added(user)},
78 :after_add => Proc.new {|user, group| group.user_added(user)},
79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
80 has_many :changesets, :dependent => :nullify
80 has_many :changesets, :dependent => :nullify
81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
82 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
82 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
83 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
83 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
84 belongs_to :auth_source
84 belongs_to :auth_source
85
85
86 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
86 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
87 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
87 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
88
88
89 acts_as_customizable
89 acts_as_customizable
90
90
91 attr_accessor :password, :password_confirmation, :generate_password
91 attr_accessor :password, :password_confirmation, :generate_password
92 attr_accessor :last_before_login_on
92 attr_accessor :last_before_login_on
93 # Prevents unauthorized assignments
93 # Prevents unauthorized assignments
94 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
94 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
95
95
96 LOGIN_LENGTH_LIMIT = 60
96 LOGIN_LENGTH_LIMIT = 60
97 MAIL_LENGTH_LIMIT = 60
97 MAIL_LENGTH_LIMIT = 60
98
98
99 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
99 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
100 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
100 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
101 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
101 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
102 # Login must contain letters, numbers, underscores only
102 # Login must contain letters, numbers, underscores only
103 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
103 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
104 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
104 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
105 validates_length_of :firstname, :lastname, :maximum => 30
105 validates_length_of :firstname, :lastname, :maximum => 30
106 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
106 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
107 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
107 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
108 validates_confirmation_of :password, :allow_nil => true
108 validates_confirmation_of :password, :allow_nil => true
109 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
109 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
110 validate :validate_password_length
110 validate :validate_password_length
111
111
112 before_create :set_mail_notification
112 before_create :set_mail_notification
113 before_save :generate_password_if_needed, :update_hashed_password
113 before_save :generate_password_if_needed, :update_hashed_password
114 before_destroy :remove_references_before_destroy
114 before_destroy :remove_references_before_destroy
115 after_save :update_notified_project_ids, :destroy_tokens
115 after_save :update_notified_project_ids, :destroy_tokens
116
116
117 scope :in_group, lambda {|group|
117 scope :in_group, lambda {|group|
118 group_id = group.is_a?(Group) ? group.id : group.to_i
118 group_id = group.is_a?(Group) ? group.id : group.to_i
119 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
119 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
120 }
120 }
121 scope :not_in_group, lambda {|group|
121 scope :not_in_group, lambda {|group|
122 group_id = group.is_a?(Group) ? group.id : group.to_i
122 group_id = group.is_a?(Group) ? group.id : group.to_i
123 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
123 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
124 }
124 }
125 scope :sorted, lambda { order(*User.fields_for_order_statement)}
125 scope :sorted, lambda { order(*User.fields_for_order_statement)}
126
126
127 def set_mail_notification
127 def set_mail_notification
128 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
128 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
129 true
129 true
130 end
130 end
131
131
132 def update_hashed_password
132 def update_hashed_password
133 # update hashed_password if password was set
133 # update hashed_password if password was set
134 if self.password && self.auth_source_id.blank?
134 if self.password && self.auth_source_id.blank?
135 salt_password(password)
135 salt_password(password)
136 end
136 end
137 end
137 end
138
138
139 alias :base_reload :reload
139 alias :base_reload :reload
140 def reload(*args)
140 def reload(*args)
141 @name = nil
141 @name = nil
142 @projects_by_role = nil
142 @projects_by_role = nil
143 @membership_by_project_id = nil
143 @membership_by_project_id = nil
144 @notified_projects_ids = nil
144 @notified_projects_ids = nil
145 @notified_projects_ids_changed = false
145 @notified_projects_ids_changed = false
146 @builtin_role = nil
146 @builtin_role = nil
147 base_reload(*args)
147 base_reload(*args)
148 end
148 end
149
149
150 def mail=(arg)
150 def mail=(arg)
151 write_attribute(:mail, arg.to_s.strip)
151 write_attribute(:mail, arg.to_s.strip)
152 end
152 end
153
153
154 def identity_url=(url)
154 def identity_url=(url)
155 if url.blank?
155 if url.blank?
156 write_attribute(:identity_url, '')
156 write_attribute(:identity_url, '')
157 else
157 else
158 begin
158 begin
159 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
159 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
160 rescue OpenIdAuthentication::InvalidOpenId
160 rescue OpenIdAuthentication::InvalidOpenId
161 # Invalid url, don't save
161 # Invalid url, don't save
162 end
162 end
163 end
163 end
164 self.read_attribute(:identity_url)
164 self.read_attribute(:identity_url)
165 end
165 end
166
166
167 # Returns the user that matches provided login and password, or nil
167 # Returns the user that matches provided login and password, or nil
168 def self.try_to_login(login, password, active_only=true)
168 def self.try_to_login(login, password, active_only=true)
169 login = login.to_s
169 login = login.to_s
170 password = password.to_s
170 password = password.to_s
171
171
172 # Make sure no one can sign in with an empty login or password
172 # Make sure no one can sign in with an empty login or password
173 return nil if login.empty? || password.empty?
173 return nil if login.empty? || password.empty?
174 user = find_by_login(login)
174 user = find_by_login(login)
175 if user
175 if user
176 # user is already in local database
176 # user is already in local database
177 return nil unless user.check_password?(password)
177 return nil unless user.check_password?(password)
178 return nil if !user.active? && active_only
178 return nil if !user.active? && active_only
179 else
179 else
180 # user is not yet registered, try to authenticate with available sources
180 # user is not yet registered, try to authenticate with available sources
181 attrs = AuthSource.authenticate(login, password)
181 attrs = AuthSource.authenticate(login, password)
182 if attrs
182 if attrs
183 user = new(attrs)
183 user = new(attrs)
184 user.login = login
184 user.login = login
185 user.language = Setting.default_language
185 user.language = Setting.default_language
186 if user.save
186 if user.save
187 user.reload
187 user.reload
188 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
188 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
189 end
189 end
190 end
190 end
191 end
191 end
192 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
192 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
193 user
193 user
194 rescue => text
194 rescue => text
195 raise text
195 raise text
196 end
196 end
197
197
198 # Returns the user who matches the given autologin +key+ or nil
198 # Returns the user who matches the given autologin +key+ or nil
199 def self.try_to_autologin(key)
199 def self.try_to_autologin(key)
200 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
200 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
201 if user
201 if user
202 user.update_column(:last_login_on, Time.now)
202 user.update_column(:last_login_on, Time.now)
203 user
203 user
204 end
204 end
205 end
205 end
206
206
207 def self.name_formatter(formatter = nil)
207 def self.name_formatter(formatter = nil)
208 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
208 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
209 end
209 end
210
210
211 # Returns an array of fields names than can be used to make an order statement for users
211 # Returns an array of fields names than can be used to make an order statement for users
212 # according to how user names are displayed
212 # according to how user names are displayed
213 # Examples:
213 # Examples:
214 #
214 #
215 # User.fields_for_order_statement => ['users.login', 'users.id']
215 # User.fields_for_order_statement => ['users.login', 'users.id']
216 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
216 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
217 def self.fields_for_order_statement(table=nil)
217 def self.fields_for_order_statement(table=nil)
218 table ||= table_name
218 table ||= table_name
219 name_formatter[:order].map {|field| "#{table}.#{field}"}
219 name_formatter[:order].map {|field| "#{table}.#{field}"}
220 end
220 end
221
221
222 # Return user's full name for display
222 # Return user's full name for display
223 def name(formatter = nil)
223 def name(formatter = nil)
224 f = self.class.name_formatter(formatter)
224 f = self.class.name_formatter(formatter)
225 if formatter
225 if formatter
226 eval('"' + f[:string] + '"')
226 eval('"' + f[:string] + '"')
227 else
227 else
228 @name ||= eval('"' + f[:string] + '"')
228 @name ||= eval('"' + f[:string] + '"')
229 end
229 end
230 end
230 end
231
231
232 def active?
232 def active?
233 self.status == STATUS_ACTIVE
233 self.status == STATUS_ACTIVE
234 end
234 end
235
235
236 def registered?
236 def registered?
237 self.status == STATUS_REGISTERED
237 self.status == STATUS_REGISTERED
238 end
238 end
239
239
240 def locked?
240 def locked?
241 self.status == STATUS_LOCKED
241 self.status == STATUS_LOCKED
242 end
242 end
243
243
244 def activate
244 def activate
245 self.status = STATUS_ACTIVE
245 self.status = STATUS_ACTIVE
246 end
246 end
247
247
248 def register
248 def register
249 self.status = STATUS_REGISTERED
249 self.status = STATUS_REGISTERED
250 end
250 end
251
251
252 def lock
252 def lock
253 self.status = STATUS_LOCKED
253 self.status = STATUS_LOCKED
254 end
254 end
255
255
256 def activate!
256 def activate!
257 update_attribute(:status, STATUS_ACTIVE)
257 update_attribute(:status, STATUS_ACTIVE)
258 end
258 end
259
259
260 def register!
260 def register!
261 update_attribute(:status, STATUS_REGISTERED)
261 update_attribute(:status, STATUS_REGISTERED)
262 end
262 end
263
263
264 def lock!
264 def lock!
265 update_attribute(:status, STATUS_LOCKED)
265 update_attribute(:status, STATUS_LOCKED)
266 end
266 end
267
267
268 # Returns true if +clear_password+ is the correct user's password, otherwise false
268 # Returns true if +clear_password+ is the correct user's password, otherwise false
269 def check_password?(clear_password)
269 def check_password?(clear_password)
270 if auth_source_id.present?
270 if auth_source_id.present?
271 auth_source.authenticate(self.login, clear_password)
271 auth_source.authenticate(self.login, clear_password)
272 else
272 else
273 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
273 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
274 end
274 end
275 end
275 end
276
276
277 # Generates a random salt and computes hashed_password for +clear_password+
277 # Generates a random salt and computes hashed_password for +clear_password+
278 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
278 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
279 def salt_password(clear_password)
279 def salt_password(clear_password)
280 self.salt = User.generate_salt
280 self.salt = User.generate_salt
281 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
281 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
282 self.passwd_changed_on = Time.now
282 self.passwd_changed_on = Time.now
283 end
283 end
284
284
285 # Does the backend storage allow this user to change their password?
285 # Does the backend storage allow this user to change their password?
286 def change_password_allowed?
286 def change_password_allowed?
287 return true if auth_source.nil?
287 return true if auth_source.nil?
288 return auth_source.allow_password_changes?
288 return auth_source.allow_password_changes?
289 end
289 end
290
290
291 def must_change_password?
291 def must_change_password?
292 must_change_passwd? && change_password_allowed?
292 must_change_passwd? && change_password_allowed?
293 end
293 end
294
294
295 def generate_password?
295 def generate_password?
296 generate_password == '1' || generate_password == true
296 generate_password == '1' || generate_password == true
297 end
297 end
298
298
299 # Generate and set a random password on given length
299 # Generate and set a random password on given length
300 def random_password(length=40)
300 def random_password(length=40)
301 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
301 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
302 chars -= %w(0 O 1 l)
302 chars -= %w(0 O 1 l)
303 password = ''
303 password = ''
304 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
304 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
305 self.password = password
305 self.password = password
306 self.password_confirmation = password
306 self.password_confirmation = password
307 self
307 self
308 end
308 end
309
309
310 def pref
310 def pref
311 self.preference ||= UserPreference.new(:user => self)
311 self.preference ||= UserPreference.new(:user => self)
312 end
312 end
313
313
314 def time_zone
314 def time_zone
315 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
315 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
316 end
316 end
317
317
318 def force_default_language?
318 def force_default_language?
319 Setting.force_default_language_for_loggedin?
319 Setting.force_default_language_for_loggedin?
320 end
320 end
321
321
322 def language
322 def language
323 if force_default_language?
323 if force_default_language?
324 Setting.default_language
324 Setting.default_language
325 else
325 else
326 super
326 super
327 end
327 end
328 end
328 end
329
329
330 def wants_comments_in_reverse_order?
330 def wants_comments_in_reverse_order?
331 self.pref[:comments_sorting] == 'desc'
331 self.pref[:comments_sorting] == 'desc'
332 end
332 end
333
333
334 # Return user's RSS key (a 40 chars long string), used to access feeds
334 # Return user's RSS key (a 40 chars long string), used to access feeds
335 def rss_key
335 def rss_key
336 if rss_token.nil?
336 if rss_token.nil?
337 create_rss_token(:action => 'feeds')
337 create_rss_token(:action => 'feeds')
338 end
338 end
339 rss_token.value
339 rss_token.value
340 end
340 end
341
341
342 # Return user's API key (a 40 chars long string), used to access the API
342 # Return user's API key (a 40 chars long string), used to access the API
343 def api_key
343 def api_key
344 if api_token.nil?
344 if api_token.nil?
345 create_api_token(:action => 'api')
345 create_api_token(:action => 'api')
346 end
346 end
347 api_token.value
347 api_token.value
348 end
348 end
349
349
350 # Return an array of project ids for which the user has explicitly turned mail notifications on
350 # Return an array of project ids for which the user has explicitly turned mail notifications on
351 def notified_projects_ids
351 def notified_projects_ids
352 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
352 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
353 end
353 end
354
354
355 def notified_project_ids=(ids)
355 def notified_project_ids=(ids)
356 @notified_projects_ids_changed = true
356 @notified_projects_ids_changed = true
357 @notified_projects_ids = ids
357 @notified_projects_ids = ids
358 end
358 end
359
359
360 # Updates per project notifications (after_save callback)
360 # Updates per project notifications (after_save callback)
361 def update_notified_project_ids
361 def update_notified_project_ids
362 if @notified_projects_ids_changed
362 if @notified_projects_ids_changed
363 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
363 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
364 members.update_all(:mail_notification => false)
364 members.update_all(:mail_notification => false)
365 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
365 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
366 end
366 end
367 end
367 end
368 private :update_notified_project_ids
368 private :update_notified_project_ids
369
369
370 def valid_notification_options
370 def valid_notification_options
371 self.class.valid_notification_options(self)
371 self.class.valid_notification_options(self)
372 end
372 end
373
373
374 # Only users that belong to more than 1 project can select projects for which they are notified
374 # Only users that belong to more than 1 project can select projects for which they are notified
375 def self.valid_notification_options(user=nil)
375 def self.valid_notification_options(user=nil)
376 # Note that @user.membership.size would fail since AR ignores
376 # Note that @user.membership.size would fail since AR ignores
377 # :include association option when doing a count
377 # :include association option when doing a count
378 if user.nil? || user.memberships.length < 1
378 if user.nil? || user.memberships.length < 1
379 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
379 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
380 else
380 else
381 MAIL_NOTIFICATION_OPTIONS
381 MAIL_NOTIFICATION_OPTIONS
382 end
382 end
383 end
383 end
384
384
385 # Find a user account by matching the exact login and then a case-insensitive
385 # Find a user account by matching the exact login and then a case-insensitive
386 # version. Exact matches will be given priority.
386 # version. Exact matches will be given priority.
387 def self.find_by_login(login)
387 def self.find_by_login(login)
388 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
388 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
389 if login.present?
389 if login.present?
390 # First look for an exact match
390 # First look for an exact match
391 user = where(:login => login).detect {|u| u.login == login}
391 user = where(:login => login).detect {|u| u.login == login}
392 unless user
392 unless user
393 # Fail over to case-insensitive if none was found
393 # Fail over to case-insensitive if none was found
394 user = where("LOWER(login) = ?", login.downcase).first
394 user = where("LOWER(login) = ?", login.downcase).first
395 end
395 end
396 user
396 user
397 end
397 end
398 end
398 end
399
399
400 def self.find_by_rss_key(key)
400 def self.find_by_rss_key(key)
401 Token.find_active_user('feeds', key)
401 Token.find_active_user('feeds', key)
402 end
402 end
403
403
404 def self.find_by_api_key(key)
404 def self.find_by_api_key(key)
405 Token.find_active_user('api', key)
405 Token.find_active_user('api', key)
406 end
406 end
407
407
408 # Makes find_by_mail case-insensitive
408 # Makes find_by_mail case-insensitive
409 def self.find_by_mail(mail)
409 def self.find_by_mail(mail)
410 where("LOWER(mail) = ?", mail.to_s.downcase).first
410 where("LOWER(mail) = ?", mail.to_s.downcase).first
411 end
411 end
412
412
413 # Returns true if the default admin account can no longer be used
413 # Returns true if the default admin account can no longer be used
414 def self.default_admin_account_changed?
414 def self.default_admin_account_changed?
415 !User.active.find_by_login("admin").try(:check_password?, "admin")
415 !User.active.find_by_login("admin").try(:check_password?, "admin")
416 end
416 end
417
417
418 def to_s
418 def to_s
419 name
419 name
420 end
420 end
421
421
422 CSS_CLASS_BY_STATUS = {
422 CSS_CLASS_BY_STATUS = {
423 STATUS_ANONYMOUS => 'anon',
423 STATUS_ANONYMOUS => 'anon',
424 STATUS_ACTIVE => 'active',
424 STATUS_ACTIVE => 'active',
425 STATUS_REGISTERED => 'registered',
425 STATUS_REGISTERED => 'registered',
426 STATUS_LOCKED => 'locked'
426 STATUS_LOCKED => 'locked'
427 }
427 }
428
428
429 def css_classes
429 def css_classes
430 "user #{CSS_CLASS_BY_STATUS[status]}"
430 "user #{CSS_CLASS_BY_STATUS[status]}"
431 end
431 end
432
432
433 # Returns the current day according to user's time zone
433 # Returns the current day according to user's time zone
434 def today
434 def today
435 if time_zone.nil?
435 if time_zone.nil?
436 Date.today
436 Date.today
437 else
437 else
438 Time.now.in_time_zone(time_zone).to_date
438 Time.now.in_time_zone(time_zone).to_date
439 end
439 end
440 end
440 end
441
441
442 # Returns the day of +time+ according to user's time zone
442 # Returns the day of +time+ according to user's time zone
443 def time_to_date(time)
443 def time_to_date(time)
444 if time_zone.nil?
444 if time_zone.nil?
445 time.to_date
445 time.to_date
446 else
446 else
447 time.in_time_zone(time_zone).to_date
447 time.in_time_zone(time_zone).to_date
448 end
448 end
449 end
449 end
450
450
451 def logged?
451 def logged?
452 true
452 true
453 end
453 end
454
454
455 def anonymous?
455 def anonymous?
456 !logged?
456 !logged?
457 end
457 end
458
458
459 # Returns user's membership for the given project
459 # Returns user's membership for the given project
460 # or nil if the user is not a member of project
460 # or nil if the user is not a member of project
461 def membership(project)
461 def membership(project)
462 project_id = project.is_a?(Project) ? project.id : project
462 project_id = project.is_a?(Project) ? project.id : project
463
463
464 @membership_by_project_id ||= Hash.new {|h, project_id|
464 @membership_by_project_id ||= Hash.new {|h, project_id|
465 h[project_id] = memberships.where(:project_id => project_id).first
465 h[project_id] = memberships.where(:project_id => project_id).first
466 }
466 }
467 @membership_by_project_id[project_id]
467 @membership_by_project_id[project_id]
468 end
468 end
469
469
470 # Returns the user's bult-in role
470 # Returns the user's bult-in role
471 def builtin_role
471 def builtin_role
472 @builtin_role ||= Role.non_member
472 @builtin_role ||= Role.non_member
473 end
473 end
474
474
475 # Return user's roles for project
475 # Return user's roles for project
476 def roles_for_project(project)
476 def roles_for_project(project)
477 roles = []
478 # No role on archived projects
477 # No role on archived projects
479 return roles if project.nil? || project.archived?
478 return [] if project.nil? || project.archived?
480 if membership = membership(project)
479 if membership = membership(project)
481 roles = membership.roles
480 membership.roles.dup
481 elsif project.is_public?
482 project.override_roles(builtin_role)
482 else
483 else
483 roles << builtin_role
484 []
484 end
485 end
485 roles
486 end
486 end
487
487
488 # Return true if the user is a member of project
488 # Return true if the user is a member of project
489 def member_of?(project)
489 def member_of?(project)
490 projects.to_a.include?(project)
490 projects.to_a.include?(project)
491 end
491 end
492
492
493 # Returns a hash of user's projects grouped by roles
493 # Returns a hash of user's projects grouped by roles
494 def projects_by_role
494 def projects_by_role
495 return @projects_by_role if @projects_by_role
495 return @projects_by_role if @projects_by_role
496
496
497 @projects_by_role = Hash.new([])
497 hash = Hash.new([])
498 memberships.each do |membership|
498
499 if membership.project
499 members = Member.joins(:project).
500 membership.roles.each do |role|
500 where("#{Project.table_name}.status <> 9").
501 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
501 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Member.table_name}.user_id = ?)", self.id, true, Group.builtin_id(self)).
502 @projects_by_role[role] << membership.project
502 preload(:project, :roles)
503
504 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
505 members.each do |member|
506 if member.project
507 member.roles.each do |role|
508 hash[role] = [] unless hash.key?(role)
509 hash[role] << member.project
503 end
510 end
504 end
511 end
505 end
512 end
506 @projects_by_role.each do |role, projects|
513
514 hash.each do |role, projects|
507 projects.uniq!
515 projects.uniq!
508 end
516 end
509
517
510 @projects_by_role
518 @projects_by_role = hash
511 end
519 end
512
520
513 # Returns true if user is arg or belongs to arg
521 # Returns true if user is arg or belongs to arg
514 def is_or_belongs_to?(arg)
522 def is_or_belongs_to?(arg)
515 if arg.is_a?(User)
523 if arg.is_a?(User)
516 self == arg
524 self == arg
517 elsif arg.is_a?(Group)
525 elsif arg.is_a?(Group)
518 arg.users.include?(self)
526 arg.users.include?(self)
519 else
527 else
520 false
528 false
521 end
529 end
522 end
530 end
523
531
524 # Return true if the user is allowed to do the specified action on a specific context
532 # Return true if the user is allowed to do the specified action on a specific context
525 # Action can be:
533 # Action can be:
526 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
534 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
527 # * a permission Symbol (eg. :edit_project)
535 # * a permission Symbol (eg. :edit_project)
528 # Context can be:
536 # Context can be:
529 # * a project : returns true if user is allowed to do the specified action on this project
537 # * a project : returns true if user is allowed to do the specified action on this project
530 # * an array of projects : returns true if user is allowed on every project
538 # * an array of projects : returns true if user is allowed on every project
531 # * nil with options[:global] set : check if user has at least one role allowed for this action,
539 # * nil with options[:global] set : check if user has at least one role allowed for this action,
532 # or falls back to Non Member / Anonymous permissions depending if the user is logged
540 # or falls back to Non Member / Anonymous permissions depending if the user is logged
533 def allowed_to?(action, context, options={}, &block)
541 def allowed_to?(action, context, options={}, &block)
534 if context && context.is_a?(Project)
542 if context && context.is_a?(Project)
535 return false unless context.allows_to?(action)
543 return false unless context.allows_to?(action)
536 # Admin users are authorized for anything else
544 # Admin users are authorized for anything else
537 return true if admin?
545 return true if admin?
538
546
539 roles = roles_for_project(context)
547 roles = roles_for_project(context)
540 return false unless roles
548 return false unless roles
541 roles.any? {|role|
549 roles.any? {|role|
542 (context.is_public? || role.member?) &&
550 (context.is_public? || role.member?) &&
543 role.allowed_to?(action) &&
551 role.allowed_to?(action) &&
544 (block_given? ? yield(role, self) : true)
552 (block_given? ? yield(role, self) : true)
545 }
553 }
546 elsif context && context.is_a?(Array)
554 elsif context && context.is_a?(Array)
547 if context.empty?
555 if context.empty?
548 false
556 false
549 else
557 else
550 # Authorize if user is authorized on every element of the array
558 # Authorize if user is authorized on every element of the array
551 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
559 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
552 end
560 end
553 elsif options[:global]
561 elsif options[:global]
554 # Admin users are always authorized
562 # Admin users are always authorized
555 return true if admin?
563 return true if admin?
556
564
557 # authorize if user has at least one role that has this permission
565 # authorize if user has at least one role that has this permission
558 roles = memberships.collect {|m| m.roles}.flatten.uniq
566 roles = memberships.collect {|m| m.roles}.flatten.uniq
559 roles << (self.logged? ? Role.non_member : Role.anonymous)
567 roles << (self.logged? ? Role.non_member : Role.anonymous)
560 roles.any? {|role|
568 roles.any? {|role|
561 role.allowed_to?(action) &&
569 role.allowed_to?(action) &&
562 (block_given? ? yield(role, self) : true)
570 (block_given? ? yield(role, self) : true)
563 }
571 }
564 else
572 else
565 false
573 false
566 end
574 end
567 end
575 end
568
576
569 # Is the user allowed to do the specified action on any project?
577 # Is the user allowed to do the specified action on any project?
570 # See allowed_to? for the actions and valid options.
578 # See allowed_to? for the actions and valid options.
571 #
579 #
572 # NB: this method is not used anywhere in the core codebase as of
580 # NB: this method is not used anywhere in the core codebase as of
573 # 2.5.2, but it's used by many plugins so if we ever want to remove
581 # 2.5.2, but it's used by many plugins so if we ever want to remove
574 # it it has to be carefully deprecated for a version or two.
582 # it it has to be carefully deprecated for a version or two.
575 def allowed_to_globally?(action, options={}, &block)
583 def allowed_to_globally?(action, options={}, &block)
576 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
584 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
577 end
585 end
578
586
579 # Returns true if the user is allowed to delete the user's own account
587 # Returns true if the user is allowed to delete the user's own account
580 def own_account_deletable?
588 def own_account_deletable?
581 Setting.unsubscribe? &&
589 Setting.unsubscribe? &&
582 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
590 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
583 end
591 end
584
592
585 safe_attributes 'login',
593 safe_attributes 'login',
586 'firstname',
594 'firstname',
587 'lastname',
595 'lastname',
588 'mail',
596 'mail',
589 'mail_notification',
597 'mail_notification',
590 'notified_project_ids',
598 'notified_project_ids',
591 'language',
599 'language',
592 'custom_field_values',
600 'custom_field_values',
593 'custom_fields',
601 'custom_fields',
594 'identity_url'
602 'identity_url'
595
603
596 safe_attributes 'status',
604 safe_attributes 'status',
597 'auth_source_id',
605 'auth_source_id',
598 'generate_password',
606 'generate_password',
599 'must_change_passwd',
607 'must_change_passwd',
600 :if => lambda {|user, current_user| current_user.admin?}
608 :if => lambda {|user, current_user| current_user.admin?}
601
609
602 safe_attributes 'group_ids',
610 safe_attributes 'group_ids',
603 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
611 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
604
612
605 # Utility method to help check if a user should be notified about an
613 # Utility method to help check if a user should be notified about an
606 # event.
614 # event.
607 #
615 #
608 # TODO: only supports Issue events currently
616 # TODO: only supports Issue events currently
609 def notify_about?(object)
617 def notify_about?(object)
610 if mail_notification == 'all'
618 if mail_notification == 'all'
611 true
619 true
612 elsif mail_notification.blank? || mail_notification == 'none'
620 elsif mail_notification.blank? || mail_notification == 'none'
613 false
621 false
614 else
622 else
615 case object
623 case object
616 when Issue
624 when Issue
617 case mail_notification
625 case mail_notification
618 when 'selected', 'only_my_events'
626 when 'selected', 'only_my_events'
619 # user receives notifications for created/assigned issues on unselected projects
627 # user receives notifications for created/assigned issues on unselected projects
620 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
628 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
621 when 'only_assigned'
629 when 'only_assigned'
622 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
630 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
623 when 'only_owner'
631 when 'only_owner'
624 object.author == self
632 object.author == self
625 end
633 end
626 when News
634 when News
627 # always send to project members except when mail_notification is set to 'none'
635 # always send to project members except when mail_notification is set to 'none'
628 true
636 true
629 end
637 end
630 end
638 end
631 end
639 end
632
640
633 def self.current=(user)
641 def self.current=(user)
634 RequestStore.store[:current_user] = user
642 RequestStore.store[:current_user] = user
635 end
643 end
636
644
637 def self.current
645 def self.current
638 RequestStore.store[:current_user] ||= User.anonymous
646 RequestStore.store[:current_user] ||= User.anonymous
639 end
647 end
640
648
641 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
649 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
642 # one anonymous user per database.
650 # one anonymous user per database.
643 def self.anonymous
651 def self.anonymous
644 anonymous_user = AnonymousUser.first
652 anonymous_user = AnonymousUser.first
645 if anonymous_user.nil?
653 if anonymous_user.nil?
646 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
654 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
647 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
655 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
648 end
656 end
649 anonymous_user
657 anonymous_user
650 end
658 end
651
659
652 # Salts all existing unsalted passwords
660 # Salts all existing unsalted passwords
653 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
661 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
654 # This method is used in the SaltPasswords migration and is to be kept as is
662 # This method is used in the SaltPasswords migration and is to be kept as is
655 def self.salt_unsalted_passwords!
663 def self.salt_unsalted_passwords!
656 transaction do
664 transaction do
657 User.where("salt IS NULL OR salt = ''").find_each do |user|
665 User.where("salt IS NULL OR salt = ''").find_each do |user|
658 next if user.hashed_password.blank?
666 next if user.hashed_password.blank?
659 salt = User.generate_salt
667 salt = User.generate_salt
660 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
668 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
661 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
669 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
662 end
670 end
663 end
671 end
664 end
672 end
665
673
666 protected
674 protected
667
675
668 def validate_password_length
676 def validate_password_length
669 return if password.blank? && generate_password?
677 return if password.blank? && generate_password?
670 # Password length validation based on setting
678 # Password length validation based on setting
671 if !password.nil? && password.size < Setting.password_min_length.to_i
679 if !password.nil? && password.size < Setting.password_min_length.to_i
672 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
680 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
673 end
681 end
674 end
682 end
675
683
676 private
684 private
677
685
678 def generate_password_if_needed
686 def generate_password_if_needed
679 if generate_password? && auth_source.nil?
687 if generate_password? && auth_source.nil?
680 length = [Setting.password_min_length.to_i + 2, 10].max
688 length = [Setting.password_min_length.to_i + 2, 10].max
681 random_password(length)
689 random_password(length)
682 end
690 end
683 end
691 end
684
692
685 # Delete all outstanding password reset tokens on password or email change.
693 # Delete all outstanding password reset tokens on password or email change.
686 # Delete the autologin tokens on password change to prohibit session leakage.
694 # Delete the autologin tokens on password change to prohibit session leakage.
687 # This helps to keep the account secure in case the associated email account
695 # This helps to keep the account secure in case the associated email account
688 # was compromised.
696 # was compromised.
689 def destroy_tokens
697 def destroy_tokens
690 tokens = []
698 tokens = []
691 tokens |= ['recovery', 'autologin'] if hashed_password_changed?
699 tokens |= ['recovery', 'autologin'] if hashed_password_changed?
692 tokens |= ['recovery'] if mail_changed?
700 tokens |= ['recovery'] if mail_changed?
693
701
694 if tokens.any?
702 if tokens.any?
695 Token.where(:user_id => id, :action => tokens).delete_all
703 Token.where(:user_id => id, :action => tokens).delete_all
696 end
704 end
697 end
705 end
698
706
699 # Removes references that are not handled by associations
707 # Removes references that are not handled by associations
700 # Things that are not deleted are reassociated with the anonymous user
708 # Things that are not deleted are reassociated with the anonymous user
701 def remove_references_before_destroy
709 def remove_references_before_destroy
702 return if self.id.nil?
710 return if self.id.nil?
703
711
704 substitute = User.anonymous
712 substitute = User.anonymous
705 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
713 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
706 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
714 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
707 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
715 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
708 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
716 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
709 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
717 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
710 JournalDetail.
718 JournalDetail.
711 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
719 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
712 update_all(['old_value = ?', substitute.id.to_s])
720 update_all(['old_value = ?', substitute.id.to_s])
713 JournalDetail.
721 JournalDetail.
714 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
722 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
715 update_all(['value = ?', substitute.id.to_s])
723 update_all(['value = ?', substitute.id.to_s])
716 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
724 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
717 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
725 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
718 # Remove private queries and keep public ones
726 # Remove private queries and keep public ones
719 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
727 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
720 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
728 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
721 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
729 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
722 Token.delete_all ['user_id = ?', id]
730 Token.delete_all ['user_id = ?', id]
723 Watcher.delete_all ['user_id = ?', id]
731 Watcher.delete_all ['user_id = ?', id]
724 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
732 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
725 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
733 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
726 end
734 end
727
735
728 # Return password digest
736 # Return password digest
729 def self.hash_password(clear_password)
737 def self.hash_password(clear_password)
730 Digest::SHA1.hexdigest(clear_password || "")
738 Digest::SHA1.hexdigest(clear_password || "")
731 end
739 end
732
740
733 # Returns a 128bits random salt as a hex string (32 chars long)
741 # Returns a 128bits random salt as a hex string (32 chars long)
734 def self.generate_salt
742 def self.generate_salt
735 Redmine::Utils.random_hex(16)
743 Redmine::Utils.random_hex(16)
736 end
744 end
737
745
738 end
746 end
739
747
740 class AnonymousUser < User
748 class AnonymousUser < User
741 validate :validate_anonymous_uniqueness, :on => :create
749 validate :validate_anonymous_uniqueness, :on => :create
742
750
743 def validate_anonymous_uniqueness
751 def validate_anonymous_uniqueness
744 # There should be only one AnonymousUser in the database
752 # There should be only one AnonymousUser in the database
745 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
753 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
746 end
754 end
747
755
748 def available_custom_fields
756 def available_custom_fields
749 []
757 []
750 end
758 end
751
759
752 # Overrides a few properties
760 # Overrides a few properties
753 def logged?; false end
761 def logged?; false end
754 def admin; false end
762 def admin; false end
755 def name(*args); I18n.t(:label_user_anonymous) end
763 def name(*args); I18n.t(:label_user_anonymous) end
756 def mail; nil end
764 def mail; nil end
757 def time_zone; nil end
765 def time_zone; nil end
758 def rss_key; nil end
766 def rss_key; nil end
759
767
760 def pref
768 def pref
761 UserPreference.new(:user => self)
769 UserPreference.new(:user => self)
762 end
770 end
763
771
764 # Returns the user's bult-in role
772 # Returns the user's bult-in role
765 def builtin_role
773 def builtin_role
766 @builtin_role ||= Role.anonymous
774 @builtin_role ||= Role.anonymous
767 end
775 end
768
776
769 def membership(*args)
777 def membership(*args)
770 nil
778 nil
771 end
779 end
772
780
773 def member_of?(*args)
781 def member_of?(*args)
774 false
782 false
775 end
783 end
776
784
777 # Anonymous user can not be destroyed
785 # Anonymous user can not be destroyed
778 def destroy
786 def destroy
779 false
787 false
780 end
788 end
781 end
789 end
@@ -1,8 +1,10
1 <%= error_messages_for @group %>
1 <%= error_messages_for @group %>
2
2
3 <div class="box tabular">
3 <div class="box tabular">
4 <p><%= f.text_field :name, :required => true, :size => 60 %></p>
4 <p><%= f.text_field :name, :required => true, :size => 60,
5 :disabled => !@group.safe_attribute?('name') %></p>
6
5 <% @group.custom_field_values.each do |value| %>
7 <% @group.custom_field_values.each do |value| %>
6 <p><%= custom_field_tag_with_label :group, value %></p>
8 <p><%= custom_field_tag_with_label :group, value %></p>
7 <% end %>
9 <% end %>
8 </div>
10 </div>
@@ -1,4 +1,4
1 <%= labelled_form_for @group do |f| %>
1 <%= labelled_form_for @group, :url => group_path(@group) do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
4 <% end %>
@@ -1,3 +1,3
1 <%= title [l(:label_group_plural), groups_path], @group.name %>
1 <%= title [l(:label_group_plural), groups_path], @group.name %>
2
2
3 <%= render_tabs group_settings_tabs %>
3 <%= render_tabs group_settings_tabs(@group) %>
@@ -1,10 +1,11
1 api.array :groups do
1 api.array :groups do
2 @groups.each do |group|
2 @groups.each do |group|
3 api.group do
3 api.group do
4 api.id group.id
4 api.id group.id
5 api.name group.lastname
5 api.name group.lastname
6 api.builtin group.builtin_type if group.builtin_type
6
7
7 render_api_custom_values group.visible_custom_field_values, api
8 render_api_custom_values group.visible_custom_field_values, api
8 end
9 end
9 end
10 end
10 end
11 end
@@ -1,25 +1,25
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <%= title l(:label_group_plural) %>
5 <%= title l(:label_group_plural) %>
6 <% if @groups.any? %>
6 <% if @groups.any? %>
7 <table class="list groups">
7 <table class="list groups">
8 <thead><tr>
8 <thead><tr>
9 <th><%=l(:label_group)%></th>
9 <th><%=l(:label_group)%></th>
10 <th><%=l(:label_user_plural)%></th>
10 <th><%=l(:label_user_plural)%></th>
11 <th></th>
11 <th></th>
12 </tr></thead>
12 </tr></thead>
13 <tbody>
13 <tbody>
14 <% @groups.each do |group| %>
14 <% @groups.each do |group| %>
15 <tr id="group-<%= group.id %>" class="<%= cycle 'odd', 'even' %>">
15 <tr id="group-<%= group.id %>" class="<%= cycle 'odd', 'even' %> <%= "builtin" if group.builtin? %>">
16 <td class="name"><%= link_to h(group), edit_group_path(group) %></td>
16 <td class="name"><%= link_to h(group), edit_group_path(group) %></td>
17 <td class="user_count"><%= @user_count_by_group_id[group.id] || 0 %></td>
17 <td class="user_count"><%= (@user_count_by_group_id[group.id] || 0) unless group.builtin? %></td>
18 <td class="buttons"><%= delete_link group %></td>
18 <td class="buttons"><%= delete_link group unless group.builtin? %></td>
19 </tr>
19 </tr>
20 <% end %>
20 <% end %>
21 </tbody>
21 </tbody>
22 </table>
22 </table>
23 <% else %>
23 <% else %>
24 <p class="nodata"><%= l(:label_no_data) %></p>
24 <p class="nodata"><%= l(:label_no_data) %></p>
25 <% end %>
25 <% end %>
@@ -1,30 +1,31
1 api.group do
1 api.group do
2 api.id @group.id
2 api.id @group.id
3 api.name @group.lastname
3 api.name @group.lastname
4 api.builtin @group.builtin_type if @group.builtin_type
4
5
5 render_api_custom_values @group.visible_custom_field_values, api
6 render_api_custom_values @group.visible_custom_field_values, api
6
7
7 api.array :users do
8 api.array :users do
8 @group.users.each do |user|
9 @group.users.each do |user|
9 api.user :id => user.id, :name => user.name
10 api.user :id => user.id, :name => user.name
10 end
11 end
11 end if include_in_api_response?('users')
12 end if include_in_api_response?('users') && !@group.builtin?
12
13
13 api.array :memberships do
14 api.array :memberships do
14 @group.memberships.each do |membership|
15 @group.memberships.each do |membership|
15 api.membership do
16 api.membership do
16 api.id membership.id
17 api.id membership.id
17 api.project :id => membership.project.id, :name => membership.project.name
18 api.project :id => membership.project.id, :name => membership.project.name
18 api.array :roles do
19 api.array :roles do
19 membership.member_roles.each do |member_role|
20 membership.member_roles.each do |member_role|
20 if member_role.role
21 if member_role.role
21 attrs = {:id => member_role.role.id, :name => member_role.role.name}
22 attrs = {:id => member_role.role.id, :name => member_role.role.name}
22 attrs.merge!(:inherited => true) if member_role.inherited_from.present?
23 attrs.merge!(:inherited => true) if member_role.inherited_from.present?
23 api.role attrs
24 api.role attrs
24 end
25 end
25 end
26 end
26 end
27 end
27 end if membership.project
28 end if membership.project
28 end
29 end
29 end if include_in_api_response?('memberships')
30 end if include_in_api_response?('memberships')
30 end
31 end
@@ -1,9 +1,9
1 <%= form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>
1 <%= form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>
2 <div class="box">
2 <div class="box">
3 <% Group.all.sort.each do |group| %>
3 <% Group.givable.sort.each do |group| %>
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group), :id => nil %> <%=h group %></label><br />
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group), :id => nil %> <%=h group %></label><br />
5 <% end %>
5 <% end %>
6 <%= hidden_field_tag 'user[group_ids][]', '' %>
6 <%= hidden_field_tag 'user[group_ids][]', '' %>
7 </div>
7 </div>
8 <%= submit_tag l(:button_save) %>
8 <%= submit_tag l(:button_save) %>
9 <% end %>
9 <% end %>
@@ -1,1108 +1,1110
1 en:
1 en:
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 direction: ltr
3 direction: ltr
4 date:
4 date:
5 formats:
5 formats:
6 # Use the strftime parameters for formats.
6 # Use the strftime parameters for formats.
7 # When no format has been given, it uses default.
7 # When no format has been given, it uses default.
8 # You can provide other formats here if you like!
8 # You can provide other formats here if you like!
9 default: "%m/%d/%Y"
9 default: "%m/%d/%Y"
10 short: "%b %d"
10 short: "%b %d"
11 long: "%B %d, %Y"
11 long: "%B %d, %Y"
12
12
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15
15
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 # Used in date_select and datime_select.
19 # Used in date_select and datime_select.
20 order:
20 order:
21 - :year
21 - :year
22 - :month
22 - :month
23 - :day
23 - :day
24
24
25 time:
25 time:
26 formats:
26 formats:
27 default: "%m/%d/%Y %I:%M %p"
27 default: "%m/%d/%Y %I:%M %p"
28 time: "%I:%M %p"
28 time: "%I:%M %p"
29 short: "%d %b %H:%M"
29 short: "%d %b %H:%M"
30 long: "%B %d, %Y %H:%M"
30 long: "%B %d, %Y %H:%M"
31 am: "am"
31 am: "am"
32 pm: "pm"
32 pm: "pm"
33
33
34 datetime:
34 datetime:
35 distance_in_words:
35 distance_in_words:
36 half_a_minute: "half a minute"
36 half_a_minute: "half a minute"
37 less_than_x_seconds:
37 less_than_x_seconds:
38 one: "less than 1 second"
38 one: "less than 1 second"
39 other: "less than %{count} seconds"
39 other: "less than %{count} seconds"
40 x_seconds:
40 x_seconds:
41 one: "1 second"
41 one: "1 second"
42 other: "%{count} seconds"
42 other: "%{count} seconds"
43 less_than_x_minutes:
43 less_than_x_minutes:
44 one: "less than a minute"
44 one: "less than a minute"
45 other: "less than %{count} minutes"
45 other: "less than %{count} minutes"
46 x_minutes:
46 x_minutes:
47 one: "1 minute"
47 one: "1 minute"
48 other: "%{count} minutes"
48 other: "%{count} minutes"
49 about_x_hours:
49 about_x_hours:
50 one: "about 1 hour"
50 one: "about 1 hour"
51 other: "about %{count} hours"
51 other: "about %{count} hours"
52 x_hours:
52 x_hours:
53 one: "1 hour"
53 one: "1 hour"
54 other: "%{count} hours"
54 other: "%{count} hours"
55 x_days:
55 x_days:
56 one: "1 day"
56 one: "1 day"
57 other: "%{count} days"
57 other: "%{count} days"
58 about_x_months:
58 about_x_months:
59 one: "about 1 month"
59 one: "about 1 month"
60 other: "about %{count} months"
60 other: "about %{count} months"
61 x_months:
61 x_months:
62 one: "1 month"
62 one: "1 month"
63 other: "%{count} months"
63 other: "%{count} months"
64 about_x_years:
64 about_x_years:
65 one: "about 1 year"
65 one: "about 1 year"
66 other: "about %{count} years"
66 other: "about %{count} years"
67 over_x_years:
67 over_x_years:
68 one: "over 1 year"
68 one: "over 1 year"
69 other: "over %{count} years"
69 other: "over %{count} years"
70 almost_x_years:
70 almost_x_years:
71 one: "almost 1 year"
71 one: "almost 1 year"
72 other: "almost %{count} years"
72 other: "almost %{count} years"
73
73
74 number:
74 number:
75 format:
75 format:
76 separator: "."
76 separator: "."
77 delimiter: ""
77 delimiter: ""
78 precision: 3
78 precision: 3
79
79
80 human:
80 human:
81 format:
81 format:
82 delimiter: ""
82 delimiter: ""
83 precision: 3
83 precision: 3
84 storage_units:
84 storage_units:
85 format: "%n %u"
85 format: "%n %u"
86 units:
86 units:
87 byte:
87 byte:
88 one: "Byte"
88 one: "Byte"
89 other: "Bytes"
89 other: "Bytes"
90 kb: "KB"
90 kb: "KB"
91 mb: "MB"
91 mb: "MB"
92 gb: "GB"
92 gb: "GB"
93 tb: "TB"
93 tb: "TB"
94
94
95 # Used in array.to_sentence.
95 # Used in array.to_sentence.
96 support:
96 support:
97 array:
97 array:
98 sentence_connector: "and"
98 sentence_connector: "and"
99 skip_last_comma: false
99 skip_last_comma: false
100
100
101 activerecord:
101 activerecord:
102 errors:
102 errors:
103 template:
103 template:
104 header:
104 header:
105 one: "1 error prohibited this %{model} from being saved"
105 one: "1 error prohibited this %{model} from being saved"
106 other: "%{count} errors prohibited this %{model} from being saved"
106 other: "%{count} errors prohibited this %{model} from being saved"
107 messages:
107 messages:
108 inclusion: "is not included in the list"
108 inclusion: "is not included in the list"
109 exclusion: "is reserved"
109 exclusion: "is reserved"
110 invalid: "is invalid"
110 invalid: "is invalid"
111 confirmation: "doesn't match confirmation"
111 confirmation: "doesn't match confirmation"
112 accepted: "must be accepted"
112 accepted: "must be accepted"
113 empty: "can't be empty"
113 empty: "can't be empty"
114 blank: "can't be blank"
114 blank: "can't be blank"
115 too_long: "is too long (maximum is %{count} characters)"
115 too_long: "is too long (maximum is %{count} characters)"
116 too_short: "is too short (minimum is %{count} characters)"
116 too_short: "is too short (minimum is %{count} characters)"
117 wrong_length: "is the wrong length (should be %{count} characters)"
117 wrong_length: "is the wrong length (should be %{count} characters)"
118 taken: "has already been taken"
118 taken: "has already been taken"
119 not_a_number: "is not a number"
119 not_a_number: "is not a number"
120 not_a_date: "is not a valid date"
120 not_a_date: "is not a valid date"
121 greater_than: "must be greater than %{count}"
121 greater_than: "must be greater than %{count}"
122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
123 equal_to: "must be equal to %{count}"
123 equal_to: "must be equal to %{count}"
124 less_than: "must be less than %{count}"
124 less_than: "must be less than %{count}"
125 less_than_or_equal_to: "must be less than or equal to %{count}"
125 less_than_or_equal_to: "must be less than or equal to %{count}"
126 odd: "must be odd"
126 odd: "must be odd"
127 even: "must be even"
127 even: "must be even"
128 greater_than_start_date: "must be greater than start date"
128 greater_than_start_date: "must be greater than start date"
129 not_same_project: "doesn't belong to the same project"
129 not_same_project: "doesn't belong to the same project"
130 circular_dependency: "This relation would create a circular dependency"
130 circular_dependency: "This relation would create a circular dependency"
131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
133
133
134 actionview_instancetag_blank_option: Please select
134 actionview_instancetag_blank_option: Please select
135
135
136 general_text_No: 'No'
136 general_text_No: 'No'
137 general_text_Yes: 'Yes'
137 general_text_Yes: 'Yes'
138 general_text_no: 'no'
138 general_text_no: 'no'
139 general_text_yes: 'yes'
139 general_text_yes: 'yes'
140 general_lang_name: 'English'
140 general_lang_name: 'English'
141 general_csv_separator: ','
141 general_csv_separator: ','
142 general_csv_decimal_separator: '.'
142 general_csv_decimal_separator: '.'
143 general_csv_encoding: ISO-8859-1
143 general_csv_encoding: ISO-8859-1
144 general_first_day_of_week: '7'
144 general_first_day_of_week: '7'
145
145
146 notice_account_updated: Account was successfully updated.
146 notice_account_updated: Account was successfully updated.
147 notice_account_invalid_creditentials: Invalid user or password
147 notice_account_invalid_creditentials: Invalid user or password
148 notice_account_password_updated: Password was successfully updated.
148 notice_account_password_updated: Password was successfully updated.
149 notice_account_wrong_password: Wrong password
149 notice_account_wrong_password: Wrong password
150 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
150 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
151 notice_account_unknown_email: Unknown user.
151 notice_account_unknown_email: Unknown user.
152 notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>.
152 notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>.
153 notice_account_locked: Your account is locked.
153 notice_account_locked: Your account is locked.
154 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
154 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
155 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
155 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
156 notice_account_activated: Your account has been activated. You can now log in.
156 notice_account_activated: Your account has been activated. You can now log in.
157 notice_successful_create: Successful creation.
157 notice_successful_create: Successful creation.
158 notice_successful_update: Successful update.
158 notice_successful_update: Successful update.
159 notice_successful_delete: Successful deletion.
159 notice_successful_delete: Successful deletion.
160 notice_successful_connection: Successful connection.
160 notice_successful_connection: Successful connection.
161 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
161 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
162 notice_locking_conflict: Data has been updated by another user.
162 notice_locking_conflict: Data has been updated by another user.
163 notice_not_authorized: You are not authorized to access this page.
163 notice_not_authorized: You are not authorized to access this page.
164 notice_not_authorized_archived_project: The project you're trying to access has been archived.
164 notice_not_authorized_archived_project: The project you're trying to access has been archived.
165 notice_email_sent: "An email was sent to %{value}"
165 notice_email_sent: "An email was sent to %{value}"
166 notice_email_error: "An error occurred while sending mail (%{value})"
166 notice_email_error: "An error occurred while sending mail (%{value})"
167 notice_feeds_access_key_reseted: Your Atom access key was reset.
167 notice_feeds_access_key_reseted: Your Atom access key was reset.
168 notice_api_access_key_reseted: Your API access key was reset.
168 notice_api_access_key_reseted: Your API access key was reset.
169 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
169 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
170 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
170 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
171 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
171 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
172 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
172 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
173 notice_account_pending: "Your account was created and is now pending administrator approval."
173 notice_account_pending: "Your account was created and is now pending administrator approval."
174 notice_default_data_loaded: Default configuration successfully loaded.
174 notice_default_data_loaded: Default configuration successfully loaded.
175 notice_unable_delete_version: Unable to delete version.
175 notice_unable_delete_version: Unable to delete version.
176 notice_unable_delete_time_entry: Unable to delete time log entry.
176 notice_unable_delete_time_entry: Unable to delete time log entry.
177 notice_issue_done_ratios_updated: Issue done ratios updated.
177 notice_issue_done_ratios_updated: Issue done ratios updated.
178 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
178 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
179 notice_issue_successful_create: "Issue %{id} created."
179 notice_issue_successful_create: "Issue %{id} created."
180 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
180 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
181 notice_account_deleted: "Your account has been permanently deleted."
181 notice_account_deleted: "Your account has been permanently deleted."
182 notice_user_successful_create: "User %{id} created."
182 notice_user_successful_create: "User %{id} created."
183 notice_new_password_must_be_different: The new password must be different from the current password
183 notice_new_password_must_be_different: The new password must be different from the current password
184
184
185 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
185 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
186 error_scm_not_found: "The entry or revision was not found in the repository."
186 error_scm_not_found: "The entry or revision was not found in the repository."
187 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
187 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
188 error_scm_annotate: "The entry does not exist or cannot be annotated."
188 error_scm_annotate: "The entry does not exist or cannot be annotated."
189 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
189 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
190 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
190 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
191 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
191 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
192 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
192 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
193 error_can_not_delete_custom_field: Unable to delete custom field
193 error_can_not_delete_custom_field: Unable to delete custom field
194 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
194 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
195 error_can_not_remove_role: "This role is in use and cannot be deleted."
195 error_can_not_remove_role: "This role is in use and cannot be deleted."
196 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
196 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
197 error_can_not_archive_project: This project cannot be archived
197 error_can_not_archive_project: This project cannot be archived
198 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
198 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
199 error_workflow_copy_source: 'Please select a source tracker or role'
199 error_workflow_copy_source: 'Please select a source tracker or role'
200 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
200 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
201 error_unable_delete_issue_status: 'Unable to delete issue status'
201 error_unable_delete_issue_status: 'Unable to delete issue status'
202 error_unable_to_connect: "Unable to connect (%{value})"
202 error_unable_to_connect: "Unable to connect (%{value})"
203 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
203 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
204 error_session_expired: "Your session has expired. Please login again."
204 error_session_expired: "Your session has expired. Please login again."
205 warning_attachments_not_saved: "%{count} file(s) could not be saved."
205 warning_attachments_not_saved: "%{count} file(s) could not be saved."
206
206
207 mail_subject_lost_password: "Your %{value} password"
207 mail_subject_lost_password: "Your %{value} password"
208 mail_body_lost_password: 'To change your password, click on the following link:'
208 mail_body_lost_password: 'To change your password, click on the following link:'
209 mail_subject_register: "Your %{value} account activation"
209 mail_subject_register: "Your %{value} account activation"
210 mail_body_register: 'To activate your account, click on the following link:'
210 mail_body_register: 'To activate your account, click on the following link:'
211 mail_body_account_information_external: "You can use your %{value} account to log in."
211 mail_body_account_information_external: "You can use your %{value} account to log in."
212 mail_body_account_information: Your account information
212 mail_body_account_information: Your account information
213 mail_subject_account_activation_request: "%{value} account activation request"
213 mail_subject_account_activation_request: "%{value} account activation request"
214 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
214 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
215 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
215 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
216 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
216 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
217 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
217 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
218 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
218 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
219 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
219 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
220 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
220 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
221
221
222 field_name: Name
222 field_name: Name
223 field_description: Description
223 field_description: Description
224 field_summary: Summary
224 field_summary: Summary
225 field_is_required: Required
225 field_is_required: Required
226 field_firstname: First name
226 field_firstname: First name
227 field_lastname: Last name
227 field_lastname: Last name
228 field_mail: Email
228 field_mail: Email
229 field_filename: File
229 field_filename: File
230 field_filesize: Size
230 field_filesize: Size
231 field_downloads: Downloads
231 field_downloads: Downloads
232 field_author: Author
232 field_author: Author
233 field_created_on: Created
233 field_created_on: Created
234 field_updated_on: Updated
234 field_updated_on: Updated
235 field_closed_on: Closed
235 field_closed_on: Closed
236 field_field_format: Format
236 field_field_format: Format
237 field_is_for_all: For all projects
237 field_is_for_all: For all projects
238 field_possible_values: Possible values
238 field_possible_values: Possible values
239 field_regexp: Regular expression
239 field_regexp: Regular expression
240 field_min_length: Minimum length
240 field_min_length: Minimum length
241 field_max_length: Maximum length
241 field_max_length: Maximum length
242 field_value: Value
242 field_value: Value
243 field_category: Category
243 field_category: Category
244 field_title: Title
244 field_title: Title
245 field_project: Project
245 field_project: Project
246 field_issue: Issue
246 field_issue: Issue
247 field_status: Status
247 field_status: Status
248 field_notes: Notes
248 field_notes: Notes
249 field_is_closed: Issue closed
249 field_is_closed: Issue closed
250 field_is_default: Default value
250 field_is_default: Default value
251 field_tracker: Tracker
251 field_tracker: Tracker
252 field_subject: Subject
252 field_subject: Subject
253 field_due_date: Due date
253 field_due_date: Due date
254 field_assigned_to: Assignee
254 field_assigned_to: Assignee
255 field_priority: Priority
255 field_priority: Priority
256 field_fixed_version: Target version
256 field_fixed_version: Target version
257 field_user: User
257 field_user: User
258 field_principal: Principal
258 field_principal: Principal
259 field_role: Role
259 field_role: Role
260 field_homepage: Homepage
260 field_homepage: Homepage
261 field_is_public: Public
261 field_is_public: Public
262 field_parent: Subproject of
262 field_parent: Subproject of
263 field_is_in_roadmap: Issues displayed in roadmap
263 field_is_in_roadmap: Issues displayed in roadmap
264 field_login: Login
264 field_login: Login
265 field_mail_notification: Email notifications
265 field_mail_notification: Email notifications
266 field_admin: Administrator
266 field_admin: Administrator
267 field_last_login_on: Last connection
267 field_last_login_on: Last connection
268 field_language: Language
268 field_language: Language
269 field_effective_date: Date
269 field_effective_date: Date
270 field_password: Password
270 field_password: Password
271 field_new_password: New password
271 field_new_password: New password
272 field_password_confirmation: Confirmation
272 field_password_confirmation: Confirmation
273 field_version: Version
273 field_version: Version
274 field_type: Type
274 field_type: Type
275 field_host: Host
275 field_host: Host
276 field_port: Port
276 field_port: Port
277 field_account: Account
277 field_account: Account
278 field_base_dn: Base DN
278 field_base_dn: Base DN
279 field_attr_login: Login attribute
279 field_attr_login: Login attribute
280 field_attr_firstname: Firstname attribute
280 field_attr_firstname: Firstname attribute
281 field_attr_lastname: Lastname attribute
281 field_attr_lastname: Lastname attribute
282 field_attr_mail: Email attribute
282 field_attr_mail: Email attribute
283 field_onthefly: On-the-fly user creation
283 field_onthefly: On-the-fly user creation
284 field_start_date: Start date
284 field_start_date: Start date
285 field_done_ratio: "% Done"
285 field_done_ratio: "% Done"
286 field_auth_source: Authentication mode
286 field_auth_source: Authentication mode
287 field_hide_mail: Hide my email address
287 field_hide_mail: Hide my email address
288 field_comments: Comment
288 field_comments: Comment
289 field_url: URL
289 field_url: URL
290 field_start_page: Start page
290 field_start_page: Start page
291 field_subproject: Subproject
291 field_subproject: Subproject
292 field_hours: Hours
292 field_hours: Hours
293 field_activity: Activity
293 field_activity: Activity
294 field_spent_on: Date
294 field_spent_on: Date
295 field_identifier: Identifier
295 field_identifier: Identifier
296 field_is_filter: Used as a filter
296 field_is_filter: Used as a filter
297 field_issue_to: Related issue
297 field_issue_to: Related issue
298 field_delay: Delay
298 field_delay: Delay
299 field_assignable: Issues can be assigned to this role
299 field_assignable: Issues can be assigned to this role
300 field_redirect_existing_links: Redirect existing links
300 field_redirect_existing_links: Redirect existing links
301 field_estimated_hours: Estimated time
301 field_estimated_hours: Estimated time
302 field_column_names: Columns
302 field_column_names: Columns
303 field_time_entries: Log time
303 field_time_entries: Log time
304 field_time_zone: Time zone
304 field_time_zone: Time zone
305 field_searchable: Searchable
305 field_searchable: Searchable
306 field_default_value: Default value
306 field_default_value: Default value
307 field_comments_sorting: Display comments
307 field_comments_sorting: Display comments
308 field_parent_title: Parent page
308 field_parent_title: Parent page
309 field_editable: Editable
309 field_editable: Editable
310 field_watcher: Watcher
310 field_watcher: Watcher
311 field_identity_url: OpenID URL
311 field_identity_url: OpenID URL
312 field_content: Content
312 field_content: Content
313 field_group_by: Group results by
313 field_group_by: Group results by
314 field_sharing: Sharing
314 field_sharing: Sharing
315 field_parent_issue: Parent task
315 field_parent_issue: Parent task
316 field_member_of_group: "Assignee's group"
316 field_member_of_group: "Assignee's group"
317 field_assigned_to_role: "Assignee's role"
317 field_assigned_to_role: "Assignee's role"
318 field_text: Text field
318 field_text: Text field
319 field_visible: Visible
319 field_visible: Visible
320 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
320 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
321 field_issues_visibility: Issues visibility
321 field_issues_visibility: Issues visibility
322 field_is_private: Private
322 field_is_private: Private
323 field_commit_logs_encoding: Commit messages encoding
323 field_commit_logs_encoding: Commit messages encoding
324 field_scm_path_encoding: Path encoding
324 field_scm_path_encoding: Path encoding
325 field_path_to_repository: Path to repository
325 field_path_to_repository: Path to repository
326 field_root_directory: Root directory
326 field_root_directory: Root directory
327 field_cvsroot: CVSROOT
327 field_cvsroot: CVSROOT
328 field_cvs_module: Module
328 field_cvs_module: Module
329 field_repository_is_default: Main repository
329 field_repository_is_default: Main repository
330 field_multiple: Multiple values
330 field_multiple: Multiple values
331 field_auth_source_ldap_filter: LDAP filter
331 field_auth_source_ldap_filter: LDAP filter
332 field_core_fields: Standard fields
332 field_core_fields: Standard fields
333 field_timeout: "Timeout (in seconds)"
333 field_timeout: "Timeout (in seconds)"
334 field_board_parent: Parent forum
334 field_board_parent: Parent forum
335 field_private_notes: Private notes
335 field_private_notes: Private notes
336 field_inherit_members: Inherit members
336 field_inherit_members: Inherit members
337 field_generate_password: Generate password
337 field_generate_password: Generate password
338 field_must_change_passwd: Must change password at next logon
338 field_must_change_passwd: Must change password at next logon
339
339
340 setting_app_title: Application title
340 setting_app_title: Application title
341 setting_app_subtitle: Application subtitle
341 setting_app_subtitle: Application subtitle
342 setting_welcome_text: Welcome text
342 setting_welcome_text: Welcome text
343 setting_default_language: Default language
343 setting_default_language: Default language
344 setting_login_required: Authentication required
344 setting_login_required: Authentication required
345 setting_self_registration: Self-registration
345 setting_self_registration: Self-registration
346 setting_attachment_max_size: Maximum attachment size
346 setting_attachment_max_size: Maximum attachment size
347 setting_issues_export_limit: Issues export limit
347 setting_issues_export_limit: Issues export limit
348 setting_mail_from: Emission email address
348 setting_mail_from: Emission email address
349 setting_bcc_recipients: Blind carbon copy recipients (bcc)
349 setting_bcc_recipients: Blind carbon copy recipients (bcc)
350 setting_plain_text_mail: Plain text mail (no HTML)
350 setting_plain_text_mail: Plain text mail (no HTML)
351 setting_host_name: Host name and path
351 setting_host_name: Host name and path
352 setting_text_formatting: Text formatting
352 setting_text_formatting: Text formatting
353 setting_wiki_compression: Wiki history compression
353 setting_wiki_compression: Wiki history compression
354 setting_feeds_limit: Maximum number of items in Atom feeds
354 setting_feeds_limit: Maximum number of items in Atom feeds
355 setting_default_projects_public: New projects are public by default
355 setting_default_projects_public: New projects are public by default
356 setting_autofetch_changesets: Fetch commits automatically
356 setting_autofetch_changesets: Fetch commits automatically
357 setting_sys_api_enabled: Enable WS for repository management
357 setting_sys_api_enabled: Enable WS for repository management
358 setting_commit_ref_keywords: Referencing keywords
358 setting_commit_ref_keywords: Referencing keywords
359 setting_commit_fix_keywords: Fixing keywords
359 setting_commit_fix_keywords: Fixing keywords
360 setting_autologin: Autologin
360 setting_autologin: Autologin
361 setting_date_format: Date format
361 setting_date_format: Date format
362 setting_time_format: Time format
362 setting_time_format: Time format
363 setting_cross_project_issue_relations: Allow cross-project issue relations
363 setting_cross_project_issue_relations: Allow cross-project issue relations
364 setting_cross_project_subtasks: Allow cross-project subtasks
364 setting_cross_project_subtasks: Allow cross-project subtasks
365 setting_issue_list_default_columns: Default columns displayed on the issue list
365 setting_issue_list_default_columns: Default columns displayed on the issue list
366 setting_repositories_encodings: Attachments and repositories encodings
366 setting_repositories_encodings: Attachments and repositories encodings
367 setting_emails_header: Email header
367 setting_emails_header: Email header
368 setting_emails_footer: Email footer
368 setting_emails_footer: Email footer
369 setting_protocol: Protocol
369 setting_protocol: Protocol
370 setting_per_page_options: Objects per page options
370 setting_per_page_options: Objects per page options
371 setting_user_format: Users display format
371 setting_user_format: Users display format
372 setting_activity_days_default: Days displayed on project activity
372 setting_activity_days_default: Days displayed on project activity
373 setting_display_subprojects_issues: Display subprojects issues on main projects by default
373 setting_display_subprojects_issues: Display subprojects issues on main projects by default
374 setting_enabled_scm: Enabled SCM
374 setting_enabled_scm: Enabled SCM
375 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
375 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
376 setting_mail_handler_api_enabled: Enable WS for incoming emails
376 setting_mail_handler_api_enabled: Enable WS for incoming emails
377 setting_mail_handler_api_key: API key
377 setting_mail_handler_api_key: API key
378 setting_sequential_project_identifiers: Generate sequential project identifiers
378 setting_sequential_project_identifiers: Generate sequential project identifiers
379 setting_gravatar_enabled: Use Gravatar user icons
379 setting_gravatar_enabled: Use Gravatar user icons
380 setting_gravatar_default: Default Gravatar image
380 setting_gravatar_default: Default Gravatar image
381 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
381 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
382 setting_file_max_size_displayed: Maximum size of text files displayed inline
382 setting_file_max_size_displayed: Maximum size of text files displayed inline
383 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
383 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
384 setting_openid: Allow OpenID login and registration
384 setting_openid: Allow OpenID login and registration
385 setting_password_min_length: Minimum password length
385 setting_password_min_length: Minimum password length
386 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
386 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
387 setting_default_projects_modules: Default enabled modules for new projects
387 setting_default_projects_modules: Default enabled modules for new projects
388 setting_issue_done_ratio: Calculate the issue done ratio with
388 setting_issue_done_ratio: Calculate the issue done ratio with
389 setting_issue_done_ratio_issue_field: Use the issue field
389 setting_issue_done_ratio_issue_field: Use the issue field
390 setting_issue_done_ratio_issue_status: Use the issue status
390 setting_issue_done_ratio_issue_status: Use the issue status
391 setting_start_of_week: Start calendars on
391 setting_start_of_week: Start calendars on
392 setting_rest_api_enabled: Enable REST web service
392 setting_rest_api_enabled: Enable REST web service
393 setting_cache_formatted_text: Cache formatted text
393 setting_cache_formatted_text: Cache formatted text
394 setting_default_notification_option: Default notification option
394 setting_default_notification_option: Default notification option
395 setting_commit_logtime_enabled: Enable time logging
395 setting_commit_logtime_enabled: Enable time logging
396 setting_commit_logtime_activity_id: Activity for logged time
396 setting_commit_logtime_activity_id: Activity for logged time
397 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
397 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
398 setting_issue_group_assignment: Allow issue assignment to groups
398 setting_issue_group_assignment: Allow issue assignment to groups
399 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
399 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
400 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
400 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
401 setting_unsubscribe: Allow users to delete their own account
401 setting_unsubscribe: Allow users to delete their own account
402 setting_session_lifetime: Session maximum lifetime
402 setting_session_lifetime: Session maximum lifetime
403 setting_session_timeout: Session inactivity timeout
403 setting_session_timeout: Session inactivity timeout
404 setting_thumbnails_enabled: Display attachment thumbnails
404 setting_thumbnails_enabled: Display attachment thumbnails
405 setting_thumbnails_size: Thumbnails size (in pixels)
405 setting_thumbnails_size: Thumbnails size (in pixels)
406 setting_non_working_week_days: Non-working days
406 setting_non_working_week_days: Non-working days
407 setting_jsonp_enabled: Enable JSONP support
407 setting_jsonp_enabled: Enable JSONP support
408 setting_default_projects_tracker_ids: Default trackers for new projects
408 setting_default_projects_tracker_ids: Default trackers for new projects
409 setting_mail_handler_excluded_filenames: Exclude attachments by name
409 setting_mail_handler_excluded_filenames: Exclude attachments by name
410 setting_force_default_language_for_anonymous: Force default language for anonymous users
410 setting_force_default_language_for_anonymous: Force default language for anonymous users
411 setting_force_default_language_for_loggedin: Force default language for logged-in users
411 setting_force_default_language_for_loggedin: Force default language for logged-in users
412
412
413 permission_add_project: Create project
413 permission_add_project: Create project
414 permission_add_subprojects: Create subprojects
414 permission_add_subprojects: Create subprojects
415 permission_edit_project: Edit project
415 permission_edit_project: Edit project
416 permission_close_project: Close / reopen the project
416 permission_close_project: Close / reopen the project
417 permission_select_project_modules: Select project modules
417 permission_select_project_modules: Select project modules
418 permission_manage_members: Manage members
418 permission_manage_members: Manage members
419 permission_manage_project_activities: Manage project activities
419 permission_manage_project_activities: Manage project activities
420 permission_manage_versions: Manage versions
420 permission_manage_versions: Manage versions
421 permission_manage_categories: Manage issue categories
421 permission_manage_categories: Manage issue categories
422 permission_view_issues: View Issues
422 permission_view_issues: View Issues
423 permission_add_issues: Add issues
423 permission_add_issues: Add issues
424 permission_edit_issues: Edit issues
424 permission_edit_issues: Edit issues
425 permission_manage_issue_relations: Manage issue relations
425 permission_manage_issue_relations: Manage issue relations
426 permission_set_issues_private: Set issues public or private
426 permission_set_issues_private: Set issues public or private
427 permission_set_own_issues_private: Set own issues public or private
427 permission_set_own_issues_private: Set own issues public or private
428 permission_add_issue_notes: Add notes
428 permission_add_issue_notes: Add notes
429 permission_edit_issue_notes: Edit notes
429 permission_edit_issue_notes: Edit notes
430 permission_edit_own_issue_notes: Edit own notes
430 permission_edit_own_issue_notes: Edit own notes
431 permission_view_private_notes: View private notes
431 permission_view_private_notes: View private notes
432 permission_set_notes_private: Set notes as private
432 permission_set_notes_private: Set notes as private
433 permission_move_issues: Move issues
433 permission_move_issues: Move issues
434 permission_delete_issues: Delete issues
434 permission_delete_issues: Delete issues
435 permission_manage_public_queries: Manage public queries
435 permission_manage_public_queries: Manage public queries
436 permission_save_queries: Save queries
436 permission_save_queries: Save queries
437 permission_view_gantt: View gantt chart
437 permission_view_gantt: View gantt chart
438 permission_view_calendar: View calendar
438 permission_view_calendar: View calendar
439 permission_view_issue_watchers: View watchers list
439 permission_view_issue_watchers: View watchers list
440 permission_add_issue_watchers: Add watchers
440 permission_add_issue_watchers: Add watchers
441 permission_delete_issue_watchers: Delete watchers
441 permission_delete_issue_watchers: Delete watchers
442 permission_log_time: Log spent time
442 permission_log_time: Log spent time
443 permission_view_time_entries: View spent time
443 permission_view_time_entries: View spent time
444 permission_edit_time_entries: Edit time logs
444 permission_edit_time_entries: Edit time logs
445 permission_edit_own_time_entries: Edit own time logs
445 permission_edit_own_time_entries: Edit own time logs
446 permission_manage_news: Manage news
446 permission_manage_news: Manage news
447 permission_comment_news: Comment news
447 permission_comment_news: Comment news
448 permission_view_documents: View documents
448 permission_view_documents: View documents
449 permission_add_documents: Add documents
449 permission_add_documents: Add documents
450 permission_edit_documents: Edit documents
450 permission_edit_documents: Edit documents
451 permission_delete_documents: Delete documents
451 permission_delete_documents: Delete documents
452 permission_manage_files: Manage files
452 permission_manage_files: Manage files
453 permission_view_files: View files
453 permission_view_files: View files
454 permission_manage_wiki: Manage wiki
454 permission_manage_wiki: Manage wiki
455 permission_rename_wiki_pages: Rename wiki pages
455 permission_rename_wiki_pages: Rename wiki pages
456 permission_delete_wiki_pages: Delete wiki pages
456 permission_delete_wiki_pages: Delete wiki pages
457 permission_view_wiki_pages: View wiki
457 permission_view_wiki_pages: View wiki
458 permission_view_wiki_edits: View wiki history
458 permission_view_wiki_edits: View wiki history
459 permission_edit_wiki_pages: Edit wiki pages
459 permission_edit_wiki_pages: Edit wiki pages
460 permission_delete_wiki_pages_attachments: Delete attachments
460 permission_delete_wiki_pages_attachments: Delete attachments
461 permission_protect_wiki_pages: Protect wiki pages
461 permission_protect_wiki_pages: Protect wiki pages
462 permission_manage_repository: Manage repository
462 permission_manage_repository: Manage repository
463 permission_browse_repository: Browse repository
463 permission_browse_repository: Browse repository
464 permission_view_changesets: View changesets
464 permission_view_changesets: View changesets
465 permission_commit_access: Commit access
465 permission_commit_access: Commit access
466 permission_manage_boards: Manage forums
466 permission_manage_boards: Manage forums
467 permission_view_messages: View messages
467 permission_view_messages: View messages
468 permission_add_messages: Post messages
468 permission_add_messages: Post messages
469 permission_edit_messages: Edit messages
469 permission_edit_messages: Edit messages
470 permission_edit_own_messages: Edit own messages
470 permission_edit_own_messages: Edit own messages
471 permission_delete_messages: Delete messages
471 permission_delete_messages: Delete messages
472 permission_delete_own_messages: Delete own messages
472 permission_delete_own_messages: Delete own messages
473 permission_export_wiki_pages: Export wiki pages
473 permission_export_wiki_pages: Export wiki pages
474 permission_manage_subtasks: Manage subtasks
474 permission_manage_subtasks: Manage subtasks
475 permission_manage_related_issues: Manage related issues
475 permission_manage_related_issues: Manage related issues
476
476
477 project_module_issue_tracking: Issue tracking
477 project_module_issue_tracking: Issue tracking
478 project_module_time_tracking: Time tracking
478 project_module_time_tracking: Time tracking
479 project_module_news: News
479 project_module_news: News
480 project_module_documents: Documents
480 project_module_documents: Documents
481 project_module_files: Files
481 project_module_files: Files
482 project_module_wiki: Wiki
482 project_module_wiki: Wiki
483 project_module_repository: Repository
483 project_module_repository: Repository
484 project_module_boards: Forums
484 project_module_boards: Forums
485 project_module_calendar: Calendar
485 project_module_calendar: Calendar
486 project_module_gantt: Gantt
486 project_module_gantt: Gantt
487
487
488 label_user: User
488 label_user: User
489 label_user_plural: Users
489 label_user_plural: Users
490 label_user_new: New user
490 label_user_new: New user
491 label_user_anonymous: Anonymous
491 label_user_anonymous: Anonymous
492 label_project: Project
492 label_project: Project
493 label_project_new: New project
493 label_project_new: New project
494 label_project_plural: Projects
494 label_project_plural: Projects
495 label_x_projects:
495 label_x_projects:
496 zero: no projects
496 zero: no projects
497 one: 1 project
497 one: 1 project
498 other: "%{count} projects"
498 other: "%{count} projects"
499 label_project_all: All Projects
499 label_project_all: All Projects
500 label_project_latest: Latest projects
500 label_project_latest: Latest projects
501 label_issue: Issue
501 label_issue: Issue
502 label_issue_new: New issue
502 label_issue_new: New issue
503 label_issue_plural: Issues
503 label_issue_plural: Issues
504 label_issue_view_all: View all issues
504 label_issue_view_all: View all issues
505 label_issues_by: "Issues by %{value}"
505 label_issues_by: "Issues by %{value}"
506 label_issue_added: Issue added
506 label_issue_added: Issue added
507 label_issue_updated: Issue updated
507 label_issue_updated: Issue updated
508 label_issue_note_added: Note added
508 label_issue_note_added: Note added
509 label_issue_status_updated: Status updated
509 label_issue_status_updated: Status updated
510 label_issue_assigned_to_updated: Assignee updated
510 label_issue_assigned_to_updated: Assignee updated
511 label_issue_priority_updated: Priority updated
511 label_issue_priority_updated: Priority updated
512 label_document: Document
512 label_document: Document
513 label_document_new: New document
513 label_document_new: New document
514 label_document_plural: Documents
514 label_document_plural: Documents
515 label_document_added: Document added
515 label_document_added: Document added
516 label_role: Role
516 label_role: Role
517 label_role_plural: Roles
517 label_role_plural: Roles
518 label_role_new: New role
518 label_role_new: New role
519 label_role_and_permissions: Roles and permissions
519 label_role_and_permissions: Roles and permissions
520 label_role_anonymous: Anonymous
520 label_role_anonymous: Anonymous
521 label_role_non_member: Non member
521 label_role_non_member: Non member
522 label_member: Member
522 label_member: Member
523 label_member_new: New member
523 label_member_new: New member
524 label_member_plural: Members
524 label_member_plural: Members
525 label_tracker: Tracker
525 label_tracker: Tracker
526 label_tracker_plural: Trackers
526 label_tracker_plural: Trackers
527 label_tracker_new: New tracker
527 label_tracker_new: New tracker
528 label_workflow: Workflow
528 label_workflow: Workflow
529 label_issue_status: Issue status
529 label_issue_status: Issue status
530 label_issue_status_plural: Issue statuses
530 label_issue_status_plural: Issue statuses
531 label_issue_status_new: New status
531 label_issue_status_new: New status
532 label_issue_category: Issue category
532 label_issue_category: Issue category
533 label_issue_category_plural: Issue categories
533 label_issue_category_plural: Issue categories
534 label_issue_category_new: New category
534 label_issue_category_new: New category
535 label_custom_field: Custom field
535 label_custom_field: Custom field
536 label_custom_field_plural: Custom fields
536 label_custom_field_plural: Custom fields
537 label_custom_field_new: New custom field
537 label_custom_field_new: New custom field
538 label_enumerations: Enumerations
538 label_enumerations: Enumerations
539 label_enumeration_new: New value
539 label_enumeration_new: New value
540 label_information: Information
540 label_information: Information
541 label_information_plural: Information
541 label_information_plural: Information
542 label_please_login: Please log in
542 label_please_login: Please log in
543 label_register: Register
543 label_register: Register
544 label_login_with_open_id_option: or login with OpenID
544 label_login_with_open_id_option: or login with OpenID
545 label_password_lost: Lost password
545 label_password_lost: Lost password
546 label_home: Home
546 label_home: Home
547 label_my_page: My page
547 label_my_page: My page
548 label_my_account: My account
548 label_my_account: My account
549 label_my_projects: My projects
549 label_my_projects: My projects
550 label_my_page_block: My page block
550 label_my_page_block: My page block
551 label_administration: Administration
551 label_administration: Administration
552 label_login: Sign in
552 label_login: Sign in
553 label_logout: Sign out
553 label_logout: Sign out
554 label_help: Help
554 label_help: Help
555 label_reported_issues: Reported issues
555 label_reported_issues: Reported issues
556 label_assigned_to_me_issues: Issues assigned to me
556 label_assigned_to_me_issues: Issues assigned to me
557 label_last_login: Last connection
557 label_last_login: Last connection
558 label_registered_on: Registered on
558 label_registered_on: Registered on
559 label_activity: Activity
559 label_activity: Activity
560 label_overall_activity: Overall activity
560 label_overall_activity: Overall activity
561 label_user_activity: "%{value}'s activity"
561 label_user_activity: "%{value}'s activity"
562 label_new: New
562 label_new: New
563 label_logged_as: Logged in as
563 label_logged_as: Logged in as
564 label_environment: Environment
564 label_environment: Environment
565 label_authentication: Authentication
565 label_authentication: Authentication
566 label_auth_source: Authentication mode
566 label_auth_source: Authentication mode
567 label_auth_source_new: New authentication mode
567 label_auth_source_new: New authentication mode
568 label_auth_source_plural: Authentication modes
568 label_auth_source_plural: Authentication modes
569 label_subproject_plural: Subprojects
569 label_subproject_plural: Subprojects
570 label_subproject_new: New subproject
570 label_subproject_new: New subproject
571 label_and_its_subprojects: "%{value} and its subprojects"
571 label_and_its_subprojects: "%{value} and its subprojects"
572 label_min_max_length: Min - Max length
572 label_min_max_length: Min - Max length
573 label_list: List
573 label_list: List
574 label_date: Date
574 label_date: Date
575 label_integer: Integer
575 label_integer: Integer
576 label_float: Float
576 label_float: Float
577 label_boolean: Boolean
577 label_boolean: Boolean
578 label_string: Text
578 label_string: Text
579 label_text: Long text
579 label_text: Long text
580 label_attribute: Attribute
580 label_attribute: Attribute
581 label_attribute_plural: Attributes
581 label_attribute_plural: Attributes
582 label_no_data: No data to display
582 label_no_data: No data to display
583 label_change_status: Change status
583 label_change_status: Change status
584 label_history: History
584 label_history: History
585 label_attachment: File
585 label_attachment: File
586 label_attachment_new: New file
586 label_attachment_new: New file
587 label_attachment_delete: Delete file
587 label_attachment_delete: Delete file
588 label_attachment_plural: Files
588 label_attachment_plural: Files
589 label_file_added: File added
589 label_file_added: File added
590 label_report: Report
590 label_report: Report
591 label_report_plural: Reports
591 label_report_plural: Reports
592 label_news: News
592 label_news: News
593 label_news_new: Add news
593 label_news_new: Add news
594 label_news_plural: News
594 label_news_plural: News
595 label_news_latest: Latest news
595 label_news_latest: Latest news
596 label_news_view_all: View all news
596 label_news_view_all: View all news
597 label_news_added: News added
597 label_news_added: News added
598 label_news_comment_added: Comment added to a news
598 label_news_comment_added: Comment added to a news
599 label_settings: Settings
599 label_settings: Settings
600 label_overview: Overview
600 label_overview: Overview
601 label_version: Version
601 label_version: Version
602 label_version_new: New version
602 label_version_new: New version
603 label_version_plural: Versions
603 label_version_plural: Versions
604 label_close_versions: Close completed versions
604 label_close_versions: Close completed versions
605 label_confirmation: Confirmation
605 label_confirmation: Confirmation
606 label_export_to: 'Also available in:'
606 label_export_to: 'Also available in:'
607 label_read: Read...
607 label_read: Read...
608 label_public_projects: Public projects
608 label_public_projects: Public projects
609 label_open_issues: open
609 label_open_issues: open
610 label_open_issues_plural: open
610 label_open_issues_plural: open
611 label_closed_issues: closed
611 label_closed_issues: closed
612 label_closed_issues_plural: closed
612 label_closed_issues_plural: closed
613 label_x_open_issues_abbr_on_total:
613 label_x_open_issues_abbr_on_total:
614 zero: 0 open / %{total}
614 zero: 0 open / %{total}
615 one: 1 open / %{total}
615 one: 1 open / %{total}
616 other: "%{count} open / %{total}"
616 other: "%{count} open / %{total}"
617 label_x_open_issues_abbr:
617 label_x_open_issues_abbr:
618 zero: 0 open
618 zero: 0 open
619 one: 1 open
619 one: 1 open
620 other: "%{count} open"
620 other: "%{count} open"
621 label_x_closed_issues_abbr:
621 label_x_closed_issues_abbr:
622 zero: 0 closed
622 zero: 0 closed
623 one: 1 closed
623 one: 1 closed
624 other: "%{count} closed"
624 other: "%{count} closed"
625 label_x_issues:
625 label_x_issues:
626 zero: 0 issues
626 zero: 0 issues
627 one: 1 issue
627 one: 1 issue
628 other: "%{count} issues"
628 other: "%{count} issues"
629 label_total: Total
629 label_total: Total
630 label_total_time: Total time
630 label_total_time: Total time
631 label_permissions: Permissions
631 label_permissions: Permissions
632 label_current_status: Current status
632 label_current_status: Current status
633 label_new_statuses_allowed: New statuses allowed
633 label_new_statuses_allowed: New statuses allowed
634 label_all: all
634 label_all: all
635 label_any: any
635 label_any: any
636 label_none: none
636 label_none: none
637 label_nobody: nobody
637 label_nobody: nobody
638 label_next: Next
638 label_next: Next
639 label_previous: Previous
639 label_previous: Previous
640 label_used_by: Used by
640 label_used_by: Used by
641 label_details: Details
641 label_details: Details
642 label_add_note: Add a note
642 label_add_note: Add a note
643 label_per_page: Per page
643 label_per_page: Per page
644 label_calendar: Calendar
644 label_calendar: Calendar
645 label_months_from: months from
645 label_months_from: months from
646 label_gantt: Gantt
646 label_gantt: Gantt
647 label_internal: Internal
647 label_internal: Internal
648 label_last_changes: "last %{count} changes"
648 label_last_changes: "last %{count} changes"
649 label_change_view_all: View all changes
649 label_change_view_all: View all changes
650 label_personalize_page: Personalize this page
650 label_personalize_page: Personalize this page
651 label_comment: Comment
651 label_comment: Comment
652 label_comment_plural: Comments
652 label_comment_plural: Comments
653 label_x_comments:
653 label_x_comments:
654 zero: no comments
654 zero: no comments
655 one: 1 comment
655 one: 1 comment
656 other: "%{count} comments"
656 other: "%{count} comments"
657 label_comment_add: Add a comment
657 label_comment_add: Add a comment
658 label_comment_added: Comment added
658 label_comment_added: Comment added
659 label_comment_delete: Delete comments
659 label_comment_delete: Delete comments
660 label_query: Custom query
660 label_query: Custom query
661 label_query_plural: Custom queries
661 label_query_plural: Custom queries
662 label_query_new: New query
662 label_query_new: New query
663 label_my_queries: My custom queries
663 label_my_queries: My custom queries
664 label_filter_add: Add filter
664 label_filter_add: Add filter
665 label_filter_plural: Filters
665 label_filter_plural: Filters
666 label_equals: is
666 label_equals: is
667 label_not_equals: is not
667 label_not_equals: is not
668 label_in_less_than: in less than
668 label_in_less_than: in less than
669 label_in_more_than: in more than
669 label_in_more_than: in more than
670 label_in_the_next_days: in the next
670 label_in_the_next_days: in the next
671 label_in_the_past_days: in the past
671 label_in_the_past_days: in the past
672 label_greater_or_equal: '>='
672 label_greater_or_equal: '>='
673 label_less_or_equal: '<='
673 label_less_or_equal: '<='
674 label_between: between
674 label_between: between
675 label_in: in
675 label_in: in
676 label_today: today
676 label_today: today
677 label_all_time: all time
677 label_all_time: all time
678 label_yesterday: yesterday
678 label_yesterday: yesterday
679 label_this_week: this week
679 label_this_week: this week
680 label_last_week: last week
680 label_last_week: last week
681 label_last_n_weeks: "last %{count} weeks"
681 label_last_n_weeks: "last %{count} weeks"
682 label_last_n_days: "last %{count} days"
682 label_last_n_days: "last %{count} days"
683 label_this_month: this month
683 label_this_month: this month
684 label_last_month: last month
684 label_last_month: last month
685 label_this_year: this year
685 label_this_year: this year
686 label_date_range: Date range
686 label_date_range: Date range
687 label_less_than_ago: less than days ago
687 label_less_than_ago: less than days ago
688 label_more_than_ago: more than days ago
688 label_more_than_ago: more than days ago
689 label_ago: days ago
689 label_ago: days ago
690 label_contains: contains
690 label_contains: contains
691 label_not_contains: doesn't contain
691 label_not_contains: doesn't contain
692 label_any_issues_in_project: any issues in project
692 label_any_issues_in_project: any issues in project
693 label_any_issues_not_in_project: any issues not in project
693 label_any_issues_not_in_project: any issues not in project
694 label_no_issues_in_project: no issues in project
694 label_no_issues_in_project: no issues in project
695 label_day_plural: days
695 label_day_plural: days
696 label_repository: Repository
696 label_repository: Repository
697 label_repository_new: New repository
697 label_repository_new: New repository
698 label_repository_plural: Repositories
698 label_repository_plural: Repositories
699 label_browse: Browse
699 label_browse: Browse
700 label_branch: Branch
700 label_branch: Branch
701 label_tag: Tag
701 label_tag: Tag
702 label_revision: Revision
702 label_revision: Revision
703 label_revision_plural: Revisions
703 label_revision_plural: Revisions
704 label_revision_id: "Revision %{value}"
704 label_revision_id: "Revision %{value}"
705 label_associated_revisions: Associated revisions
705 label_associated_revisions: Associated revisions
706 label_added: added
706 label_added: added
707 label_modified: modified
707 label_modified: modified
708 label_copied: copied
708 label_copied: copied
709 label_renamed: renamed
709 label_renamed: renamed
710 label_deleted: deleted
710 label_deleted: deleted
711 label_latest_revision: Latest revision
711 label_latest_revision: Latest revision
712 label_latest_revision_plural: Latest revisions
712 label_latest_revision_plural: Latest revisions
713 label_view_revisions: View revisions
713 label_view_revisions: View revisions
714 label_view_all_revisions: View all revisions
714 label_view_all_revisions: View all revisions
715 label_max_size: Maximum size
715 label_max_size: Maximum size
716 label_sort_highest: Move to top
716 label_sort_highest: Move to top
717 label_sort_higher: Move up
717 label_sort_higher: Move up
718 label_sort_lower: Move down
718 label_sort_lower: Move down
719 label_sort_lowest: Move to bottom
719 label_sort_lowest: Move to bottom
720 label_roadmap: Roadmap
720 label_roadmap: Roadmap
721 label_roadmap_due_in: "Due in %{value}"
721 label_roadmap_due_in: "Due in %{value}"
722 label_roadmap_overdue: "%{value} late"
722 label_roadmap_overdue: "%{value} late"
723 label_roadmap_no_issues: No issues for this version
723 label_roadmap_no_issues: No issues for this version
724 label_search: Search
724 label_search: Search
725 label_result_plural: Results
725 label_result_plural: Results
726 label_all_words: All words
726 label_all_words: All words
727 label_wiki: Wiki
727 label_wiki: Wiki
728 label_wiki_edit: Wiki edit
728 label_wiki_edit: Wiki edit
729 label_wiki_edit_plural: Wiki edits
729 label_wiki_edit_plural: Wiki edits
730 label_wiki_page: Wiki page
730 label_wiki_page: Wiki page
731 label_wiki_page_plural: Wiki pages
731 label_wiki_page_plural: Wiki pages
732 label_index_by_title: Index by title
732 label_index_by_title: Index by title
733 label_index_by_date: Index by date
733 label_index_by_date: Index by date
734 label_current_version: Current version
734 label_current_version: Current version
735 label_preview: Preview
735 label_preview: Preview
736 label_feed_plural: Feeds
736 label_feed_plural: Feeds
737 label_changes_details: Details of all changes
737 label_changes_details: Details of all changes
738 label_issue_tracking: Issue tracking
738 label_issue_tracking: Issue tracking
739 label_spent_time: Spent time
739 label_spent_time: Spent time
740 label_overall_spent_time: Overall spent time
740 label_overall_spent_time: Overall spent time
741 label_f_hour: "%{value} hour"
741 label_f_hour: "%{value} hour"
742 label_f_hour_plural: "%{value} hours"
742 label_f_hour_plural: "%{value} hours"
743 label_time_tracking: Time tracking
743 label_time_tracking: Time tracking
744 label_change_plural: Changes
744 label_change_plural: Changes
745 label_statistics: Statistics
745 label_statistics: Statistics
746 label_commits_per_month: Commits per month
746 label_commits_per_month: Commits per month
747 label_commits_per_author: Commits per author
747 label_commits_per_author: Commits per author
748 label_diff: diff
748 label_diff: diff
749 label_view_diff: View differences
749 label_view_diff: View differences
750 label_diff_inline: inline
750 label_diff_inline: inline
751 label_diff_side_by_side: side by side
751 label_diff_side_by_side: side by side
752 label_options: Options
752 label_options: Options
753 label_copy_workflow_from: Copy workflow from
753 label_copy_workflow_from: Copy workflow from
754 label_permissions_report: Permissions report
754 label_permissions_report: Permissions report
755 label_watched_issues: Watched issues
755 label_watched_issues: Watched issues
756 label_related_issues: Related issues
756 label_related_issues: Related issues
757 label_applied_status: Applied status
757 label_applied_status: Applied status
758 label_loading: Loading...
758 label_loading: Loading...
759 label_relation_new: New relation
759 label_relation_new: New relation
760 label_relation_delete: Delete relation
760 label_relation_delete: Delete relation
761 label_relates_to: Related to
761 label_relates_to: Related to
762 label_duplicates: Duplicates
762 label_duplicates: Duplicates
763 label_duplicated_by: Duplicated by
763 label_duplicated_by: Duplicated by
764 label_blocks: Blocks
764 label_blocks: Blocks
765 label_blocked_by: Blocked by
765 label_blocked_by: Blocked by
766 label_precedes: Precedes
766 label_precedes: Precedes
767 label_follows: Follows
767 label_follows: Follows
768 label_copied_to: Copied to
768 label_copied_to: Copied to
769 label_copied_from: Copied from
769 label_copied_from: Copied from
770 label_end_to_start: end to start
770 label_end_to_start: end to start
771 label_end_to_end: end to end
771 label_end_to_end: end to end
772 label_start_to_start: start to start
772 label_start_to_start: start to start
773 label_start_to_end: start to end
773 label_start_to_end: start to end
774 label_stay_logged_in: Stay logged in
774 label_stay_logged_in: Stay logged in
775 label_disabled: disabled
775 label_disabled: disabled
776 label_show_completed_versions: Show completed versions
776 label_show_completed_versions: Show completed versions
777 label_me: me
777 label_me: me
778 label_board: Forum
778 label_board: Forum
779 label_board_new: New forum
779 label_board_new: New forum
780 label_board_plural: Forums
780 label_board_plural: Forums
781 label_board_locked: Locked
781 label_board_locked: Locked
782 label_board_sticky: Sticky
782 label_board_sticky: Sticky
783 label_topic_plural: Topics
783 label_topic_plural: Topics
784 label_message_plural: Messages
784 label_message_plural: Messages
785 label_message_last: Last message
785 label_message_last: Last message
786 label_message_new: New message
786 label_message_new: New message
787 label_message_posted: Message added
787 label_message_posted: Message added
788 label_reply_plural: Replies
788 label_reply_plural: Replies
789 label_send_information: Send account information to the user
789 label_send_information: Send account information to the user
790 label_year: Year
790 label_year: Year
791 label_month: Month
791 label_month: Month
792 label_week: Week
792 label_week: Week
793 label_date_from: From
793 label_date_from: From
794 label_date_to: To
794 label_date_to: To
795 label_language_based: Based on user's language
795 label_language_based: Based on user's language
796 label_sort_by: "Sort by %{value}"
796 label_sort_by: "Sort by %{value}"
797 label_send_test_email: Send a test email
797 label_send_test_email: Send a test email
798 label_feeds_access_key: Atom access key
798 label_feeds_access_key: Atom access key
799 label_missing_feeds_access_key: Missing a Atom access key
799 label_missing_feeds_access_key: Missing a Atom access key
800 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
800 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
801 label_module_plural: Modules
801 label_module_plural: Modules
802 label_added_time_by: "Added by %{author} %{age} ago"
802 label_added_time_by: "Added by %{author} %{age} ago"
803 label_updated_time_by: "Updated by %{author} %{age} ago"
803 label_updated_time_by: "Updated by %{author} %{age} ago"
804 label_updated_time: "Updated %{value} ago"
804 label_updated_time: "Updated %{value} ago"
805 label_jump_to_a_project: Jump to a project...
805 label_jump_to_a_project: Jump to a project...
806 label_file_plural: Files
806 label_file_plural: Files
807 label_changeset_plural: Changesets
807 label_changeset_plural: Changesets
808 label_default_columns: Default columns
808 label_default_columns: Default columns
809 label_no_change_option: (No change)
809 label_no_change_option: (No change)
810 label_bulk_edit_selected_issues: Bulk edit selected issues
810 label_bulk_edit_selected_issues: Bulk edit selected issues
811 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
811 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
812 label_theme: Theme
812 label_theme: Theme
813 label_default: Default
813 label_default: Default
814 label_search_titles_only: Search titles only
814 label_search_titles_only: Search titles only
815 label_user_mail_option_all: "For any event on all my projects"
815 label_user_mail_option_all: "For any event on all my projects"
816 label_user_mail_option_selected: "For any event on the selected projects only..."
816 label_user_mail_option_selected: "For any event on the selected projects only..."
817 label_user_mail_option_none: "No events"
817 label_user_mail_option_none: "No events"
818 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
818 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
819 label_user_mail_option_only_assigned: "Only for things I am assigned to"
819 label_user_mail_option_only_assigned: "Only for things I am assigned to"
820 label_user_mail_option_only_owner: "Only for things I am the owner of"
820 label_user_mail_option_only_owner: "Only for things I am the owner of"
821 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
821 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
822 label_registration_activation_by_email: account activation by email
822 label_registration_activation_by_email: account activation by email
823 label_registration_manual_activation: manual account activation
823 label_registration_manual_activation: manual account activation
824 label_registration_automatic_activation: automatic account activation
824 label_registration_automatic_activation: automatic account activation
825 label_display_per_page: "Per page: %{value}"
825 label_display_per_page: "Per page: %{value}"
826 label_age: Age
826 label_age: Age
827 label_change_properties: Change properties
827 label_change_properties: Change properties
828 label_general: General
828 label_general: General
829 label_more: More
829 label_more: More
830 label_scm: SCM
830 label_scm: SCM
831 label_plugins: Plugins
831 label_plugins: Plugins
832 label_ldap_authentication: LDAP authentication
832 label_ldap_authentication: LDAP authentication
833 label_downloads_abbr: D/L
833 label_downloads_abbr: D/L
834 label_optional_description: Optional description
834 label_optional_description: Optional description
835 label_add_another_file: Add another file
835 label_add_another_file: Add another file
836 label_preferences: Preferences
836 label_preferences: Preferences
837 label_chronological_order: In chronological order
837 label_chronological_order: In chronological order
838 label_reverse_chronological_order: In reverse chronological order
838 label_reverse_chronological_order: In reverse chronological order
839 label_planning: Planning
839 label_planning: Planning
840 label_incoming_emails: Incoming emails
840 label_incoming_emails: Incoming emails
841 label_generate_key: Generate a key
841 label_generate_key: Generate a key
842 label_issue_watchers: Watchers
842 label_issue_watchers: Watchers
843 label_example: Example
843 label_example: Example
844 label_display: Display
844 label_display: Display
845 label_sort: Sort
845 label_sort: Sort
846 label_ascending: Ascending
846 label_ascending: Ascending
847 label_descending: Descending
847 label_descending: Descending
848 label_date_from_to: From %{start} to %{end}
848 label_date_from_to: From %{start} to %{end}
849 label_wiki_content_added: Wiki page added
849 label_wiki_content_added: Wiki page added
850 label_wiki_content_updated: Wiki page updated
850 label_wiki_content_updated: Wiki page updated
851 label_group: Group
851 label_group: Group
852 label_group_plural: Groups
852 label_group_plural: Groups
853 label_group_new: New group
853 label_group_new: New group
854 label_group_anonymous: Anonymous users
855 label_group_non_member: Non member users
854 label_time_entry_plural: Spent time
856 label_time_entry_plural: Spent time
855 label_version_sharing_none: Not shared
857 label_version_sharing_none: Not shared
856 label_version_sharing_descendants: With subprojects
858 label_version_sharing_descendants: With subprojects
857 label_version_sharing_hierarchy: With project hierarchy
859 label_version_sharing_hierarchy: With project hierarchy
858 label_version_sharing_tree: With project tree
860 label_version_sharing_tree: With project tree
859 label_version_sharing_system: With all projects
861 label_version_sharing_system: With all projects
860 label_update_issue_done_ratios: Update issue done ratios
862 label_update_issue_done_ratios: Update issue done ratios
861 label_copy_source: Source
863 label_copy_source: Source
862 label_copy_target: Target
864 label_copy_target: Target
863 label_copy_same_as_target: Same as target
865 label_copy_same_as_target: Same as target
864 label_display_used_statuses_only: Only display statuses that are used by this tracker
866 label_display_used_statuses_only: Only display statuses that are used by this tracker
865 label_api_access_key: API access key
867 label_api_access_key: API access key
866 label_missing_api_access_key: Missing an API access key
868 label_missing_api_access_key: Missing an API access key
867 label_api_access_key_created_on: "API access key created %{value} ago"
869 label_api_access_key_created_on: "API access key created %{value} ago"
868 label_profile: Profile
870 label_profile: Profile
869 label_subtask_plural: Subtasks
871 label_subtask_plural: Subtasks
870 label_project_copy_notifications: Send email notifications during the project copy
872 label_project_copy_notifications: Send email notifications during the project copy
871 label_principal_search: "Search for user or group:"
873 label_principal_search: "Search for user or group:"
872 label_user_search: "Search for user:"
874 label_user_search: "Search for user:"
873 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
875 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
874 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
876 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
875 label_issues_visibility_all: All issues
877 label_issues_visibility_all: All issues
876 label_issues_visibility_public: All non private issues
878 label_issues_visibility_public: All non private issues
877 label_issues_visibility_own: Issues created by or assigned to the user
879 label_issues_visibility_own: Issues created by or assigned to the user
878 label_git_report_last_commit: Report last commit for files and directories
880 label_git_report_last_commit: Report last commit for files and directories
879 label_parent_revision: Parent
881 label_parent_revision: Parent
880 label_child_revision: Child
882 label_child_revision: Child
881 label_export_options: "%{export_format} export options"
883 label_export_options: "%{export_format} export options"
882 label_copy_attachments: Copy attachments
884 label_copy_attachments: Copy attachments
883 label_copy_subtasks: Copy subtasks
885 label_copy_subtasks: Copy subtasks
884 label_item_position: "%{position} of %{count}"
886 label_item_position: "%{position} of %{count}"
885 label_completed_versions: Completed versions
887 label_completed_versions: Completed versions
886 label_search_for_watchers: Search for watchers to add
888 label_search_for_watchers: Search for watchers to add
887 label_session_expiration: Session expiration
889 label_session_expiration: Session expiration
888 label_show_closed_projects: View closed projects
890 label_show_closed_projects: View closed projects
889 label_status_transitions: Status transitions
891 label_status_transitions: Status transitions
890 label_fields_permissions: Fields permissions
892 label_fields_permissions: Fields permissions
891 label_readonly: Read-only
893 label_readonly: Read-only
892 label_required: Required
894 label_required: Required
893 label_hidden: Hidden
895 label_hidden: Hidden
894 label_attribute_of_project: "Project's %{name}"
896 label_attribute_of_project: "Project's %{name}"
895 label_attribute_of_issue: "Issue's %{name}"
897 label_attribute_of_issue: "Issue's %{name}"
896 label_attribute_of_author: "Author's %{name}"
898 label_attribute_of_author: "Author's %{name}"
897 label_attribute_of_assigned_to: "Assignee's %{name}"
899 label_attribute_of_assigned_to: "Assignee's %{name}"
898 label_attribute_of_user: "User's %{name}"
900 label_attribute_of_user: "User's %{name}"
899 label_attribute_of_fixed_version: "Target version's %{name}"
901 label_attribute_of_fixed_version: "Target version's %{name}"
900 label_cross_project_descendants: With subprojects
902 label_cross_project_descendants: With subprojects
901 label_cross_project_tree: With project tree
903 label_cross_project_tree: With project tree
902 label_cross_project_hierarchy: With project hierarchy
904 label_cross_project_hierarchy: With project hierarchy
903 label_cross_project_system: With all projects
905 label_cross_project_system: With all projects
904 label_gantt_progress_line: Progress line
906 label_gantt_progress_line: Progress line
905 label_visibility_private: to me only
907 label_visibility_private: to me only
906 label_visibility_roles: to these roles only
908 label_visibility_roles: to these roles only
907 label_visibility_public: to any users
909 label_visibility_public: to any users
908 label_link: Link
910 label_link: Link
909 label_only: only
911 label_only: only
910 label_drop_down_list: drop-down list
912 label_drop_down_list: drop-down list
911 label_checkboxes: checkboxes
913 label_checkboxes: checkboxes
912 label_radio_buttons: radio buttons
914 label_radio_buttons: radio buttons
913 label_link_values_to: Link values to URL
915 label_link_values_to: Link values to URL
914 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
916 label_custom_field_select_type: Select the type of object to which the custom field is to be attached
915 label_check_for_updates: Check for updates
917 label_check_for_updates: Check for updates
916 label_latest_compatible_version: Latest compatible version
918 label_latest_compatible_version: Latest compatible version
917 label_unknown_plugin: Unknown plugin
919 label_unknown_plugin: Unknown plugin
918
920
919 button_login: Login
921 button_login: Login
920 button_submit: Submit
922 button_submit: Submit
921 button_save: Save
923 button_save: Save
922 button_check_all: Check all
924 button_check_all: Check all
923 button_uncheck_all: Uncheck all
925 button_uncheck_all: Uncheck all
924 button_collapse_all: Collapse all
926 button_collapse_all: Collapse all
925 button_expand_all: Expand all
927 button_expand_all: Expand all
926 button_delete: Delete
928 button_delete: Delete
927 button_create: Create
929 button_create: Create
928 button_create_and_continue: Create and continue
930 button_create_and_continue: Create and continue
929 button_test: Test
931 button_test: Test
930 button_edit: Edit
932 button_edit: Edit
931 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
933 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
932 button_add: Add
934 button_add: Add
933 button_change: Change
935 button_change: Change
934 button_apply: Apply
936 button_apply: Apply
935 button_clear: Clear
937 button_clear: Clear
936 button_lock: Lock
938 button_lock: Lock
937 button_unlock: Unlock
939 button_unlock: Unlock
938 button_download: Download
940 button_download: Download
939 button_list: List
941 button_list: List
940 button_view: View
942 button_view: View
941 button_move: Move
943 button_move: Move
942 button_move_and_follow: Move and follow
944 button_move_and_follow: Move and follow
943 button_back: Back
945 button_back: Back
944 button_cancel: Cancel
946 button_cancel: Cancel
945 button_activate: Activate
947 button_activate: Activate
946 button_sort: Sort
948 button_sort: Sort
947 button_log_time: Log time
949 button_log_time: Log time
948 button_rollback: Rollback to this version
950 button_rollback: Rollback to this version
949 button_watch: Watch
951 button_watch: Watch
950 button_unwatch: Unwatch
952 button_unwatch: Unwatch
951 button_reply: Reply
953 button_reply: Reply
952 button_archive: Archive
954 button_archive: Archive
953 button_unarchive: Unarchive
955 button_unarchive: Unarchive
954 button_reset: Reset
956 button_reset: Reset
955 button_rename: Rename
957 button_rename: Rename
956 button_change_password: Change password
958 button_change_password: Change password
957 button_copy: Copy
959 button_copy: Copy
958 button_copy_and_follow: Copy and follow
960 button_copy_and_follow: Copy and follow
959 button_annotate: Annotate
961 button_annotate: Annotate
960 button_update: Update
962 button_update: Update
961 button_configure: Configure
963 button_configure: Configure
962 button_quote: Quote
964 button_quote: Quote
963 button_duplicate: Duplicate
965 button_duplicate: Duplicate
964 button_show: Show
966 button_show: Show
965 button_hide: Hide
967 button_hide: Hide
966 button_edit_section: Edit this section
968 button_edit_section: Edit this section
967 button_export: Export
969 button_export: Export
968 button_delete_my_account: Delete my account
970 button_delete_my_account: Delete my account
969 button_close: Close
971 button_close: Close
970 button_reopen: Reopen
972 button_reopen: Reopen
971
973
972 status_active: active
974 status_active: active
973 status_registered: registered
975 status_registered: registered
974 status_locked: locked
976 status_locked: locked
975
977
976 project_status_active: active
978 project_status_active: active
977 project_status_closed: closed
979 project_status_closed: closed
978 project_status_archived: archived
980 project_status_archived: archived
979
981
980 version_status_open: open
982 version_status_open: open
981 version_status_locked: locked
983 version_status_locked: locked
982 version_status_closed: closed
984 version_status_closed: closed
983
985
984 field_active: Active
986 field_active: Active
985
987
986 text_select_mail_notifications: Select actions for which email notifications should be sent.
988 text_select_mail_notifications: Select actions for which email notifications should be sent.
987 text_regexp_info: eg. ^[A-Z0-9]+$
989 text_regexp_info: eg. ^[A-Z0-9]+$
988 text_min_max_length_info: 0 means no restriction
990 text_min_max_length_info: 0 means no restriction
989 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
991 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
990 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
992 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
991 text_workflow_edit: Select a role and a tracker to edit the workflow
993 text_workflow_edit: Select a role and a tracker to edit the workflow
992 text_are_you_sure: Are you sure?
994 text_are_you_sure: Are you sure?
993 text_journal_changed: "%{label} changed from %{old} to %{new}"
995 text_journal_changed: "%{label} changed from %{old} to %{new}"
994 text_journal_changed_no_detail: "%{label} updated"
996 text_journal_changed_no_detail: "%{label} updated"
995 text_journal_set_to: "%{label} set to %{value}"
997 text_journal_set_to: "%{label} set to %{value}"
996 text_journal_deleted: "%{label} deleted (%{old})"
998 text_journal_deleted: "%{label} deleted (%{old})"
997 text_journal_added: "%{label} %{value} added"
999 text_journal_added: "%{label} %{value} added"
998 text_tip_issue_begin_day: issue beginning this day
1000 text_tip_issue_begin_day: issue beginning this day
999 text_tip_issue_end_day: issue ending this day
1001 text_tip_issue_end_day: issue ending this day
1000 text_tip_issue_begin_end_day: issue beginning and ending this day
1002 text_tip_issue_begin_end_day: issue beginning and ending this day
1001 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.'
1003 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.'
1002 text_caracters_maximum: "%{count} characters maximum."
1004 text_caracters_maximum: "%{count} characters maximum."
1003 text_caracters_minimum: "Must be at least %{count} characters long."
1005 text_caracters_minimum: "Must be at least %{count} characters long."
1004 text_length_between: "Length between %{min} and %{max} characters."
1006 text_length_between: "Length between %{min} and %{max} characters."
1005 text_tracker_no_workflow: No workflow defined for this tracker
1007 text_tracker_no_workflow: No workflow defined for this tracker
1006 text_unallowed_characters: Unallowed characters
1008 text_unallowed_characters: Unallowed characters
1007 text_comma_separated: Multiple values allowed (comma separated).
1009 text_comma_separated: Multiple values allowed (comma separated).
1008 text_line_separated: Multiple values allowed (one line for each value).
1010 text_line_separated: Multiple values allowed (one line for each value).
1009 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1011 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
1010 text_issue_added: "Issue %{id} has been reported by %{author}."
1012 text_issue_added: "Issue %{id} has been reported by %{author}."
1011 text_issue_updated: "Issue %{id} has been updated by %{author}."
1013 text_issue_updated: "Issue %{id} has been updated by %{author}."
1012 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1014 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
1013 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1015 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
1014 text_issue_category_destroy_assignments: Remove category assignments
1016 text_issue_category_destroy_assignments: Remove category assignments
1015 text_issue_category_reassign_to: Reassign issues to this category
1017 text_issue_category_reassign_to: Reassign issues to this category
1016 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
1018 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
1017 text_no_configuration_data: "Roles, trackers, issue 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."
1019 text_no_configuration_data: "Roles, trackers, issue 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."
1018 text_load_default_configuration: Load the default configuration
1020 text_load_default_configuration: Load the default configuration
1019 text_status_changed_by_changeset: "Applied in changeset %{value}."
1021 text_status_changed_by_changeset: "Applied in changeset %{value}."
1020 text_time_logged_by_changeset: "Applied in changeset %{value}."
1022 text_time_logged_by_changeset: "Applied in changeset %{value}."
1021 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1023 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1022 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1024 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1023 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1025 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1024 text_select_project_modules: 'Select modules to enable for this project:'
1026 text_select_project_modules: 'Select modules to enable for this project:'
1025 text_default_administrator_account_changed: Default administrator account changed
1027 text_default_administrator_account_changed: Default administrator account changed
1026 text_file_repository_writable: Attachments directory writable
1028 text_file_repository_writable: Attachments directory writable
1027 text_plugin_assets_writable: Plugin assets directory writable
1029 text_plugin_assets_writable: Plugin assets directory writable
1028 text_rmagick_available: RMagick available (optional)
1030 text_rmagick_available: RMagick available (optional)
1029 text_convert_available: ImageMagick convert available (optional)
1031 text_convert_available: ImageMagick convert available (optional)
1030 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1032 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1031 text_destroy_time_entries: Delete reported hours
1033 text_destroy_time_entries: Delete reported hours
1032 text_assign_time_entries_to_project: Assign reported hours to the project
1034 text_assign_time_entries_to_project: Assign reported hours to the project
1033 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1035 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1034 text_user_wrote: "%{value} wrote:"
1036 text_user_wrote: "%{value} wrote:"
1035 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1037 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1036 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1038 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1037 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them."
1039 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them."
1038 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
1040 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
1039 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1041 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1040 text_custom_field_possible_values_info: 'One line for each value'
1042 text_custom_field_possible_values_info: 'One line for each value'
1041 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1043 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1042 text_wiki_page_nullify_children: "Keep child pages as root pages"
1044 text_wiki_page_nullify_children: "Keep child pages as root pages"
1043 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1045 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1044 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1046 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1045 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
1047 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
1046 text_zoom_in: Zoom in
1048 text_zoom_in: Zoom in
1047 text_zoom_out: Zoom out
1049 text_zoom_out: Zoom out
1048 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1050 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1049 text_scm_path_encoding_note: "Default: UTF-8"
1051 text_scm_path_encoding_note: "Default: UTF-8"
1050 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1052 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1051 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1053 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1052 text_scm_command: Command
1054 text_scm_command: Command
1053 text_scm_command_version: Version
1055 text_scm_command_version: Version
1054 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1056 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1055 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1057 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1056 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1058 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1057 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1059 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1058 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1060 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1059 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1061 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1060 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1062 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1061 text_project_closed: This project is closed and read-only.
1063 text_project_closed: This project is closed and read-only.
1062 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1064 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1063
1065
1064 default_role_manager: Manager
1066 default_role_manager: Manager
1065 default_role_developer: Developer
1067 default_role_developer: Developer
1066 default_role_reporter: Reporter
1068 default_role_reporter: Reporter
1067 default_tracker_bug: Bug
1069 default_tracker_bug: Bug
1068 default_tracker_feature: Feature
1070 default_tracker_feature: Feature
1069 default_tracker_support: Support
1071 default_tracker_support: Support
1070 default_issue_status_new: New
1072 default_issue_status_new: New
1071 default_issue_status_in_progress: In Progress
1073 default_issue_status_in_progress: In Progress
1072 default_issue_status_resolved: Resolved
1074 default_issue_status_resolved: Resolved
1073 default_issue_status_feedback: Feedback
1075 default_issue_status_feedback: Feedback
1074 default_issue_status_closed: Closed
1076 default_issue_status_closed: Closed
1075 default_issue_status_rejected: Rejected
1077 default_issue_status_rejected: Rejected
1076 default_doc_category_user: User documentation
1078 default_doc_category_user: User documentation
1077 default_doc_category_tech: Technical documentation
1079 default_doc_category_tech: Technical documentation
1078 default_priority_low: Low
1080 default_priority_low: Low
1079 default_priority_normal: Normal
1081 default_priority_normal: Normal
1080 default_priority_high: High
1082 default_priority_high: High
1081 default_priority_urgent: Urgent
1083 default_priority_urgent: Urgent
1082 default_priority_immediate: Immediate
1084 default_priority_immediate: Immediate
1083 default_activity_design: Design
1085 default_activity_design: Design
1084 default_activity_development: Development
1086 default_activity_development: Development
1085
1087
1086 enumeration_issue_priorities: Issue priorities
1088 enumeration_issue_priorities: Issue priorities
1087 enumeration_doc_categories: Document categories
1089 enumeration_doc_categories: Document categories
1088 enumeration_activities: Activities (time tracking)
1090 enumeration_activities: Activities (time tracking)
1089 enumeration_system_activity: System Activity
1091 enumeration_system_activity: System Activity
1090 description_filter: Filter
1092 description_filter: Filter
1091 description_search: Searchfield
1093 description_search: Searchfield
1092 description_choose_project: Projects
1094 description_choose_project: Projects
1093 description_project_scope: Search scope
1095 description_project_scope: Search scope
1094 description_notes: Notes
1096 description_notes: Notes
1095 description_message_content: Message content
1097 description_message_content: Message content
1096 description_query_sort_criteria_attribute: Sort attribute
1098 description_query_sort_criteria_attribute: Sort attribute
1097 description_query_sort_criteria_direction: Sort direction
1099 description_query_sort_criteria_direction: Sort direction
1098 description_user_mail_notification: Mail notification settings
1100 description_user_mail_notification: Mail notification settings
1099 description_available_columns: Available Columns
1101 description_available_columns: Available Columns
1100 description_selected_columns: Selected Columns
1102 description_selected_columns: Selected Columns
1101 description_all_columns: All Columns
1103 description_all_columns: All Columns
1102 description_issue_category_reassign: Choose issue category
1104 description_issue_category_reassign: Choose issue category
1103 description_wiki_subpages_reassign: Choose new parent page
1105 description_wiki_subpages_reassign: Choose new parent page
1104 description_date_range_list: Choose range from list
1106 description_date_range_list: Choose range from list
1105 description_date_range_interval: Choose range by selecting start and end date
1107 description_date_range_interval: Choose range by selecting start and end date
1106 description_date_from: Enter start date
1108 description_date_from: Enter start date
1107 description_date_to: Enter end date
1109 description_date_to: Enter end date
1108 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
1110 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
@@ -1,1128 +1,1130
1 # French translations for Ruby on Rails
1 # French translations for Ruby on Rails
2 # by Christian Lescuyer (christian@flyingcoders.com)
2 # by Christian Lescuyer (christian@flyingcoders.com)
3 # contributor: Sebastien Grosjean - ZenCocoon.com
3 # contributor: Sebastien Grosjean - ZenCocoon.com
4 # contributor: Thibaut Cuvelier - Developpez.com
4 # contributor: Thibaut Cuvelier - Developpez.com
5
5
6 fr:
6 fr:
7 direction: ltr
7 direction: ltr
8 date:
8 date:
9 formats:
9 formats:
10 default: "%d/%m/%Y"
10 default: "%d/%m/%Y"
11 short: "%e %b"
11 short: "%e %b"
12 long: "%e %B %Y"
12 long: "%e %B %Y"
13 long_ordinal: "%e %B %Y"
13 long_ordinal: "%e %B %Y"
14 only_day: "%e"
14 only_day: "%e"
15
15
16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
18
18
19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
19 # Don't forget the nil at the beginning; there's no such thing as a 0th month
20 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
20 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
21 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
21 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
22 # Used in date_select and datime_select.
22 # Used in date_select and datime_select.
23 order:
23 order:
24 - :day
24 - :day
25 - :month
25 - :month
26 - :year
26 - :year
27
27
28 time:
28 time:
29 formats:
29 formats:
30 default: "%d/%m/%Y %H:%M"
30 default: "%d/%m/%Y %H:%M"
31 time: "%H:%M"
31 time: "%H:%M"
32 short: "%d %b %H:%M"
32 short: "%d %b %H:%M"
33 long: "%A %d %B %Y %H:%M:%S %Z"
33 long: "%A %d %B %Y %H:%M:%S %Z"
34 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
34 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
35 only_second: "%S"
35 only_second: "%S"
36 am: 'am'
36 am: 'am'
37 pm: 'pm'
37 pm: 'pm'
38
38
39 datetime:
39 datetime:
40 distance_in_words:
40 distance_in_words:
41 half_a_minute: "30 secondes"
41 half_a_minute: "30 secondes"
42 less_than_x_seconds:
42 less_than_x_seconds:
43 zero: "moins d'une seconde"
43 zero: "moins d'une seconde"
44 one: "moins d'une seconde"
44 one: "moins d'une seconde"
45 other: "moins de %{count} secondes"
45 other: "moins de %{count} secondes"
46 x_seconds:
46 x_seconds:
47 one: "1 seconde"
47 one: "1 seconde"
48 other: "%{count} secondes"
48 other: "%{count} secondes"
49 less_than_x_minutes:
49 less_than_x_minutes:
50 zero: "moins d'une minute"
50 zero: "moins d'une minute"
51 one: "moins d'une minute"
51 one: "moins d'une minute"
52 other: "moins de %{count} minutes"
52 other: "moins de %{count} minutes"
53 x_minutes:
53 x_minutes:
54 one: "1 minute"
54 one: "1 minute"
55 other: "%{count} minutes"
55 other: "%{count} minutes"
56 about_x_hours:
56 about_x_hours:
57 one: "environ une heure"
57 one: "environ une heure"
58 other: "environ %{count} heures"
58 other: "environ %{count} heures"
59 x_hours:
59 x_hours:
60 one: "une heure"
60 one: "une heure"
61 other: "%{count} heures"
61 other: "%{count} heures"
62 x_days:
62 x_days:
63 one: "un jour"
63 one: "un jour"
64 other: "%{count} jours"
64 other: "%{count} jours"
65 about_x_months:
65 about_x_months:
66 one: "environ un mois"
66 one: "environ un mois"
67 other: "environ %{count} mois"
67 other: "environ %{count} mois"
68 x_months:
68 x_months:
69 one: "un mois"
69 one: "un mois"
70 other: "%{count} mois"
70 other: "%{count} mois"
71 about_x_years:
71 about_x_years:
72 one: "environ un an"
72 one: "environ un an"
73 other: "environ %{count} ans"
73 other: "environ %{count} ans"
74 over_x_years:
74 over_x_years:
75 one: "plus d'un an"
75 one: "plus d'un an"
76 other: "plus de %{count} ans"
76 other: "plus de %{count} ans"
77 almost_x_years:
77 almost_x_years:
78 one: "presqu'un an"
78 one: "presqu'un an"
79 other: "presque %{count} ans"
79 other: "presque %{count} ans"
80 prompts:
80 prompts:
81 year: "Année"
81 year: "Année"
82 month: "Mois"
82 month: "Mois"
83 day: "Jour"
83 day: "Jour"
84 hour: "Heure"
84 hour: "Heure"
85 minute: "Minute"
85 minute: "Minute"
86 second: "Seconde"
86 second: "Seconde"
87
87
88 number:
88 number:
89 format:
89 format:
90 precision: 3
90 precision: 3
91 separator: ','
91 separator: ','
92 delimiter: ' '
92 delimiter: ' '
93 currency:
93 currency:
94 format:
94 format:
95 unit: '€'
95 unit: '€'
96 precision: 2
96 precision: 2
97 format: '%n %u'
97 format: '%n %u'
98 human:
98 human:
99 format:
99 format:
100 precision: 3
100 precision: 3
101 storage_units:
101 storage_units:
102 format: "%n %u"
102 format: "%n %u"
103 units:
103 units:
104 byte:
104 byte:
105 one: "octet"
105 one: "octet"
106 other: "octets"
106 other: "octets"
107 kb: "ko"
107 kb: "ko"
108 mb: "Mo"
108 mb: "Mo"
109 gb: "Go"
109 gb: "Go"
110 tb: "To"
110 tb: "To"
111
111
112 support:
112 support:
113 array:
113 array:
114 sentence_connector: 'et'
114 sentence_connector: 'et'
115 skip_last_comma: true
115 skip_last_comma: true
116 word_connector: ", "
116 word_connector: ", "
117 two_words_connector: " et "
117 two_words_connector: " et "
118 last_word_connector: " et "
118 last_word_connector: " et "
119
119
120 activerecord:
120 activerecord:
121 errors:
121 errors:
122 template:
122 template:
123 header:
123 header:
124 one: "Impossible d'enregistrer %{model} : une erreur"
124 one: "Impossible d'enregistrer %{model} : une erreur"
125 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
125 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
126 body: "Veuillez vérifier les champs suivants :"
126 body: "Veuillez vérifier les champs suivants :"
127 messages:
127 messages:
128 inclusion: "n'est pas inclus(e) dans la liste"
128 inclusion: "n'est pas inclus(e) dans la liste"
129 exclusion: "n'est pas disponible"
129 exclusion: "n'est pas disponible"
130 invalid: "n'est pas valide"
130 invalid: "n'est pas valide"
131 confirmation: "ne concorde pas avec la confirmation"
131 confirmation: "ne concorde pas avec la confirmation"
132 accepted: "doit être accepté(e)"
132 accepted: "doit être accepté(e)"
133 empty: "doit être renseigné(e)"
133 empty: "doit être renseigné(e)"
134 blank: "doit être renseigné(e)"
134 blank: "doit être renseigné(e)"
135 too_long: "est trop long (pas plus de %{count} caractères)"
135 too_long: "est trop long (pas plus de %{count} caractères)"
136 too_short: "est trop court (au moins %{count} caractères)"
136 too_short: "est trop court (au moins %{count} caractères)"
137 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
137 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
138 taken: "est déjà utilisé"
138 taken: "est déjà utilisé"
139 not_a_number: "n'est pas un nombre"
139 not_a_number: "n'est pas un nombre"
140 not_a_date: "n'est pas une date valide"
140 not_a_date: "n'est pas une date valide"
141 greater_than: "doit être supérieur à %{count}"
141 greater_than: "doit être supérieur à %{count}"
142 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
142 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
143 equal_to: "doit être égal à %{count}"
143 equal_to: "doit être égal à %{count}"
144 less_than: "doit être inférieur à %{count}"
144 less_than: "doit être inférieur à %{count}"
145 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
145 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
146 odd: "doit être impair"
146 odd: "doit être impair"
147 even: "doit être pair"
147 even: "doit être pair"
148 greater_than_start_date: "doit être postérieure à la date de début"
148 greater_than_start_date: "doit être postérieure à la date de début"
149 not_same_project: "n'appartient pas au même projet"
149 not_same_project: "n'appartient pas au même projet"
150 circular_dependency: "Cette relation créerait une dépendance circulaire"
150 circular_dependency: "Cette relation créerait une dépendance circulaire"
151 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
151 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
152 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précèdent"
152 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précèdent"
153
153
154 actionview_instancetag_blank_option: Choisir
154 actionview_instancetag_blank_option: Choisir
155
155
156 general_text_No: 'Non'
156 general_text_No: 'Non'
157 general_text_Yes: 'Oui'
157 general_text_Yes: 'Oui'
158 general_text_no: 'non'
158 general_text_no: 'non'
159 general_text_yes: 'oui'
159 general_text_yes: 'oui'
160 general_lang_name: 'Français'
160 general_lang_name: 'Français'
161 general_csv_separator: ';'
161 general_csv_separator: ';'
162 general_csv_decimal_separator: ','
162 general_csv_decimal_separator: ','
163 general_csv_encoding: ISO-8859-1
163 general_csv_encoding: ISO-8859-1
164 general_first_day_of_week: '1'
164 general_first_day_of_week: '1'
165
165
166 notice_account_updated: Le compte a été mis à jour avec succès.
166 notice_account_updated: Le compte a été mis à jour avec succès.
167 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
167 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
168 notice_account_password_updated: Mot de passe mis à jour avec succès.
168 notice_account_password_updated: Mot de passe mis à jour avec succès.
169 notice_account_wrong_password: Mot de passe incorrect
169 notice_account_wrong_password: Mot de passe incorrect
170 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
170 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
171 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
171 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
172 notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez <a href="%{url}">cliquer sur ce lien</a>.
172 notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez <a href="%{url}">cliquer sur ce lien</a>.
173 notice_account_locked: Votre compte est verrouillé.
173 notice_account_locked: Votre compte est verrouillé.
174 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
174 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
175 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
175 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
176 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
176 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
177 notice_successful_create: Création effectuée avec succès.
177 notice_successful_create: Création effectuée avec succès.
178 notice_successful_update: Mise à jour effectuée avec succès.
178 notice_successful_update: Mise à jour effectuée avec succès.
179 notice_successful_delete: Suppression effectuée avec succès.
179 notice_successful_delete: Suppression effectuée avec succès.
180 notice_successful_connection: Connexion réussie.
180 notice_successful_connection: Connexion réussie.
181 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
181 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
182 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
182 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
183 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
183 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
184 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
184 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
185 notice_email_sent: "Un email a été envoyé à %{value}"
185 notice_email_sent: "Un email a été envoyé à %{value}"
186 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
186 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
187 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
187 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
188 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
188 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
189 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
189 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
190 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
190 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
191 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
191 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
192 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
192 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
193 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
193 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
194 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
194 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
195 notice_unable_delete_version: Impossible de supprimer cette version.
195 notice_unable_delete_version: Impossible de supprimer cette version.
196 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
196 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
197 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
197 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
198 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
198 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
199 notice_issue_successful_create: "Demande %{id} créée."
199 notice_issue_successful_create: "Demande %{id} créée."
200 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
200 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
201 notice_account_deleted: "Votre compte a été définitivement supprimé."
201 notice_account_deleted: "Votre compte a été définitivement supprimé."
202 notice_user_successful_create: "Utilisateur %{id} créé."
202 notice_user_successful_create: "Utilisateur %{id} créé."
203 notice_new_password_must_be_different: Votre nouveau mot de passe doit être différent de votre mot de passe actuel
203 notice_new_password_must_be_different: Votre nouveau mot de passe doit être différent de votre mot de passe actuel
204
204
205 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
205 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
206 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
206 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
207 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
207 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
208 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
208 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
209 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
209 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
210 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
210 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
211 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
211 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
212 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
212 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
213 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
213 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
214 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
214 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
215 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
215 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
216 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
216 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
217 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
217 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
218 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
218 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
219 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
219 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
220 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
220 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
221 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
221 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
222 error_unable_to_connect: Connexion impossible (%{value})
222 error_unable_to_connect: Connexion impossible (%{value})
223 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
223 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
224 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
224 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
225 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
225 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
226
226
227 mail_subject_lost_password: "Votre mot de passe %{value}"
227 mail_subject_lost_password: "Votre mot de passe %{value}"
228 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
228 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
229 mail_subject_register: "Activation de votre compte %{value}"
229 mail_subject_register: "Activation de votre compte %{value}"
230 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
230 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
231 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
231 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
232 mail_body_account_information: Paramètres de connexion de votre compte
232 mail_body_account_information: Paramètres de connexion de votre compte
233 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
233 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
234 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
234 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
235 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
235 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
236 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
236 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
237 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
237 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
238 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
238 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
239 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
239 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
240 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
240 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
241
241
242 field_name: Nom
242 field_name: Nom
243 field_description: Description
243 field_description: Description
244 field_summary: Résumé
244 field_summary: Résumé
245 field_is_required: Obligatoire
245 field_is_required: Obligatoire
246 field_firstname: Prénom
246 field_firstname: Prénom
247 field_lastname: Nom
247 field_lastname: Nom
248 field_mail: Email
248 field_mail: Email
249 field_filename: Fichier
249 field_filename: Fichier
250 field_filesize: Taille
250 field_filesize: Taille
251 field_downloads: Téléchargements
251 field_downloads: Téléchargements
252 field_author: Auteur
252 field_author: Auteur
253 field_created_on: Créé
253 field_created_on: Créé
254 field_updated_on: Mis-à-jour
254 field_updated_on: Mis-à-jour
255 field_closed_on: Fermé
255 field_closed_on: Fermé
256 field_field_format: Format
256 field_field_format: Format
257 field_is_for_all: Pour tous les projets
257 field_is_for_all: Pour tous les projets
258 field_possible_values: Valeurs possibles
258 field_possible_values: Valeurs possibles
259 field_regexp: Expression régulière
259 field_regexp: Expression régulière
260 field_min_length: Longueur minimum
260 field_min_length: Longueur minimum
261 field_max_length: Longueur maximum
261 field_max_length: Longueur maximum
262 field_value: Valeur
262 field_value: Valeur
263 field_category: Catégorie
263 field_category: Catégorie
264 field_title: Titre
264 field_title: Titre
265 field_project: Projet
265 field_project: Projet
266 field_issue: Demande
266 field_issue: Demande
267 field_status: Statut
267 field_status: Statut
268 field_notes: Notes
268 field_notes: Notes
269 field_is_closed: Demande fermée
269 field_is_closed: Demande fermée
270 field_is_default: Valeur par défaut
270 field_is_default: Valeur par défaut
271 field_tracker: Tracker
271 field_tracker: Tracker
272 field_subject: Sujet
272 field_subject: Sujet
273 field_due_date: Echéance
273 field_due_date: Echéance
274 field_assigned_to: Assigné à
274 field_assigned_to: Assigné à
275 field_priority: Priorité
275 field_priority: Priorité
276 field_fixed_version: Version cible
276 field_fixed_version: Version cible
277 field_user: Utilisateur
277 field_user: Utilisateur
278 field_principal: Principal
278 field_principal: Principal
279 field_role: Rôle
279 field_role: Rôle
280 field_homepage: Site web
280 field_homepage: Site web
281 field_is_public: Public
281 field_is_public: Public
282 field_parent: Sous-projet de
282 field_parent: Sous-projet de
283 field_is_in_roadmap: Demandes affichées dans la roadmap
283 field_is_in_roadmap: Demandes affichées dans la roadmap
284 field_login: Identifiant
284 field_login: Identifiant
285 field_mail_notification: Notifications par mail
285 field_mail_notification: Notifications par mail
286 field_admin: Administrateur
286 field_admin: Administrateur
287 field_last_login_on: Dernière connexion
287 field_last_login_on: Dernière connexion
288 field_language: Langue
288 field_language: Langue
289 field_effective_date: Date
289 field_effective_date: Date
290 field_password: Mot de passe
290 field_password: Mot de passe
291 field_new_password: Nouveau mot de passe
291 field_new_password: Nouveau mot de passe
292 field_password_confirmation: Confirmation
292 field_password_confirmation: Confirmation
293 field_version: Version
293 field_version: Version
294 field_type: Type
294 field_type: Type
295 field_host: Hôte
295 field_host: Hôte
296 field_port: Port
296 field_port: Port
297 field_account: Compte
297 field_account: Compte
298 field_base_dn: Base DN
298 field_base_dn: Base DN
299 field_attr_login: Attribut Identifiant
299 field_attr_login: Attribut Identifiant
300 field_attr_firstname: Attribut Prénom
300 field_attr_firstname: Attribut Prénom
301 field_attr_lastname: Attribut Nom
301 field_attr_lastname: Attribut Nom
302 field_attr_mail: Attribut Email
302 field_attr_mail: Attribut Email
303 field_onthefly: Création des utilisateurs à la volée
303 field_onthefly: Création des utilisateurs à la volée
304 field_start_date: Début
304 field_start_date: Début
305 field_done_ratio: "% réalisé"
305 field_done_ratio: "% réalisé"
306 field_auth_source: Mode d'authentification
306 field_auth_source: Mode d'authentification
307 field_hide_mail: Cacher mon adresse mail
307 field_hide_mail: Cacher mon adresse mail
308 field_comments: Commentaire
308 field_comments: Commentaire
309 field_url: URL
309 field_url: URL
310 field_start_page: Page de démarrage
310 field_start_page: Page de démarrage
311 field_subproject: Sous-projet
311 field_subproject: Sous-projet
312 field_hours: Heures
312 field_hours: Heures
313 field_activity: Activité
313 field_activity: Activité
314 field_spent_on: Date
314 field_spent_on: Date
315 field_identifier: Identifiant
315 field_identifier: Identifiant
316 field_is_filter: Utilisé comme filtre
316 field_is_filter: Utilisé comme filtre
317 field_issue_to: Demande liée
317 field_issue_to: Demande liée
318 field_delay: Retard
318 field_delay: Retard
319 field_assignable: Demandes assignables à ce rôle
319 field_assignable: Demandes assignables à ce rôle
320 field_redirect_existing_links: Rediriger les liens existants
320 field_redirect_existing_links: Rediriger les liens existants
321 field_estimated_hours: Temps estimé
321 field_estimated_hours: Temps estimé
322 field_column_names: Colonnes
322 field_column_names: Colonnes
323 field_time_entries: Temps passé
323 field_time_entries: Temps passé
324 field_time_zone: Fuseau horaire
324 field_time_zone: Fuseau horaire
325 field_searchable: Utilisé pour les recherches
325 field_searchable: Utilisé pour les recherches
326 field_default_value: Valeur par défaut
326 field_default_value: Valeur par défaut
327 field_comments_sorting: Afficher les commentaires
327 field_comments_sorting: Afficher les commentaires
328 field_parent_title: Page parent
328 field_parent_title: Page parent
329 field_editable: Modifiable
329 field_editable: Modifiable
330 field_watcher: Observateur
330 field_watcher: Observateur
331 field_identity_url: URL OpenID
331 field_identity_url: URL OpenID
332 field_content: Contenu
332 field_content: Contenu
333 field_group_by: Grouper par
333 field_group_by: Grouper par
334 field_sharing: Partage
334 field_sharing: Partage
335 field_parent_issue: Tâche parente
335 field_parent_issue: Tâche parente
336 field_member_of_group: Groupe de l'assigné
336 field_member_of_group: Groupe de l'assigné
337 field_assigned_to_role: Rôle de l'assigné
337 field_assigned_to_role: Rôle de l'assigné
338 field_text: Champ texte
338 field_text: Champ texte
339 field_visible: Visible
339 field_visible: Visible
340 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
340 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
341 field_issues_visibility: Visibilité des demandes
341 field_issues_visibility: Visibilité des demandes
342 field_is_private: Privée
342 field_is_private: Privée
343 field_commit_logs_encoding: Encodage des messages de commit
343 field_commit_logs_encoding: Encodage des messages de commit
344 field_scm_path_encoding: Encodage des chemins
344 field_scm_path_encoding: Encodage des chemins
345 field_path_to_repository: Chemin du dépôt
345 field_path_to_repository: Chemin du dépôt
346 field_root_directory: Répertoire racine
346 field_root_directory: Répertoire racine
347 field_cvsroot: CVSROOT
347 field_cvsroot: CVSROOT
348 field_cvs_module: Module
348 field_cvs_module: Module
349 field_repository_is_default: Dépôt principal
349 field_repository_is_default: Dépôt principal
350 field_multiple: Valeurs multiples
350 field_multiple: Valeurs multiples
351 field_auth_source_ldap_filter: Filtre LDAP
351 field_auth_source_ldap_filter: Filtre LDAP
352 field_core_fields: Champs standards
352 field_core_fields: Champs standards
353 field_timeout: "Timeout (en secondes)"
353 field_timeout: "Timeout (en secondes)"
354 field_board_parent: Forum parent
354 field_board_parent: Forum parent
355 field_private_notes: Notes privées
355 field_private_notes: Notes privées
356 field_inherit_members: Hériter les membres
356 field_inherit_members: Hériter les membres
357 field_generate_password: Générer un mot de passe
357 field_generate_password: Générer un mot de passe
358 field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion
358 field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion
359
359
360 setting_app_title: Titre de l'application
360 setting_app_title: Titre de l'application
361 setting_app_subtitle: Sous-titre de l'application
361 setting_app_subtitle: Sous-titre de l'application
362 setting_welcome_text: Texte d'accueil
362 setting_welcome_text: Texte d'accueil
363 setting_default_language: Langue par défaut
363 setting_default_language: Langue par défaut
364 setting_login_required: Authentification obligatoire
364 setting_login_required: Authentification obligatoire
365 setting_self_registration: Inscription des nouveaux utilisateurs
365 setting_self_registration: Inscription des nouveaux utilisateurs
366 setting_attachment_max_size: Taille maximale des fichiers
366 setting_attachment_max_size: Taille maximale des fichiers
367 setting_issues_export_limit: Limite d'exportation des demandes
367 setting_issues_export_limit: Limite d'exportation des demandes
368 setting_mail_from: Adresse d'émission
368 setting_mail_from: Adresse d'émission
369 setting_bcc_recipients: Destinataires en copie cachée (cci)
369 setting_bcc_recipients: Destinataires en copie cachée (cci)
370 setting_plain_text_mail: Mail en texte brut (non HTML)
370 setting_plain_text_mail: Mail en texte brut (non HTML)
371 setting_host_name: Nom d'hôte et chemin
371 setting_host_name: Nom d'hôte et chemin
372 setting_text_formatting: Formatage du texte
372 setting_text_formatting: Formatage du texte
373 setting_wiki_compression: Compression de l'historique des pages wiki
373 setting_wiki_compression: Compression de l'historique des pages wiki
374 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
374 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
375 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
375 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
376 setting_autofetch_changesets: Récupération automatique des commits
376 setting_autofetch_changesets: Récupération automatique des commits
377 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
377 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
378 setting_commit_ref_keywords: Mots-clés de référencement
378 setting_commit_ref_keywords: Mots-clés de référencement
379 setting_commit_fix_keywords: Mots-clés de résolution
379 setting_commit_fix_keywords: Mots-clés de résolution
380 setting_autologin: Durée maximale de connexion automatique
380 setting_autologin: Durée maximale de connexion automatique
381 setting_date_format: Format de date
381 setting_date_format: Format de date
382 setting_time_format: Format d'heure
382 setting_time_format: Format d'heure
383 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
383 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
384 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
384 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
385 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
385 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
386 setting_repositories_encodings: Encodages des fichiers et des dépôts
386 setting_repositories_encodings: Encodages des fichiers et des dépôts
387 setting_emails_header: En-tête des emails
387 setting_emails_header: En-tête des emails
388 setting_emails_footer: Pied-de-page des emails
388 setting_emails_footer: Pied-de-page des emails
389 setting_protocol: Protocole
389 setting_protocol: Protocole
390 setting_per_page_options: Options d'objets affichés par page
390 setting_per_page_options: Options d'objets affichés par page
391 setting_user_format: Format d'affichage des utilisateurs
391 setting_user_format: Format d'affichage des utilisateurs
392 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
392 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
393 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
393 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
394 setting_enabled_scm: SCM activés
394 setting_enabled_scm: SCM activés
395 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
395 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
396 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
396 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
397 setting_mail_handler_api_key: Clé de protection de l'API
397 setting_mail_handler_api_key: Clé de protection de l'API
398 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
398 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
399 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
399 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
400 setting_gravatar_default: Image Gravatar par défaut
400 setting_gravatar_default: Image Gravatar par défaut
401 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
401 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
402 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
402 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
403 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
403 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
404 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
404 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
405 setting_password_min_length: Longueur minimum des mots de passe
405 setting_password_min_length: Longueur minimum des mots de passe
406 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
406 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
407 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
407 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
408 setting_issue_done_ratio: Calcul de l'avancement des demandes
408 setting_issue_done_ratio: Calcul de l'avancement des demandes
409 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
409 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
410 setting_issue_done_ratio_issue_status: Utiliser le statut
410 setting_issue_done_ratio_issue_status: Utiliser le statut
411 setting_start_of_week: Jour de début des calendriers
411 setting_start_of_week: Jour de début des calendriers
412 setting_rest_api_enabled: Activer l'API REST
412 setting_rest_api_enabled: Activer l'API REST
413 setting_cache_formatted_text: Mettre en cache le texte formaté
413 setting_cache_formatted_text: Mettre en cache le texte formaté
414 setting_default_notification_option: Option de notification par défaut
414 setting_default_notification_option: Option de notification par défaut
415 setting_commit_logtime_enabled: Permettre la saisie de temps
415 setting_commit_logtime_enabled: Permettre la saisie de temps
416 setting_commit_logtime_activity_id: Activité pour le temps saisi
416 setting_commit_logtime_activity_id: Activité pour le temps saisi
417 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
417 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
418 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
418 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
419 setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour
419 setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour
420 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
420 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
421 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
421 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
422 setting_session_lifetime: Durée de vie maximale des sessions
422 setting_session_lifetime: Durée de vie maximale des sessions
423 setting_session_timeout: Durée maximale d'inactivité
423 setting_session_timeout: Durée maximale d'inactivité
424 setting_thumbnails_enabled: Afficher les vignettes des images
424 setting_thumbnails_enabled: Afficher les vignettes des images
425 setting_thumbnails_size: Taille des vignettes (en pixels)
425 setting_thumbnails_size: Taille des vignettes (en pixels)
426 setting_non_working_week_days: Jours non travaillés
426 setting_non_working_week_days: Jours non travaillés
427 setting_jsonp_enabled: Activer le support JSONP
427 setting_jsonp_enabled: Activer le support JSONP
428 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
428 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
429 setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom
429 setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom
430 setting_force_default_language_for_anonymous: Forcer la langue par défault pour les utilisateurs anonymes
430 setting_force_default_language_for_anonymous: Forcer la langue par défault pour les utilisateurs anonymes
431 setting_force_default_language_for_loggedin: Forcer la langue par défault pour les utilisateurs identifiés
431 setting_force_default_language_for_loggedin: Forcer la langue par défault pour les utilisateurs identifiés
432
432
433 permission_add_project: Créer un projet
433 permission_add_project: Créer un projet
434 permission_add_subprojects: Créer des sous-projets
434 permission_add_subprojects: Créer des sous-projets
435 permission_edit_project: Modifier le projet
435 permission_edit_project: Modifier le projet
436 permission_close_project: Fermer / réouvrir le projet
436 permission_close_project: Fermer / réouvrir le projet
437 permission_select_project_modules: Choisir les modules
437 permission_select_project_modules: Choisir les modules
438 permission_manage_members: Gérer les membres
438 permission_manage_members: Gérer les membres
439 permission_manage_project_activities: Gérer les activités
439 permission_manage_project_activities: Gérer les activités
440 permission_manage_versions: Gérer les versions
440 permission_manage_versions: Gérer les versions
441 permission_manage_categories: Gérer les catégories de demandes
441 permission_manage_categories: Gérer les catégories de demandes
442 permission_view_issues: Voir les demandes
442 permission_view_issues: Voir les demandes
443 permission_add_issues: Créer des demandes
443 permission_add_issues: Créer des demandes
444 permission_edit_issues: Modifier les demandes
444 permission_edit_issues: Modifier les demandes
445 permission_manage_issue_relations: Gérer les relations
445 permission_manage_issue_relations: Gérer les relations
446 permission_set_issues_private: Rendre les demandes publiques ou privées
446 permission_set_issues_private: Rendre les demandes publiques ou privées
447 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
447 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
448 permission_add_issue_notes: Ajouter des notes
448 permission_add_issue_notes: Ajouter des notes
449 permission_edit_issue_notes: Modifier les notes
449 permission_edit_issue_notes: Modifier les notes
450 permission_edit_own_issue_notes: Modifier ses propres notes
450 permission_edit_own_issue_notes: Modifier ses propres notes
451 permission_view_private_notes: Voir les notes privées
451 permission_view_private_notes: Voir les notes privées
452 permission_set_notes_private: Rendre les notes privées
452 permission_set_notes_private: Rendre les notes privées
453 permission_move_issues: Déplacer les demandes
453 permission_move_issues: Déplacer les demandes
454 permission_delete_issues: Supprimer les demandes
454 permission_delete_issues: Supprimer les demandes
455 permission_manage_public_queries: Gérer les requêtes publiques
455 permission_manage_public_queries: Gérer les requêtes publiques
456 permission_save_queries: Sauvegarder les requêtes
456 permission_save_queries: Sauvegarder les requêtes
457 permission_view_gantt: Voir le gantt
457 permission_view_gantt: Voir le gantt
458 permission_view_calendar: Voir le calendrier
458 permission_view_calendar: Voir le calendrier
459 permission_view_issue_watchers: Voir la liste des observateurs
459 permission_view_issue_watchers: Voir la liste des observateurs
460 permission_add_issue_watchers: Ajouter des observateurs
460 permission_add_issue_watchers: Ajouter des observateurs
461 permission_delete_issue_watchers: Supprimer des observateurs
461 permission_delete_issue_watchers: Supprimer des observateurs
462 permission_log_time: Saisir le temps passé
462 permission_log_time: Saisir le temps passé
463 permission_view_time_entries: Voir le temps passé
463 permission_view_time_entries: Voir le temps passé
464 permission_edit_time_entries: Modifier les temps passés
464 permission_edit_time_entries: Modifier les temps passés
465 permission_edit_own_time_entries: Modifier son propre temps passé
465 permission_edit_own_time_entries: Modifier son propre temps passé
466 permission_manage_news: Gérer les annonces
466 permission_manage_news: Gérer les annonces
467 permission_comment_news: Commenter les annonces
467 permission_comment_news: Commenter les annonces
468 permission_view_documents: Voir les documents
468 permission_view_documents: Voir les documents
469 permission_add_documents: Ajouter des documents
469 permission_add_documents: Ajouter des documents
470 permission_edit_documents: Modifier les documents
470 permission_edit_documents: Modifier les documents
471 permission_delete_documents: Supprimer les documents
471 permission_delete_documents: Supprimer les documents
472 permission_manage_files: Gérer les fichiers
472 permission_manage_files: Gérer les fichiers
473 permission_view_files: Voir les fichiers
473 permission_view_files: Voir les fichiers
474 permission_manage_wiki: Gérer le wiki
474 permission_manage_wiki: Gérer le wiki
475 permission_rename_wiki_pages: Renommer les pages
475 permission_rename_wiki_pages: Renommer les pages
476 permission_delete_wiki_pages: Supprimer les pages
476 permission_delete_wiki_pages: Supprimer les pages
477 permission_view_wiki_pages: Voir le wiki
477 permission_view_wiki_pages: Voir le wiki
478 permission_view_wiki_edits: "Voir l'historique des modifications"
478 permission_view_wiki_edits: "Voir l'historique des modifications"
479 permission_edit_wiki_pages: Modifier les pages
479 permission_edit_wiki_pages: Modifier les pages
480 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
480 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
481 permission_protect_wiki_pages: Protéger les pages
481 permission_protect_wiki_pages: Protéger les pages
482 permission_manage_repository: Gérer le dépôt de sources
482 permission_manage_repository: Gérer le dépôt de sources
483 permission_browse_repository: Parcourir les sources
483 permission_browse_repository: Parcourir les sources
484 permission_view_changesets: Voir les révisions
484 permission_view_changesets: Voir les révisions
485 permission_commit_access: Droit de commit
485 permission_commit_access: Droit de commit
486 permission_manage_boards: Gérer les forums
486 permission_manage_boards: Gérer les forums
487 permission_view_messages: Voir les messages
487 permission_view_messages: Voir les messages
488 permission_add_messages: Poster un message
488 permission_add_messages: Poster un message
489 permission_edit_messages: Modifier les messages
489 permission_edit_messages: Modifier les messages
490 permission_edit_own_messages: Modifier ses propres messages
490 permission_edit_own_messages: Modifier ses propres messages
491 permission_delete_messages: Supprimer les messages
491 permission_delete_messages: Supprimer les messages
492 permission_delete_own_messages: Supprimer ses propres messages
492 permission_delete_own_messages: Supprimer ses propres messages
493 permission_export_wiki_pages: Exporter les pages
493 permission_export_wiki_pages: Exporter les pages
494 permission_manage_subtasks: Gérer les sous-tâches
494 permission_manage_subtasks: Gérer les sous-tâches
495 permission_manage_related_issues: Gérer les demandes associées
495 permission_manage_related_issues: Gérer les demandes associées
496
496
497 project_module_issue_tracking: Suivi des demandes
497 project_module_issue_tracking: Suivi des demandes
498 project_module_time_tracking: Suivi du temps passé
498 project_module_time_tracking: Suivi du temps passé
499 project_module_news: Publication d'annonces
499 project_module_news: Publication d'annonces
500 project_module_documents: Publication de documents
500 project_module_documents: Publication de documents
501 project_module_files: Publication de fichiers
501 project_module_files: Publication de fichiers
502 project_module_wiki: Wiki
502 project_module_wiki: Wiki
503 project_module_repository: Dépôt de sources
503 project_module_repository: Dépôt de sources
504 project_module_boards: Forums de discussion
504 project_module_boards: Forums de discussion
505 project_module_calendar: Calendrier
505 project_module_calendar: Calendrier
506 project_module_gantt: Gantt
506 project_module_gantt: Gantt
507
507
508 label_user: Utilisateur
508 label_user: Utilisateur
509 label_user_plural: Utilisateurs
509 label_user_plural: Utilisateurs
510 label_user_new: Nouvel utilisateur
510 label_user_new: Nouvel utilisateur
511 label_user_anonymous: Anonyme
511 label_user_anonymous: Anonyme
512 label_project: Projet
512 label_project: Projet
513 label_project_new: Nouveau projet
513 label_project_new: Nouveau projet
514 label_project_plural: Projets
514 label_project_plural: Projets
515 label_x_projects:
515 label_x_projects:
516 zero: aucun projet
516 zero: aucun projet
517 one: un projet
517 one: un projet
518 other: "%{count} projets"
518 other: "%{count} projets"
519 label_project_all: Tous les projets
519 label_project_all: Tous les projets
520 label_project_latest: Derniers projets
520 label_project_latest: Derniers projets
521 label_issue: Demande
521 label_issue: Demande
522 label_issue_new: Nouvelle demande
522 label_issue_new: Nouvelle demande
523 label_issue_plural: Demandes
523 label_issue_plural: Demandes
524 label_issue_view_all: Voir toutes les demandes
524 label_issue_view_all: Voir toutes les demandes
525 label_issues_by: "Demandes par %{value}"
525 label_issues_by: "Demandes par %{value}"
526 label_issue_added: Demande ajoutée
526 label_issue_added: Demande ajoutée
527 label_issue_updated: Demande mise à jour
527 label_issue_updated: Demande mise à jour
528 label_issue_note_added: Note ajoutée
528 label_issue_note_added: Note ajoutée
529 label_issue_status_updated: Statut changé
529 label_issue_status_updated: Statut changé
530 label_issue_assigned_to_updated: Assigné changé
530 label_issue_assigned_to_updated: Assigné changé
531 label_issue_priority_updated: Priorité changée
531 label_issue_priority_updated: Priorité changée
532 label_document: Document
532 label_document: Document
533 label_document_new: Nouveau document
533 label_document_new: Nouveau document
534 label_document_plural: Documents
534 label_document_plural: Documents
535 label_document_added: Document ajouté
535 label_document_added: Document ajouté
536 label_role: Rôle
536 label_role: Rôle
537 label_role_plural: Rôles
537 label_role_plural: Rôles
538 label_role_new: Nouveau rôle
538 label_role_new: Nouveau rôle
539 label_role_and_permissions: Rôles et permissions
539 label_role_and_permissions: Rôles et permissions
540 label_role_anonymous: Anonyme
540 label_role_anonymous: Anonyme
541 label_role_non_member: Non membre
541 label_role_non_member: Non membre
542 label_member: Membre
542 label_member: Membre
543 label_member_new: Nouveau membre
543 label_member_new: Nouveau membre
544 label_member_plural: Membres
544 label_member_plural: Membres
545 label_tracker: Tracker
545 label_tracker: Tracker
546 label_tracker_plural: Trackers
546 label_tracker_plural: Trackers
547 label_tracker_new: Nouveau tracker
547 label_tracker_new: Nouveau tracker
548 label_workflow: Workflow
548 label_workflow: Workflow
549 label_issue_status: Statut de demandes
549 label_issue_status: Statut de demandes
550 label_issue_status_plural: Statuts de demandes
550 label_issue_status_plural: Statuts de demandes
551 label_issue_status_new: Nouveau statut
551 label_issue_status_new: Nouveau statut
552 label_issue_category: Catégorie de demandes
552 label_issue_category: Catégorie de demandes
553 label_issue_category_plural: Catégories de demandes
553 label_issue_category_plural: Catégories de demandes
554 label_issue_category_new: Nouvelle catégorie
554 label_issue_category_new: Nouvelle catégorie
555 label_custom_field: Champ personnalisé
555 label_custom_field: Champ personnalisé
556 label_custom_field_plural: Champs personnalisés
556 label_custom_field_plural: Champs personnalisés
557 label_custom_field_new: Nouveau champ personnalisé
557 label_custom_field_new: Nouveau champ personnalisé
558 label_enumerations: Listes de valeurs
558 label_enumerations: Listes de valeurs
559 label_enumeration_new: Nouvelle valeur
559 label_enumeration_new: Nouvelle valeur
560 label_information: Information
560 label_information: Information
561 label_information_plural: Informations
561 label_information_plural: Informations
562 label_please_login: Identification
562 label_please_login: Identification
563 label_register: S'enregistrer
563 label_register: S'enregistrer
564 label_login_with_open_id_option: S'authentifier avec OpenID
564 label_login_with_open_id_option: S'authentifier avec OpenID
565 label_password_lost: Mot de passe perdu
565 label_password_lost: Mot de passe perdu
566 label_home: Accueil
566 label_home: Accueil
567 label_my_page: Ma page
567 label_my_page: Ma page
568 label_my_account: Mon compte
568 label_my_account: Mon compte
569 label_my_projects: Mes projets
569 label_my_projects: Mes projets
570 label_my_page_block: Blocs disponibles
570 label_my_page_block: Blocs disponibles
571 label_administration: Administration
571 label_administration: Administration
572 label_login: Connexion
572 label_login: Connexion
573 label_logout: Déconnexion
573 label_logout: Déconnexion
574 label_help: Aide
574 label_help: Aide
575 label_reported_issues: Demandes soumises
575 label_reported_issues: Demandes soumises
576 label_assigned_to_me_issues: Demandes qui me sont assignées
576 label_assigned_to_me_issues: Demandes qui me sont assignées
577 label_last_login: Dernière connexion
577 label_last_login: Dernière connexion
578 label_registered_on: Inscrit le
578 label_registered_on: Inscrit le
579 label_activity: Activité
579 label_activity: Activité
580 label_overall_activity: Activité globale
580 label_overall_activity: Activité globale
581 label_user_activity: "Activité de %{value}"
581 label_user_activity: "Activité de %{value}"
582 label_new: Nouveau
582 label_new: Nouveau
583 label_logged_as: Connecté en tant que
583 label_logged_as: Connecté en tant que
584 label_environment: Environnement
584 label_environment: Environnement
585 label_authentication: Authentification
585 label_authentication: Authentification
586 label_auth_source: Mode d'authentification
586 label_auth_source: Mode d'authentification
587 label_auth_source_new: Nouveau mode d'authentification
587 label_auth_source_new: Nouveau mode d'authentification
588 label_auth_source_plural: Modes d'authentification
588 label_auth_source_plural: Modes d'authentification
589 label_subproject_plural: Sous-projets
589 label_subproject_plural: Sous-projets
590 label_subproject_new: Nouveau sous-projet
590 label_subproject_new: Nouveau sous-projet
591 label_and_its_subprojects: "%{value} et ses sous-projets"
591 label_and_its_subprojects: "%{value} et ses sous-projets"
592 label_min_max_length: Longueurs mini - maxi
592 label_min_max_length: Longueurs mini - maxi
593 label_list: Liste
593 label_list: Liste
594 label_date: Date
594 label_date: Date
595 label_integer: Entier
595 label_integer: Entier
596 label_float: Nombre décimal
596 label_float: Nombre décimal
597 label_boolean: Booléen
597 label_boolean: Booléen
598 label_string: Texte
598 label_string: Texte
599 label_text: Texte long
599 label_text: Texte long
600 label_attribute: Attribut
600 label_attribute: Attribut
601 label_attribute_plural: Attributs
601 label_attribute_plural: Attributs
602 label_no_data: Aucune donnée à afficher
602 label_no_data: Aucune donnée à afficher
603 label_change_status: Changer le statut
603 label_change_status: Changer le statut
604 label_history: Historique
604 label_history: Historique
605 label_attachment: Fichier
605 label_attachment: Fichier
606 label_attachment_new: Nouveau fichier
606 label_attachment_new: Nouveau fichier
607 label_attachment_delete: Supprimer le fichier
607 label_attachment_delete: Supprimer le fichier
608 label_attachment_plural: Fichiers
608 label_attachment_plural: Fichiers
609 label_file_added: Fichier ajouté
609 label_file_added: Fichier ajouté
610 label_report: Rapport
610 label_report: Rapport
611 label_report_plural: Rapports
611 label_report_plural: Rapports
612 label_news: Annonce
612 label_news: Annonce
613 label_news_new: Nouvelle annonce
613 label_news_new: Nouvelle annonce
614 label_news_plural: Annonces
614 label_news_plural: Annonces
615 label_news_latest: Dernières annonces
615 label_news_latest: Dernières annonces
616 label_news_view_all: Voir toutes les annonces
616 label_news_view_all: Voir toutes les annonces
617 label_news_added: Annonce ajoutée
617 label_news_added: Annonce ajoutée
618 label_news_comment_added: Commentaire ajouté à une annonce
618 label_news_comment_added: Commentaire ajouté à une annonce
619 label_settings: Configuration
619 label_settings: Configuration
620 label_overview: Aperçu
620 label_overview: Aperçu
621 label_version: Version
621 label_version: Version
622 label_version_new: Nouvelle version
622 label_version_new: Nouvelle version
623 label_version_plural: Versions
623 label_version_plural: Versions
624 label_close_versions: Fermer les versions terminées
624 label_close_versions: Fermer les versions terminées
625 label_confirmation: Confirmation
625 label_confirmation: Confirmation
626 label_export_to: 'Formats disponibles :'
626 label_export_to: 'Formats disponibles :'
627 label_read: Lire...
627 label_read: Lire...
628 label_public_projects: Projets publics
628 label_public_projects: Projets publics
629 label_open_issues: ouvert
629 label_open_issues: ouvert
630 label_open_issues_plural: ouverts
630 label_open_issues_plural: ouverts
631 label_closed_issues: fermé
631 label_closed_issues: fermé
632 label_closed_issues_plural: fermés
632 label_closed_issues_plural: fermés
633 label_x_open_issues_abbr_on_total:
633 label_x_open_issues_abbr_on_total:
634 zero: 0 ouverte sur %{total}
634 zero: 0 ouverte sur %{total}
635 one: 1 ouverte sur %{total}
635 one: 1 ouverte sur %{total}
636 other: "%{count} ouvertes sur %{total}"
636 other: "%{count} ouvertes sur %{total}"
637 label_x_open_issues_abbr:
637 label_x_open_issues_abbr:
638 zero: 0 ouverte
638 zero: 0 ouverte
639 one: 1 ouverte
639 one: 1 ouverte
640 other: "%{count} ouvertes"
640 other: "%{count} ouvertes"
641 label_x_closed_issues_abbr:
641 label_x_closed_issues_abbr:
642 zero: 0 fermée
642 zero: 0 fermée
643 one: 1 fermée
643 one: 1 fermée
644 other: "%{count} fermées"
644 other: "%{count} fermées"
645 label_x_issues:
645 label_x_issues:
646 zero: 0 demande
646 zero: 0 demande
647 one: 1 demande
647 one: 1 demande
648 other: "%{count} demandes"
648 other: "%{count} demandes"
649 label_total: Total
649 label_total: Total
650 label_total_time: Temps total
650 label_total_time: Temps total
651 label_permissions: Permissions
651 label_permissions: Permissions
652 label_current_status: Statut actuel
652 label_current_status: Statut actuel
653 label_new_statuses_allowed: Nouveaux statuts autorisés
653 label_new_statuses_allowed: Nouveaux statuts autorisés
654 label_all: tous
654 label_all: tous
655 label_any: tous
655 label_any: tous
656 label_none: aucun
656 label_none: aucun
657 label_nobody: personne
657 label_nobody: personne
658 label_next: Suivant
658 label_next: Suivant
659 label_previous: Précédent
659 label_previous: Précédent
660 label_used_by: Utilisé par
660 label_used_by: Utilisé par
661 label_details: Détails
661 label_details: Détails
662 label_add_note: Ajouter une note
662 label_add_note: Ajouter une note
663 label_per_page: Par page
663 label_per_page: Par page
664 label_calendar: Calendrier
664 label_calendar: Calendrier
665 label_months_from: mois depuis
665 label_months_from: mois depuis
666 label_gantt: Gantt
666 label_gantt: Gantt
667 label_internal: Interne
667 label_internal: Interne
668 label_last_changes: "%{count} derniers changements"
668 label_last_changes: "%{count} derniers changements"
669 label_change_view_all: Voir tous les changements
669 label_change_view_all: Voir tous les changements
670 label_personalize_page: Personnaliser cette page
670 label_personalize_page: Personnaliser cette page
671 label_comment: Commentaire
671 label_comment: Commentaire
672 label_comment_plural: Commentaires
672 label_comment_plural: Commentaires
673 label_x_comments:
673 label_x_comments:
674 zero: aucun commentaire
674 zero: aucun commentaire
675 one: un commentaire
675 one: un commentaire
676 other: "%{count} commentaires"
676 other: "%{count} commentaires"
677 label_comment_add: Ajouter un commentaire
677 label_comment_add: Ajouter un commentaire
678 label_comment_added: Commentaire ajouté
678 label_comment_added: Commentaire ajouté
679 label_comment_delete: Supprimer les commentaires
679 label_comment_delete: Supprimer les commentaires
680 label_query: Rapport personnalisé
680 label_query: Rapport personnalisé
681 label_query_plural: Rapports personnalisés
681 label_query_plural: Rapports personnalisés
682 label_query_new: Nouveau rapport
682 label_query_new: Nouveau rapport
683 label_my_queries: Mes rapports personnalisés
683 label_my_queries: Mes rapports personnalisés
684 label_filter_add: Ajouter le filtre
684 label_filter_add: Ajouter le filtre
685 label_filter_plural: Filtres
685 label_filter_plural: Filtres
686 label_equals: égal
686 label_equals: égal
687 label_not_equals: différent
687 label_not_equals: différent
688 label_in_less_than: dans moins de
688 label_in_less_than: dans moins de
689 label_in_more_than: dans plus de
689 label_in_more_than: dans plus de
690 label_in_the_next_days: dans les prochains jours
690 label_in_the_next_days: dans les prochains jours
691 label_in_the_past_days: dans les derniers jours
691 label_in_the_past_days: dans les derniers jours
692 label_greater_or_equal: '>='
692 label_greater_or_equal: '>='
693 label_less_or_equal: '<='
693 label_less_or_equal: '<='
694 label_between: entre
694 label_between: entre
695 label_in: dans
695 label_in: dans
696 label_today: aujourd'hui
696 label_today: aujourd'hui
697 label_all_time: toute la période
697 label_all_time: toute la période
698 label_yesterday: hier
698 label_yesterday: hier
699 label_this_week: cette semaine
699 label_this_week: cette semaine
700 label_last_week: la semaine dernière
700 label_last_week: la semaine dernière
701 label_last_n_weeks: "les %{count} dernières semaines"
701 label_last_n_weeks: "les %{count} dernières semaines"
702 label_last_n_days: "les %{count} derniers jours"
702 label_last_n_days: "les %{count} derniers jours"
703 label_this_month: ce mois-ci
703 label_this_month: ce mois-ci
704 label_last_month: le mois dernier
704 label_last_month: le mois dernier
705 label_this_year: cette année
705 label_this_year: cette année
706 label_date_range: Période
706 label_date_range: Période
707 label_less_than_ago: il y a moins de
707 label_less_than_ago: il y a moins de
708 label_more_than_ago: il y a plus de
708 label_more_than_ago: il y a plus de
709 label_ago: il y a
709 label_ago: il y a
710 label_contains: contient
710 label_contains: contient
711 label_not_contains: ne contient pas
711 label_not_contains: ne contient pas
712 label_any_issues_in_project: une demande du projet
712 label_any_issues_in_project: une demande du projet
713 label_any_issues_not_in_project: une demande hors du projet
713 label_any_issues_not_in_project: une demande hors du projet
714 label_no_issues_in_project: aucune demande du projet
714 label_no_issues_in_project: aucune demande du projet
715 label_day_plural: jours
715 label_day_plural: jours
716 label_repository: Dépôt
716 label_repository: Dépôt
717 label_repository_new: Nouveau dépôt
717 label_repository_new: Nouveau dépôt
718 label_repository_plural: Dépôts
718 label_repository_plural: Dépôts
719 label_browse: Parcourir
719 label_browse: Parcourir
720 label_branch: Branche
720 label_branch: Branche
721 label_tag: Tag
721 label_tag: Tag
722 label_revision: Révision
722 label_revision: Révision
723 label_revision_plural: Révisions
723 label_revision_plural: Révisions
724 label_revision_id: "Révision %{value}"
724 label_revision_id: "Révision %{value}"
725 label_associated_revisions: Révisions associées
725 label_associated_revisions: Révisions associées
726 label_added: ajouté
726 label_added: ajouté
727 label_modified: modifié
727 label_modified: modifié
728 label_copied: copié
728 label_copied: copié
729 label_renamed: renommé
729 label_renamed: renommé
730 label_deleted: supprimé
730 label_deleted: supprimé
731 label_latest_revision: Dernière révision
731 label_latest_revision: Dernière révision
732 label_latest_revision_plural: Dernières révisions
732 label_latest_revision_plural: Dernières révisions
733 label_view_revisions: Voir les révisions
733 label_view_revisions: Voir les révisions
734 label_view_all_revisions: Voir toutes les révisions
734 label_view_all_revisions: Voir toutes les révisions
735 label_max_size: Taille maximale
735 label_max_size: Taille maximale
736 label_sort_highest: Remonter en premier
736 label_sort_highest: Remonter en premier
737 label_sort_higher: Remonter
737 label_sort_higher: Remonter
738 label_sort_lower: Descendre
738 label_sort_lower: Descendre
739 label_sort_lowest: Descendre en dernier
739 label_sort_lowest: Descendre en dernier
740 label_roadmap: Roadmap
740 label_roadmap: Roadmap
741 label_roadmap_due_in: "Échéance dans %{value}"
741 label_roadmap_due_in: "Échéance dans %{value}"
742 label_roadmap_overdue: "En retard de %{value}"
742 label_roadmap_overdue: "En retard de %{value}"
743 label_roadmap_no_issues: Aucune demande pour cette version
743 label_roadmap_no_issues: Aucune demande pour cette version
744 label_search: Recherche
744 label_search: Recherche
745 label_result_plural: Résultats
745 label_result_plural: Résultats
746 label_all_words: Tous les mots
746 label_all_words: Tous les mots
747 label_wiki: Wiki
747 label_wiki: Wiki
748 label_wiki_edit: Révision wiki
748 label_wiki_edit: Révision wiki
749 label_wiki_edit_plural: Révisions wiki
749 label_wiki_edit_plural: Révisions wiki
750 label_wiki_page: Page wiki
750 label_wiki_page: Page wiki
751 label_wiki_page_plural: Pages wiki
751 label_wiki_page_plural: Pages wiki
752 label_index_by_title: Index par titre
752 label_index_by_title: Index par titre
753 label_index_by_date: Index par date
753 label_index_by_date: Index par date
754 label_current_version: Version actuelle
754 label_current_version: Version actuelle
755 label_preview: Prévisualisation
755 label_preview: Prévisualisation
756 label_feed_plural: Flux Atom
756 label_feed_plural: Flux Atom
757 label_changes_details: Détails de tous les changements
757 label_changes_details: Détails de tous les changements
758 label_issue_tracking: Suivi des demandes
758 label_issue_tracking: Suivi des demandes
759 label_spent_time: Temps passé
759 label_spent_time: Temps passé
760 label_overall_spent_time: Temps passé global
760 label_overall_spent_time: Temps passé global
761 label_f_hour: "%{value} heure"
761 label_f_hour: "%{value} heure"
762 label_f_hour_plural: "%{value} heures"
762 label_f_hour_plural: "%{value} heures"
763 label_time_tracking: Suivi du temps
763 label_time_tracking: Suivi du temps
764 label_change_plural: Changements
764 label_change_plural: Changements
765 label_statistics: Statistiques
765 label_statistics: Statistiques
766 label_commits_per_month: Commits par mois
766 label_commits_per_month: Commits par mois
767 label_commits_per_author: Commits par auteur
767 label_commits_per_author: Commits par auteur
768 label_diff: diff
768 label_diff: diff
769 label_view_diff: Voir les différences
769 label_view_diff: Voir les différences
770 label_diff_inline: en ligne
770 label_diff_inline: en ligne
771 label_diff_side_by_side: côte à côte
771 label_diff_side_by_side: côte à côte
772 label_options: Options
772 label_options: Options
773 label_copy_workflow_from: Copier le workflow de
773 label_copy_workflow_from: Copier le workflow de
774 label_permissions_report: Synthèse des permissions
774 label_permissions_report: Synthèse des permissions
775 label_watched_issues: Demandes surveillées
775 label_watched_issues: Demandes surveillées
776 label_related_issues: Demandes liées
776 label_related_issues: Demandes liées
777 label_applied_status: Statut appliqué
777 label_applied_status: Statut appliqué
778 label_loading: Chargement...
778 label_loading: Chargement...
779 label_relation_new: Nouvelle relation
779 label_relation_new: Nouvelle relation
780 label_relation_delete: Supprimer la relation
780 label_relation_delete: Supprimer la relation
781 label_relates_to: Lié à
781 label_relates_to: Lié à
782 label_duplicates: Duplique
782 label_duplicates: Duplique
783 label_duplicated_by: Dupliqué par
783 label_duplicated_by: Dupliqué par
784 label_blocks: Bloque
784 label_blocks: Bloque
785 label_blocked_by: Bloqué par
785 label_blocked_by: Bloqué par
786 label_precedes: Précède
786 label_precedes: Précède
787 label_follows: Suit
787 label_follows: Suit
788 label_copied_to: Copié vers
788 label_copied_to: Copié vers
789 label_copied_from: Copié depuis
789 label_copied_from: Copié depuis
790 label_end_to_start: fin à début
790 label_end_to_start: fin à début
791 label_end_to_end: fin à fin
791 label_end_to_end: fin à fin
792 label_start_to_start: début à début
792 label_start_to_start: début à début
793 label_start_to_end: début à fin
793 label_start_to_end: début à fin
794 label_stay_logged_in: Rester connecté
794 label_stay_logged_in: Rester connecté
795 label_disabled: désactivé
795 label_disabled: désactivé
796 label_show_completed_versions: Voir les versions passées
796 label_show_completed_versions: Voir les versions passées
797 label_me: moi
797 label_me: moi
798 label_board: Forum
798 label_board: Forum
799 label_board_new: Nouveau forum
799 label_board_new: Nouveau forum
800 label_board_plural: Forums
800 label_board_plural: Forums
801 label_board_locked: Verrouillé
801 label_board_locked: Verrouillé
802 label_board_sticky: Sticky
802 label_board_sticky: Sticky
803 label_topic_plural: Discussions
803 label_topic_plural: Discussions
804 label_message_plural: Messages
804 label_message_plural: Messages
805 label_message_last: Dernier message
805 label_message_last: Dernier message
806 label_message_new: Nouveau message
806 label_message_new: Nouveau message
807 label_message_posted: Message ajouté
807 label_message_posted: Message ajouté
808 label_reply_plural: Réponses
808 label_reply_plural: Réponses
809 label_send_information: Envoyer les informations à l'utilisateur
809 label_send_information: Envoyer les informations à l'utilisateur
810 label_year: Année
810 label_year: Année
811 label_month: Mois
811 label_month: Mois
812 label_week: Semaine
812 label_week: Semaine
813 label_date_from: Du
813 label_date_from: Du
814 label_date_to: Au
814 label_date_to: Au
815 label_language_based: Basé sur la langue de l'utilisateur
815 label_language_based: Basé sur la langue de l'utilisateur
816 label_sort_by: "Trier par %{value}"
816 label_sort_by: "Trier par %{value}"
817 label_send_test_email: Envoyer un email de test
817 label_send_test_email: Envoyer un email de test
818 label_feeds_access_key: Clé d'accès Atom
818 label_feeds_access_key: Clé d'accès Atom
819 label_missing_feeds_access_key: Clé d'accès Atom manquante
819 label_missing_feeds_access_key: Clé d'accès Atom manquante
820 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
820 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
821 label_module_plural: Modules
821 label_module_plural: Modules
822 label_added_time_by: "Ajouté par %{author} il y a %{age}"
822 label_added_time_by: "Ajouté par %{author} il y a %{age}"
823 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
823 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
824 label_updated_time: "Mis à jour il y a %{value}"
824 label_updated_time: "Mis à jour il y a %{value}"
825 label_jump_to_a_project: Aller à un projet...
825 label_jump_to_a_project: Aller à un projet...
826 label_file_plural: Fichiers
826 label_file_plural: Fichiers
827 label_changeset_plural: Révisions
827 label_changeset_plural: Révisions
828 label_default_columns: Colonnes par défaut
828 label_default_columns: Colonnes par défaut
829 label_no_change_option: (Pas de changement)
829 label_no_change_option: (Pas de changement)
830 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
830 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
831 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
831 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
832 label_theme: Thème
832 label_theme: Thème
833 label_default: Défaut
833 label_default: Défaut
834 label_search_titles_only: Uniquement dans les titres
834 label_search_titles_only: Uniquement dans les titres
835 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
835 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
836 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
836 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
837 label_user_mail_option_none: Aucune notification
837 label_user_mail_option_none: Aucune notification
838 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
838 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
839 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
839 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
840 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
840 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
841 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
841 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
842 label_registration_activation_by_email: activation du compte par email
842 label_registration_activation_by_email: activation du compte par email
843 label_registration_manual_activation: activation manuelle du compte
843 label_registration_manual_activation: activation manuelle du compte
844 label_registration_automatic_activation: activation automatique du compte
844 label_registration_automatic_activation: activation automatique du compte
845 label_display_per_page: "Par page : %{value}"
845 label_display_per_page: "Par page : %{value}"
846 label_age: Âge
846 label_age: Âge
847 label_change_properties: Changer les propriétés
847 label_change_properties: Changer les propriétés
848 label_general: Général
848 label_general: Général
849 label_more: Plus
849 label_more: Plus
850 label_scm: SCM
850 label_scm: SCM
851 label_plugins: Plugins
851 label_plugins: Plugins
852 label_ldap_authentication: Authentification LDAP
852 label_ldap_authentication: Authentification LDAP
853 label_downloads_abbr: D/L
853 label_downloads_abbr: D/L
854 label_optional_description: Description facultative
854 label_optional_description: Description facultative
855 label_add_another_file: Ajouter un autre fichier
855 label_add_another_file: Ajouter un autre fichier
856 label_preferences: Préférences
856 label_preferences: Préférences
857 label_chronological_order: Dans l'ordre chronologique
857 label_chronological_order: Dans l'ordre chronologique
858 label_reverse_chronological_order: Dans l'ordre chronologique inverse
858 label_reverse_chronological_order: Dans l'ordre chronologique inverse
859 label_planning: Planning
859 label_planning: Planning
860 label_incoming_emails: Emails entrants
860 label_incoming_emails: Emails entrants
861 label_generate_key: Générer une clé
861 label_generate_key: Générer une clé
862 label_issue_watchers: Observateurs
862 label_issue_watchers: Observateurs
863 label_example: Exemple
863 label_example: Exemple
864 label_display: Affichage
864 label_display: Affichage
865 label_sort: Tri
865 label_sort: Tri
866 label_ascending: Croissant
866 label_ascending: Croissant
867 label_descending: Décroissant
867 label_descending: Décroissant
868 label_date_from_to: Du %{start} au %{end}
868 label_date_from_to: Du %{start} au %{end}
869 label_wiki_content_added: Page wiki ajoutée
869 label_wiki_content_added: Page wiki ajoutée
870 label_wiki_content_updated: Page wiki mise à jour
870 label_wiki_content_updated: Page wiki mise à jour
871 label_group: Groupe
871 label_group: Groupe
872 label_group_plural: Groupes
872 label_group_plural: Groupes
873 label_group_new: Nouveau groupe
873 label_group_new: Nouveau groupe
874 label_group_anonymous: Utilisateurs anonymes
875 label_group_non_member: Utilisateurs non membres
874 label_time_entry_plural: Temps passé
876 label_time_entry_plural: Temps passé
875 label_version_sharing_none: Non partagé
877 label_version_sharing_none: Non partagé
876 label_version_sharing_descendants: Avec les sous-projets
878 label_version_sharing_descendants: Avec les sous-projets
877 label_version_sharing_hierarchy: Avec toute la hiérarchie
879 label_version_sharing_hierarchy: Avec toute la hiérarchie
878 label_version_sharing_tree: Avec tout l'arbre
880 label_version_sharing_tree: Avec tout l'arbre
879 label_version_sharing_system: Avec tous les projets
881 label_version_sharing_system: Avec tous les projets
880 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
882 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
881 label_copy_source: Source
883 label_copy_source: Source
882 label_copy_target: Cible
884 label_copy_target: Cible
883 label_copy_same_as_target: Comme la cible
885 label_copy_same_as_target: Comme la cible
884 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
886 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
885 label_api_access_key: Clé d'accès API
887 label_api_access_key: Clé d'accès API
886 label_missing_api_access_key: Clé d'accès API manquante
888 label_missing_api_access_key: Clé d'accès API manquante
887 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
889 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
888 label_profile: Profil
890 label_profile: Profil
889 label_subtask_plural: Sous-tâches
891 label_subtask_plural: Sous-tâches
890 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
892 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
891 label_principal_search: "Rechercher un utilisateur ou un groupe :"
893 label_principal_search: "Rechercher un utilisateur ou un groupe :"
892 label_user_search: "Rechercher un utilisateur :"
894 label_user_search: "Rechercher un utilisateur :"
893 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
895 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
894 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
896 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
895 label_issues_visibility_all: Toutes les demandes
897 label_issues_visibility_all: Toutes les demandes
896 label_issues_visibility_public: Toutes les demandes non privées
898 label_issues_visibility_public: Toutes les demandes non privées
897 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
899 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
898 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
900 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
899 label_parent_revision: Parent
901 label_parent_revision: Parent
900 label_child_revision: Enfant
902 label_child_revision: Enfant
901 label_export_options: Options d'exportation %{export_format}
903 label_export_options: Options d'exportation %{export_format}
902 label_copy_attachments: Copier les fichiers
904 label_copy_attachments: Copier les fichiers
903 label_copy_subtasks: Copier les sous-tâches
905 label_copy_subtasks: Copier les sous-tâches
904 label_item_position: "%{position} sur %{count}"
906 label_item_position: "%{position} sur %{count}"
905 label_completed_versions: Versions passées
907 label_completed_versions: Versions passées
906 label_search_for_watchers: Rechercher des observateurs
908 label_search_for_watchers: Rechercher des observateurs
907 label_session_expiration: Expiration des sessions
909 label_session_expiration: Expiration des sessions
908 label_show_closed_projects: Voir les projets fermés
910 label_show_closed_projects: Voir les projets fermés
909 label_status_transitions: Changements de statut
911 label_status_transitions: Changements de statut
910 label_fields_permissions: Permissions sur les champs
912 label_fields_permissions: Permissions sur les champs
911 label_readonly: Lecture
913 label_readonly: Lecture
912 label_required: Obligatoire
914 label_required: Obligatoire
913 label_hidden: Caché
915 label_hidden: Caché
914 label_attribute_of_project: "%{name} du projet"
916 label_attribute_of_project: "%{name} du projet"
915 label_attribute_of_issue: "%{name} de la demande"
917 label_attribute_of_issue: "%{name} de la demande"
916 label_attribute_of_author: "%{name} de l'auteur"
918 label_attribute_of_author: "%{name} de l'auteur"
917 label_attribute_of_assigned_to: "%{name} de l'assigné"
919 label_attribute_of_assigned_to: "%{name} de l'assigné"
918 label_attribute_of_user: "%{name} de l'utilisateur"
920 label_attribute_of_user: "%{name} de l'utilisateur"
919 label_attribute_of_fixed_version: "%{name} de la version cible"
921 label_attribute_of_fixed_version: "%{name} de la version cible"
920 label_cross_project_descendants: Avec les sous-projets
922 label_cross_project_descendants: Avec les sous-projets
921 label_cross_project_tree: Avec tout l'arbre
923 label_cross_project_tree: Avec tout l'arbre
922 label_cross_project_hierarchy: Avec toute la hiérarchie
924 label_cross_project_hierarchy: Avec toute la hiérarchie
923 label_cross_project_system: Avec tous les projets
925 label_cross_project_system: Avec tous les projets
924 label_gantt_progress_line: Ligne de progression
926 label_gantt_progress_line: Ligne de progression
925 label_visibility_private: par moi uniquement
927 label_visibility_private: par moi uniquement
926 label_visibility_roles: par ces rôles uniquement
928 label_visibility_roles: par ces rôles uniquement
927 label_visibility_public: par tout le monde
929 label_visibility_public: par tout le monde
928 label_link: Lien
930 label_link: Lien
929 label_only: seulement
931 label_only: seulement
930 label_drop_down_list: liste déroulante
932 label_drop_down_list: liste déroulante
931 label_checkboxes: cases à cocher
933 label_checkboxes: cases à cocher
932 label_radio_buttons: boutons radio
934 label_radio_buttons: boutons radio
933 label_link_values_to: Lier les valeurs vers l'URL
935 label_link_values_to: Lier les valeurs vers l'URL
934 label_custom_field_select_type: Selectionner le type d'objet auquel attacher le champ personnalisé
936 label_custom_field_select_type: Selectionner le type d'objet auquel attacher le champ personnalisé
935 label_check_for_updates: Vérifier les mises à jour
937 label_check_for_updates: Vérifier les mises à jour
936 label_latest_compatible_version: Dernière version compatible
938 label_latest_compatible_version: Dernière version compatible
937 label_unknown_plugin: Plugin inconnu
939 label_unknown_plugin: Plugin inconnu
938
940
939 button_login: Connexion
941 button_login: Connexion
940 button_submit: Soumettre
942 button_submit: Soumettre
941 button_save: Sauvegarder
943 button_save: Sauvegarder
942 button_check_all: Tout cocher
944 button_check_all: Tout cocher
943 button_uncheck_all: Tout décocher
945 button_uncheck_all: Tout décocher
944 button_collapse_all: Plier tout
946 button_collapse_all: Plier tout
945 button_expand_all: Déplier tout
947 button_expand_all: Déplier tout
946 button_delete: Supprimer
948 button_delete: Supprimer
947 button_create: Créer
949 button_create: Créer
948 button_create_and_continue: Créer et continuer
950 button_create_and_continue: Créer et continuer
949 button_test: Tester
951 button_test: Tester
950 button_edit: Modifier
952 button_edit: Modifier
951 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
953 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
952 button_add: Ajouter
954 button_add: Ajouter
953 button_change: Changer
955 button_change: Changer
954 button_apply: Appliquer
956 button_apply: Appliquer
955 button_clear: Effacer
957 button_clear: Effacer
956 button_lock: Verrouiller
958 button_lock: Verrouiller
957 button_unlock: Déverrouiller
959 button_unlock: Déverrouiller
958 button_download: Télécharger
960 button_download: Télécharger
959 button_list: Lister
961 button_list: Lister
960 button_view: Voir
962 button_view: Voir
961 button_move: Déplacer
963 button_move: Déplacer
962 button_move_and_follow: Déplacer et suivre
964 button_move_and_follow: Déplacer et suivre
963 button_back: Retour
965 button_back: Retour
964 button_cancel: Annuler
966 button_cancel: Annuler
965 button_activate: Activer
967 button_activate: Activer
966 button_sort: Trier
968 button_sort: Trier
967 button_log_time: Saisir temps
969 button_log_time: Saisir temps
968 button_rollback: Revenir à cette version
970 button_rollback: Revenir à cette version
969 button_watch: Surveiller
971 button_watch: Surveiller
970 button_unwatch: Ne plus surveiller
972 button_unwatch: Ne plus surveiller
971 button_reply: Répondre
973 button_reply: Répondre
972 button_archive: Archiver
974 button_archive: Archiver
973 button_unarchive: Désarchiver
975 button_unarchive: Désarchiver
974 button_reset: Réinitialiser
976 button_reset: Réinitialiser
975 button_rename: Renommer
977 button_rename: Renommer
976 button_change_password: Changer de mot de passe
978 button_change_password: Changer de mot de passe
977 button_copy: Copier
979 button_copy: Copier
978 button_copy_and_follow: Copier et suivre
980 button_copy_and_follow: Copier et suivre
979 button_annotate: Annoter
981 button_annotate: Annoter
980 button_update: Mettre à jour
982 button_update: Mettre à jour
981 button_configure: Configurer
983 button_configure: Configurer
982 button_quote: Citer
984 button_quote: Citer
983 button_duplicate: Dupliquer
985 button_duplicate: Dupliquer
984 button_show: Afficher
986 button_show: Afficher
985 button_hide: Cacher
987 button_hide: Cacher
986 button_edit_section: Modifier cette section
988 button_edit_section: Modifier cette section
987 button_export: Exporter
989 button_export: Exporter
988 button_delete_my_account: Supprimer mon compte
990 button_delete_my_account: Supprimer mon compte
989 button_close: Fermer
991 button_close: Fermer
990 button_reopen: Réouvrir
992 button_reopen: Réouvrir
991
993
992 status_active: actif
994 status_active: actif
993 status_registered: enregistré
995 status_registered: enregistré
994 status_locked: verrouillé
996 status_locked: verrouillé
995
997
996 project_status_active: actif
998 project_status_active: actif
997 project_status_closed: fermé
999 project_status_closed: fermé
998 project_status_archived: archivé
1000 project_status_archived: archivé
999
1001
1000 version_status_open: ouvert
1002 version_status_open: ouvert
1001 version_status_locked: verrouillé
1003 version_status_locked: verrouillé
1002 version_status_closed: fermé
1004 version_status_closed: fermé
1003
1005
1004 field_active: Actif
1006 field_active: Actif
1005
1007
1006 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
1008 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
1007 text_regexp_info: ex. ^[A-Z0-9]+$
1009 text_regexp_info: ex. ^[A-Z0-9]+$
1008 text_min_max_length_info: 0 pour aucune restriction
1010 text_min_max_length_info: 0 pour aucune restriction
1009 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
1011 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
1010 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
1012 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
1011 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
1013 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
1012 text_are_you_sure: Êtes-vous sûr ?
1014 text_are_you_sure: Êtes-vous sûr ?
1013 text_journal_changed: "%{label} changé de %{old} à %{new}"
1015 text_journal_changed: "%{label} changé de %{old} à %{new}"
1014 text_journal_changed_no_detail: "%{label} mis à jour"
1016 text_journal_changed_no_detail: "%{label} mis à jour"
1015 text_journal_set_to: "%{label} mis à %{value}"
1017 text_journal_set_to: "%{label} mis à %{value}"
1016 text_journal_deleted: "%{label} %{old} supprimé"
1018 text_journal_deleted: "%{label} %{old} supprimé"
1017 text_journal_added: "%{label} %{value} ajouté"
1019 text_journal_added: "%{label} %{value} ajouté"
1018 text_tip_issue_begin_day: tâche commençant ce jour
1020 text_tip_issue_begin_day: tâche commençant ce jour
1019 text_tip_issue_end_day: tâche finissant ce jour
1021 text_tip_issue_end_day: tâche finissant ce jour
1020 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
1022 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
1021 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés, doit commencer par une minuscule.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
1023 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés, doit commencer par une minuscule.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
1022 text_caracters_maximum: "%{count} caractères maximum."
1024 text_caracters_maximum: "%{count} caractères maximum."
1023 text_caracters_minimum: "%{count} caractères minimum."
1025 text_caracters_minimum: "%{count} caractères minimum."
1024 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
1026 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
1025 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
1027 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
1026 text_unallowed_characters: Caractères non autorisés
1028 text_unallowed_characters: Caractères non autorisés
1027 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
1029 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
1028 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
1030 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
1029 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
1031 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
1030 text_issue_added: "La demande %{id} a été soumise par %{author}."
1032 text_issue_added: "La demande %{id} a été soumise par %{author}."
1031 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
1033 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
1032 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
1034 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
1033 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
1035 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
1034 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
1036 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
1035 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
1037 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
1036 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
1038 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
1037 text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
1039 text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
1038 text_load_default_configuration: Charger le paramétrage par défaut
1040 text_load_default_configuration: Charger le paramétrage par défaut
1039 text_status_changed_by_changeset: "Appliqué par commit %{value}."
1041 text_status_changed_by_changeset: "Appliqué par commit %{value}."
1040 text_time_logged_by_changeset: "Appliqué par commit %{value}"
1042 text_time_logged_by_changeset: "Appliqué par commit %{value}"
1041 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
1043 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
1042 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
1044 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
1043 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1045 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1044 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
1046 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
1045 text_default_administrator_account_changed: Compte administrateur par défaut changé
1047 text_default_administrator_account_changed: Compte administrateur par défaut changé
1046 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
1048 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
1047 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
1049 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
1048 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
1050 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
1049 text_convert_available: Binaire convert de ImageMagick présent (optionel)
1051 text_convert_available: Binaire convert de ImageMagick présent (optionel)
1050 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
1052 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
1051 text_destroy_time_entries: Supprimer les heures
1053 text_destroy_time_entries: Supprimer les heures
1052 text_assign_time_entries_to_project: Reporter les heures sur le projet
1054 text_assign_time_entries_to_project: Reporter les heures sur le projet
1053 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
1055 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
1054 text_user_wrote: "%{value} a écrit :"
1056 text_user_wrote: "%{value} a écrit :"
1055 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
1057 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
1056 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
1058 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
1057 text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer."
1059 text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer."
1058 text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
1060 text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
1059 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
1061 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
1060 text_custom_field_possible_values_info: 'Une ligne par valeur'
1062 text_custom_field_possible_values_info: 'Une ligne par valeur'
1061 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
1063 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
1062 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
1064 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
1063 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
1065 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
1064 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
1066 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
1065 text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
1067 text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
1066 text_zoom_in: Zoom avant
1068 text_zoom_in: Zoom avant
1067 text_zoom_out: Zoom arrière
1069 text_zoom_out: Zoom arrière
1068 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
1070 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
1069 text_scm_path_encoding_note: "Défaut : UTF-8"
1071 text_scm_path_encoding_note: "Défaut : UTF-8"
1070 text_git_repository_note: "Le dépôt est vide et local (exemples : /gitrepo, c:\\gitrepo)"
1072 text_git_repository_note: "Le dépôt est vide et local (exemples : /gitrepo, c:\\gitrepo)"
1071 text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1073 text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1072 text_scm_command: Commande
1074 text_scm_command: Commande
1073 text_scm_command_version: Version
1075 text_scm_command_version: Version
1074 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1076 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1075 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1077 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1076 text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)"
1078 text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)"
1077 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
1079 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
1078 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
1080 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
1079 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
1081 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
1080 text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
1082 text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
1081 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1083 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1082 text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet."
1084 text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet."
1083
1085
1084 default_role_manager: Manager
1086 default_role_manager: Manager
1085 default_role_developer: Développeur
1087 default_role_developer: Développeur
1086 default_role_reporter: Rapporteur
1088 default_role_reporter: Rapporteur
1087 default_tracker_bug: Anomalie
1089 default_tracker_bug: Anomalie
1088 default_tracker_feature: Evolution
1090 default_tracker_feature: Evolution
1089 default_tracker_support: Assistance
1091 default_tracker_support: Assistance
1090 default_issue_status_new: Nouveau
1092 default_issue_status_new: Nouveau
1091 default_issue_status_in_progress: En cours
1093 default_issue_status_in_progress: En cours
1092 default_issue_status_resolved: Résolu
1094 default_issue_status_resolved: Résolu
1093 default_issue_status_feedback: Commentaire
1095 default_issue_status_feedback: Commentaire
1094 default_issue_status_closed: Fermé
1096 default_issue_status_closed: Fermé
1095 default_issue_status_rejected: Rejeté
1097 default_issue_status_rejected: Rejeté
1096 default_doc_category_user: Documentation utilisateur
1098 default_doc_category_user: Documentation utilisateur
1097 default_doc_category_tech: Documentation technique
1099 default_doc_category_tech: Documentation technique
1098 default_priority_low: Bas
1100 default_priority_low: Bas
1099 default_priority_normal: Normal
1101 default_priority_normal: Normal
1100 default_priority_high: Haut
1102 default_priority_high: Haut
1101 default_priority_urgent: Urgent
1103 default_priority_urgent: Urgent
1102 default_priority_immediate: Immédiat
1104 default_priority_immediate: Immédiat
1103 default_activity_design: Conception
1105 default_activity_design: Conception
1104 default_activity_development: Développement
1106 default_activity_development: Développement
1105
1107
1106 enumeration_issue_priorities: Priorités des demandes
1108 enumeration_issue_priorities: Priorités des demandes
1107 enumeration_doc_categories: Catégories des documents
1109 enumeration_doc_categories: Catégories des documents
1108 enumeration_activities: Activités (suivi du temps)
1110 enumeration_activities: Activités (suivi du temps)
1109 enumeration_system_activity: Activité système
1111 enumeration_system_activity: Activité système
1110 description_filter: Filtre
1112 description_filter: Filtre
1111 description_search: Champ de recherche
1113 description_search: Champ de recherche
1112 description_choose_project: Projets
1114 description_choose_project: Projets
1113 description_project_scope: Périmètre de recherche
1115 description_project_scope: Périmètre de recherche
1114 description_notes: Notes
1116 description_notes: Notes
1115 description_message_content: Contenu du message
1117 description_message_content: Contenu du message
1116 description_query_sort_criteria_attribute: Critère de tri
1118 description_query_sort_criteria_attribute: Critère de tri
1117 description_query_sort_criteria_direction: Ordre de tri
1119 description_query_sort_criteria_direction: Ordre de tri
1118 description_user_mail_notification: Option de notification
1120 description_user_mail_notification: Option de notification
1119 description_available_columns: Colonnes disponibles
1121 description_available_columns: Colonnes disponibles
1120 description_selected_columns: Colonnes sélectionnées
1122 description_selected_columns: Colonnes sélectionnées
1121 description_all_columns: Toutes les colonnes
1123 description_all_columns: Toutes les colonnes
1122 description_issue_category_reassign: Choisir une catégorie
1124 description_issue_category_reassign: Choisir une catégorie
1123 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1125 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1124 description_date_range_list: Choisir une période prédéfinie
1126 description_date_range_list: Choisir une période prédéfinie
1125 description_date_range_interval: Choisir une période
1127 description_date_range_interval: Choisir une période
1126 description_date_from: Date de début
1128 description_date_from: Date de début
1127 description_date_to: Date de fin
1129 description_date_to: Date de fin
1128 text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
1130 text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
@@ -1,544 +1,554
1 package Apache::Authn::Redmine;
1 package Apache::Authn::Redmine;
2
2
3 =head1 Apache::Authn::Redmine
3 =head1 Apache::Authn::Redmine
4
4
5 Redmine - a mod_perl module to authenticate webdav subversion users
5 Redmine - a mod_perl module to authenticate webdav subversion users
6 against redmine database
6 against redmine database
7
7
8 =head1 SYNOPSIS
8 =head1 SYNOPSIS
9
9
10 This module allow anonymous users to browse public project and
10 This module allow anonymous users to browse public project and
11 registred users to browse and commit their project. Authentication is
11 registred users to browse and commit their project. Authentication is
12 done against the redmine database or the LDAP configured in redmine.
12 done against the redmine database or the LDAP configured in redmine.
13
13
14 This method is far simpler than the one with pam_* and works with all
14 This method is far simpler than the one with pam_* and works with all
15 database without an hassle but you need to have apache/mod_perl on the
15 database without an hassle but you need to have apache/mod_perl on the
16 svn server.
16 svn server.
17
17
18 =head1 INSTALLATION
18 =head1 INSTALLATION
19
19
20 For this to automagically work, you need to have a recent reposman.rb
20 For this to automagically work, you need to have a recent reposman.rb
21 (after r860) and if you already use reposman, read the last section to
21 (after r860) and if you already use reposman, read the last section to
22 migrate.
22 migrate.
23
23
24 Sorry ruby users but you need some perl modules, at least mod_perl2,
24 Sorry ruby users but you need some perl modules, at least mod_perl2,
25 DBI and DBD::mysql (or the DBD driver for you database as it should
25 DBI and DBD::mysql (or the DBD driver for you database as it should
26 work on allmost all databases).
26 work on allmost all databases).
27
27
28 On debian/ubuntu you must do :
28 On debian/ubuntu you must do :
29
29
30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31
31
32 If your Redmine users use LDAP authentication, you will also need
32 If your Redmine users use LDAP authentication, you will also need
33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34
34
35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36
36
37 =head1 CONFIGURATION
37 =head1 CONFIGURATION
38
38
39 ## This module has to be in your perl path
39 ## This module has to be in your perl path
40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 PerlLoadModule Apache::Authn::Redmine
41 PerlLoadModule Apache::Authn::Redmine
42 <Location /svn>
42 <Location /svn>
43 DAV svn
43 DAV svn
44 SVNParentPath "/var/svn"
44 SVNParentPath "/var/svn"
45
45
46 AuthType Basic
46 AuthType Basic
47 AuthName redmine
47 AuthName redmine
48 Require valid-user
48 Require valid-user
49
49
50 PerlAccessHandler Apache::Authn::Redmine::access_handler
50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52
52
53 ## for mysql
53 ## for mysql
54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 ## for postgres
55 ## for postgres
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57
57
58 RedmineDbUser "redmine"
58 RedmineDbUser "redmine"
59 RedmineDbPass "password"
59 RedmineDbPass "password"
60 ## Optional where clause (fulltext search would be slow and
60 ## Optional where clause (fulltext search would be slow and
61 ## database dependant).
61 ## database dependant).
62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 ## Optional credentials cache size
63 ## Optional credentials cache size
64 # RedmineCacheCredsMax 50
64 # RedmineCacheCredsMax 50
65 </Location>
65 </Location>
66
66
67 To be able to browse repository inside redmine, you must add something
67 To be able to browse repository inside redmine, you must add something
68 like that :
68 like that :
69
69
70 <Location /svn-private>
70 <Location /svn-private>
71 DAV svn
71 DAV svn
72 SVNParentPath "/var/svn"
72 SVNParentPath "/var/svn"
73 Order deny,allow
73 Order deny,allow
74 Deny from all
74 Deny from all
75 # only allow reading orders
75 # only allow reading orders
76 <Limit GET PROPFIND OPTIONS REPORT>
76 <Limit GET PROPFIND OPTIONS REPORT>
77 Allow from redmine.server.ip
77 Allow from redmine.server.ip
78 </Limit>
78 </Limit>
79 </Location>
79 </Location>
80
80
81 and you will have to use this reposman.rb command line to create repository :
81 and you will have to use this reposman.rb command line to create repository :
82
82
83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84
84
85 =head1 REPOSITORIES NAMING
85 =head1 REPOSITORIES NAMING
86
86
87 A projet repository must be named with the projet identifier. In case
87 A projet repository must be named with the projet identifier. In case
88 of multiple repositories for the same project, use the project identifier
88 of multiple repositories for the same project, use the project identifier
89 and the repository identifier separated with a dot:
89 and the repository identifier separated with a dot:
90
90
91 /var/svn/foo
91 /var/svn/foo
92 /var/svn/foo.otherrepo
92 /var/svn/foo.otherrepo
93
93
94 =head1 MIGRATION FROM OLDER RELEASES
94 =head1 MIGRATION FROM OLDER RELEASES
95
95
96 If you use an older reposman.rb (r860 or before), you need to change
96 If you use an older reposman.rb (r860 or before), you need to change
97 rights on repositories to allow the apache user to read and write
97 rights on repositories to allow the apache user to read and write
98 S<them :>
98 S<them :>
99
99
100 sudo chown -R www-data /var/svn/*
100 sudo chown -R www-data /var/svn/*
101 sudo chmod -R u+w /var/svn/*
101 sudo chmod -R u+w /var/svn/*
102
102
103 And you need to upgrade at least reposman.rb (after r860).
103 And you need to upgrade at least reposman.rb (after r860).
104
104
105 =head1 GIT SMART HTTP SUPPORT
105 =head1 GIT SMART HTTP SUPPORT
106
106
107 Git's smart HTTP protocol (available since Git 1.7.0) will not work with the
107 Git's smart HTTP protocol (available since Git 1.7.0) will not work with the
108 above settings. Redmine.pm normally does access control depending on the HTTP
108 above settings. Redmine.pm normally does access control depending on the HTTP
109 method used: read-only methods are OK for everyone in public projects and
109 method used: read-only methods are OK for everyone in public projects and
110 members with read rights in private projects. The rest require membership with
110 members with read rights in private projects. The rest require membership with
111 commit rights in the project.
111 commit rights in the project.
112
112
113 However, this scheme doesn't work for Git's smart HTTP protocol, as it will use
113 However, this scheme doesn't work for Git's smart HTTP protocol, as it will use
114 POST even for a simple clone. Instead, read-only requests must be detected using
114 POST even for a simple clone. Instead, read-only requests must be detected using
115 the full URL (including the query string): anything that doesn't belong to the
115 the full URL (including the query string): anything that doesn't belong to the
116 git-receive-pack service is read-only.
116 git-receive-pack service is read-only.
117
117
118 To activate this mode of operation, add this line inside your <Location /git>
118 To activate this mode of operation, add this line inside your <Location /git>
119 block:
119 block:
120
120
121 RedmineGitSmartHttp yes
121 RedmineGitSmartHttp yes
122
122
123 Here's a sample Apache configuration which integrates git-http-backend with
123 Here's a sample Apache configuration which integrates git-http-backend with
124 a MySQL database and this new option:
124 a MySQL database and this new option:
125
125
126 SetEnv GIT_PROJECT_ROOT /var/www/git/
126 SetEnv GIT_PROJECT_ROOT /var/www/git/
127 SetEnv GIT_HTTP_EXPORT_ALL
127 SetEnv GIT_HTTP_EXPORT_ALL
128 ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
128 ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
129 <Location /git>
129 <Location /git>
130 Order allow,deny
130 Order allow,deny
131 Allow from all
131 Allow from all
132
132
133 AuthType Basic
133 AuthType Basic
134 AuthName Git
134 AuthName Git
135 Require valid-user
135 Require valid-user
136
136
137 PerlAccessHandler Apache::Authn::Redmine::access_handler
137 PerlAccessHandler Apache::Authn::Redmine::access_handler
138 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
138 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
139 # for mysql
139 # for mysql
140 RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1"
140 RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1"
141 RedmineDbUser "redmine"
141 RedmineDbUser "redmine"
142 RedmineDbPass "xxx"
142 RedmineDbPass "xxx"
143 RedmineGitSmartHttp yes
143 RedmineGitSmartHttp yes
144 </Location>
144 </Location>
145
145
146 Make sure that all the names of the repositories under /var/www/git/ have a
146 Make sure that all the names of the repositories under /var/www/git/ have a
147 matching identifier for some project: /var/www/git/myproject and
147 matching identifier for some project: /var/www/git/myproject and
148 /var/www/git/myproject.git will work. You can put both bare and non-bare
148 /var/www/git/myproject.git will work. You can put both bare and non-bare
149 repositories in /var/www/git, though bare repositories are strongly
149 repositories in /var/www/git, though bare repositories are strongly
150 recommended. You should create them with the rights of the user running Redmine,
150 recommended. You should create them with the rights of the user running Redmine,
151 like this:
151 like this:
152
152
153 cd /var/www/git
153 cd /var/www/git
154 sudo -u user-running-redmine mkdir myproject
154 sudo -u user-running-redmine mkdir myproject
155 cd myproject
155 cd myproject
156 sudo -u user-running-redmine git init --bare
156 sudo -u user-running-redmine git init --bare
157
157
158 Once you have activated this option, you have three options when cloning a
158 Once you have activated this option, you have three options when cloning a
159 repository:
159 repository:
160
160
161 - Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password
161 - Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password
162 all the time.
162 all the time.
163
163
164 - Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but
164 - Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but
165 this could reveal accidentally your password to the console in some versions
165 this could reveal accidentally your password to the console in some versions
166 of Git, and you would have to ensure that .git/config is not readable except
166 of Git, and you would have to ensure that .git/config is not readable except
167 by the owner for each of your projects.
167 by the owner for each of your projects.
168
168
169 - Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc
169 - Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc
170 file. This is the recommended solution, as you only have one file to protect
170 file. This is the recommended solution, as you only have one file to protect
171 and passwords will not be leaked accidentally to the console.
171 and passwords will not be leaked accidentally to the console.
172
172
173 IMPORTANT NOTE: It is *very important* that the file cannot be read by other
173 IMPORTANT NOTE: It is *very important* that the file cannot be read by other
174 users, as it will contain your password in cleartext. To create the file, you
174 users, as it will contain your password in cleartext. To create the file, you
175 can use the following commands, replacing yourhost, youruser and yourpassword
175 can use the following commands, replacing yourhost, youruser and yourpassword
176 with the right values:
176 with the right values:
177
177
178 touch ~/.netrc
178 touch ~/.netrc
179 chmod 600 ~/.netrc
179 chmod 600 ~/.netrc
180 echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc
180 echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc
181
181
182 =cut
182 =cut
183
183
184 use strict;
184 use strict;
185 use warnings FATAL => 'all', NONFATAL => 'redefine';
185 use warnings FATAL => 'all', NONFATAL => 'redefine';
186
186
187 use DBI;
187 use DBI;
188 use Digest::SHA;
188 use Digest::SHA;
189 # optional module for LDAP authentication
189 # optional module for LDAP authentication
190 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
190 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
191
191
192 use Apache2::Module;
192 use Apache2::Module;
193 use Apache2::Access;
193 use Apache2::Access;
194 use Apache2::ServerRec qw();
194 use Apache2::ServerRec qw();
195 use Apache2::RequestRec qw();
195 use Apache2::RequestRec qw();
196 use Apache2::RequestUtil qw();
196 use Apache2::RequestUtil qw();
197 use Apache2::Const qw(:common :override :cmd_how);
197 use Apache2::Const qw(:common :override :cmd_how);
198 use APR::Pool ();
198 use APR::Pool ();
199 use APR::Table ();
199 use APR::Table ();
200
200
201 # use Apache2::Directive qw();
201 # use Apache2::Directive qw();
202
202
203 my @directives = (
203 my @directives = (
204 {
204 {
205 name => 'RedmineDSN',
205 name => 'RedmineDSN',
206 req_override => OR_AUTHCFG,
206 req_override => OR_AUTHCFG,
207 args_how => TAKE1,
207 args_how => TAKE1,
208 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
208 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
209 },
209 },
210 {
210 {
211 name => 'RedmineDbUser',
211 name => 'RedmineDbUser',
212 req_override => OR_AUTHCFG,
212 req_override => OR_AUTHCFG,
213 args_how => TAKE1,
213 args_how => TAKE1,
214 },
214 },
215 {
215 {
216 name => 'RedmineDbPass',
216 name => 'RedmineDbPass',
217 req_override => OR_AUTHCFG,
217 req_override => OR_AUTHCFG,
218 args_how => TAKE1,
218 args_how => TAKE1,
219 },
219 },
220 {
220 {
221 name => 'RedmineDbWhereClause',
221 name => 'RedmineDbWhereClause',
222 req_override => OR_AUTHCFG,
222 req_override => OR_AUTHCFG,
223 args_how => TAKE1,
223 args_how => TAKE1,
224 },
224 },
225 {
225 {
226 name => 'RedmineCacheCredsMax',
226 name => 'RedmineCacheCredsMax',
227 req_override => OR_AUTHCFG,
227 req_override => OR_AUTHCFG,
228 args_how => TAKE1,
228 args_how => TAKE1,
229 errmsg => 'RedmineCacheCredsMax must be decimal number',
229 errmsg => 'RedmineCacheCredsMax must be decimal number',
230 },
230 },
231 {
231 {
232 name => 'RedmineGitSmartHttp',
232 name => 'RedmineGitSmartHttp',
233 req_override => OR_AUTHCFG,
233 req_override => OR_AUTHCFG,
234 args_how => TAKE1,
234 args_how => TAKE1,
235 },
235 },
236 );
236 );
237
237
238 sub RedmineDSN {
238 sub RedmineDSN {
239 my ($self, $parms, $arg) = @_;
239 my ($self, $parms, $arg) = @_;
240 $self->{RedmineDSN} = $arg;
240 $self->{RedmineDSN} = $arg;
241 my $query = "SELECT
241 my $query = "SELECT
242 users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
242 users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
243 FROM projects, users, roles
243 FROM projects, users, roles
244 WHERE
244 WHERE
245 users.login=?
245 users.login=?
246 AND projects.identifier=?
246 AND projects.identifier=?
247 AND users.status=1
247 AND users.status=1
248 AND (
248 AND (
249 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
249 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
250 OR
250 OR
251 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
251 (cast(projects.is_public as CHAR) IN ('t', '1')
252 AND (roles.builtin=1
253 OR roles.id IN (SELECT member_roles.role_id FROM members, member_roles, users g
254 WHERE members.user_id = g.id AND members.project_id = projects.id AND members.id = member_roles.member_id
255 AND g.type = 'GroupNonMember'))
256 )
252 )
257 )
253 AND roles.permissions IS NOT NULL";
258 AND roles.permissions IS NOT NULL";
254 $self->{RedmineQuery} = trim($query);
259 $self->{RedmineQuery} = trim($query);
255 }
260 }
256
261
257 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
262 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
258 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
263 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
259 sub RedmineDbWhereClause {
264 sub RedmineDbWhereClause {
260 my ($self, $parms, $arg) = @_;
265 my ($self, $parms, $arg) = @_;
261 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
266 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
262 }
267 }
263
268
264 sub RedmineCacheCredsMax {
269 sub RedmineCacheCredsMax {
265 my ($self, $parms, $arg) = @_;
270 my ($self, $parms, $arg) = @_;
266 if ($arg) {
271 if ($arg) {
267 $self->{RedmineCachePool} = APR::Pool->new;
272 $self->{RedmineCachePool} = APR::Pool->new;
268 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
273 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
269 $self->{RedmineCacheCredsCount} = 0;
274 $self->{RedmineCacheCredsCount} = 0;
270 $self->{RedmineCacheCredsMax} = $arg;
275 $self->{RedmineCacheCredsMax} = $arg;
271 }
276 }
272 }
277 }
273
278
274 sub RedmineGitSmartHttp {
279 sub RedmineGitSmartHttp {
275 my ($self, $parms, $arg) = @_;
280 my ($self, $parms, $arg) = @_;
276 $arg = lc $arg;
281 $arg = lc $arg;
277
282
278 if ($arg eq "yes" || $arg eq "true") {
283 if ($arg eq "yes" || $arg eq "true") {
279 $self->{RedmineGitSmartHttp} = 1;
284 $self->{RedmineGitSmartHttp} = 1;
280 } else {
285 } else {
281 $self->{RedmineGitSmartHttp} = 0;
286 $self->{RedmineGitSmartHttp} = 0;
282 }
287 }
283 }
288 }
284
289
285 sub trim {
290 sub trim {
286 my $string = shift;
291 my $string = shift;
287 $string =~ s/\s{2,}/ /g;
292 $string =~ s/\s{2,}/ /g;
288 return $string;
293 return $string;
289 }
294 }
290
295
291 sub set_val {
296 sub set_val {
292 my ($key, $self, $parms, $arg) = @_;
297 my ($key, $self, $parms, $arg) = @_;
293 $self->{$key} = $arg;
298 $self->{$key} = $arg;
294 }
299 }
295
300
296 Apache2::Module::add(__PACKAGE__, \@directives);
301 Apache2::Module::add(__PACKAGE__, \@directives);
297
302
298
303
299 my %read_only_methods = map { $_ => 1 } qw/GET HEAD PROPFIND REPORT OPTIONS/;
304 my %read_only_methods = map { $_ => 1 } qw/GET HEAD PROPFIND REPORT OPTIONS/;
300
305
301 sub request_is_read_only {
306 sub request_is_read_only {
302 my ($r) = @_;
307 my ($r) = @_;
303 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
308 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
304
309
305 # Do we use Git's smart HTTP protocol, or not?
310 # Do we use Git's smart HTTP protocol, or not?
306 if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
311 if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
307 my $uri = $r->unparsed_uri;
312 my $uri = $r->unparsed_uri;
308 my $location = $r->location;
313 my $location = $r->location;
309 my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
314 my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
310 return $is_read_only;
315 return $is_read_only;
311 } else {
316 } else {
312 # Standard behaviour: check the HTTP method
317 # Standard behaviour: check the HTTP method
313 my $method = $r->method;
318 my $method = $r->method;
314 return defined $read_only_methods{$method};
319 return defined $read_only_methods{$method};
315 }
320 }
316 }
321 }
317
322
318 sub access_handler {
323 sub access_handler {
319 my $r = shift;
324 my $r = shift;
320
325
321 unless ($r->some_auth_required) {
326 unless ($r->some_auth_required) {
322 $r->log_reason("No authentication has been configured");
327 $r->log_reason("No authentication has been configured");
323 return FORBIDDEN;
328 return FORBIDDEN;
324 }
329 }
325
330
326 return OK unless request_is_read_only($r);
331 return OK unless request_is_read_only($r);
327
332
328 my $project_id = get_project_identifier($r);
333 my $project_id = get_project_identifier($r);
329
334
330 $r->set_handlers(PerlAuthenHandler => [\&OK])
335 $r->set_handlers(PerlAuthenHandler => [\&OK])
331 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
336 if is_public_project($project_id, $r) && anonymous_allowed_to_browse_repository($project_id, $r);
332
337
333 return OK
338 return OK
334 }
339 }
335
340
336 sub authen_handler {
341 sub authen_handler {
337 my $r = shift;
342 my $r = shift;
338
343
339 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
344 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
340 return $res unless $res == OK;
345 return $res unless $res == OK;
341
346
342 if (is_member($r->user, $redmine_pass, $r)) {
347 if (is_member($r->user, $redmine_pass, $r)) {
343 return OK;
348 return OK;
344 } else {
349 } else {
345 $r->note_auth_failure();
350 $r->note_auth_failure();
346 return DECLINED;
351 return DECLINED;
347 }
352 }
348 }
353 }
349
354
350 # check if authentication is forced
355 # check if authentication is forced
351 sub is_authentication_forced {
356 sub is_authentication_forced {
352 my $r = shift;
357 my $r = shift;
353
358
354 my $dbh = connect_database($r);
359 my $dbh = connect_database($r);
355 my $sth = $dbh->prepare(
360 my $sth = $dbh->prepare(
356 "SELECT value FROM settings where settings.name = 'login_required';"
361 "SELECT value FROM settings where settings.name = 'login_required';"
357 );
362 );
358
363
359 $sth->execute();
364 $sth->execute();
360 my $ret = 0;
365 my $ret = 0;
361 if (my @row = $sth->fetchrow_array) {
366 if (my @row = $sth->fetchrow_array) {
362 if ($row[0] eq "1" || $row[0] eq "t") {
367 if ($row[0] eq "1" || $row[0] eq "t") {
363 $ret = 1;
368 $ret = 1;
364 }
369 }
365 }
370 }
366 $sth->finish();
371 $sth->finish();
367 undef $sth;
372 undef $sth;
368
373
369 $dbh->disconnect();
374 $dbh->disconnect();
370 undef $dbh;
375 undef $dbh;
371
376
372 $ret;
377 $ret;
373 }
378 }
374
379
375 sub is_public_project {
380 sub is_public_project {
376 my $project_id = shift;
381 my $project_id = shift;
377 my $r = shift;
382 my $r = shift;
378
383
379 if (is_authentication_forced($r)) {
384 if (is_authentication_forced($r)) {
380 return 0;
385 return 0;
381 }
386 }
382
387
383 my $dbh = connect_database($r);
388 my $dbh = connect_database($r);
384 my $sth = $dbh->prepare(
389 my $sth = $dbh->prepare(
385 "SELECT is_public FROM projects WHERE projects.identifier = ? AND projects.status <> 9;"
390 "SELECT is_public FROM projects WHERE projects.identifier = ? AND projects.status <> 9;"
386 );
391 );
387
392
388 $sth->execute($project_id);
393 $sth->execute($project_id);
389 my $ret = 0;
394 my $ret = 0;
390 if (my @row = $sth->fetchrow_array) {
395 if (my @row = $sth->fetchrow_array) {
391 if ($row[0] eq "1" || $row[0] eq "t") {
396 if ($row[0] eq "1" || $row[0] eq "t") {
392 $ret = 1;
397 $ret = 1;
393 }
398 }
394 }
399 }
395 $sth->finish();
400 $sth->finish();
396 undef $sth;
401 undef $sth;
397 $dbh->disconnect();
402 $dbh->disconnect();
398 undef $dbh;
403 undef $dbh;
399
404
400 $ret;
405 $ret;
401 }
406 }
402
407
403 sub anonymous_role_allows_browse_repository {
408 sub anonymous_allowed_to_browse_repository {
409 my $project_id = shift;
404 my $r = shift;
410 my $r = shift;
405
411
406 my $dbh = connect_database($r);
412 my $dbh = connect_database($r);
407 my $sth = $dbh->prepare(
413 my $sth = $dbh->prepare(
408 "SELECT permissions FROM roles WHERE builtin = 2;"
414 "SELECT permissions FROM roles WHERE permissions like '%browse_repository%'
415 AND (roles.builtin = 2
416 OR roles.id IN (SELECT member_roles.role_id FROM projects, members, member_roles, users
417 WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id
418 AND projects.identifier = ? AND users.type = 'GroupAnonymous'));"
409 );
419 );
410
420
411 $sth->execute();
421 $sth->execute($project_id);
412 my $ret = 0;
422 my $ret = 0;
413 if (my @row = $sth->fetchrow_array) {
423 if (my @row = $sth->fetchrow_array) {
414 if ($row[0] =~ /:browse_repository/) {
424 if ($row[0] =~ /:browse_repository/) {
415 $ret = 1;
425 $ret = 1;
416 }
426 }
417 }
427 }
418 $sth->finish();
428 $sth->finish();
419 undef $sth;
429 undef $sth;
420 $dbh->disconnect();
430 $dbh->disconnect();
421 undef $dbh;
431 undef $dbh;
422
432
423 $ret;
433 $ret;
424 }
434 }
425
435
426 # perhaps we should use repository right (other read right) to check public access.
436 # perhaps we should use repository right (other read right) to check public access.
427 # it could be faster BUT it doesn't work for the moment.
437 # it could be faster BUT it doesn't work for the moment.
428 # sub is_public_project_by_file {
438 # sub is_public_project_by_file {
429 # my $project_id = shift;
439 # my $project_id = shift;
430 # my $r = shift;
440 # my $r = shift;
431
441
432 # my $tree = Apache2::Directive::conftree();
442 # my $tree = Apache2::Directive::conftree();
433 # my $node = $tree->lookup('Location', $r->location);
443 # my $node = $tree->lookup('Location', $r->location);
434 # my $hash = $node->as_hash;
444 # my $hash = $node->as_hash;
435
445
436 # my $svnparentpath = $hash->{SVNParentPath};
446 # my $svnparentpath = $hash->{SVNParentPath};
437 # my $repos_path = $svnparentpath . "/" . $project_id;
447 # my $repos_path = $svnparentpath . "/" . $project_id;
438 # return 1 if (stat($repos_path))[2] & 00007;
448 # return 1 if (stat($repos_path))[2] & 00007;
439 # }
449 # }
440
450
441 sub is_member {
451 sub is_member {
442 my $redmine_user = shift;
452 my $redmine_user = shift;
443 my $redmine_pass = shift;
453 my $redmine_pass = shift;
444 my $r = shift;
454 my $r = shift;
445
455
446 my $dbh = connect_database($r);
456 my $dbh = connect_database($r);
447 my $project_id = get_project_identifier($r);
457 my $project_id = get_project_identifier($r);
448
458
449 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
459 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
450
460
451 my $access_mode = request_is_read_only($r) ? "R" : "W";
461 my $access_mode = request_is_read_only($r) ? "R" : "W";
452
462
453 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
463 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
454 my $usrprojpass;
464 my $usrprojpass;
455 if ($cfg->{RedmineCacheCredsMax}) {
465 if ($cfg->{RedmineCacheCredsMax}) {
456 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
466 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
457 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
467 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
458 }
468 }
459 my $query = $cfg->{RedmineQuery};
469 my $query = $cfg->{RedmineQuery};
460 my $sth = $dbh->prepare($query);
470 my $sth = $dbh->prepare($query);
461 $sth->execute($redmine_user, $project_id);
471 $sth->execute($redmine_user, $project_id);
462
472
463 my $ret;
473 my $ret;
464 while (my ($hashed_password, $salt, $auth_source_id, $permissions, $project_status) = $sth->fetchrow_array) {
474 while (my ($hashed_password, $salt, $auth_source_id, $permissions, $project_status) = $sth->fetchrow_array) {
465 if ($project_status eq "9" || ($project_status ne "1" && $access_mode eq "W")) {
475 if ($project_status eq "9" || ($project_status ne "1" && $access_mode eq "W")) {
466 last;
476 last;
467 }
477 }
468
478
469 unless ($auth_source_id) {
479 unless ($auth_source_id) {
470 my $method = $r->method;
480 my $method = $r->method;
471 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
481 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
472 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
482 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
473 $ret = 1;
483 $ret = 1;
474 last;
484 last;
475 }
485 }
476 } elsif ($CanUseLDAPAuth) {
486 } elsif ($CanUseLDAPAuth) {
477 my $sthldap = $dbh->prepare(
487 my $sthldap = $dbh->prepare(
478 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
488 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
479 );
489 );
480 $sthldap->execute($auth_source_id);
490 $sthldap->execute($auth_source_id);
481 while (my @rowldap = $sthldap->fetchrow_array) {
491 while (my @rowldap = $sthldap->fetchrow_array) {
482 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
492 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
483 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
493 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
484 if ($bind_as =~ m/\$login/) {
494 if ($bind_as =~ m/\$login/) {
485 # replace $login with $redmine_user and use $redmine_pass
495 # replace $login with $redmine_user and use $redmine_pass
486 $bind_as =~ s/\$login/$redmine_user/g;
496 $bind_as =~ s/\$login/$redmine_user/g;
487 $bind_pw = $redmine_pass
497 $bind_pw = $redmine_pass
488 }
498 }
489 my $ldap = Authen::Simple::LDAP->new(
499 my $ldap = Authen::Simple::LDAP->new(
490 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
500 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
491 port => $rowldap[1],
501 port => $rowldap[1],
492 basedn => $rowldap[5],
502 basedn => $rowldap[5],
493 binddn => $bind_as,
503 binddn => $bind_as,
494 bindpw => $bind_pw,
504 bindpw => $bind_pw,
495 filter => "(".$rowldap[6]."=%s)"
505 filter => "(".$rowldap[6]."=%s)"
496 );
506 );
497 my $method = $r->method;
507 my $method = $r->method;
498 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
508 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
499
509
500 }
510 }
501 $sthldap->finish();
511 $sthldap->finish();
502 undef $sthldap;
512 undef $sthldap;
503 }
513 }
504 }
514 }
505 $sth->finish();
515 $sth->finish();
506 undef $sth;
516 undef $sth;
507 $dbh->disconnect();
517 $dbh->disconnect();
508 undef $dbh;
518 undef $dbh;
509
519
510 if ($cfg->{RedmineCacheCredsMax} and $ret) {
520 if ($cfg->{RedmineCacheCredsMax} and $ret) {
511 if (defined $usrprojpass) {
521 if (defined $usrprojpass) {
512 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
522 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
513 } else {
523 } else {
514 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
524 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
515 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
525 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
516 $cfg->{RedmineCacheCredsCount}++;
526 $cfg->{RedmineCacheCredsCount}++;
517 } else {
527 } else {
518 $cfg->{RedmineCacheCreds}->clear();
528 $cfg->{RedmineCacheCreds}->clear();
519 $cfg->{RedmineCacheCredsCount} = 0;
529 $cfg->{RedmineCacheCredsCount} = 0;
520 }
530 }
521 }
531 }
522 }
532 }
523
533
524 $ret;
534 $ret;
525 }
535 }
526
536
527 sub get_project_identifier {
537 sub get_project_identifier {
528 my $r = shift;
538 my $r = shift;
529
539
530 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
540 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
531 my $location = $r->location;
541 my $location = $r->location;
532 $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
542 $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
533 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
543 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
534 $identifier;
544 $identifier;
535 }
545 }
536
546
537 sub connect_database {
547 sub connect_database {
538 my $r = shift;
548 my $r = shift;
539
549
540 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
550 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
541 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
551 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
542 }
552 }
543
553
544 1;
554 1;
@@ -1,1193 +1,1196
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 #content h1, h2, h3, h4 {color: #555;}
5 #content h1, h2, h3, h4 {color: #555;}
6 h2, .wiki h1 {font-size: 20px;}
6 h2, .wiki h1 {font-size: 20px;}
7 h3, .wiki h2 {font-size: 16px;}
7 h3, .wiki h2 {font-size: 16px;}
8 h4, .wiki h3 {font-size: 13px;}
8 h4, .wiki h3 {font-size: 13px;}
9 h4 {border-bottom: 1px dotted #bbb;}
9 h4 {border-bottom: 1px dotted #bbb;}
10
10
11 /***** Layout *****/
11 /***** Layout *****/
12 #wrapper {background: white;}
12 #wrapper {background: white;}
13
13
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 #top-menu ul {margin: 0; padding: 0;}
15 #top-menu ul {margin: 0; padding: 0;}
16 #top-menu li {
16 #top-menu li {
17 float:left;
17 float:left;
18 list-style-type:none;
18 list-style-type:none;
19 margin: 0px 0px 0px 0px;
19 margin: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
21 white-space:nowrap;
21 white-space:nowrap;
22 }
22 }
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25
25
26 #account {float:right;}
26 #account {float:right;}
27
27
28 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
28 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
29 #header a {color:#f8f8f8;}
29 #header a {color:#f8f8f8;}
30 #header h1 a.ancestor { font-size: 80%; }
30 #header h1 a.ancestor { font-size: 80%; }
31 #quick-search {float:right;}
31 #quick-search {float:right;}
32
32
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
34 #main-menu ul {margin: 0; padding: 0;}
34 #main-menu ul {margin: 0; padding: 0;}
35 #main-menu li {
35 #main-menu li {
36 float:left;
36 float:left;
37 list-style-type:none;
37 list-style-type:none;
38 margin: 0px 2px 0px 0px;
38 margin: 0px 2px 0px 0px;
39 padding: 0px 0px 0px 0px;
39 padding: 0px 0px 0px 0px;
40 white-space:nowrap;
40 white-space:nowrap;
41 }
41 }
42 #main-menu li a {
42 #main-menu li a {
43 display: block;
43 display: block;
44 color: #fff;
44 color: #fff;
45 text-decoration: none;
45 text-decoration: none;
46 font-weight: bold;
46 font-weight: bold;
47 margin: 0;
47 margin: 0;
48 padding: 4px 10px 4px 10px;
48 padding: 4px 10px 4px 10px;
49 }
49 }
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
52
52
53 #admin-menu ul {margin: 0; padding: 0;}
53 #admin-menu ul {margin: 0; padding: 0;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
55
55
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
70
70
71 #main {background-color:#EEEEEE;}
71 #main {background-color:#EEEEEE;}
72
72
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
74 * html #sidebar{ width: 22%; }
74 * html #sidebar{ width: 22%; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
78 #sidebar .contextual { margin-right: 1em; }
78 #sidebar .contextual { margin-right: 1em; }
79 #sidebar ul {margin: 0; padding: 0;}
79 #sidebar ul {margin: 0; padding: 0;}
80 #sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
80 #sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
81
81
82 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
82 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
83 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
83 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
84 html>body #content { min-height: 600px; }
84 html>body #content { min-height: 600px; }
85 * html body #content { height: 600px; } /* IE */
85 * html body #content { height: 600px; } /* IE */
86
86
87 #main.nosidebar #sidebar{ display: none; }
87 #main.nosidebar #sidebar{ display: none; }
88 #main.nosidebar #content{ width: auto; border-right: 0; }
88 #main.nosidebar #content{ width: auto; border-right: 0; }
89
89
90 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
90 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
91
91
92 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
92 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
93 #login-form table td {padding: 6px;}
93 #login-form table td {padding: 6px;}
94 #login-form label {font-weight: bold;}
94 #login-form label {font-weight: bold;}
95 #login-form input#username, #login-form input#password { width: 300px; }
95 #login-form input#username, #login-form input#password { width: 300px; }
96
96
97 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
97 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
98 div.modal h3.title {display:none;}
98 div.modal h3.title {display:none;}
99 div.modal p.buttons {text-align:right; margin-bottom:0;}
99 div.modal p.buttons {text-align:right; margin-bottom:0;}
100
100
101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102
102
103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104
104
105 /***** Links *****/
105 /***** Links *****/
106 a, a:link, a:visited{ color: #169; text-decoration: none; }
106 a, a:link, a:visited{ color: #169; text-decoration: none; }
107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 a img{ border: 0; }
108 a img{ border: 0; }
109
109
110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
111 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
112 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
112 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
113
113
114 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
114 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
115 #sidebar a.selected:hover {text-decoration:none;}
115 #sidebar a.selected:hover {text-decoration:none;}
116 #admin-menu a {line-height:1.7em;}
116 #admin-menu a {line-height:1.7em;}
117 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
117 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
118
118
119 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
119 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
120 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
120 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
121
121
122 a#toggle-completed-versions {color:#999;}
122 a#toggle-completed-versions {color:#999;}
123 /***** Tables *****/
123 /***** Tables *****/
124 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
124 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
125 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
125 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
126 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
126 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
127 table.list td.id { width: 2%; text-align: center;}
127 table.list td.id { width: 2%; text-align: center;}
128 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
128 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
129 table.list td.tick {width:15%}
129 table.list td.tick {width:15%}
130 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
130 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
131 table.list td.checkbox input {padding:0px;}
131 table.list td.checkbox input {padding:0px;}
132 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
132 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
133 table.list td.buttons a { padding-right: 0.6em; }
133 table.list td.buttons a { padding-right: 0.6em; }
134 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
134 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
135 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
135 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
136
136
137 tr.project td.name a { white-space:nowrap; }
137 tr.project td.name a { white-space:nowrap; }
138 tr.project.closed, tr.project.archived { color: #aaa; }
138 tr.project.closed, tr.project.archived { color: #aaa; }
139 tr.project.closed a, tr.project.archived a { color: #aaa; }
139 tr.project.closed a, tr.project.archived a { color: #aaa; }
140
140
141 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
141 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
142 tr.project.idnt-1 td.name {padding-left: 0.5em;}
142 tr.project.idnt-1 td.name {padding-left: 0.5em;}
143 tr.project.idnt-2 td.name {padding-left: 2em;}
143 tr.project.idnt-2 td.name {padding-left: 2em;}
144 tr.project.idnt-3 td.name {padding-left: 3.5em;}
144 tr.project.idnt-3 td.name {padding-left: 3.5em;}
145 tr.project.idnt-4 td.name {padding-left: 5em;}
145 tr.project.idnt-4 td.name {padding-left: 5em;}
146 tr.project.idnt-5 td.name {padding-left: 6.5em;}
146 tr.project.idnt-5 td.name {padding-left: 6.5em;}
147 tr.project.idnt-6 td.name {padding-left: 8em;}
147 tr.project.idnt-6 td.name {padding-left: 8em;}
148 tr.project.idnt-7 td.name {padding-left: 9.5em;}
148 tr.project.idnt-7 td.name {padding-left: 9.5em;}
149 tr.project.idnt-8 td.name {padding-left: 11em;}
149 tr.project.idnt-8 td.name {padding-left: 11em;}
150 tr.project.idnt-9 td.name {padding-left: 12.5em;}
150 tr.project.idnt-9 td.name {padding-left: 12.5em;}
151
151
152 tr.issue { text-align: center; white-space: nowrap; }
152 tr.issue { text-align: center; white-space: nowrap; }
153 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
153 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
154 tr.issue td.relations { text-align: left; }
154 tr.issue td.relations { text-align: left; }
155 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
155 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
156 tr.issue td.relations span {white-space: nowrap;}
156 tr.issue td.relations span {white-space: nowrap;}
157 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
157 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
158 table.issues td.description pre {white-space:normal;}
158 table.issues td.description pre {white-space:normal;}
159
159
160 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
160 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
161 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
161 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
162 tr.issue.idnt-2 td.subject {padding-left: 2em;}
162 tr.issue.idnt-2 td.subject {padding-left: 2em;}
163 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
163 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
164 tr.issue.idnt-4 td.subject {padding-left: 5em;}
164 tr.issue.idnt-4 td.subject {padding-left: 5em;}
165 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
165 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
166 tr.issue.idnt-6 td.subject {padding-left: 8em;}
166 tr.issue.idnt-6 td.subject {padding-left: 8em;}
167 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
167 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
168 tr.issue.idnt-8 td.subject {padding-left: 11em;}
168 tr.issue.idnt-8 td.subject {padding-left: 11em;}
169 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
169 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
170
170
171 table.issue-report {table-layout:fixed;}
171 table.issue-report {table-layout:fixed;}
172
172
173 tr.entry { border: 1px solid #f8f8f8; }
173 tr.entry { border: 1px solid #f8f8f8; }
174 tr.entry td { white-space: nowrap; }
174 tr.entry td { white-space: nowrap; }
175 tr.entry td.filename {width:30%; text-align:left;}
175 tr.entry td.filename {width:30%; text-align:left;}
176 tr.entry td.filename_no_report {width:70%; text-align:left;}
176 tr.entry td.filename_no_report {width:70%; text-align:left;}
177 tr.entry td.size { text-align: right; font-size: 90%; }
177 tr.entry td.size { text-align: right; font-size: 90%; }
178 tr.entry td.revision, tr.entry td.author { text-align: center; }
178 tr.entry td.revision, tr.entry td.author { text-align: center; }
179 tr.entry td.age { text-align: right; }
179 tr.entry td.age { text-align: right; }
180 tr.entry.file td.filename a { margin-left: 16px; }
180 tr.entry.file td.filename a { margin-left: 16px; }
181 tr.entry.file td.filename_no_report a { margin-left: 16px; }
181 tr.entry.file td.filename_no_report a { margin-left: 16px; }
182
182
183 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
183 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
184 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
184 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
185
185
186 tr.changeset { height: 20px }
186 tr.changeset { height: 20px }
187 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
187 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
188 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
188 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
189 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
189 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
190 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
190 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
191
191
192 table.files tbody th {text-align:left;}
192 table.files tbody th {text-align:left;}
193 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
193 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
194 table.files tr.file td.digest { font-size: 80%; }
194 table.files tr.file td.digest { font-size: 80%; }
195
195
196 table.members td.roles, table.memberships td.roles { width: 45%; }
196 table.members td.roles, table.memberships td.roles { width: 45%; }
197
197
198 tr.message { height: 2.6em; }
198 tr.message { height: 2.6em; }
199 tr.message td.subject { padding-left: 20px; }
199 tr.message td.subject { padding-left: 20px; }
200 tr.message td.created_on { white-space: nowrap; }
200 tr.message td.created_on { white-space: nowrap; }
201 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
201 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
202 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
202 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
203 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
203 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
204
204
205 tr.version.closed, tr.version.closed a { color: #999; }
205 tr.version.closed, tr.version.closed a { color: #999; }
206 tr.version td.name { padding-left: 20px; }
206 tr.version td.name { padding-left: 20px; }
207 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
207 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
208 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
208 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
209
209
210 tr.user td {width:13%;white-space: nowrap;}
210 tr.user td {width:13%;white-space: nowrap;}
211 tr.user td.username, tr.user td.firstname, tr.user td.lastname, tr.user td.email {text-align:left;}
211 tr.user td.username, tr.user td.firstname, tr.user td.lastname, tr.user td.email {text-align:left;}
212 tr.user td.email { width:18%; }
212 tr.user td.email { width:18%; }
213 tr.user.locked, tr.user.registered { color: #aaa; }
213 tr.user.locked, tr.user.registered { color: #aaa; }
214 tr.user.locked a, tr.user.registered a { color: #aaa; }
214 tr.user.locked a, tr.user.registered a { color: #aaa; }
215
215
216 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
216 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
217
217
218 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
218 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
219
219
220 tr.time-entry { text-align: center; white-space: nowrap; }
220 tr.time-entry { text-align: center; white-space: nowrap; }
221 tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; }
221 tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; }
222 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
222 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
223 td.hours .hours-dec { font-size: 0.9em; }
223 td.hours .hours-dec { font-size: 0.9em; }
224
224
225 table.plugins td { vertical-align: middle; }
225 table.plugins td { vertical-align: middle; }
226 table.plugins td.configure { text-align: right; padding-right: 1em; }
226 table.plugins td.configure { text-align: right; padding-right: 1em; }
227 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
227 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
228 table.plugins span.description { display: block; font-size: 0.9em; }
228 table.plugins span.description { display: block; font-size: 0.9em; }
229 table.plugins span.url { display: block; font-size: 0.9em; }
229 table.plugins span.url { display: block; font-size: 0.9em; }
230
230
231 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; text-align:left; }
231 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; text-align:left; }
232 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
232 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
233 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
233 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
234 tr.group:hover a.toggle-all { display:inline;}
234 tr.group:hover a.toggle-all { display:inline;}
235 a.toggle-all:hover {text-decoration:none;}
235 a.toggle-all:hover {text-decoration:none;}
236
236
237 table.list tbody tr:hover { background-color:#ffffdd; }
237 table.list tbody tr:hover { background-color:#ffffdd; }
238 table.list tbody tr.group:hover { background-color:inherit; }
238 table.list tbody tr.group:hover { background-color:inherit; }
239 table td {padding:2px;}
239 table td {padding:2px;}
240 table p {margin:0;}
240 table p {margin:0;}
241 .odd {background-color:#f6f7f8;}
241 .odd {background-color:#f6f7f8;}
242 .even {background-color: #fff;}
242 .even {background-color: #fff;}
243
243
244 tr.builtin td.name {font-style:italic;}
245
244 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
246 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
245 a.sort.asc { background-image: url(../images/sort_asc.png); }
247 a.sort.asc { background-image: url(../images/sort_asc.png); }
246 a.sort.desc { background-image: url(../images/sort_desc.png); }
248 a.sort.desc { background-image: url(../images/sort_desc.png); }
247
249
248 table.attributes { width: 100% }
250 table.attributes { width: 100% }
249 table.attributes th { vertical-align: top; text-align: left; }
251 table.attributes th { vertical-align: top; text-align: left; }
250 table.attributes td { vertical-align: top; }
252 table.attributes td { vertical-align: top; }
251
253
252 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
254 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
253 table.boards td.last-message {text-align:left;font-size:80%;}
255 table.boards td.last-message {text-align:left;font-size:80%;}
254
256
255 table.messages td.last_message {text-align:left;}
257 table.messages td.last_message {text-align:left;}
256
258
257 #query_form_content {font-size:90%;}
259 #query_form_content {font-size:90%;}
258
260
259 table.query-columns {
261 table.query-columns {
260 border-collapse: collapse;
262 border-collapse: collapse;
261 border: 0;
263 border: 0;
262 }
264 }
263
265
264 table.query-columns td.buttons {
266 table.query-columns td.buttons {
265 vertical-align: middle;
267 vertical-align: middle;
266 text-align: center;
268 text-align: center;
267 }
269 }
268 table.query-columns td.buttons input[type=button] {width:35px;}
270 table.query-columns td.buttons input[type=button] {width:35px;}
269
271
270 td.center {text-align:center;}
272 td.center {text-align:center;}
271
273
272 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
274 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
273
275
274 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
276 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
275 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
277 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
276 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
278 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
277 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
279 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
278
280
279 #watchers select {width: 95%; display: block;}
281 #watchers select {width: 95%; display: block;}
280 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
282 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
281 #watchers a.delete:hover {opacity: 1;}
283 #watchers a.delete:hover {opacity: 1;}
282 #watchers img.gravatar {margin: 0 4px 2px 0;}
284 #watchers img.gravatar {margin: 0 4px 2px 0;}
283
285
284 span#watchers_inputs {overflow:auto; display:block;}
286 span#watchers_inputs {overflow:auto; display:block;}
285 span.search_for_watchers {display:block;}
287 span.search_for_watchers {display:block;}
286 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
288 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
287 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
289 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
288
290
289
291
290 .highlight { background-color: #FCFD8D;}
292 .highlight { background-color: #FCFD8D;}
291 .highlight.token-1 { background-color: #faa;}
293 .highlight.token-1 { background-color: #faa;}
292 .highlight.token-2 { background-color: #afa;}
294 .highlight.token-2 { background-color: #afa;}
293 .highlight.token-3 { background-color: #aaf;}
295 .highlight.token-3 { background-color: #aaf;}
294
296
295 .box{
297 .box{
296 padding:6px;
298 padding:6px;
297 margin-bottom: 10px;
299 margin-bottom: 10px;
298 background-color:#f6f6f6;
300 background-color:#f6f6f6;
299 color:#505050;
301 color:#505050;
300 line-height:1.5em;
302 line-height:1.5em;
301 border: 1px solid #e4e4e4;
303 border: 1px solid #e4e4e4;
302 word-wrap: break-word;
304 word-wrap: break-word;
303 }
305 }
304
306
305 div.square {
307 div.square {
306 border: 1px solid #999;
308 border: 1px solid #999;
307 float: left;
309 float: left;
308 margin: .3em .4em 0 .4em;
310 margin: .3em .4em 0 .4em;
309 overflow: hidden;
311 overflow: hidden;
310 width: .6em; height: .6em;
312 width: .6em; height: .6em;
311 }
313 }
312 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
314 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
313 .contextual input, .contextual select {font-size:0.9em;}
315 .contextual input, .contextual select {font-size:0.9em;}
314 .message .contextual { margin-top: 0; }
316 .message .contextual { margin-top: 0; }
315
317
316 .splitcontent {overflow:auto;}
318 .splitcontent {overflow:auto;}
317 .splitcontentleft{float:left; width:49%;}
319 .splitcontentleft{float:left; width:49%;}
318 .splitcontentright{float:right; width:49%;}
320 .splitcontentright{float:right; width:49%;}
319 form {display: inline;}
321 form {display: inline;}
320 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
322 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
321 fieldset {border: 1px solid #e4e4e4; margin:0;}
323 fieldset {border: 1px solid #e4e4e4; margin:0;}
322 legend {color: #484848;}
324 legend {color: #484848;}
323 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
325 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
324 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
326 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
325 blockquote blockquote { margin-left: 0;}
327 blockquote blockquote { margin-left: 0;}
326 abbr { border-bottom: 1px dotted; cursor: help; }
328 abbr { border-bottom: 1px dotted; cursor: help; }
327 textarea.wiki-edit {width:99%; resize:vertical;}
329 textarea.wiki-edit {width:99%; resize:vertical;}
328 li p {margin-top: 0;}
330 li p {margin-top: 0;}
329 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7; word-wrap: break-word;}
331 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7; word-wrap: break-word;}
330 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
332 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
331 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
333 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
332 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
334 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
333
335
334 div.issue div.subject div div { padding-left: 16px; }
336 div.issue div.subject div div { padding-left: 16px; }
335 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
337 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
336 div.issue div.subject>div>p { margin-top: 0.5em; }
338 div.issue div.subject>div>p { margin-top: 0.5em; }
337 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
339 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
338 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
340 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
339 div.issue .next-prev-links {color:#999;}
341 div.issue .next-prev-links {color:#999;}
340 div.issue table.attributes th {width:22%;}
342 div.issue table.attributes th {width:22%;}
341 div.issue table.attributes td {width:28%;}
343 div.issue table.attributes td {width:28%;}
342
344
343 #issue_tree table.issues, #relations table.issues { border: 0; }
345 #issue_tree table.issues, #relations table.issues { border: 0; }
344 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
346 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
345 #relations td.buttons {padding:0;}
347 #relations td.buttons {padding:0;}
346
348
347 fieldset.collapsible {border-width: 1px 0 0 0;}
349 fieldset.collapsible {border-width: 1px 0 0 0;}
348 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
350 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
349 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
351 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
350
352
351 fieldset#date-range p { margin: 2px 0 2px 0; }
353 fieldset#date-range p { margin: 2px 0 2px 0; }
352 fieldset#filters table { border-collapse: collapse; }
354 fieldset#filters table { border-collapse: collapse; }
353 fieldset#filters table td { padding: 0; vertical-align: middle; }
355 fieldset#filters table td { padding: 0; vertical-align: middle; }
354 fieldset#filters tr.filter { height: 2.1em; }
356 fieldset#filters tr.filter { height: 2.1em; }
355 fieldset#filters td.field { width:230px; }
357 fieldset#filters td.field { width:230px; }
356 fieldset#filters td.operator { width:180px; }
358 fieldset#filters td.operator { width:180px; }
357 fieldset#filters td.operator select {max-width:170px;}
359 fieldset#filters td.operator select {max-width:170px;}
358 fieldset#filters td.values { white-space:nowrap; }
360 fieldset#filters td.values { white-space:nowrap; }
359 fieldset#filters td.values select {min-width:130px;}
361 fieldset#filters td.values select {min-width:130px;}
360 fieldset#filters td.values input {height:1em;}
362 fieldset#filters td.values input {height:1em;}
361 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
363 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
362
364
363 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
365 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
364 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
366 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
365
367
366 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
368 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
367 div#issue-changesets div.changeset { padding: 4px;}
369 div#issue-changesets div.changeset { padding: 4px;}
368 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
370 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
369 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
371 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
370
372
371 .journal ul.details img {margin:0 0 -3px 4px;}
373 .journal ul.details img {margin:0 0 -3px 4px;}
372 div.journal {overflow:auto;}
374 div.journal {overflow:auto;}
373 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
375 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
374
376
375 div#activity dl, #search-results { margin-left: 2em; }
377 div#activity dl, #search-results { margin-left: 2em; }
376 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
378 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
377 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
379 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
378 div#activity dt.me .time { border-bottom: 1px solid #999; }
380 div#activity dt.me .time { border-bottom: 1px solid #999; }
379 div#activity dt .time { color: #777; font-size: 80%; }
381 div#activity dt .time { color: #777; font-size: 80%; }
380 div#activity dd .description, #search-results dd .description { font-style: italic; }
382 div#activity dd .description, #search-results dd .description { font-style: italic; }
381 div#activity span.project:after, #search-results span.project:after { content: " -"; }
383 div#activity span.project:after, #search-results span.project:after { content: " -"; }
382 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
384 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
383 div#activity dt.grouped {margin-left:5em;}
385 div#activity dt.grouped {margin-left:5em;}
384 div#activity dd.grouped {margin-left:9em;}
386 div#activity dd.grouped {margin-left:9em;}
385
387
386 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
388 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
387
389
388 div#search-results-counts {float:right;}
390 div#search-results-counts {float:right;}
389 div#search-results-counts ul { margin-top: 0.5em; }
391 div#search-results-counts ul { margin-top: 0.5em; }
390 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
392 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
391
393
392 dt.issue { background-image: url(../images/ticket.png); }
394 dt.issue { background-image: url(../images/ticket.png); }
393 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
395 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
394 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
396 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
395 dt.issue-note { background-image: url(../images/ticket_note.png); }
397 dt.issue-note { background-image: url(../images/ticket_note.png); }
396 dt.changeset { background-image: url(../images/changeset.png); }
398 dt.changeset { background-image: url(../images/changeset.png); }
397 dt.news { background-image: url(../images/news.png); }
399 dt.news { background-image: url(../images/news.png); }
398 dt.message { background-image: url(../images/message.png); }
400 dt.message { background-image: url(../images/message.png); }
399 dt.reply { background-image: url(../images/comments.png); }
401 dt.reply { background-image: url(../images/comments.png); }
400 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
402 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
401 dt.attachment { background-image: url(../images/attachment.png); }
403 dt.attachment { background-image: url(../images/attachment.png); }
402 dt.document { background-image: url(../images/document.png); }
404 dt.document { background-image: url(../images/document.png); }
403 dt.project { background-image: url(../images/projects.png); }
405 dt.project { background-image: url(../images/projects.png); }
404 dt.time-entry { background-image: url(../images/time.png); }
406 dt.time-entry { background-image: url(../images/time.png); }
405
407
406 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
408 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
407
409
408 div#roadmap .related-issues { margin-bottom: 1em; }
410 div#roadmap .related-issues { margin-bottom: 1em; }
409 div#roadmap .related-issues td.checkbox { display: none; }
411 div#roadmap .related-issues td.checkbox { display: none; }
410 div#roadmap .wiki h1:first-child { display: none; }
412 div#roadmap .wiki h1:first-child { display: none; }
411 div#roadmap .wiki h1 { font-size: 120%; }
413 div#roadmap .wiki h1 { font-size: 120%; }
412 div#roadmap .wiki h2 { font-size: 110%; }
414 div#roadmap .wiki h2 { font-size: 110%; }
413 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
415 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
414
416
415 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
417 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
416 div#version-summary fieldset { margin-bottom: 1em; }
418 div#version-summary fieldset { margin-bottom: 1em; }
417 div#version-summary fieldset.time-tracking table { width:100%; }
419 div#version-summary fieldset.time-tracking table { width:100%; }
418 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
420 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
419
421
420 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
422 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
421 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
423 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
422 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
424 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
423 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
425 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
424 table#time-report .hours-dec { font-size: 0.9em; }
426 table#time-report .hours-dec { font-size: 0.9em; }
425
427
426 div.wiki-page .contextual a {opacity: 0.4}
428 div.wiki-page .contextual a {opacity: 0.4}
427 div.wiki-page .contextual a:hover {opacity: 1}
429 div.wiki-page .contextual a:hover {opacity: 1}
428
430
429 form .attributes select { width: 60%; }
431 form .attributes select { width: 60%; }
430 input#issue_subject { width: 99%; }
432 input#issue_subject { width: 99%; }
431 select#issue_done_ratio { width: 95px; }
433 select#issue_done_ratio { width: 95px; }
432
434
433 ul.projects {margin:0; padding-left:1em;}
435 ul.projects {margin:0; padding-left:1em;}
434 ul.projects ul {padding-left:1.6em;}
436 ul.projects ul {padding-left:1.6em;}
435 ul.projects.root {margin:0; padding:0;}
437 ul.projects.root {margin:0; padding:0;}
436 ul.projects li {list-style-type:none;}
438 ul.projects li {list-style-type:none;}
437
439
438 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
440 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
439 #projects-index ul.projects li.root {margin-bottom: 1em;}
441 #projects-index ul.projects li.root {margin-bottom: 1em;}
440 #projects-index ul.projects li.child {margin-top: 1em;}
442 #projects-index ul.projects li.child {margin-top: 1em;}
441 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
443 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
442 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
444 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
443
445
444 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
446 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
445
447
446 #related-issues li img {vertical-align:middle;}
448 #related-issues li img {vertical-align:middle;}
447
449
448 ul.properties {padding:0; font-size: 0.9em; color: #777;}
450 ul.properties {padding:0; font-size: 0.9em; color: #777;}
449 ul.properties li {list-style-type:none;}
451 ul.properties li {list-style-type:none;}
450 ul.properties li span {font-style:italic;}
452 ul.properties li span {font-style:italic;}
451
453
452 .total-hours { font-size: 110%; font-weight: bold; }
454 .total-hours { font-size: 110%; font-weight: bold; }
453 .total-hours span.hours-int { font-size: 120%; }
455 .total-hours span.hours-int { font-size: 120%; }
454
456
455 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
457 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
456 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
458 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
457
459
458 #workflow_copy_form select { width: 200px; }
460 #workflow_copy_form select { width: 200px; }
459 table.transitions td.enabled {background: #bfb;}
461 table.transitions td.enabled {background: #bfb;}
460 #workflow_form table select {font-size:90%; max-width:100px;}
462 #workflow_form table select {font-size:90%; max-width:100px;}
461 table.fields_permissions td.readonly {background:#ddd;}
463 table.fields_permissions td.readonly {background:#ddd;}
462 table.fields_permissions td.required {background:#d88;}
464 table.fields_permissions td.required {background:#d88;}
463
465
464 select.expandable {vertical-align:top;}
466 select.expandable {vertical-align:top;}
465
467
466 textarea#custom_field_possible_values {width: 95%; resize:vertical}
468 textarea#custom_field_possible_values {width: 95%; resize:vertical}
467 textarea#custom_field_default_value {width: 95%; resize:vertical}
469 textarea#custom_field_default_value {width: 95%; resize:vertical}
468
470
469 input#content_comments {width: 99%}
471 input#content_comments {width: 99%}
470
472
471 p.pagination {margin-top:8px; font-size: 90%}
473 p.pagination {margin-top:8px; font-size: 90%}
472
474
473 /***** Tabular forms ******/
475 /***** Tabular forms ******/
474 .tabular p{
476 .tabular p{
475 margin: 0;
477 margin: 0;
476 padding: 3px 0 3px 0;
478 padding: 3px 0 3px 0;
477 padding-left: 180px; /* width of left column containing the label elements */
479 padding-left: 180px; /* width of left column containing the label elements */
478 min-height: 1.8em;
480 min-height: 1.8em;
479 clear:left;
481 clear:left;
480 }
482 }
481
483
482 html>body .tabular p {overflow:hidden;}
484 html>body .tabular p {overflow:hidden;}
483
485
484 .tabular input, .tabular select {max-width:95%}
486 .tabular input, .tabular select {max-width:95%}
485 .tabular textarea {width:95%; resize:vertical;}
487 .tabular textarea {width:95%; resize:vertical;}
486 .tabular span[title] {border-bottom:1px dotted #aaa;}
488 .tabular span[title] {border-bottom:1px dotted #aaa;}
487
489
488 .tabular label{
490 .tabular label{
489 font-weight: bold;
491 font-weight: bold;
490 float: left;
492 float: left;
491 text-align: right;
493 text-align: right;
492 /* width of left column */
494 /* width of left column */
493 margin-left: -180px;
495 margin-left: -180px;
494 /* width of labels. Should be smaller than left column to create some right margin */
496 /* width of labels. Should be smaller than left column to create some right margin */
495 width: 175px;
497 width: 175px;
496 }
498 }
497
499
498 .tabular label.floating{
500 .tabular label.floating{
499 font-weight: normal;
501 font-weight: normal;
500 margin-left: 0px;
502 margin-left: 0px;
501 text-align: left;
503 text-align: left;
502 width: 270px;
504 width: 270px;
503 }
505 }
504
506
505 .tabular label.block{
507 .tabular label.block{
506 font-weight: normal;
508 font-weight: normal;
507 margin-left: 0px !important;
509 margin-left: 0px !important;
508 text-align: left;
510 text-align: left;
509 float: none;
511 float: none;
510 display: block;
512 display: block;
511 width: auto !important;
513 width: auto !important;
512 }
514 }
513
515
514 .tabular label.inline{
516 .tabular label.inline{
515 font-weight: normal;
517 font-weight: normal;
516 float:none;
518 float:none;
517 margin-left: 5px !important;
519 margin-left: 5px !important;
518 width: auto;
520 width: auto;
519 }
521 }
520
522
521 label.no-css {
523 label.no-css {
522 font-weight: inherit;
524 font-weight: inherit;
523 float:none;
525 float:none;
524 text-align:left;
526 text-align:left;
525 margin-left:0px;
527 margin-left:0px;
526 width:auto;
528 width:auto;
527 }
529 }
528 input#time_entry_comments { width: 90%;}
530 input#time_entry_comments { width: 90%;}
529
531
530 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
532 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
531
533
532 .tabular.settings p{ padding-left: 300px; }
534 .tabular.settings p{ padding-left: 300px; }
533 .tabular.settings label{ margin-left: -300px; width: 295px; }
535 .tabular.settings label{ margin-left: -300px; width: 295px; }
534 .tabular.settings textarea { width: 99%; }
536 .tabular.settings textarea { width: 99%; }
535
537
536 .settings.enabled_scm table {width:100%}
538 .settings.enabled_scm table {width:100%}
537 .settings.enabled_scm td.scm_name{ font-weight: bold; }
539 .settings.enabled_scm td.scm_name{ font-weight: bold; }
538
540
539 fieldset.settings label { display: block; }
541 fieldset.settings label { display: block; }
540 fieldset#notified_events .parent { padding-left: 20px; }
542 fieldset#notified_events .parent { padding-left: 20px; }
541
543
542 span.required {color: #bb0000;}
544 span.required {color: #bb0000;}
543 .summary {font-style: italic;}
545 .summary {font-style: italic;}
544
546
545 .check_box_group {
547 .check_box_group {
546 display:block;
548 display:block;
547 width:95%;
549 width:95%;
548 max-height:300px;
550 max-height:300px;
549 overflow-y:auto;
551 overflow-y:auto;
550 padding:2px 4px 4px 2px;
552 padding:2px 4px 4px 2px;
551 background:#fff;
553 background:#fff;
552 border:1px solid #9EB1C2;
554 border:1px solid #9EB1C2;
553 border-radius:2px
555 border-radius:2px
554 }
556 }
555 .check_box_group label {
557 .check_box_group label {
556 font-weight: normal;
558 font-weight: normal;
557 margin-left: 0px !important;
559 margin-left: 0px !important;
558 text-align: left;
560 text-align: left;
559 float: none;
561 float: none;
560 display: block;
562 display: block;
561 width: auto;
563 width: auto;
562 }
564 }
563 .check_box_group.bool_cf {border:0; background:inherit;}
565 .check_box_group.bool_cf {border:0; background:inherit;}
564 .check_box_group.bool_cf label {display: inline;}
566 .check_box_group.bool_cf label {display: inline;}
565
567
566 #attachments_fields input.description {margin-left:4px; width:340px;}
568 #attachments_fields input.description {margin-left:4px; width:340px;}
567 #attachments_fields span {display:block; white-space:nowrap;}
569 #attachments_fields span {display:block; white-space:nowrap;}
568 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
570 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
569 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
571 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
570 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
572 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
571 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
573 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
572 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
574 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
573 a.remove-upload:hover {text-decoration:none !important;}
575 a.remove-upload:hover {text-decoration:none !important;}
574
576
575 div.fileover { background-color: lavender; }
577 div.fileover { background-color: lavender; }
576
578
577 div.attachments { margin-top: 12px; }
579 div.attachments { margin-top: 12px; }
578 div.attachments p { margin:4px 0 2px 0; }
580 div.attachments p { margin:4px 0 2px 0; }
579 div.attachments img { vertical-align: middle; }
581 div.attachments img { vertical-align: middle; }
580 div.attachments span.author { font-size: 0.9em; color: #888; }
582 div.attachments span.author { font-size: 0.9em; color: #888; }
581
583
582 div.thumbnails {margin-top:0.6em;}
584 div.thumbnails {margin-top:0.6em;}
583 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
585 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
584 div.thumbnails img {margin: 3px;}
586 div.thumbnails img {margin: 3px;}
585
587
586 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
588 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
587 .other-formats span + span:before { content: "| "; }
589 .other-formats span + span:before { content: "| "; }
588
590
589 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
591 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
590
592
591 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
593 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
592 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
594 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
593
595
594 textarea.text_cf {width:95%; resize:vertical;}
596 textarea.text_cf {width:95%; resize:vertical;}
595 input.string_cf, input.link_cf {width:95%;}
597 input.string_cf, input.link_cf {width:95%;}
596 select.bool_cf {width:auto !important;}
598 select.bool_cf {width:auto !important;}
597
599
598 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
600 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
599
601
600 #tab-content-members .splitcontentleft, #tab-content-memberships .splitcontentleft, #tab-content-users .splitcontentleft {width: 64%;}
602 #tab-content-members .splitcontentleft, #tab-content-memberships .splitcontentleft, #tab-content-users .splitcontentleft {width: 64%;}
601 #tab-content-members .splitcontentright, #tab-content-memberships .splitcontentright, #tab-content-users .splitcontentright {width: 34%;}
603 #tab-content-members .splitcontentright, #tab-content-memberships .splitcontentright, #tab-content-users .splitcontentright {width: 34%;}
602 #tab-content-members fieldset, #tab-content-memberships fieldset, #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
604 #tab-content-members fieldset, #tab-content-memberships fieldset, #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
603 #tab-content-members fieldset legend, #tab-content-memberships fieldset legend, #tab-content-users fieldset legend {font-weight: bold;}
605 #tab-content-members fieldset legend, #tab-content-memberships fieldset legend, #tab-content-users fieldset legend {font-weight: bold;}
604 #tab-content-members fieldset label, #tab-content-memberships fieldset label, #tab-content-users fieldset label {display: block;}
606 #tab-content-members fieldset label, #tab-content-memberships fieldset label, #tab-content-users fieldset label {display: block;}
605 #tab-content-members #principals, #tab-content-users #principals {max-height: 400px; overflow: auto;}
607 #tab-content-members #principals, #tab-content-users #principals {max-height: 400px; overflow: auto;}
606
608
607 #tab-content-memberships .splitcontentright select {width:90%}
609 #tab-content-memberships .splitcontentright select {width:90%}
608
610
609 #users_for_watcher {height: 200px; overflow:auto;}
611 #users_for_watcher {height: 200px; overflow:auto;}
610 #users_for_watcher label {display: block;}
612 #users_for_watcher label {display: block;}
611
613
612 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
614 table.members td.name {padding-left: 20px;}
615 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
613
616
614 input#principal_search, input#user_search {width:90%}
617 input#principal_search, input#user_search {width:90%}
615
618
616 input.autocomplete {
619 input.autocomplete {
617 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
620 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
618 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
621 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
619 }
622 }
620 input.autocomplete.ajax-loading {
623 input.autocomplete.ajax-loading {
621 background-image: url(../images/loading.gif);
624 background-image: url(../images/loading.gif);
622 }
625 }
623
626
624 .role-visibility {padding-left:2em;}
627 .role-visibility {padding-left:2em;}
625
628
626 /***** Flash & error messages ****/
629 /***** Flash & error messages ****/
627 #errorExplanation, div.flash, .nodata, .warning, .conflict {
630 #errorExplanation, div.flash, .nodata, .warning, .conflict {
628 padding: 4px 4px 4px 30px;
631 padding: 4px 4px 4px 30px;
629 margin-bottom: 12px;
632 margin-bottom: 12px;
630 font-size: 1.1em;
633 font-size: 1.1em;
631 border: 2px solid;
634 border: 2px solid;
632 }
635 }
633
636
634 div.flash {margin-top: 8px;}
637 div.flash {margin-top: 8px;}
635
638
636 div.flash.error, #errorExplanation {
639 div.flash.error, #errorExplanation {
637 background: url(../images/exclamation.png) 8px 50% no-repeat;
640 background: url(../images/exclamation.png) 8px 50% no-repeat;
638 background-color: #ffe3e3;
641 background-color: #ffe3e3;
639 border-color: #dd0000;
642 border-color: #dd0000;
640 color: #880000;
643 color: #880000;
641 }
644 }
642
645
643 div.flash.notice {
646 div.flash.notice {
644 background: url(../images/true.png) 8px 5px no-repeat;
647 background: url(../images/true.png) 8px 5px no-repeat;
645 background-color: #dfffdf;
648 background-color: #dfffdf;
646 border-color: #9fcf9f;
649 border-color: #9fcf9f;
647 color: #005f00;
650 color: #005f00;
648 }
651 }
649
652
650 div.flash.warning, .conflict {
653 div.flash.warning, .conflict {
651 background: url(../images/warning.png) 8px 5px no-repeat;
654 background: url(../images/warning.png) 8px 5px no-repeat;
652 background-color: #FFEBC1;
655 background-color: #FFEBC1;
653 border-color: #FDBF3B;
656 border-color: #FDBF3B;
654 color: #A6750C;
657 color: #A6750C;
655 text-align: left;
658 text-align: left;
656 }
659 }
657
660
658 .nodata, .warning {
661 .nodata, .warning {
659 text-align: center;
662 text-align: center;
660 background-color: #FFEBC1;
663 background-color: #FFEBC1;
661 border-color: #FDBF3B;
664 border-color: #FDBF3B;
662 color: #A6750C;
665 color: #A6750C;
663 }
666 }
664
667
665 #errorExplanation ul { font-size: 0.9em;}
668 #errorExplanation ul { font-size: 0.9em;}
666 #errorExplanation h2, #errorExplanation p { display: none; }
669 #errorExplanation h2, #errorExplanation p { display: none; }
667
670
668 .conflict-details {font-size:80%;}
671 .conflict-details {font-size:80%;}
669
672
670 /***** Ajax indicator ******/
673 /***** Ajax indicator ******/
671 #ajax-indicator {
674 #ajax-indicator {
672 position: absolute; /* fixed not supported by IE */
675 position: absolute; /* fixed not supported by IE */
673 background-color:#eee;
676 background-color:#eee;
674 border: 1px solid #bbb;
677 border: 1px solid #bbb;
675 top:35%;
678 top:35%;
676 left:40%;
679 left:40%;
677 width:20%;
680 width:20%;
678 font-weight:bold;
681 font-weight:bold;
679 text-align:center;
682 text-align:center;
680 padding:0.6em;
683 padding:0.6em;
681 z-index:100;
684 z-index:100;
682 opacity: 0.5;
685 opacity: 0.5;
683 }
686 }
684
687
685 html>body #ajax-indicator { position: fixed; }
688 html>body #ajax-indicator { position: fixed; }
686
689
687 #ajax-indicator span {
690 #ajax-indicator span {
688 background-position: 0% 40%;
691 background-position: 0% 40%;
689 background-repeat: no-repeat;
692 background-repeat: no-repeat;
690 background-image: url(../images/loading.gif);
693 background-image: url(../images/loading.gif);
691 padding-left: 26px;
694 padding-left: 26px;
692 vertical-align: bottom;
695 vertical-align: bottom;
693 }
696 }
694
697
695 /***** Calendar *****/
698 /***** Calendar *****/
696 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
699 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
697 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
700 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
698 table.cal thead th.week-number {width: auto;}
701 table.cal thead th.week-number {width: auto;}
699 table.cal tbody tr {height: 100px;}
702 table.cal tbody tr {height: 100px;}
700 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
703 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
701 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
704 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
702 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
705 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
703 table.cal td.odd p.day-num {color: #bbb;}
706 table.cal td.odd p.day-num {color: #bbb;}
704 table.cal td.today {background:#ffffdd;}
707 table.cal td.today {background:#ffffdd;}
705 table.cal td.today p.day-num {font-weight: bold;}
708 table.cal td.today p.day-num {font-weight: bold;}
706 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
709 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
707 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
710 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
708 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
711 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
709 p.cal.legend span {display:block;}
712 p.cal.legend span {display:block;}
710
713
711 /***** Tooltips ******/
714 /***** Tooltips ******/
712 .tooltip{position:relative;z-index:24;}
715 .tooltip{position:relative;z-index:24;}
713 .tooltip:hover{z-index:25;color:#000;}
716 .tooltip:hover{z-index:25;color:#000;}
714 .tooltip span.tip{display: none; text-align:left;}
717 .tooltip span.tip{display: none; text-align:left;}
715
718
716 div.tooltip:hover span.tip{
719 div.tooltip:hover span.tip{
717 display:block;
720 display:block;
718 position:absolute;
721 position:absolute;
719 top:12px; left:24px; width:270px;
722 top:12px; left:24px; width:270px;
720 border:1px solid #555;
723 border:1px solid #555;
721 background-color:#fff;
724 background-color:#fff;
722 padding: 4px;
725 padding: 4px;
723 font-size: 0.8em;
726 font-size: 0.8em;
724 color:#505050;
727 color:#505050;
725 }
728 }
726
729
727 img.ui-datepicker-trigger {
730 img.ui-datepicker-trigger {
728 cursor: pointer;
731 cursor: pointer;
729 vertical-align: middle;
732 vertical-align: middle;
730 margin-left: 4px;
733 margin-left: 4px;
731 }
734 }
732
735
733 /***** Progress bar *****/
736 /***** Progress bar *****/
734 table.progress {
737 table.progress {
735 border-collapse: collapse;
738 border-collapse: collapse;
736 border-spacing: 0pt;
739 border-spacing: 0pt;
737 empty-cells: show;
740 empty-cells: show;
738 text-align: center;
741 text-align: center;
739 float:left;
742 float:left;
740 margin: 1px 6px 1px 0px;
743 margin: 1px 6px 1px 0px;
741 }
744 }
742
745
743 table.progress td { height: 1em; }
746 table.progress td { height: 1em; }
744 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
747 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
745 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
748 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
746 table.progress td.todo { background: #eee none repeat scroll 0%; }
749 table.progress td.todo { background: #eee none repeat scroll 0%; }
747 p.percent {font-size: 80%;}
750 p.percent {font-size: 80%;}
748 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
751 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
749
752
750 #roadmap table.progress td { height: 1.2em; }
753 #roadmap table.progress td { height: 1.2em; }
751 /***** Tabs *****/
754 /***** Tabs *****/
752 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
755 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
753 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
756 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
754 #content .tabs ul li {
757 #content .tabs ul li {
755 float:left;
758 float:left;
756 list-style-type:none;
759 list-style-type:none;
757 white-space:nowrap;
760 white-space:nowrap;
758 margin-right:4px;
761 margin-right:4px;
759 background:#fff;
762 background:#fff;
760 position:relative;
763 position:relative;
761 margin-bottom:-1px;
764 margin-bottom:-1px;
762 }
765 }
763 #content .tabs ul li a{
766 #content .tabs ul li a{
764 display:block;
767 display:block;
765 font-size: 0.9em;
768 font-size: 0.9em;
766 text-decoration:none;
769 text-decoration:none;
767 line-height:1.3em;
770 line-height:1.3em;
768 padding:4px 6px 4px 6px;
771 padding:4px 6px 4px 6px;
769 border: 1px solid #ccc;
772 border: 1px solid #ccc;
770 border-bottom: 1px solid #bbbbbb;
773 border-bottom: 1px solid #bbbbbb;
771 background-color: #f6f6f6;
774 background-color: #f6f6f6;
772 color:#999;
775 color:#999;
773 font-weight:bold;
776 font-weight:bold;
774 border-top-left-radius:3px;
777 border-top-left-radius:3px;
775 border-top-right-radius:3px;
778 border-top-right-radius:3px;
776 }
779 }
777
780
778 #content .tabs ul li a:hover {
781 #content .tabs ul li a:hover {
779 background-color: #ffffdd;
782 background-color: #ffffdd;
780 text-decoration:none;
783 text-decoration:none;
781 }
784 }
782
785
783 #content .tabs ul li a.selected {
786 #content .tabs ul li a.selected {
784 background-color: #fff;
787 background-color: #fff;
785 border: 1px solid #bbbbbb;
788 border: 1px solid #bbbbbb;
786 border-bottom: 1px solid #fff;
789 border-bottom: 1px solid #fff;
787 color:#444;
790 color:#444;
788 }
791 }
789
792
790 #content .tabs ul li a.selected:hover {background-color: #fff;}
793 #content .tabs ul li a.selected:hover {background-color: #fff;}
791
794
792 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
795 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
793
796
794 button.tab-left, button.tab-right {
797 button.tab-left, button.tab-right {
795 font-size: 0.9em;
798 font-size: 0.9em;
796 cursor: pointer;
799 cursor: pointer;
797 height:24px;
800 height:24px;
798 border: 1px solid #ccc;
801 border: 1px solid #ccc;
799 border-bottom: 1px solid #bbbbbb;
802 border-bottom: 1px solid #bbbbbb;
800 position:absolute;
803 position:absolute;
801 padding:4px;
804 padding:4px;
802 width: 20px;
805 width: 20px;
803 bottom: -1px;
806 bottom: -1px;
804 }
807 }
805
808
806 button.tab-left {
809 button.tab-left {
807 right: 20px;
810 right: 20px;
808 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
811 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
809 border-top-left-radius:3px;
812 border-top-left-radius:3px;
810 }
813 }
811
814
812 button.tab-right {
815 button.tab-right {
813 right: 0;
816 right: 0;
814 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
817 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
815 border-top-right-radius:3px;
818 border-top-right-radius:3px;
816 }
819 }
817
820
818 /***** Diff *****/
821 /***** Diff *****/
819 .diff_out { background: #fcc; }
822 .diff_out { background: #fcc; }
820 .diff_out span { background: #faa; }
823 .diff_out span { background: #faa; }
821 .diff_in { background: #cfc; }
824 .diff_in { background: #cfc; }
822 .diff_in span { background: #afa; }
825 .diff_in span { background: #afa; }
823
826
824 .text-diff {
827 .text-diff {
825 padding: 1em;
828 padding: 1em;
826 background-color:#f6f6f6;
829 background-color:#f6f6f6;
827 color:#505050;
830 color:#505050;
828 border: 1px solid #e4e4e4;
831 border: 1px solid #e4e4e4;
829 }
832 }
830
833
831 /***** Wiki *****/
834 /***** Wiki *****/
832 div.wiki table {
835 div.wiki table {
833 border-collapse: collapse;
836 border-collapse: collapse;
834 margin-bottom: 1em;
837 margin-bottom: 1em;
835 }
838 }
836
839
837 div.wiki table, div.wiki td, div.wiki th {
840 div.wiki table, div.wiki td, div.wiki th {
838 border: 1px solid #bbb;
841 border: 1px solid #bbb;
839 padding: 4px;
842 padding: 4px;
840 }
843 }
841
844
842 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
845 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
843
846
844 div.wiki .external {
847 div.wiki .external {
845 background-position: 0% 60%;
848 background-position: 0% 60%;
846 background-repeat: no-repeat;
849 background-repeat: no-repeat;
847 padding-left: 12px;
850 padding-left: 12px;
848 background-image: url(../images/external.png);
851 background-image: url(../images/external.png);
849 }
852 }
850
853
851 div.wiki a.new {color: #b73535;}
854 div.wiki a.new {color: #b73535;}
852
855
853 div.wiki ul, div.wiki ol {margin-bottom:1em;}
856 div.wiki ul, div.wiki ol {margin-bottom:1em;}
854
857
855 div.wiki pre {
858 div.wiki pre {
856 margin: 1em 1em 1em 1.6em;
859 margin: 1em 1em 1em 1.6em;
857 padding: 8px;
860 padding: 8px;
858 background-color: #fafafa;
861 background-color: #fafafa;
859 border: 1px solid #e2e2e2;
862 border: 1px solid #e2e2e2;
860 width:auto;
863 width:auto;
861 overflow-x: auto;
864 overflow-x: auto;
862 overflow-y: hidden;
865 overflow-y: hidden;
863 }
866 }
864
867
865 div.wiki ul.toc {
868 div.wiki ul.toc {
866 background-color: #ffffdd;
869 background-color: #ffffdd;
867 border: 1px solid #e4e4e4;
870 border: 1px solid #e4e4e4;
868 padding: 4px;
871 padding: 4px;
869 line-height: 1.2em;
872 line-height: 1.2em;
870 margin-bottom: 12px;
873 margin-bottom: 12px;
871 margin-right: 12px;
874 margin-right: 12px;
872 margin-left: 0;
875 margin-left: 0;
873 display: table
876 display: table
874 }
877 }
875 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
878 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
876
879
877 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
880 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
878 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
881 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
879 div.wiki ul.toc ul { margin: 0; padding: 0; }
882 div.wiki ul.toc ul { margin: 0; padding: 0; }
880 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
883 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
881 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
884 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
882 div.wiki ul.toc a {
885 div.wiki ul.toc a {
883 font-size: 0.9em;
886 font-size: 0.9em;
884 font-weight: normal;
887 font-weight: normal;
885 text-decoration: none;
888 text-decoration: none;
886 color: #606060;
889 color: #606060;
887 }
890 }
888 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
891 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
889
892
890 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
893 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
891 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
894 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
892 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
895 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
893
896
894 div.wiki img {vertical-align:middle; max-width:100%;}
897 div.wiki img {vertical-align:middle; max-width:100%;}
895
898
896 /***** My page layout *****/
899 /***** My page layout *****/
897 .block-receiver {
900 .block-receiver {
898 border:1px dashed #c0c0c0;
901 border:1px dashed #c0c0c0;
899 margin-bottom: 20px;
902 margin-bottom: 20px;
900 padding: 15px 0 15px 0;
903 padding: 15px 0 15px 0;
901 }
904 }
902
905
903 .mypage-box {
906 .mypage-box {
904 margin:0 0 20px 0;
907 margin:0 0 20px 0;
905 color:#505050;
908 color:#505050;
906 line-height:1.5em;
909 line-height:1.5em;
907 }
910 }
908
911
909 .handle {cursor: move;}
912 .handle {cursor: move;}
910
913
911 a.close-icon {
914 a.close-icon {
912 display:block;
915 display:block;
913 margin-top:3px;
916 margin-top:3px;
914 overflow:hidden;
917 overflow:hidden;
915 width:12px;
918 width:12px;
916 height:12px;
919 height:12px;
917 background-repeat: no-repeat;
920 background-repeat: no-repeat;
918 cursor:pointer;
921 cursor:pointer;
919 background-image:url('../images/close.png');
922 background-image:url('../images/close.png');
920 }
923 }
921 a.close-icon:hover {background-image:url('../images/close_hl.png');}
924 a.close-icon:hover {background-image:url('../images/close_hl.png');}
922
925
923 /***** Gantt chart *****/
926 /***** Gantt chart *****/
924 .gantt_hdr {
927 .gantt_hdr {
925 position:absolute;
928 position:absolute;
926 top:0;
929 top:0;
927 height:16px;
930 height:16px;
928 border-top: 1px solid #c0c0c0;
931 border-top: 1px solid #c0c0c0;
929 border-bottom: 1px solid #c0c0c0;
932 border-bottom: 1px solid #c0c0c0;
930 border-right: 1px solid #c0c0c0;
933 border-right: 1px solid #c0c0c0;
931 text-align: center;
934 text-align: center;
932 overflow: hidden;
935 overflow: hidden;
933 }
936 }
934
937
935 .gantt_hdr.nwday {background-color:#f1f1f1;}
938 .gantt_hdr.nwday {background-color:#f1f1f1;}
936
939
937 .gantt_subjects { font-size: 0.8em; }
940 .gantt_subjects { font-size: 0.8em; }
938 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
941 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
939
942
940 .task {
943 .task {
941 position: absolute;
944 position: absolute;
942 height:8px;
945 height:8px;
943 font-size:0.8em;
946 font-size:0.8em;
944 color:#888;
947 color:#888;
945 padding:0;
948 padding:0;
946 margin:0;
949 margin:0;
947 line-height:16px;
950 line-height:16px;
948 white-space:nowrap;
951 white-space:nowrap;
949 }
952 }
950
953
951 .task.label {width:100%;}
954 .task.label {width:100%;}
952 .task.label.project, .task.label.version { font-weight: bold; }
955 .task.label.project, .task.label.version { font-weight: bold; }
953
956
954 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
957 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
955 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
958 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
956 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
959 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
957
960
958 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
961 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
959 .task_late.parent, .task_done.parent { height: 3px;}
962 .task_late.parent, .task_done.parent { height: 3px;}
960 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
963 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
961 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
964 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
962
965
963 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
966 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
964 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
967 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
965 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
968 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
966 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
969 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
967
970
968 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
971 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
969 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
972 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
970 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
973 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
971 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
974 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
972
975
973 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
976 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
974 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
977 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
975
978
976 /***** Icons *****/
979 /***** Icons *****/
977 .icon {
980 .icon {
978 background-position: 0% 50%;
981 background-position: 0% 50%;
979 background-repeat: no-repeat;
982 background-repeat: no-repeat;
980 padding-left: 20px;
983 padding-left: 20px;
981 padding-top: 2px;
984 padding-top: 2px;
982 padding-bottom: 3px;
985 padding-bottom: 3px;
983 }
986 }
984
987
985 .icon-add { background-image: url(../images/add.png); }
988 .icon-add { background-image: url(../images/add.png); }
986 .icon-edit { background-image: url(../images/edit.png); }
989 .icon-edit { background-image: url(../images/edit.png); }
987 .icon-copy { background-image: url(../images/copy.png); }
990 .icon-copy { background-image: url(../images/copy.png); }
988 .icon-duplicate { background-image: url(../images/duplicate.png); }
991 .icon-duplicate { background-image: url(../images/duplicate.png); }
989 .icon-del { background-image: url(../images/delete.png); }
992 .icon-del { background-image: url(../images/delete.png); }
990 .icon-move { background-image: url(../images/move.png); }
993 .icon-move { background-image: url(../images/move.png); }
991 .icon-save { background-image: url(../images/save.png); }
994 .icon-save { background-image: url(../images/save.png); }
992 .icon-cancel { background-image: url(../images/cancel.png); }
995 .icon-cancel { background-image: url(../images/cancel.png); }
993 .icon-multiple { background-image: url(../images/table_multiple.png); }
996 .icon-multiple { background-image: url(../images/table_multiple.png); }
994 .icon-folder { background-image: url(../images/folder.png); }
997 .icon-folder { background-image: url(../images/folder.png); }
995 .open .icon-folder { background-image: url(../images/folder_open.png); }
998 .open .icon-folder { background-image: url(../images/folder_open.png); }
996 .icon-package { background-image: url(../images/package.png); }
999 .icon-package { background-image: url(../images/package.png); }
997 .icon-user { background-image: url(../images/user.png); }
1000 .icon-user { background-image: url(../images/user.png); }
998 .icon-projects { background-image: url(../images/projects.png); }
1001 .icon-projects { background-image: url(../images/projects.png); }
999 .icon-help { background-image: url(../images/help.png); }
1002 .icon-help { background-image: url(../images/help.png); }
1000 .icon-attachment { background-image: url(../images/attachment.png); }
1003 .icon-attachment { background-image: url(../images/attachment.png); }
1001 .icon-history { background-image: url(../images/history.png); }
1004 .icon-history { background-image: url(../images/history.png); }
1002 .icon-time { background-image: url(../images/time.png); }
1005 .icon-time { background-image: url(../images/time.png); }
1003 .icon-time-add { background-image: url(../images/time_add.png); }
1006 .icon-time-add { background-image: url(../images/time_add.png); }
1004 .icon-stats { background-image: url(../images/stats.png); }
1007 .icon-stats { background-image: url(../images/stats.png); }
1005 .icon-warning { background-image: url(../images/warning.png); }
1008 .icon-warning { background-image: url(../images/warning.png); }
1006 .icon-fav { background-image: url(../images/fav.png); }
1009 .icon-fav { background-image: url(../images/fav.png); }
1007 .icon-fav-off { background-image: url(../images/fav_off.png); }
1010 .icon-fav-off { background-image: url(../images/fav_off.png); }
1008 .icon-reload { background-image: url(../images/reload.png); }
1011 .icon-reload { background-image: url(../images/reload.png); }
1009 .icon-lock { background-image: url(../images/locked.png); }
1012 .icon-lock { background-image: url(../images/locked.png); }
1010 .icon-unlock { background-image: url(../images/unlock.png); }
1013 .icon-unlock { background-image: url(../images/unlock.png); }
1011 .icon-checked { background-image: url(../images/true.png); }
1014 .icon-checked { background-image: url(../images/true.png); }
1012 .icon-details { background-image: url(../images/zoom_in.png); }
1015 .icon-details { background-image: url(../images/zoom_in.png); }
1013 .icon-report { background-image: url(../images/report.png); }
1016 .icon-report { background-image: url(../images/report.png); }
1014 .icon-comment { background-image: url(../images/comment.png); }
1017 .icon-comment { background-image: url(../images/comment.png); }
1015 .icon-summary { background-image: url(../images/lightning.png); }
1018 .icon-summary { background-image: url(../images/lightning.png); }
1016 .icon-server-authentication { background-image: url(../images/server_key.png); }
1019 .icon-server-authentication { background-image: url(../images/server_key.png); }
1017 .icon-issue { background-image: url(../images/ticket.png); }
1020 .icon-issue { background-image: url(../images/ticket.png); }
1018 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1021 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1019 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1022 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1020 .icon-passwd { background-image: url(../images/textfield_key.png); }
1023 .icon-passwd { background-image: url(../images/textfield_key.png); }
1021 .icon-test { background-image: url(../images/bullet_go.png); }
1024 .icon-test { background-image: url(../images/bullet_go.png); }
1022
1025
1023 .icon-file { background-image: url(../images/files/default.png); }
1026 .icon-file { background-image: url(../images/files/default.png); }
1024 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1027 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1025 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1028 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1026 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1029 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1027 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1030 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1028 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1031 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1029 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1032 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1030 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1033 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1031 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1034 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1032 .icon-file.text-css { background-image: url(../images/files/css.png); }
1035 .icon-file.text-css { background-image: url(../images/files/css.png); }
1033 .icon-file.text-html { background-image: url(../images/files/html.png); }
1036 .icon-file.text-html { background-image: url(../images/files/html.png); }
1034 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1037 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1035 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1038 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1036 .icon-file.image-png { background-image: url(../images/files/image.png); }
1039 .icon-file.image-png { background-image: url(../images/files/image.png); }
1037 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1040 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1038 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1041 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1039 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1042 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1040 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1043 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1041
1044
1042 img.gravatar {
1045 img.gravatar {
1043 padding: 2px;
1046 padding: 2px;
1044 border: solid 1px #d5d5d5;
1047 border: solid 1px #d5d5d5;
1045 background: #fff;
1048 background: #fff;
1046 vertical-align: middle;
1049 vertical-align: middle;
1047 }
1050 }
1048
1051
1049 div.issue img.gravatar {
1052 div.issue img.gravatar {
1050 float: left;
1053 float: left;
1051 margin: 0 6px 0 0;
1054 margin: 0 6px 0 0;
1052 padding: 5px;
1055 padding: 5px;
1053 }
1056 }
1054
1057
1055 div.issue table img.gravatar {
1058 div.issue table img.gravatar {
1056 height: 14px;
1059 height: 14px;
1057 width: 14px;
1060 width: 14px;
1058 padding: 2px;
1061 padding: 2px;
1059 float: left;
1062 float: left;
1060 margin: 0 0.5em 0 0;
1063 margin: 0 0.5em 0 0;
1061 }
1064 }
1062
1065
1063 h2 img.gravatar {margin: -2px 4px -4px 0;}
1066 h2 img.gravatar {margin: -2px 4px -4px 0;}
1064 h3 img.gravatar {margin: -4px 4px -4px 0;}
1067 h3 img.gravatar {margin: -4px 4px -4px 0;}
1065 h4 img.gravatar {margin: -6px 4px -4px 0;}
1068 h4 img.gravatar {margin: -6px 4px -4px 0;}
1066 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1069 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1067 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1070 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1068 /* Used on 12px Gravatar img tags without the icon background */
1071 /* Used on 12px Gravatar img tags without the icon background */
1069 .icon-gravatar {float: left; margin-right: 4px;}
1072 .icon-gravatar {float: left; margin-right: 4px;}
1070
1073
1071 #activity dt, .journal {clear: left;}
1074 #activity dt, .journal {clear: left;}
1072
1075
1073 .journal-link {float: right;}
1076 .journal-link {float: right;}
1074
1077
1075 h2 img { vertical-align:middle; }
1078 h2 img { vertical-align:middle; }
1076
1079
1077 .hascontextmenu { cursor: context-menu; }
1080 .hascontextmenu { cursor: context-menu; }
1078
1081
1079 /* Custom JQuery styles */
1082 /* Custom JQuery styles */
1080 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1083 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1081
1084
1082
1085
1083 /************* CodeRay styles *************/
1086 /************* CodeRay styles *************/
1084 .syntaxhl div {display: inline;}
1087 .syntaxhl div {display: inline;}
1085 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1088 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1086 .syntaxhl .code pre { overflow: auto }
1089 .syntaxhl .code pre { overflow: auto }
1087 .syntaxhl .debug { color: white !important; background: blue !important; }
1090 .syntaxhl .debug { color: white !important; background: blue !important; }
1088
1091
1089 .syntaxhl .annotation { color:#007 }
1092 .syntaxhl .annotation { color:#007 }
1090 .syntaxhl .attribute-name { color:#b48 }
1093 .syntaxhl .attribute-name { color:#b48 }
1091 .syntaxhl .attribute-value { color:#700 }
1094 .syntaxhl .attribute-value { color:#700 }
1092 .syntaxhl .binary { color:#509 }
1095 .syntaxhl .binary { color:#509 }
1093 .syntaxhl .char .content { color:#D20 }
1096 .syntaxhl .char .content { color:#D20 }
1094 .syntaxhl .char .delimiter { color:#710 }
1097 .syntaxhl .char .delimiter { color:#710 }
1095 .syntaxhl .char { color:#D20 }
1098 .syntaxhl .char { color:#D20 }
1096 .syntaxhl .class { color:#258; font-weight:bold }
1099 .syntaxhl .class { color:#258; font-weight:bold }
1097 .syntaxhl .class-variable { color:#369 }
1100 .syntaxhl .class-variable { color:#369 }
1098 .syntaxhl .color { color:#0A0 }
1101 .syntaxhl .color { color:#0A0 }
1099 .syntaxhl .comment { color:#385 }
1102 .syntaxhl .comment { color:#385 }
1100 .syntaxhl .comment .char { color:#385 }
1103 .syntaxhl .comment .char { color:#385 }
1101 .syntaxhl .comment .delimiter { color:#385 }
1104 .syntaxhl .comment .delimiter { color:#385 }
1102 .syntaxhl .complex { color:#A08 }
1105 .syntaxhl .complex { color:#A08 }
1103 .syntaxhl .constant { color:#258; font-weight:bold }
1106 .syntaxhl .constant { color:#258; font-weight:bold }
1104 .syntaxhl .decorator { color:#B0B }
1107 .syntaxhl .decorator { color:#B0B }
1105 .syntaxhl .definition { color:#099; font-weight:bold }
1108 .syntaxhl .definition { color:#099; font-weight:bold }
1106 .syntaxhl .delimiter { color:black }
1109 .syntaxhl .delimiter { color:black }
1107 .syntaxhl .directive { color:#088; font-weight:bold }
1110 .syntaxhl .directive { color:#088; font-weight:bold }
1108 .syntaxhl .doc { color:#970 }
1111 .syntaxhl .doc { color:#970 }
1109 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1112 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1110 .syntaxhl .doctype { color:#34b }
1113 .syntaxhl .doctype { color:#34b }
1111 .syntaxhl .entity { color:#800; font-weight:bold }
1114 .syntaxhl .entity { color:#800; font-weight:bold }
1112 .syntaxhl .error { color:#F00; background-color:#FAA }
1115 .syntaxhl .error { color:#F00; background-color:#FAA }
1113 .syntaxhl .escape { color:#666 }
1116 .syntaxhl .escape { color:#666 }
1114 .syntaxhl .exception { color:#C00; font-weight:bold }
1117 .syntaxhl .exception { color:#C00; font-weight:bold }
1115 .syntaxhl .float { color:#06D }
1118 .syntaxhl .float { color:#06D }
1116 .syntaxhl .function { color:#06B; font-weight:bold }
1119 .syntaxhl .function { color:#06B; font-weight:bold }
1117 .syntaxhl .global-variable { color:#d70 }
1120 .syntaxhl .global-variable { color:#d70 }
1118 .syntaxhl .hex { color:#02b }
1121 .syntaxhl .hex { color:#02b }
1119 .syntaxhl .imaginary { color:#f00 }
1122 .syntaxhl .imaginary { color:#f00 }
1120 .syntaxhl .include { color:#B44; font-weight:bold }
1123 .syntaxhl .include { color:#B44; font-weight:bold }
1121 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1124 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1122 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1125 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1123 .syntaxhl .instance-variable { color:#33B }
1126 .syntaxhl .instance-variable { color:#33B }
1124 .syntaxhl .integer { color:#06D }
1127 .syntaxhl .integer { color:#06D }
1125 .syntaxhl .key .char { color: #60f }
1128 .syntaxhl .key .char { color: #60f }
1126 .syntaxhl .key .delimiter { color: #404 }
1129 .syntaxhl .key .delimiter { color: #404 }
1127 .syntaxhl .key { color: #606 }
1130 .syntaxhl .key { color: #606 }
1128 .syntaxhl .keyword { color:#939; font-weight:bold }
1131 .syntaxhl .keyword { color:#939; font-weight:bold }
1129 .syntaxhl .label { color:#970; font-weight:bold }
1132 .syntaxhl .label { color:#970; font-weight:bold }
1130 .syntaxhl .local-variable { color:#963 }
1133 .syntaxhl .local-variable { color:#963 }
1131 .syntaxhl .namespace { color:#707; font-weight:bold }
1134 .syntaxhl .namespace { color:#707; font-weight:bold }
1132 .syntaxhl .octal { color:#40E }
1135 .syntaxhl .octal { color:#40E }
1133 .syntaxhl .operator { }
1136 .syntaxhl .operator { }
1134 .syntaxhl .predefined { color:#369; font-weight:bold }
1137 .syntaxhl .predefined { color:#369; font-weight:bold }
1135 .syntaxhl .predefined-constant { color:#069 }
1138 .syntaxhl .predefined-constant { color:#069 }
1136 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1139 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1137 .syntaxhl .preprocessor { color:#579 }
1140 .syntaxhl .preprocessor { color:#579 }
1138 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1141 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1139 .syntaxhl .regexp .content { color:#808 }
1142 .syntaxhl .regexp .content { color:#808 }
1140 .syntaxhl .regexp .delimiter { color:#404 }
1143 .syntaxhl .regexp .delimiter { color:#404 }
1141 .syntaxhl .regexp .modifier { color:#C2C }
1144 .syntaxhl .regexp .modifier { color:#C2C }
1142 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1145 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1143 .syntaxhl .reserved { color:#080; font-weight:bold }
1146 .syntaxhl .reserved { color:#080; font-weight:bold }
1144 .syntaxhl .shell .content { color:#2B2 }
1147 .syntaxhl .shell .content { color:#2B2 }
1145 .syntaxhl .shell .delimiter { color:#161 }
1148 .syntaxhl .shell .delimiter { color:#161 }
1146 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1149 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1147 .syntaxhl .string .char { color: #46a }
1150 .syntaxhl .string .char { color: #46a }
1148 .syntaxhl .string .content { color: #46a }
1151 .syntaxhl .string .content { color: #46a }
1149 .syntaxhl .string .delimiter { color: #46a }
1152 .syntaxhl .string .delimiter { color: #46a }
1150 .syntaxhl .string .modifier { color: #46a }
1153 .syntaxhl .string .modifier { color: #46a }
1151 .syntaxhl .symbol .content { color:#d33 }
1154 .syntaxhl .symbol .content { color:#d33 }
1152 .syntaxhl .symbol .delimiter { color:#d33 }
1155 .syntaxhl .symbol .delimiter { color:#d33 }
1153 .syntaxhl .symbol { color:#d33 }
1156 .syntaxhl .symbol { color:#d33 }
1154 .syntaxhl .tag { color:#070 }
1157 .syntaxhl .tag { color:#070 }
1155 .syntaxhl .type { color:#339; font-weight:bold }
1158 .syntaxhl .type { color:#339; font-weight:bold }
1156 .syntaxhl .value { color: #088; }
1159 .syntaxhl .value { color: #088; }
1157 .syntaxhl .variable { color:#037 }
1160 .syntaxhl .variable { color:#037 }
1158
1161
1159 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1162 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1160 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1163 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1161 .syntaxhl .change { color: #bbf; background: #007; }
1164 .syntaxhl .change { color: #bbf; background: #007; }
1162 .syntaxhl .head { color: #f8f; background: #505 }
1165 .syntaxhl .head { color: #f8f; background: #505 }
1163 .syntaxhl .head .filename { color: white; }
1166 .syntaxhl .head .filename { color: white; }
1164
1167
1165 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1168 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1166 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1169 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1167
1170
1168 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1171 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1169 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1172 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1170 .syntaxhl .change .change { color: #88f }
1173 .syntaxhl .change .change { color: #88f }
1171 .syntaxhl .head .head { color: #f4f }
1174 .syntaxhl .head .head { color: #f4f }
1172
1175
1173 /***** Media print specific styles *****/
1176 /***** Media print specific styles *****/
1174 @media print {
1177 @media print {
1175 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1178 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1176 #main { background: #fff; }
1179 #main { background: #fff; }
1177 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1180 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1178 #wiki_add_attachment { display:none; }
1181 #wiki_add_attachment { display:none; }
1179 .hide-when-print { display: none; }
1182 .hide-when-print { display: none; }
1180 .autoscroll {overflow-x: visible;}
1183 .autoscroll {overflow-x: visible;}
1181 table.list {margin-top:0.5em;}
1184 table.list {margin-top:0.5em;}
1182 table.list th, table.list td {border: 1px solid #aaa;}
1185 table.list th, table.list td {border: 1px solid #aaa;}
1183 }
1186 }
1184
1187
1185 /* Accessibility specific styles */
1188 /* Accessibility specific styles */
1186 .hidden-for-sighted {
1189 .hidden-for-sighted {
1187 position:absolute;
1190 position:absolute;
1188 left:-10000px;
1191 left:-10000px;
1189 top:auto;
1192 top:auto;
1190 width:1px;
1193 width:1px;
1191 height:1px;
1194 height:1px;
1192 overflow:hidden;
1195 overflow:hidden;
1193 }
1196 }
@@ -1,294 +1,309
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../test_case', __FILE__)
18 require File.expand_path('../test_case', __FILE__)
19 require 'tmpdir'
19 require 'tmpdir'
20
20
21 class RedminePmTest::RepositorySubversionTest < RedminePmTest::TestCase
21 class RedminePmTest::RepositorySubversionTest < RedminePmTest::TestCase
22 fixtures :projects, :users, :members, :roles, :member_roles, :auth_sources
22 fixtures :projects, :users, :members, :roles, :member_roles, :auth_sources
23
23
24 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
24 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
25
25
26 def test_anonymous_read_on_public_repo_with_permission_should_succeed
26 def test_anonymous_read_on_public_repo_with_permission_should_succeed
27 assert_success "ls", svn_url
27 assert_success "ls", svn_url
28 end
28 end
29
29
30 def test_anonymous_read_on_public_repo_with_anonymous_group_permission_should_succeed
31 Role.anonymous.remove_permission! :browse_repository
32 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
33 assert_success "ls", svn_url
34 end
35
30 def test_anonymous_read_on_public_repo_without_permission_should_fail
36 def test_anonymous_read_on_public_repo_without_permission_should_fail
31 Role.anonymous.remove_permission! :browse_repository
37 Role.anonymous.remove_permission! :browse_repository
32 assert_failure "ls", svn_url
38 assert_failure "ls", svn_url
33 end
39 end
34
40
35 def test_anonymous_read_on_private_repo_should_fail
41 def test_anonymous_read_on_private_repo_should_fail
36 Project.find(1).update_attribute :is_public, false
42 Project.find(1).update_attribute :is_public, false
37 assert_failure "ls", svn_url
43 assert_failure "ls", svn_url
38 end
44 end
39
45
40 def test_anonymous_commit_on_public_repo_should_fail
46 def test_anonymous_commit_on_public_repo_should_fail
41 Role.anonymous.add_permission! :commit_access
47 Role.anonymous.add_permission! :commit_access
42 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
48 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
43 end
49 end
44
50
45 def test_anonymous_commit_on_private_repo_should_fail
51 def test_anonymous_commit_on_private_repo_should_fail
46 Role.anonymous.add_permission! :commit_access
52 Role.anonymous.add_permission! :commit_access
47 Project.find(1).update_attribute :is_public, false
53 Project.find(1).update_attribute :is_public, false
48 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
54 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
49 end
55 end
50
56
51 def test_non_member_read_on_public_repo_with_permission_should_succeed
57 def test_non_member_read_on_public_repo_with_permission_should_succeed
52 Role.anonymous.remove_permission! :browse_repository
58 Role.anonymous.remove_permission! :browse_repository
53 with_credentials "miscuser8", "foo" do
59 with_credentials "miscuser8", "foo" do
54 assert_success "ls", svn_url
60 assert_success "ls", svn_url
55 end
61 end
56 end
62 end
57
63
64 def test_non_member_read_on_public_repo_with_non_member_group_permission_should_succeed
65 Role.anonymous.remove_permission! :browse_repository
66 Role.non_member.remove_permission! :browse_repository
67 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
68 with_credentials "miscuser8", "foo" do
69 assert_success "ls", svn_url
70 end
71 end
72
58 def test_non_member_read_on_public_repo_without_permission_should_fail
73 def test_non_member_read_on_public_repo_without_permission_should_fail
59 Role.anonymous.remove_permission! :browse_repository
74 Role.anonymous.remove_permission! :browse_repository
60 Role.non_member.remove_permission! :browse_repository
75 Role.non_member.remove_permission! :browse_repository
61 with_credentials "miscuser8", "foo" do
76 with_credentials "miscuser8", "foo" do
62 assert_failure "ls", svn_url
77 assert_failure "ls", svn_url
63 end
78 end
64 end
79 end
65
80
66 def test_non_member_read_on_private_repo_should_fail
81 def test_non_member_read_on_private_repo_should_fail
67 Project.find(1).update_attribute :is_public, false
82 Project.find(1).update_attribute :is_public, false
68 with_credentials "miscuser8", "foo" do
83 with_credentials "miscuser8", "foo" do
69 assert_failure "ls", svn_url
84 assert_failure "ls", svn_url
70 end
85 end
71 end
86 end
72
87
73 def test_non_member_commit_on_public_repo_should_fail
88 def test_non_member_commit_on_public_repo_should_fail
74 Role.non_member.add_permission! :commit_access
89 Role.non_member.add_permission! :commit_access
75 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
90 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
76 end
91 end
77
92
78 def test_non_member_commit_on_private_repo_should_fail
93 def test_non_member_commit_on_private_repo_should_fail
79 Role.non_member.add_permission! :commit_access
94 Role.non_member.add_permission! :commit_access
80 Project.find(1).update_attribute :is_public, false
95 Project.find(1).update_attribute :is_public, false
81 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
96 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
82 end
97 end
83
98
84 def test_member_read_on_public_repo_with_permission_should_succeed
99 def test_member_read_on_public_repo_with_permission_should_succeed
85 Role.anonymous.remove_permission! :browse_repository
100 Role.anonymous.remove_permission! :browse_repository
86 Role.non_member.remove_permission! :browse_repository
101 Role.non_member.remove_permission! :browse_repository
87 with_credentials "dlopper", "foo" do
102 with_credentials "dlopper", "foo" do
88 assert_success "ls", svn_url
103 assert_success "ls", svn_url
89 end
104 end
90 end
105 end
91
106
92 def test_member_read_on_public_repo_without_permission_should_fail
107 def test_member_read_on_public_repo_without_permission_should_fail
93 Role.anonymous.remove_permission! :browse_repository
108 Role.anonymous.remove_permission! :browse_repository
94 Role.non_member.remove_permission! :browse_repository
109 Role.non_member.remove_permission! :browse_repository
95 Role.find(2).remove_permission! :browse_repository
110 Role.find(2).remove_permission! :browse_repository
96 with_credentials "dlopper", "foo" do
111 with_credentials "dlopper", "foo" do
97 assert_failure "ls", svn_url
112 assert_failure "ls", svn_url
98 end
113 end
99 end
114 end
100
115
101 def test_member_read_on_private_repo_with_permission_should_succeed
116 def test_member_read_on_private_repo_with_permission_should_succeed
102 Project.find(1).update_attribute :is_public, false
117 Project.find(1).update_attribute :is_public, false
103 with_credentials "dlopper", "foo" do
118 with_credentials "dlopper", "foo" do
104 assert_success "ls", svn_url
119 assert_success "ls", svn_url
105 end
120 end
106 end
121 end
107
122
108 def test_member_read_on_private_repo_without_permission_should_fail
123 def test_member_read_on_private_repo_without_permission_should_fail
109 Role.find(2).remove_permission! :browse_repository
124 Role.find(2).remove_permission! :browse_repository
110 Project.find(1).update_attribute :is_public, false
125 Project.find(1).update_attribute :is_public, false
111 with_credentials "dlopper", "foo" do
126 with_credentials "dlopper", "foo" do
112 assert_failure "ls", svn_url
127 assert_failure "ls", svn_url
113 end
128 end
114 end
129 end
115
130
116 def test_member_commit_on_public_repo_with_permission_should_succeed
131 def test_member_commit_on_public_repo_with_permission_should_succeed
117 Role.find(2).add_permission! :commit_access
132 Role.find(2).add_permission! :commit_access
118 with_credentials "dlopper", "foo" do
133 with_credentials "dlopper", "foo" do
119 assert_success "mkdir --message Creating_a_directory", svn_url(random_filename)
134 assert_success "mkdir --message Creating_a_directory", svn_url(random_filename)
120 end
135 end
121 end
136 end
122
137
123 def test_member_commit_on_public_repo_without_permission_should_fail
138 def test_member_commit_on_public_repo_without_permission_should_fail
124 Role.find(2).remove_permission! :commit_access
139 Role.find(2).remove_permission! :commit_access
125 with_credentials "dlopper", "foo" do
140 with_credentials "dlopper", "foo" do
126 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
141 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
127 end
142 end
128 end
143 end
129
144
130 def test_member_commit_on_private_repo_with_permission_should_succeed
145 def test_member_commit_on_private_repo_with_permission_should_succeed
131 Role.find(2).add_permission! :commit_access
146 Role.find(2).add_permission! :commit_access
132 Project.find(1).update_attribute :is_public, false
147 Project.find(1).update_attribute :is_public, false
133 with_credentials "dlopper", "foo" do
148 with_credentials "dlopper", "foo" do
134 assert_success "mkdir --message Creating_a_directory", svn_url(random_filename)
149 assert_success "mkdir --message Creating_a_directory", svn_url(random_filename)
135 end
150 end
136 end
151 end
137
152
138 def test_member_commit_on_private_repo_without_permission_should_fail
153 def test_member_commit_on_private_repo_without_permission_should_fail
139 Role.find(2).remove_permission! :commit_access
154 Role.find(2).remove_permission! :commit_access
140 Project.find(1).update_attribute :is_public, false
155 Project.find(1).update_attribute :is_public, false
141 with_credentials "dlopper", "foo" do
156 with_credentials "dlopper", "foo" do
142 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
157 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
143 end
158 end
144 end
159 end
145
160
146 def test_invalid_credentials_should_fail
161 def test_invalid_credentials_should_fail
147 Project.find(1).update_attribute :is_public, false
162 Project.find(1).update_attribute :is_public, false
148 with_credentials "dlopper", "foo" do
163 with_credentials "dlopper", "foo" do
149 assert_success "ls", svn_url
164 assert_success "ls", svn_url
150 end
165 end
151 with_credentials "dlopper", "wrong" do
166 with_credentials "dlopper", "wrong" do
152 assert_failure "ls", svn_url
167 assert_failure "ls", svn_url
153 end
168 end
154 end
169 end
155
170
156 def test_anonymous_read_should_fail_with_login_required
171 def test_anonymous_read_should_fail_with_login_required
157 assert_success "ls", svn_url
172 assert_success "ls", svn_url
158 with_settings :login_required => '1' do
173 with_settings :login_required => '1' do
159 assert_failure "ls", svn_url
174 assert_failure "ls", svn_url
160 end
175 end
161 end
176 end
162
177
163 def test_authenticated_read_should_succeed_with_login_required
178 def test_authenticated_read_should_succeed_with_login_required
164 with_settings :login_required => '1' do
179 with_settings :login_required => '1' do
165 with_credentials "miscuser8", "foo" do
180 with_credentials "miscuser8", "foo" do
166 assert_success "ls", svn_url
181 assert_success "ls", svn_url
167 end
182 end
168 end
183 end
169 end
184 end
170
185
171 def test_read_on_archived_projects_should_fail
186 def test_read_on_archived_projects_should_fail
172 Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED
187 Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED
173 assert_failure "ls", svn_url
188 assert_failure "ls", svn_url
174 end
189 end
175
190
176 def test_read_on_archived_private_projects_should_fail
191 def test_read_on_archived_private_projects_should_fail
177 Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED
192 Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED
178 Project.find(1).update_attribute :is_public, false
193 Project.find(1).update_attribute :is_public, false
179 with_credentials "dlopper", "foo" do
194 with_credentials "dlopper", "foo" do
180 assert_failure "ls", svn_url
195 assert_failure "ls", svn_url
181 end
196 end
182 end
197 end
183
198
184 def test_read_on_closed_projects_should_succeed
199 def test_read_on_closed_projects_should_succeed
185 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
200 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
186 assert_success "ls", svn_url
201 assert_success "ls", svn_url
187 end
202 end
188
203
189 def test_read_on_closed_private_projects_should_succeed
204 def test_read_on_closed_private_projects_should_succeed
190 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
205 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
191 Project.find(1).update_attribute :is_public, false
206 Project.find(1).update_attribute :is_public, false
192 with_credentials "dlopper", "foo" do
207 with_credentials "dlopper", "foo" do
193 assert_success "ls", svn_url
208 assert_success "ls", svn_url
194 end
209 end
195 end
210 end
196
211
197 def test_commit_on_closed_projects_should_fail
212 def test_commit_on_closed_projects_should_fail
198 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
213 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
199 Role.find(2).add_permission! :commit_access
214 Role.find(2).add_permission! :commit_access
200 with_credentials "dlopper", "foo" do
215 with_credentials "dlopper", "foo" do
201 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
216 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
202 end
217 end
203 end
218 end
204
219
205 def test_commit_on_closed_private_projects_should_fail
220 def test_commit_on_closed_private_projects_should_fail
206 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
221 Project.find(1).update_attribute :status, Project::STATUS_CLOSED
207 Project.find(1).update_attribute :is_public, false
222 Project.find(1).update_attribute :is_public, false
208 Role.find(2).add_permission! :commit_access
223 Role.find(2).add_permission! :commit_access
209 with_credentials "dlopper", "foo" do
224 with_credentials "dlopper", "foo" do
210 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
225 assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename)
211 end
226 end
212 end
227 end
213
228
214 if ldap_configured?
229 if ldap_configured?
215 def test_user_with_ldap_auth_source_should_authenticate_with_ldap_credentials
230 def test_user_with_ldap_auth_source_should_authenticate_with_ldap_credentials
216 ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
231 ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
217 ldap_user.login = 'example1'
232 ldap_user.login = 'example1'
218 ldap_user.save!
233 ldap_user.save!
219
234
220 with_settings :login_required => '1' do
235 with_settings :login_required => '1' do
221 with_credentials "example1", "123456" do
236 with_credentials "example1", "123456" do
222 assert_success "ls", svn_url
237 assert_success "ls", svn_url
223 end
238 end
224 end
239 end
225
240
226 with_settings :login_required => '1' do
241 with_settings :login_required => '1' do
227 with_credentials "example1", "wrong" do
242 with_credentials "example1", "wrong" do
228 assert_failure "ls", svn_url
243 assert_failure "ls", svn_url
229 end
244 end
230 end
245 end
231 end
246 end
232 end
247 end
233
248
234 def test_checkout
249 def test_checkout
235 Dir.mktmpdir do |dir|
250 Dir.mktmpdir do |dir|
236 assert_success "checkout", svn_url, dir
251 assert_success "checkout", svn_url, dir
237 end
252 end
238 end
253 end
239
254
240 def test_read_commands
255 def test_read_commands
241 assert_success "info", svn_url
256 assert_success "info", svn_url
242 assert_success "ls", svn_url
257 assert_success "ls", svn_url
243 assert_success "log", svn_url
258 assert_success "log", svn_url
244 end
259 end
245
260
246 def test_write_commands
261 def test_write_commands
247 Role.find(2).add_permission! :commit_access
262 Role.find(2).add_permission! :commit_access
248 filename = random_filename
263 filename = random_filename
249
264
250 Dir.mktmpdir do |dir|
265 Dir.mktmpdir do |dir|
251 assert_success "checkout", svn_url, dir
266 assert_success "checkout", svn_url, dir
252 Dir.chdir(dir) do
267 Dir.chdir(dir) do
253 # creates a file in the working copy
268 # creates a file in the working copy
254 f = File.new(File.join(dir, filename), "w")
269 f = File.new(File.join(dir, filename), "w")
255 f.write "test file content"
270 f.write "test file content"
256 f.close
271 f.close
257
272
258 assert_success "add", filename
273 assert_success "add", filename
259 with_credentials "dlopper", "foo" do
274 with_credentials "dlopper", "foo" do
260 assert_success "commit --message Committing_a_file"
275 assert_success "commit --message Committing_a_file"
261 assert_success "copy --message Copying_a_file", svn_url(filename), svn_url("#{filename}_copy")
276 assert_success "copy --message Copying_a_file", svn_url(filename), svn_url("#{filename}_copy")
262 assert_success "delete --message Deleting_a_file", svn_url(filename)
277 assert_success "delete --message Deleting_a_file", svn_url(filename)
263 assert_success "mkdir --message Creating_a_directory", svn_url("#{filename}_dir")
278 assert_success "mkdir --message Creating_a_directory", svn_url("#{filename}_dir")
264 end
279 end
265 assert_success "update"
280 assert_success "update"
266
281
267 # checks that the working copy was updated
282 # checks that the working copy was updated
268 assert File.exists?(File.join(dir, "#{filename}_copy"))
283 assert File.exists?(File.join(dir, "#{filename}_copy"))
269 assert File.directory?(File.join(dir, "#{filename}_dir"))
284 assert File.directory?(File.join(dir, "#{filename}_dir"))
270 end
285 end
271 end
286 end
272 end
287 end
273
288
274 def test_read_invalid_repo_should_fail
289 def test_read_invalid_repo_should_fail
275 assert_failure "ls", svn_url("invalid")
290 assert_failure "ls", svn_url("invalid")
276 end
291 end
277
292
278 protected
293 protected
279
294
280 def execute(*args)
295 def execute(*args)
281 a = [SVN_BIN, "--no-auth-cache --non-interactive"]
296 a = [SVN_BIN, "--no-auth-cache --non-interactive"]
282 a << "--username #{username}" if username
297 a << "--username #{username}" if username
283 a << "--password #{password}" if password
298 a << "--password #{password}" if password
284
299
285 super a, *args
300 super a, *args
286 end
301 end
287
302
288 def svn_url(path=nil)
303 def svn_url(path=nil)
289 host = ENV['REDMINE_TEST_DAV_SERVER'] || '127.0.0.1'
304 host = ENV['REDMINE_TEST_DAV_SERVER'] || '127.0.0.1'
290 url = "http://#{host}/svn/ecookbook"
305 url = "http://#{host}/svn/ecookbook"
291 url << "/#{path}" if path
306 url << "/#{path}" if path
292 url
307 url
293 end
308 end
294 end
309 end
@@ -1,81 +1,82
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 module RedminePmTest
20 module RedminePmTest
21 class TestCase < ActiveSupport::TestCase
21 class TestCase < ActiveSupport::TestCase
22 attr_reader :command, :response, :status, :username, :password
22 attr_reader :command, :response, :status, :username, :password
23
23
24 # Cannot use transactional fixtures here: database
24 # Cannot use transactional fixtures here: database
25 # will be accessed from Redmine.pm with its own connection
25 # will be accessed from Redmine.pm with its own connection
26 self.use_transactional_fixtures = false
26 self.use_transactional_fixtures = false
27
27
28 def test_dummy
28 def test_dummy
29 end
29 end
30
30
31 protected
31 protected
32
32
33 def assert_response(expected, msg=nil)
33 def assert_response(expected, msg=nil)
34 case expected
34 case expected
35 when :success
35 when :success
36 assert_equal 0, status,
36 assert_equal 0, status,
37 (msg || "The command failed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}")
37 (msg || "The command failed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}")
38 when :failure
38 when :failure
39 assert_not_equal 0, status,
39 assert_not_equal 0, status,
40 (msg || "The command succeed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}")
40 (msg || "The command succeed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}")
41 else
41 else
42 assert_equal expected, status, msg
42 assert_equal expected, status, msg
43 end
43 end
44 end
44 end
45
45
46 def assert_success(*args)
46 def assert_success(*args)
47 execute *args
47 execute *args
48 assert_response :success
48 assert_response :success
49 end
49 end
50
50
51 def assert_failure(*args)
51 def assert_failure(*args)
52 execute *args
52 execute *args
53 assert_response :failure
53 assert_response :failure
54 end
54 end
55
55
56 def with_credentials(username, password)
56 def with_credentials(username, password)
57 old_username, old_password = @username, @password
57 old_username, old_password = @username, @password
58 @username, @password = username, password
58 @username, @password = username, password
59 yield if block_given?
59 yield if block_given?
60 ensure
60 ensure
61 @username, @password = old_username, old_password
61 @username, @password = old_username, old_password
62 end
62 end
63
63
64 def execute(*args)
64 def execute(*args)
65 @command = args.join(' ')
65 @command = args.join(' ')
66 @status = nil
66 @status = nil
67 IO.popen("#{command} 2>&1") do |io|
67 IO.popen("#{command} 2>&1") do |io|
68 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
68 @response = io.read
69 @response = io.read
69 end
70 end
70 @status = $?.exitstatus
71 @status = $?.exitstatus
71 end
72 end
72
73
73 def formatted_response
74 def formatted_response
74 "#{'='*40}\n#{response}#{'='*40}"
75 "#{'='*40}\n#{response}#{'='*40}"
75 end
76 end
76
77
77 def random_filename
78 def random_filename
78 Redmine::Utils.random_hex(16)
79 Redmine::Utils.random_hex(16)
79 end
80 end
80 end
81 end
81 end
82 end
@@ -1,169 +1,180
1 ---
1 ---
2 users_004:
2 users_004:
3 created_on: 2006-07-19 19:34:07 +02:00
3 created_on: 2006-07-19 19:34:07 +02:00
4 status: 1
4 status: 1
5 last_login_on:
5 last_login_on:
6 language: en
6 language: en
7 # password = foo
7 # password = foo
8 salt: 3126f764c3c5ac61cbfc103f25f934cf
8 salt: 3126f764c3c5ac61cbfc103f25f934cf
9 hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b
9 hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b
10 updated_on: 2006-07-19 19:34:07 +02:00
10 updated_on: 2006-07-19 19:34:07 +02:00
11 admin: false
11 admin: false
12 mail: rhill@somenet.foo
12 mail: rhill@somenet.foo
13 lastname: Hill
13 lastname: Hill
14 firstname: Robert
14 firstname: Robert
15 id: 4
15 id: 4
16 auth_source_id:
16 auth_source_id:
17 mail_notification: all
17 mail_notification: all
18 login: rhill
18 login: rhill
19 type: User
19 type: User
20 users_001:
20 users_001:
21 created_on: 2006-07-19 19:12:21 +02:00
21 created_on: 2006-07-19 19:12:21 +02:00
22 status: 1
22 status: 1
23 last_login_on: 2006-07-19 22:57:52 +02:00
23 last_login_on: 2006-07-19 22:57:52 +02:00
24 language: en
24 language: en
25 # password = admin
25 # password = admin
26 salt: 82090c953c4a0000a7db253b0691a6b4
26 salt: 82090c953c4a0000a7db253b0691a6b4
27 hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150
27 hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150
28 updated_on: 2006-07-19 22:57:52 +02:00
28 updated_on: 2006-07-19 22:57:52 +02:00
29 admin: true
29 admin: true
30 mail: admin@somenet.foo
30 mail: admin@somenet.foo
31 lastname: Admin
31 lastname: Admin
32 firstname: Redmine
32 firstname: Redmine
33 id: 1
33 id: 1
34 auth_source_id:
34 auth_source_id:
35 mail_notification: all
35 mail_notification: all
36 login: admin
36 login: admin
37 type: User
37 type: User
38 users_002:
38 users_002:
39 created_on: 2006-07-19 19:32:09 +02:00
39 created_on: 2006-07-19 19:32:09 +02:00
40 status: 1
40 status: 1
41 last_login_on: 2006-07-19 22:42:15 +02:00
41 last_login_on: 2006-07-19 22:42:15 +02:00
42 language: en
42 language: en
43 # password = jsmith
43 # password = jsmith
44 salt: 67eb4732624d5a7753dcea7ce0bb7d7d
44 salt: 67eb4732624d5a7753dcea7ce0bb7d7d
45 hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc
45 hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc
46 updated_on: 2006-07-19 22:42:15 +02:00
46 updated_on: 2006-07-19 22:42:15 +02:00
47 admin: false
47 admin: false
48 mail: jsmith@somenet.foo
48 mail: jsmith@somenet.foo
49 lastname: Smith
49 lastname: Smith
50 firstname: John
50 firstname: John
51 id: 2
51 id: 2
52 auth_source_id:
52 auth_source_id:
53 mail_notification: all
53 mail_notification: all
54 login: jsmith
54 login: jsmith
55 type: User
55 type: User
56 users_003:
56 users_003:
57 created_on: 2006-07-19 19:33:19 +02:00
57 created_on: 2006-07-19 19:33:19 +02:00
58 status: 1
58 status: 1
59 last_login_on:
59 last_login_on:
60 language: en
60 language: en
61 # password = foo
61 # password = foo
62 salt: 7599f9963ec07b5a3b55b354407120c0
62 salt: 7599f9963ec07b5a3b55b354407120c0
63 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
63 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
64 updated_on: 2006-07-19 19:33:19 +02:00
64 updated_on: 2006-07-19 19:33:19 +02:00
65 admin: false
65 admin: false
66 mail: dlopper@somenet.foo
66 mail: dlopper@somenet.foo
67 lastname: Lopper
67 lastname: Lopper
68 firstname: Dave
68 firstname: Dave
69 id: 3
69 id: 3
70 auth_source_id:
70 auth_source_id:
71 mail_notification: all
71 mail_notification: all
72 login: dlopper
72 login: dlopper
73 type: User
73 type: User
74 users_005:
74 users_005:
75 id: 5
75 id: 5
76 created_on: 2006-07-19 19:33:19 +02:00
76 created_on: 2006-07-19 19:33:19 +02:00
77 # Locked
77 # Locked
78 status: 3
78 status: 3
79 last_login_on:
79 last_login_on:
80 language: en
80 language: en
81 hashed_password: 1
81 hashed_password: 1
82 updated_on: 2006-07-19 19:33:19 +02:00
82 updated_on: 2006-07-19 19:33:19 +02:00
83 admin: false
83 admin: false
84 mail: dlopper2@somenet.foo
84 mail: dlopper2@somenet.foo
85 lastname: Lopper2
85 lastname: Lopper2
86 firstname: Dave2
86 firstname: Dave2
87 auth_source_id:
87 auth_source_id:
88 mail_notification: all
88 mail_notification: all
89 login: dlopper2
89 login: dlopper2
90 type: User
90 type: User
91 users_006:
91 users_006:
92 id: 6
92 id: 6
93 created_on: 2006-07-19 19:33:19 +02:00
93 created_on: 2006-07-19 19:33:19 +02:00
94 status: 0
94 status: 0
95 last_login_on:
95 last_login_on:
96 language: ''
96 language: ''
97 hashed_password: 1
97 hashed_password: 1
98 updated_on: 2006-07-19 19:33:19 +02:00
98 updated_on: 2006-07-19 19:33:19 +02:00
99 admin: false
99 admin: false
100 mail: ''
100 mail: ''
101 lastname: Anonymous
101 lastname: Anonymous
102 firstname: ''
102 firstname: ''
103 auth_source_id:
103 auth_source_id:
104 mail_notification: only_my_events
104 mail_notification: only_my_events
105 login: ''
105 login: ''
106 type: AnonymousUser
106 type: AnonymousUser
107 users_007:
107 users_007:
108 # A user who does not belong to any project
108 # A user who does not belong to any project
109 id: 7
109 id: 7
110 created_on: 2006-07-19 19:33:19 +02:00
110 created_on: 2006-07-19 19:33:19 +02:00
111 status: 1
111 status: 1
112 last_login_on:
112 last_login_on:
113 language: 'en'
113 language: 'en'
114 # password = foo
114 # password = foo
115 salt: 7599f9963ec07b5a3b55b354407120c0
115 salt: 7599f9963ec07b5a3b55b354407120c0
116 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
116 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
117 updated_on: 2006-07-19 19:33:19 +02:00
117 updated_on: 2006-07-19 19:33:19 +02:00
118 admin: false
118 admin: false
119 mail: someone@foo.bar
119 mail: someone@foo.bar
120 lastname: One
120 lastname: One
121 firstname: Some
121 firstname: Some
122 auth_source_id:
122 auth_source_id:
123 mail_notification: only_my_events
123 mail_notification: only_my_events
124 login: someone
124 login: someone
125 type: User
125 type: User
126 users_008:
126 users_008:
127 id: 8
127 id: 8
128 created_on: 2006-07-19 19:33:19 +02:00
128 created_on: 2006-07-19 19:33:19 +02:00
129 status: 1
129 status: 1
130 last_login_on:
130 last_login_on:
131 language: 'it'
131 language: 'it'
132 # password = foo
132 # password = foo
133 salt: 7599f9963ec07b5a3b55b354407120c0
133 salt: 7599f9963ec07b5a3b55b354407120c0
134 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
134 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
135 updated_on: 2006-07-19 19:33:19 +02:00
135 updated_on: 2006-07-19 19:33:19 +02:00
136 admin: false
136 admin: false
137 mail: miscuser8@foo.bar
137 mail: miscuser8@foo.bar
138 lastname: Misc
138 lastname: Misc
139 firstname: User
139 firstname: User
140 auth_source_id:
140 auth_source_id:
141 mail_notification: only_my_events
141 mail_notification: only_my_events
142 login: miscuser8
142 login: miscuser8
143 type: User
143 type: User
144 users_009:
144 users_009:
145 id: 9
145 id: 9
146 created_on: 2006-07-19 19:33:19 +02:00
146 created_on: 2006-07-19 19:33:19 +02:00
147 status: 1
147 status: 1
148 last_login_on:
148 last_login_on:
149 language: 'it'
149 language: 'it'
150 hashed_password: 1
150 hashed_password: 1
151 updated_on: 2006-07-19 19:33:19 +02:00
151 updated_on: 2006-07-19 19:33:19 +02:00
152 admin: false
152 admin: false
153 mail: miscuser9@foo.bar
153 mail: miscuser9@foo.bar
154 lastname: Misc
154 lastname: Misc
155 firstname: User
155 firstname: User
156 auth_source_id:
156 auth_source_id:
157 mail_notification: only_my_events
157 mail_notification: only_my_events
158 login: miscuser9
158 login: miscuser9
159 type: User
159 type: User
160 groups_010:
160 groups_010:
161 id: 10
161 id: 10
162 lastname: A Team
162 lastname: A Team
163 type: Group
163 type: Group
164 status: 1
164 groups_011:
165 groups_011:
165 id: 11
166 id: 11
166 lastname: B Team
167 lastname: B Team
167 type: Group
168 type: Group
169 status: 1
170 groups_non_member:
171 id: 12
172 lastname: Non member users
173 type: GroupNonMember
174 status: 1
175 groups_anonymous:
176 id: 13
177 lastname: Anonymous users
178 type: GroupAnonymous
179 status: 1
168
180
169
@@ -1,170 +1,200
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class Redmine::ApiTest::GroupsTest < Redmine::ApiTest::Base
20 class Redmine::ApiTest::GroupsTest < Redmine::ApiTest::Base
21 fixtures :users, :groups_users
21 fixtures :users, :groups_users
22
22
23 def setup
23 def setup
24 Setting.rest_api_enabled = '1'
24 Setting.rest_api_enabled = '1'
25 end
25 end
26
26
27 test "GET /groups.xml should require authentication" do
27 test "GET /groups.xml should require authentication" do
28 get '/groups.xml'
28 get '/groups.xml'
29 assert_response 401
29 assert_response 401
30 end
30 end
31
31
32 test "GET /groups.xml should return groups" do
32 test "GET /groups.xml should return givable groups" do
33 get '/groups.xml', {}, credentials('admin')
33 get '/groups.xml', {}, credentials('admin')
34 assert_response :success
34 assert_response :success
35 assert_equal 'application/xml', response.content_type
35 assert_equal 'application/xml', response.content_type
36
36
37 assert_select 'groups' do
37 assert_select 'groups' do
38 assert_select 'group', Group.givable.count
38 assert_select 'group' do
39 assert_select 'group' do
39 assert_select 'name', :text => 'A Team'
40 assert_select 'name', :text => 'A Team'
40 assert_select 'id', :text => '10'
41 assert_select 'id', :text => '10'
41 end
42 end
42 end
43 end
43 end
44 end
44
45
46 test "GET /groups.xml?builtin=1 should return all groups" do
47 get '/groups.xml?builtin=1', {}, credentials('admin')
48 assert_response :success
49 assert_equal 'application/xml', response.content_type
50
51 assert_select 'groups' do
52 assert_select 'group', Group.givable.count + 2
53 assert_select 'group' do
54 assert_select 'builtin', :text => 'non_member'
55 assert_select 'id', :text => '12'
56 end
57 assert_select 'group' do
58 assert_select 'builtin', :text => 'anonymous'
59 assert_select 'id', :text => '13'
60 end
61 end
62 end
63
45 test "GET /groups.json should require authentication" do
64 test "GET /groups.json should require authentication" do
46 get '/groups.json'
65 get '/groups.json'
47 assert_response 401
66 assert_response 401
48 end
67 end
49
68
50 test "GET /groups.json should return groups" do
69 test "GET /groups.json should return groups" do
51 get '/groups.json', {}, credentials('admin')
70 get '/groups.json', {}, credentials('admin')
52 assert_response :success
71 assert_response :success
53 assert_equal 'application/json', response.content_type
72 assert_equal 'application/json', response.content_type
54
73
55 json = MultiJson.load(response.body)
74 json = MultiJson.load(response.body)
56 groups = json['groups']
75 groups = json['groups']
57 assert_kind_of Array, groups
76 assert_kind_of Array, groups
58 group = groups.detect {|g| g['name'] == 'A Team'}
77 group = groups.detect {|g| g['name'] == 'A Team'}
59 assert_not_nil group
78 assert_not_nil group
60 assert_equal({'id' => 10, 'name' => 'A Team'}, group)
79 assert_equal({'id' => 10, 'name' => 'A Team'}, group)
61 end
80 end
62
81
63 test "GET /groups/:id.xml should return the group with its users" do
82 test "GET /groups/:id.xml should return the group" do
64 get '/groups/10.xml', {}, credentials('admin')
83 get '/groups/10.xml', {}, credentials('admin')
65 assert_response :success
84 assert_response :success
66 assert_equal 'application/xml', response.content_type
85 assert_equal 'application/xml', response.content_type
67
86
68 assert_select 'group' do
87 assert_select 'group' do
69 assert_select 'name', :text => 'A Team'
88 assert_select 'name', :text => 'A Team'
70 assert_select 'id', :text => '10'
89 assert_select 'id', :text => '10'
71 end
90 end
72 end
91 end
73
92
93 test "GET /groups/:id.xml should return the builtin group" do
94 get '/groups/12.xml', {}, credentials('admin')
95 assert_response :success
96 assert_equal 'application/xml', response.content_type
97
98 assert_select 'group' do
99 assert_select 'builtin', :text => 'non_member'
100 assert_select 'id', :text => '12'
101 end
102 end
103
74 test "GET /groups/:id.xml should include users if requested" do
104 test "GET /groups/:id.xml should include users if requested" do
75 get '/groups/10.xml?include=users', {}, credentials('admin')
105 get '/groups/10.xml?include=users', {}, credentials('admin')
76 assert_response :success
106 assert_response :success
77 assert_equal 'application/xml', response.content_type
107 assert_equal 'application/xml', response.content_type
78
108
79 assert_select 'group' do
109 assert_select 'group' do
80 assert_select 'users' do
110 assert_select 'users' do
81 assert_select 'user', Group.find(10).users.count
111 assert_select 'user', Group.find(10).users.count
82 assert_select 'user[id=8]'
112 assert_select 'user[id=8]'
83 end
113 end
84 end
114 end
85 end
115 end
86
116
87 test "GET /groups/:id.xml include memberships if requested" do
117 test "GET /groups/:id.xml include memberships if requested" do
88 get '/groups/10.xml?include=memberships', {}, credentials('admin')
118 get '/groups/10.xml?include=memberships', {}, credentials('admin')
89 assert_response :success
119 assert_response :success
90 assert_equal 'application/xml', response.content_type
120 assert_equal 'application/xml', response.content_type
91
121
92 assert_select 'group' do
122 assert_select 'group' do
93 assert_select 'memberships'
123 assert_select 'memberships'
94 end
124 end
95 end
125 end
96
126
97 test "POST /groups.xml with valid parameters should create the group" do
127 test "POST /groups.xml with valid parameters should create the group" do
98 assert_difference('Group.count') do
128 assert_difference('Group.count') do
99 post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
129 post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
100 assert_response :created
130 assert_response :created
101 assert_equal 'application/xml', response.content_type
131 assert_equal 'application/xml', response.content_type
102 end
132 end
103
133
104 group = Group.order('id DESC').first
134 group = Group.order('id DESC').first
105 assert_equal 'Test', group.name
135 assert_equal 'Test', group.name
106 assert_equal [2, 3], group.users.map(&:id).sort
136 assert_equal [2, 3], group.users.map(&:id).sort
107
137
108 assert_select 'group' do
138 assert_select 'group' do
109 assert_select 'name', :text => 'Test'
139 assert_select 'name', :text => 'Test'
110 end
140 end
111 end
141 end
112
142
113 test "POST /groups.xml with invalid parameters should return errors" do
143 test "POST /groups.xml with invalid parameters should return errors" do
114 assert_no_difference('Group.count') do
144 assert_no_difference('Group.count') do
115 post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
145 post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
116 end
146 end
117 assert_response :unprocessable_entity
147 assert_response :unprocessable_entity
118 assert_equal 'application/xml', response.content_type
148 assert_equal 'application/xml', response.content_type
119
149
120 assert_select 'errors' do
150 assert_select 'errors' do
121 assert_select 'error', :text => /Name can't be blank/
151 assert_select 'error', :text => /Name can't be blank/
122 end
152 end
123 end
153 end
124
154
125 test "PUT /groups/:id.xml with valid parameters should update the group" do
155 test "PUT /groups/:id.xml with valid parameters should update the group" do
126 put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
156 put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
127 assert_response :ok
157 assert_response :ok
128 assert_equal '', @response.body
158 assert_equal '', @response.body
129
159
130 group = Group.find(10)
160 group = Group.find(10)
131 assert_equal 'New name', group.name
161 assert_equal 'New name', group.name
132 assert_equal [2, 3], group.users.map(&:id).sort
162 assert_equal [2, 3], group.users.map(&:id).sort
133 end
163 end
134
164
135 test "PUT /groups/:id.xml with invalid parameters should return errors" do
165 test "PUT /groups/:id.xml with invalid parameters should return errors" do
136 put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
166 put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
137 assert_response :unprocessable_entity
167 assert_response :unprocessable_entity
138 assert_equal 'application/xml', response.content_type
168 assert_equal 'application/xml', response.content_type
139
169
140 assert_select 'errors' do
170 assert_select 'errors' do
141 assert_select 'error', :text => /Name can't be blank/
171 assert_select 'error', :text => /Name can't be blank/
142 end
172 end
143 end
173 end
144
174
145 test "DELETE /groups/:id.xml should delete the group" do
175 test "DELETE /groups/:id.xml should delete the group" do
146 assert_difference 'Group.count', -1 do
176 assert_difference 'Group.count', -1 do
147 delete '/groups/10.xml', {}, credentials('admin')
177 delete '/groups/10.xml', {}, credentials('admin')
148 assert_response :ok
178 assert_response :ok
149 assert_equal '', @response.body
179 assert_equal '', @response.body
150 end
180 end
151 end
181 end
152
182
153 test "POST /groups/:id/users.xml should add user to the group" do
183 test "POST /groups/:id/users.xml should add user to the group" do
154 assert_difference 'Group.find(10).users.count' do
184 assert_difference 'Group.find(10).users.count' do
155 post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
185 post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
156 assert_response :ok
186 assert_response :ok
157 assert_equal '', @response.body
187 assert_equal '', @response.body
158 end
188 end
159 assert_include User.find(5), Group.find(10).users
189 assert_include User.find(5), Group.find(10).users
160 end
190 end
161
191
162 test "DELETE /groups/:id/users/:user_id.xml should remove user from the group" do
192 test "DELETE /groups/:id/users/:user_id.xml should remove user from the group" do
163 assert_difference 'Group.find(10).users.count', -1 do
193 assert_difference 'Group.find(10).users.count', -1 do
164 delete '/groups/10/users/8.xml', {}, credentials('admin')
194 delete '/groups/10/users/8.xml', {}, credentials('admin')
165 assert_response :ok
195 assert_response :ok
166 assert_equal '', @response.body
196 assert_equal '', @response.body
167 end
197 end
168 assert_not_include User.find(8), Group.find(10).users
198 assert_not_include User.find(8), Group.find(10).users
169 end
199 end
170 end
200 end
@@ -1,220 +1,241
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssuesTest < ActionController::IntegrationTest
20 class IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :trackers,
26 :trackers,
27 :projects_trackers,
27 :projects_trackers,
28 :enabled_modules,
28 :enabled_modules,
29 :issue_statuses,
29 :issue_statuses,
30 :issues,
30 :issues,
31 :enumerations,
31 :enumerations,
32 :custom_fields,
32 :custom_fields,
33 :custom_values,
33 :custom_values,
34 :custom_fields_trackers
34 :custom_fields_trackers
35
35
36 # create an issue
36 # create an issue
37 def test_add_issue
37 def test_add_issue
38 log_user('jsmith', 'jsmith')
38 log_user('jsmith', 'jsmith')
39 get 'projects/1/issues/new', :tracker_id => '1'
39 get 'projects/1/issues/new', :tracker_id => '1'
40 assert_response :success
40 assert_response :success
41 assert_template 'issues/new'
41 assert_template 'issues/new'
42
42
43 post 'projects/1/issues', :tracker_id => "1",
43 post 'projects/1/issues', :tracker_id => "1",
44 :issue => { :start_date => "2006-12-26",
44 :issue => { :start_date => "2006-12-26",
45 :priority_id => "4",
45 :priority_id => "4",
46 :subject => "new test issue",
46 :subject => "new test issue",
47 :category_id => "",
47 :category_id => "",
48 :description => "new issue",
48 :description => "new issue",
49 :done_ratio => "0",
49 :done_ratio => "0",
50 :due_date => "",
50 :due_date => "",
51 :assigned_to_id => "" },
51 :assigned_to_id => "" },
52 :custom_fields => {'2' => 'Value for field 2'}
52 :custom_fields => {'2' => 'Value for field 2'}
53 # find created issue
53 # find created issue
54 issue = Issue.find_by_subject("new test issue")
54 issue = Issue.find_by_subject("new test issue")
55 assert_kind_of Issue, issue
55 assert_kind_of Issue, issue
56
56
57 # check redirection
57 # check redirection
58 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
58 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
59 follow_redirect!
59 follow_redirect!
60 assert_equal issue, assigns(:issue)
60 assert_equal issue, assigns(:issue)
61
61
62 # check issue attributes
62 # check issue attributes
63 assert_equal 'jsmith', issue.author.login
63 assert_equal 'jsmith', issue.author.login
64 assert_equal 1, issue.project.id
64 assert_equal 1, issue.project.id
65 assert_equal 1, issue.status.id
65 assert_equal 1, issue.status.id
66 end
66 end
67
67
68 def test_create_issue_by_anonymous_without_permission_should_fail
69 Role.anonymous.remove_permission! :add_issues
70
71 assert_no_difference 'Issue.count' do
72 post 'projects/1/issues', :tracker_id => "1", :issue => {:subject => "new test issue"}
73 end
74 assert_response 302
75 end
76
77 def test_create_issue_by_anonymous_with_custom_permission_should_succeed
78 Role.anonymous.remove_permission! :add_issues
79 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [3])
80
81 assert_difference 'Issue.count' do
82 post 'projects/1/issues', :tracker_id => "1", :issue => {:subject => "new test issue"}
83 end
84 assert_response 302
85 issue = Issue.order("id DESC").first
86 assert_equal User.anonymous, issue.author
87 end
88
68 # add then remove 2 attachments to an issue
89 # add then remove 2 attachments to an issue
69 def test_issue_attachments
90 def test_issue_attachments
70 log_user('jsmith', 'jsmith')
91 log_user('jsmith', 'jsmith')
71 set_tmp_attachments_directory
92 set_tmp_attachments_directory
72
93
73 put 'issues/1',
94 put 'issues/1',
74 :notes => 'Some notes',
95 :notes => 'Some notes',
75 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
96 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
76 assert_redirected_to "/issues/1"
97 assert_redirected_to "/issues/1"
77
98
78 # make sure attachment was saved
99 # make sure attachment was saved
79 attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
100 attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
80 assert_kind_of Attachment, attachment
101 assert_kind_of Attachment, attachment
81 assert_equal Issue.find(1), attachment.container
102 assert_equal Issue.find(1), attachment.container
82 assert_equal 'This is an attachment', attachment.description
103 assert_equal 'This is an attachment', attachment.description
83 # verify the size of the attachment stored in db
104 # verify the size of the attachment stored in db
84 #assert_equal file_data_1.length, attachment.filesize
105 #assert_equal file_data_1.length, attachment.filesize
85 # verify that the attachment was written to disk
106 # verify that the attachment was written to disk
86 assert File.exist?(attachment.diskfile)
107 assert File.exist?(attachment.diskfile)
87
108
88 # remove the attachments
109 # remove the attachments
89 Issue.find(1).attachments.each(&:destroy)
110 Issue.find(1).attachments.each(&:destroy)
90 assert_equal 0, Issue.find(1).attachments.length
111 assert_equal 0, Issue.find(1).attachments.length
91 end
112 end
92
113
93 def test_other_formats_links_on_index
114 def test_other_formats_links_on_index
94 get '/projects/ecookbook/issues'
115 get '/projects/ecookbook/issues'
95
116
96 %w(Atom PDF CSV).each do |format|
117 %w(Atom PDF CSV).each do |format|
97 assert_tag :a, :content => format,
118 assert_tag :a, :content => format,
98 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
119 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
99 :rel => 'nofollow' }
120 :rel => 'nofollow' }
100 end
121 end
101 end
122 end
102
123
103 def test_other_formats_links_on_index_without_project_id_in_url
124 def test_other_formats_links_on_index_without_project_id_in_url
104 get '/issues', :project_id => 'ecookbook'
125 get '/issues', :project_id => 'ecookbook'
105
126
106 %w(Atom PDF CSV).each do |format|
127 %w(Atom PDF CSV).each do |format|
107 assert_tag :a, :content => format,
128 assert_tag :a, :content => format,
108 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
129 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
109 :rel => 'nofollow' }
130 :rel => 'nofollow' }
110 end
131 end
111 end
132 end
112
133
113 def test_pagination_links_on_index
134 def test_pagination_links_on_index
114 Setting.per_page_options = '2'
135 Setting.per_page_options = '2'
115 get '/projects/ecookbook/issues'
136 get '/projects/ecookbook/issues'
116
137
117 assert_tag :a, :content => '2',
138 assert_tag :a, :content => '2',
118 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
139 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
119
140
120 end
141 end
121
142
122 def test_pagination_links_on_index_without_project_id_in_url
143 def test_pagination_links_on_index_without_project_id_in_url
123 Setting.per_page_options = '2'
144 Setting.per_page_options = '2'
124 get '/issues', :project_id => 'ecookbook'
145 get '/issues', :project_id => 'ecookbook'
125
146
126 assert_tag :a, :content => '2',
147 assert_tag :a, :content => '2',
127 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
148 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
128
149
129 end
150 end
130
151
131 def test_issue_with_user_custom_field
152 def test_issue_with_user_custom_field
132 @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
153 @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
133 Role.anonymous.add_permission! :add_issues, :edit_issues
154 Role.anonymous.add_permission! :add_issues, :edit_issues
134 users = Project.find(1).users
155 users = Project.find(1).users
135 tester = users.first
156 tester = users.first
136
157
137 # Issue form
158 # Issue form
138 get '/projects/ecookbook/issues/new'
159 get '/projects/ecookbook/issues/new'
139 assert_response :success
160 assert_response :success
140 assert_tag :select,
161 assert_tag :select,
141 :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
162 :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
142 :children => {:count => (users.size + 1)}, # +1 for blank value
163 :children => {:count => (users.size + 1)}, # +1 for blank value
143 :child => {
164 :child => {
144 :tag => 'option',
165 :tag => 'option',
145 :attributes => {:value => tester.id.to_s},
166 :attributes => {:value => tester.id.to_s},
146 :content => tester.name
167 :content => tester.name
147 }
168 }
148
169
149 # Create issue
170 # Create issue
150 assert_difference 'Issue.count' do
171 assert_difference 'Issue.count' do
151 post '/projects/ecookbook/issues',
172 post '/projects/ecookbook/issues',
152 :issue => {
173 :issue => {
153 :tracker_id => '1',
174 :tracker_id => '1',
154 :priority_id => '4',
175 :priority_id => '4',
155 :subject => 'Issue with user custom field',
176 :subject => 'Issue with user custom field',
156 :custom_field_values => {@field.id.to_s => users.first.id.to_s}
177 :custom_field_values => {@field.id.to_s => users.first.id.to_s}
157 }
178 }
158 end
179 end
159 issue = Issue.order('id DESC').first
180 issue = Issue.order('id DESC').first
160 assert_response 302
181 assert_response 302
161
182
162 # Issue view
183 # Issue view
163 follow_redirect!
184 follow_redirect!
164 assert_tag :th,
185 assert_tag :th,
165 :content => /Tester/,
186 :content => /Tester/,
166 :sibling => {
187 :sibling => {
167 :tag => 'td',
188 :tag => 'td',
168 :content => tester.name
189 :content => tester.name
169 }
190 }
170 assert_tag :select,
191 assert_tag :select,
171 :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
192 :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
172 :children => {:count => (users.size + 1)}, # +1 for blank value
193 :children => {:count => (users.size + 1)}, # +1 for blank value
173 :child => {
194 :child => {
174 :tag => 'option',
195 :tag => 'option',
175 :attributes => {:value => tester.id.to_s, :selected => 'selected'},
196 :attributes => {:value => tester.id.to_s, :selected => 'selected'},
176 :content => tester.name
197 :content => tester.name
177 }
198 }
178
199
179 # Update issue
200 # Update issue
180 new_tester = users[1]
201 new_tester = users[1]
181 assert_difference 'Journal.count' do
202 assert_difference 'Journal.count' do
182 put "/issues/#{issue.id}",
203 put "/issues/#{issue.id}",
183 :notes => 'Updating custom field',
204 :notes => 'Updating custom field',
184 :issue => {
205 :issue => {
185 :custom_field_values => {@field.id.to_s => new_tester.id.to_s}
206 :custom_field_values => {@field.id.to_s => new_tester.id.to_s}
186 }
207 }
187 end
208 end
188 assert_response 302
209 assert_response 302
189
210
190 # Issue view
211 # Issue view
191 follow_redirect!
212 follow_redirect!
192 assert_tag :content => 'Tester',
213 assert_tag :content => 'Tester',
193 :ancestor => {:tag => 'ul', :attributes => {:class => /details/}},
214 :ancestor => {:tag => 'ul', :attributes => {:class => /details/}},
194 :sibling => {
215 :sibling => {
195 :content => tester.name,
216 :content => tester.name,
196 :sibling => {
217 :sibling => {
197 :content => new_tester.name
218 :content => new_tester.name
198 }
219 }
199 }
220 }
200 end
221 end
201
222
202 def test_update_using_invalid_http_verbs
223 def test_update_using_invalid_http_verbs
203 subject = 'Updated by an invalid http verb'
224 subject = 'Updated by an invalid http verb'
204
225
205 get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith')
226 get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith')
206 assert_response 404
227 assert_response 404
207 assert_not_equal subject, Issue.find(1).subject
228 assert_not_equal subject, Issue.find(1).subject
208
229
209 post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith')
230 post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith')
210 assert_response 404
231 assert_response 404
211 assert_not_equal subject, Issue.find(1).subject
232 assert_not_equal subject, Issue.find(1).subject
212 end
233 end
213
234
214 def test_get_watch_should_be_invalid
235 def test_get_watch_should_be_invalid
215 assert_no_difference 'Watcher.count' do
236 assert_no_difference 'Watcher.count' do
216 get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith')
237 get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith')
217 assert_response 404
238 assert_response 404
218 end
239 end
219 end
240 end
220 end
241 end
@@ -1,136 +1,152
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class GroupTest < ActiveSupport::TestCase
20 class GroupTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users,
22 :enumerations, :users,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :groups_users
27 :groups_users
28
28
29 include Redmine::I18n
29 include Redmine::I18n
30
30
31 def test_create
31 def test_create
32 g = Group.new(:name => 'New group')
32 g = Group.new(:name => 'New group')
33 assert g.save
33 assert g.save
34 g.reload
34 g.reload
35 assert_equal 'New group', g.name
35 assert_equal 'New group', g.name
36 end
36 end
37
37
38 def test_name_should_accept_255_characters
38 def test_name_should_accept_255_characters
39 name = 'a' * 255
39 name = 'a' * 255
40 g = Group.new(:name => name)
40 g = Group.new(:name => name)
41 assert g.save
41 assert g.save
42 g.reload
42 g.reload
43 assert_equal name, g.name
43 assert_equal name, g.name
44 end
44 end
45
45
46 def test_blank_name_error_message
46 def test_blank_name_error_message
47 set_language_if_valid 'en'
47 set_language_if_valid 'en'
48 g = Group.new
48 g = Group.new
49 assert !g.save
49 assert !g.save
50 assert_include "Name can't be blank", g.errors.full_messages
50 assert_include "Name can't be blank", g.errors.full_messages
51 end
51 end
52
52
53 def test_blank_name_error_message_fr
53 def test_blank_name_error_message_fr
54 set_language_if_valid 'fr'
54 set_language_if_valid 'fr'
55 str = "Nom doit \xc3\xaatre renseign\xc3\xa9(e)"
55 str = "Nom doit \xc3\xaatre renseign\xc3\xa9(e)"
56 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
56 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
57 g = Group.new
57 g = Group.new
58 assert !g.save
58 assert !g.save
59 assert_include str, g.errors.full_messages
59 assert_include str, g.errors.full_messages
60 end
60 end
61
61
62 def test_group_roles_should_be_given_to_added_user
62 def test_group_roles_should_be_given_to_added_user
63 group = Group.find(11)
63 group = Group.find(11)
64 user = User.find(9)
64 user = User.find(9)
65 project = Project.first
65 project = Project.first
66
66
67 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
67 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
68 group.users << user
68 group.users << user
69 assert user.member_of?(project)
69 assert user.member_of?(project)
70 end
70 end
71
71
72 def test_new_roles_should_be_given_to_existing_user
72 def test_new_roles_should_be_given_to_existing_user
73 group = Group.find(11)
73 group = Group.find(11)
74 user = User.find(9)
74 user = User.find(9)
75 project = Project.first
75 project = Project.first
76
76
77 group.users << user
77 group.users << user
78 m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
78 m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
79 assert user.member_of?(project)
79 assert user.member_of?(project)
80 end
80 end
81
81
82 def test_user_roles_should_updated_when_updating_user_ids
82 def test_user_roles_should_updated_when_updating_user_ids
83 group = Group.find(11)
83 group = Group.find(11)
84 user = User.find(9)
84 user = User.find(9)
85 project = Project.first
85 project = Project.first
86
86
87 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
87 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
88 group.user_ids = [user.id]
88 group.user_ids = [user.id]
89 group.save!
89 group.save!
90 assert User.find(9).member_of?(project)
90 assert User.find(9).member_of?(project)
91
91
92 group.user_ids = [1]
92 group.user_ids = [1]
93 group.save!
93 group.save!
94 assert !User.find(9).member_of?(project)
94 assert !User.find(9).member_of?(project)
95 end
95 end
96
96
97 def test_user_roles_should_updated_when_updating_group_roles
97 def test_user_roles_should_updated_when_updating_group_roles
98 group = Group.find(11)
98 group = Group.find(11)
99 user = User.find(9)
99 user = User.find(9)
100 project = Project.first
100 project = Project.first
101 group.users << user
101 group.users << user
102 m = Member.create!(:principal => group, :project => project, :role_ids => [1])
102 m = Member.create!(:principal => group, :project => project, :role_ids => [1])
103 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
103 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
104
104
105 m.role_ids = [1, 2]
105 m.role_ids = [1, 2]
106 assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort
106 assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort
107
107
108 m.role_ids = [2]
108 m.role_ids = [2]
109 assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort
109 assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort
110
110
111 m.role_ids = [1]
111 m.role_ids = [1]
112 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
112 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
113 end
113 end
114
114
115 def test_user_memberships_should_be_removed_when_removing_group_membership
115 def test_user_memberships_should_be_removed_when_removing_group_membership
116 assert User.find(8).member_of?(Project.find(5))
116 assert User.find(8).member_of?(Project.find(5))
117 Member.find_by_project_id_and_user_id(5, 10).destroy
117 Member.find_by_project_id_and_user_id(5, 10).destroy
118 assert !User.find(8).member_of?(Project.find(5))
118 assert !User.find(8).member_of?(Project.find(5))
119 end
119 end
120
120
121 def test_user_roles_should_be_removed_when_removing_user_from_group
121 def test_user_roles_should_be_removed_when_removing_user_from_group
122 assert User.find(8).member_of?(Project.find(5))
122 assert User.find(8).member_of?(Project.find(5))
123 User.find(8).groups = []
123 User.find(8).groups = []
124 assert !User.find(8).member_of?(Project.find(5))
124 assert !User.find(8).member_of?(Project.find(5))
125 end
125 end
126
126
127 def test_destroy_should_unassign_issues
127 def test_destroy_should_unassign_issues
128 group = Group.first
128 group = Group.first
129 Issue.where(:id => 1).update_all(["assigned_to_id = ?", group.id])
129 Issue.where(:id => 1).update_all(["assigned_to_id = ?", group.id])
130
130
131 assert group.destroy
131 assert group.destroy
132 assert group.destroyed?
132 assert group.destroyed?
133
133
134 assert_equal nil, Issue.find(1).assigned_to_id
134 assert_equal nil, Issue.find(1).assigned_to_id
135 end
135 end
136
137 def test_builtin_id_with_anonymous_user_should_return_anonymous_group
138 assert_equal 13, Group.builtin_id(User.anonymous)
139 end
140
141 def test_builtin_id_with_anonymous_role_should_return_anonymous_group
142 assert_equal 13, Group.builtin_id(Role.anonymous)
143 end
144
145 def test_builtin_id_with_user_should_return_non_member_group
146 assert_equal 12, Group.builtin_id(User.find(1))
147 end
148
149 def test_builtin_id_with_non_member_role_should_return_non_member_group
150 assert_equal 12, Group.builtin_id(Role.non_member)
151 end
136 end
152 end
@@ -1,2368 +1,2399
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues, :journals, :journal_details,
28 :issues, :journals, :journal_details,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def teardown
34 def teardown
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_initialize
38 def test_initialize
39 issue = Issue.new
39 issue = Issue.new
40
40
41 assert_nil issue.project_id
41 assert_nil issue.project_id
42 assert_nil issue.tracker_id
42 assert_nil issue.tracker_id
43 assert_nil issue.author_id
43 assert_nil issue.author_id
44 assert_nil issue.assigned_to_id
44 assert_nil issue.assigned_to_id
45 assert_nil issue.category_id
45 assert_nil issue.category_id
46
46
47 assert_equal IssueStatus.default, issue.status
47 assert_equal IssueStatus.default, issue.status
48 assert_equal IssuePriority.default, issue.priority
48 assert_equal IssuePriority.default, issue.priority
49 end
49 end
50
50
51 def test_create
51 def test_create
52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
53 :status_id => 1, :priority => IssuePriority.all.first,
53 :status_id => 1, :priority => IssuePriority.all.first,
54 :subject => 'test_create',
54 :subject => 'test_create',
55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
56 assert issue.save
56 assert issue.save
57 issue.reload
57 issue.reload
58 assert_equal 1.5, issue.estimated_hours
58 assert_equal 1.5, issue.estimated_hours
59 end
59 end
60
60
61 def test_create_minimal
61 def test_create_minimal
62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
63 :status_id => 1, :priority => IssuePriority.all.first,
63 :status_id => 1, :priority => IssuePriority.all.first,
64 :subject => 'test_create')
64 :subject => 'test_create')
65 assert issue.save
65 assert issue.save
66 assert issue.description.nil?
66 assert issue.description.nil?
67 assert_nil issue.estimated_hours
67 assert_nil issue.estimated_hours
68 end
68 end
69
69
70 def test_start_date_format_should_be_validated
70 def test_start_date_format_should_be_validated
71 set_language_if_valid 'en'
71 set_language_if_valid 'en'
72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
73 issue = Issue.new(:start_date => invalid_date)
73 issue = Issue.new(:start_date => invalid_date)
74 assert !issue.valid?
74 assert !issue.valid?
75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
76 end
76 end
77 end
77 end
78
78
79 def test_due_date_format_should_be_validated
79 def test_due_date_format_should_be_validated
80 set_language_if_valid 'en'
80 set_language_if_valid 'en'
81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
82 issue = Issue.new(:due_date => invalid_date)
82 issue = Issue.new(:due_date => invalid_date)
83 assert !issue.valid?
83 assert !issue.valid?
84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
85 end
85 end
86 end
86 end
87
87
88 def test_due_date_lesser_than_start_date_should_not_validate
88 def test_due_date_lesser_than_start_date_should_not_validate
89 set_language_if_valid 'en'
89 set_language_if_valid 'en'
90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
91 assert !issue.valid?
91 assert !issue.valid?
92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
93 end
93 end
94
94
95 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
95 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
96 issue = Issue.generate(:start_date => '2013-06-04')
96 issue = Issue.generate(:start_date => '2013-06-04')
97 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
97 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
98 assert !issue.valid?
98 assert !issue.valid?
99 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
99 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
100 end
100 end
101
101
102 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
102 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
103 issue = Issue.generate!(:start_date => '2013-06-04')
103 issue = Issue.generate!(:start_date => '2013-06-04')
104 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
104 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
105 issue.start_date = '2013-06-07'
105 issue.start_date = '2013-06-07'
106 assert !issue.valid?
106 assert !issue.valid?
107 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
107 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
108 end
108 end
109
109
110 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
110 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
111 issue = Issue.generate!(:start_date => '2013-06-04')
111 issue = Issue.generate!(:start_date => '2013-06-04')
112 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
112 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
113 assert issue.valid?
113 assert issue.valid?
114 end
114 end
115
115
116 def test_estimated_hours_should_be_validated
116 def test_estimated_hours_should_be_validated
117 set_language_if_valid 'en'
117 set_language_if_valid 'en'
118 ['-2'].each do |invalid|
118 ['-2'].each do |invalid|
119 issue = Issue.new(:estimated_hours => invalid)
119 issue = Issue.new(:estimated_hours => invalid)
120 assert !issue.valid?
120 assert !issue.valid?
121 assert_include 'Estimated time is invalid', issue.errors.full_messages
121 assert_include 'Estimated time is invalid', issue.errors.full_messages
122 end
122 end
123 end
123 end
124
124
125 def test_create_with_required_custom_field
125 def test_create_with_required_custom_field
126 set_language_if_valid 'en'
126 set_language_if_valid 'en'
127 field = IssueCustomField.find_by_name('Database')
127 field = IssueCustomField.find_by_name('Database')
128 field.update_attribute(:is_required, true)
128 field.update_attribute(:is_required, true)
129
129
130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
131 :status_id => 1, :subject => 'test_create',
131 :status_id => 1, :subject => 'test_create',
132 :description => 'IssueTest#test_create_with_required_custom_field')
132 :description => 'IssueTest#test_create_with_required_custom_field')
133 assert issue.available_custom_fields.include?(field)
133 assert issue.available_custom_fields.include?(field)
134 # No value for the custom field
134 # No value for the custom field
135 assert !issue.save
135 assert !issue.save
136 assert_equal ["Database can't be blank"], issue.errors.full_messages
136 assert_equal ["Database can't be blank"], issue.errors.full_messages
137 # Blank value
137 # Blank value
138 issue.custom_field_values = { field.id => '' }
138 issue.custom_field_values = { field.id => '' }
139 assert !issue.save
139 assert !issue.save
140 assert_equal ["Database can't be blank"], issue.errors.full_messages
140 assert_equal ["Database can't be blank"], issue.errors.full_messages
141 # Invalid value
141 # Invalid value
142 issue.custom_field_values = { field.id => 'SQLServer' }
142 issue.custom_field_values = { field.id => 'SQLServer' }
143 assert !issue.save
143 assert !issue.save
144 assert_equal ["Database is not included in the list"], issue.errors.full_messages
144 assert_equal ["Database is not included in the list"], issue.errors.full_messages
145 # Valid value
145 # Valid value
146 issue.custom_field_values = { field.id => 'PostgreSQL' }
146 issue.custom_field_values = { field.id => 'PostgreSQL' }
147 assert issue.save
147 assert issue.save
148 issue.reload
148 issue.reload
149 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
149 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
150 end
150 end
151
151
152 def test_create_with_group_assignment
152 def test_create_with_group_assignment
153 with_settings :issue_group_assignment => '1' do
153 with_settings :issue_group_assignment => '1' do
154 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
154 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
155 :subject => 'Group assignment',
155 :subject => 'Group assignment',
156 :assigned_to_id => 11).save
156 :assigned_to_id => 11).save
157 issue = Issue.order('id DESC').first
157 issue = Issue.order('id DESC').first
158 assert_kind_of Group, issue.assigned_to
158 assert_kind_of Group, issue.assigned_to
159 assert_equal Group.find(11), issue.assigned_to
159 assert_equal Group.find(11), issue.assigned_to
160 end
160 end
161 end
161 end
162
162
163 def test_create_with_parent_issue_id
163 def test_create_with_parent_issue_id
164 issue = Issue.new(:project_id => 1, :tracker_id => 1,
164 issue = Issue.new(:project_id => 1, :tracker_id => 1,
165 :author_id => 1, :subject => 'Group assignment',
165 :author_id => 1, :subject => 'Group assignment',
166 :parent_issue_id => 1)
166 :parent_issue_id => 1)
167 assert_save issue
167 assert_save issue
168 assert_equal 1, issue.parent_issue_id
168 assert_equal 1, issue.parent_issue_id
169 assert_equal Issue.find(1), issue.parent
169 assert_equal Issue.find(1), issue.parent
170 end
170 end
171
171
172 def test_create_with_sharp_parent_issue_id
172 def test_create_with_sharp_parent_issue_id
173 issue = Issue.new(:project_id => 1, :tracker_id => 1,
173 issue = Issue.new(:project_id => 1, :tracker_id => 1,
174 :author_id => 1, :subject => 'Group assignment',
174 :author_id => 1, :subject => 'Group assignment',
175 :parent_issue_id => "#1")
175 :parent_issue_id => "#1")
176 assert_save issue
176 assert_save issue
177 assert_equal 1, issue.parent_issue_id
177 assert_equal 1, issue.parent_issue_id
178 assert_equal Issue.find(1), issue.parent
178 assert_equal Issue.find(1), issue.parent
179 end
179 end
180
180
181 def test_create_with_invalid_parent_issue_id
181 def test_create_with_invalid_parent_issue_id
182 set_language_if_valid 'en'
182 set_language_if_valid 'en'
183 issue = Issue.new(:project_id => 1, :tracker_id => 1,
183 issue = Issue.new(:project_id => 1, :tracker_id => 1,
184 :author_id => 1, :subject => 'Group assignment',
184 :author_id => 1, :subject => 'Group assignment',
185 :parent_issue_id => '01ABC')
185 :parent_issue_id => '01ABC')
186 assert !issue.save
186 assert !issue.save
187 assert_equal '01ABC', issue.parent_issue_id
187 assert_equal '01ABC', issue.parent_issue_id
188 assert_include 'Parent task is invalid', issue.errors.full_messages
188 assert_include 'Parent task is invalid', issue.errors.full_messages
189 end
189 end
190
190
191 def test_create_with_invalid_sharp_parent_issue_id
191 def test_create_with_invalid_sharp_parent_issue_id
192 set_language_if_valid 'en'
192 set_language_if_valid 'en'
193 issue = Issue.new(:project_id => 1, :tracker_id => 1,
193 issue = Issue.new(:project_id => 1, :tracker_id => 1,
194 :author_id => 1, :subject => 'Group assignment',
194 :author_id => 1, :subject => 'Group assignment',
195 :parent_issue_id => '#01ABC')
195 :parent_issue_id => '#01ABC')
196 assert !issue.save
196 assert !issue.save
197 assert_equal '#01ABC', issue.parent_issue_id
197 assert_equal '#01ABC', issue.parent_issue_id
198 assert_include 'Parent task is invalid', issue.errors.full_messages
198 assert_include 'Parent task is invalid', issue.errors.full_messages
199 end
199 end
200
200
201 def assert_visibility_match(user, issues)
201 def assert_visibility_match(user, issues)
202 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
202 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
203 end
203 end
204
204
205 def test_visible_scope_for_anonymous
205 def test_visible_scope_for_anonymous
206 # Anonymous user should see issues of public projects only
206 # Anonymous user should see issues of public projects only
207 issues = Issue.visible(User.anonymous).all
207 issues = Issue.visible(User.anonymous).all
208 assert issues.any?
208 assert issues.any?
209 assert_nil issues.detect {|issue| !issue.project.is_public?}
209 assert_nil issues.detect {|issue| !issue.project.is_public?}
210 assert_nil issues.detect {|issue| issue.is_private?}
210 assert_nil issues.detect {|issue| issue.is_private?}
211 assert_visibility_match User.anonymous, issues
211 assert_visibility_match User.anonymous, issues
212 end
212 end
213
213
214 def test_visible_scope_for_anonymous_without_view_issues_permissions
214 def test_visible_scope_for_anonymous_without_view_issues_permissions
215 # Anonymous user should not see issues without permission
215 # Anonymous user should not see issues without permission
216 Role.anonymous.remove_permission!(:view_issues)
216 Role.anonymous.remove_permission!(:view_issues)
217 issues = Issue.visible(User.anonymous).all
217 issues = Issue.visible(User.anonymous).all
218 assert issues.empty?
218 assert issues.empty?
219 assert_visibility_match User.anonymous, issues
219 assert_visibility_match User.anonymous, issues
220 end
220 end
221
221
222 def test_visible_scope_for_anonymous_without_view_issues_permissions_and_membership
223 Role.anonymous.remove_permission!(:view_issues)
224 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
225
226 issues = Issue.visible(User.anonymous).all
227 assert issues.any?
228 assert_equal [1], issues.map(&:project_id).uniq.sort
229 assert_visibility_match User.anonymous, issues
230 end
231
222 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
232 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
223 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
233 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
224 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
234 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
225 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
235 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
226 assert !issue.visible?(User.anonymous)
236 assert !issue.visible?(User.anonymous)
227 end
237 end
228
238
229 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
239 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
230 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
240 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
231 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
241 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
232 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
242 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
233 assert !issue.visible?(User.anonymous)
243 assert !issue.visible?(User.anonymous)
234 end
244 end
235
245
236 def test_visible_scope_for_non_member
246 def test_visible_scope_for_non_member
237 user = User.find(9)
247 user = User.find(9)
238 assert user.projects.empty?
248 assert user.projects.empty?
239 # Non member user should see issues of public projects only
249 # Non member user should see issues of public projects only
240 issues = Issue.visible(user).all
250 issues = Issue.visible(user).all
241 assert issues.any?
251 assert issues.any?
242 assert_nil issues.detect {|issue| !issue.project.is_public?}
252 assert_nil issues.detect {|issue| !issue.project.is_public?}
243 assert_nil issues.detect {|issue| issue.is_private?}
253 assert_nil issues.detect {|issue| issue.is_private?}
244 assert_visibility_match user, issues
254 assert_visibility_match user, issues
245 end
255 end
246
256
247 def test_visible_scope_for_non_member_with_own_issues_visibility
257 def test_visible_scope_for_non_member_with_own_issues_visibility
248 Role.non_member.update_attribute :issues_visibility, 'own'
258 Role.non_member.update_attribute :issues_visibility, 'own'
249 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
259 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
250 user = User.find(9)
260 user = User.find(9)
251
261
252 issues = Issue.visible(user).all
262 issues = Issue.visible(user).all
253 assert issues.any?
263 assert issues.any?
254 assert_nil issues.detect {|issue| issue.author != user}
264 assert_nil issues.detect {|issue| issue.author != user}
255 assert_visibility_match user, issues
265 assert_visibility_match user, issues
256 end
266 end
257
267
258 def test_visible_scope_for_non_member_without_view_issues_permissions
268 def test_visible_scope_for_non_member_without_view_issues_permissions
259 # Non member user should not see issues without permission
269 # Non member user should not see issues without permission
260 Role.non_member.remove_permission!(:view_issues)
270 Role.non_member.remove_permission!(:view_issues)
261 user = User.find(9)
271 user = User.find(9)
262 assert user.projects.empty?
272 assert user.projects.empty?
263 issues = Issue.visible(user).all
273 issues = Issue.visible(user).all
264 assert issues.empty?
274 assert issues.empty?
265 assert_visibility_match user, issues
275 assert_visibility_match user, issues
266 end
276 end
267
277
278 def test_visible_scope_for_non_member_without_view_issues_permissions_and_membership
279 Role.non_member.remove_permission!(:view_issues)
280 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
281 user = User.find(9)
282
283 issues = Issue.visible(user).all
284 assert issues.any?
285 assert_equal [1], issues.map(&:project_id).uniq.sort
286 assert_visibility_match user, issues
287 end
288
268 def test_visible_scope_for_member
289 def test_visible_scope_for_member
269 user = User.find(9)
290 user = User.find(9)
270 # User should see issues of projects for which user has view_issues permissions only
291 # User should see issues of projects for which user has view_issues permissions only
271 Role.non_member.remove_permission!(:view_issues)
292 Role.non_member.remove_permission!(:view_issues)
272 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
293 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
273 issues = Issue.visible(user).all
294 issues = Issue.visible(user).all
274 assert issues.any?
295 assert issues.any?
275 assert_nil issues.detect {|issue| issue.project_id != 3}
296 assert_nil issues.detect {|issue| issue.project_id != 3}
276 assert_nil issues.detect {|issue| issue.is_private?}
297 assert_nil issues.detect {|issue| issue.is_private?}
277 assert_visibility_match user, issues
298 assert_visibility_match user, issues
278 end
299 end
279
300
280 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
301 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
281 user = User.find(8)
302 user = User.find(8)
282 assert user.groups.any?
303 assert user.groups.any?
283 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
304 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
284 Role.non_member.remove_permission!(:view_issues)
305 Role.non_member.remove_permission!(:view_issues)
285
306
286 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
307 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
287 :status_id => 1, :priority => IssuePriority.all.first,
308 :status_id => 1, :priority => IssuePriority.all.first,
288 :subject => 'Assignment test',
309 :subject => 'Assignment test',
289 :assigned_to => user.groups.first,
310 :assigned_to => user.groups.first,
290 :is_private => true)
311 :is_private => true)
291
312
292 Role.find(2).update_attribute :issues_visibility, 'default'
313 Role.find(2).update_attribute :issues_visibility, 'default'
293 issues = Issue.visible(User.find(8)).all
314 issues = Issue.visible(User.find(8)).all
294 assert issues.any?
315 assert issues.any?
295 assert issues.include?(issue)
316 assert issues.include?(issue)
296
317
297 Role.find(2).update_attribute :issues_visibility, 'own'
318 Role.find(2).update_attribute :issues_visibility, 'own'
298 issues = Issue.visible(User.find(8)).all
319 issues = Issue.visible(User.find(8)).all
299 assert issues.any?
320 assert issues.any?
300 assert issues.include?(issue)
321 assert issues.include?(issue)
301 end
322 end
302
323
303 def test_visible_scope_for_admin
324 def test_visible_scope_for_admin
304 user = User.find(1)
325 user = User.find(1)
305 user.members.each(&:destroy)
326 user.members.each(&:destroy)
306 assert user.projects.empty?
327 assert user.projects.empty?
307 issues = Issue.visible(user).all
328 issues = Issue.visible(user).all
308 assert issues.any?
329 assert issues.any?
309 # Admin should see issues on private projects that admin does not belong to
330 # Admin should see issues on private projects that admin does not belong to
310 assert issues.detect {|issue| !issue.project.is_public?}
331 assert issues.detect {|issue| !issue.project.is_public?}
311 # Admin should see private issues of other users
332 # Admin should see private issues of other users
312 assert issues.detect {|issue| issue.is_private? && issue.author != user}
333 assert issues.detect {|issue| issue.is_private? && issue.author != user}
313 assert_visibility_match user, issues
334 assert_visibility_match user, issues
314 end
335 end
315
336
316 def test_visible_scope_with_project
337 def test_visible_scope_with_project
317 project = Project.find(1)
338 project = Project.find(1)
318 issues = Issue.visible(User.find(2), :project => project).all
339 issues = Issue.visible(User.find(2), :project => project).all
319 projects = issues.collect(&:project).uniq
340 projects = issues.collect(&:project).uniq
320 assert_equal 1, projects.size
341 assert_equal 1, projects.size
321 assert_equal project, projects.first
342 assert_equal project, projects.first
322 end
343 end
323
344
324 def test_visible_scope_with_project_and_subprojects
345 def test_visible_scope_with_project_and_subprojects
325 project = Project.find(1)
346 project = Project.find(1)
326 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
347 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
327 projects = issues.collect(&:project).uniq
348 projects = issues.collect(&:project).uniq
328 assert projects.size > 1
349 assert projects.size > 1
329 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
350 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
330 end
351 end
331
352
332 def test_visible_and_nested_set_scopes
353 def test_visible_and_nested_set_scopes
333 user = User.generate!
354 user = User.generate!
334 parent = Issue.generate!(:assigned_to => user)
355 parent = Issue.generate!(:assigned_to => user)
335 assert parent.visible?(user)
356 assert parent.visible?(user)
336 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
357 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
337 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
358 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
338 parent.reload
359 parent.reload
339 child1.reload
360 child1.reload
340 child2.reload
361 child2.reload
341 assert child1.visible?(user)
362 assert child1.visible?(user)
342 assert child2.visible?(user)
363 assert child2.visible?(user)
343 assert_equal 2, parent.descendants.count
364 assert_equal 2, parent.descendants.count
344 assert_equal 2, parent.descendants.visible(user).count
365 assert_equal 2, parent.descendants.visible(user).count
345 # awesome_nested_set 2-1-stable branch has regression.
366 # awesome_nested_set 2-1-stable branch has regression.
346 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
367 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
347 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
368 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
348 assert_equal 2, parent.descendants.collect{|i| i}.size
369 assert_equal 2, parent.descendants.collect{|i| i}.size
349 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
370 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
350 end
371 end
351
372
352 def test_open_scope
373 def test_open_scope
353 issues = Issue.open.all
374 issues = Issue.open.all
354 assert_nil issues.detect(&:closed?)
375 assert_nil issues.detect(&:closed?)
355 end
376 end
356
377
357 def test_open_scope_with_arg
378 def test_open_scope_with_arg
358 issues = Issue.open(false).all
379 issues = Issue.open(false).all
359 assert_equal issues, issues.select(&:closed?)
380 assert_equal issues, issues.select(&:closed?)
360 end
381 end
361
382
362 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
383 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
363 version = Version.find(2)
384 version = Version.find(2)
364 assert version.fixed_issues.any?
385 assert version.fixed_issues.any?
365 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
386 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
366 end
387 end
367
388
368 def test_fixed_version_scope_with_empty_array_should_return_no_result
389 def test_fixed_version_scope_with_empty_array_should_return_no_result
369 assert_equal 0, Issue.fixed_version([]).count
390 assert_equal 0, Issue.fixed_version([]).count
370 end
391 end
371
392
372 def test_errors_full_messages_should_include_custom_fields_errors
393 def test_errors_full_messages_should_include_custom_fields_errors
373 field = IssueCustomField.find_by_name('Database')
394 field = IssueCustomField.find_by_name('Database')
374
395
375 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
396 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
376 :status_id => 1, :subject => 'test_create',
397 :status_id => 1, :subject => 'test_create',
377 :description => 'IssueTest#test_create_with_required_custom_field')
398 :description => 'IssueTest#test_create_with_required_custom_field')
378 assert issue.available_custom_fields.include?(field)
399 assert issue.available_custom_fields.include?(field)
379 # Invalid value
400 # Invalid value
380 issue.custom_field_values = { field.id => 'SQLServer' }
401 issue.custom_field_values = { field.id => 'SQLServer' }
381
402
382 assert !issue.valid?
403 assert !issue.valid?
383 assert_equal 1, issue.errors.full_messages.size
404 assert_equal 1, issue.errors.full_messages.size
384 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
405 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
385 issue.errors.full_messages.first
406 issue.errors.full_messages.first
386 end
407 end
387
408
388 def test_update_issue_with_required_custom_field
409 def test_update_issue_with_required_custom_field
389 field = IssueCustomField.find_by_name('Database')
410 field = IssueCustomField.find_by_name('Database')
390 field.update_attribute(:is_required, true)
411 field.update_attribute(:is_required, true)
391
412
392 issue = Issue.find(1)
413 issue = Issue.find(1)
393 assert_nil issue.custom_value_for(field)
414 assert_nil issue.custom_value_for(field)
394 assert issue.available_custom_fields.include?(field)
415 assert issue.available_custom_fields.include?(field)
395 # No change to custom values, issue can be saved
416 # No change to custom values, issue can be saved
396 assert issue.save
417 assert issue.save
397 # Blank value
418 # Blank value
398 issue.custom_field_values = { field.id => '' }
419 issue.custom_field_values = { field.id => '' }
399 assert !issue.save
420 assert !issue.save
400 # Valid value
421 # Valid value
401 issue.custom_field_values = { field.id => 'PostgreSQL' }
422 issue.custom_field_values = { field.id => 'PostgreSQL' }
402 assert issue.save
423 assert issue.save
403 issue.reload
424 issue.reload
404 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
425 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
405 end
426 end
406
427
407 def test_should_not_update_attributes_if_custom_fields_validation_fails
428 def test_should_not_update_attributes_if_custom_fields_validation_fails
408 issue = Issue.find(1)
429 issue = Issue.find(1)
409 field = IssueCustomField.find_by_name('Database')
430 field = IssueCustomField.find_by_name('Database')
410 assert issue.available_custom_fields.include?(field)
431 assert issue.available_custom_fields.include?(field)
411
432
412 issue.custom_field_values = { field.id => 'Invalid' }
433 issue.custom_field_values = { field.id => 'Invalid' }
413 issue.subject = 'Should be not be saved'
434 issue.subject = 'Should be not be saved'
414 assert !issue.save
435 assert !issue.save
415
436
416 issue.reload
437 issue.reload
417 assert_equal "Can't print recipes", issue.subject
438 assert_equal "Can't print recipes", issue.subject
418 end
439 end
419
440
420 def test_should_not_recreate_custom_values_objects_on_update
441 def test_should_not_recreate_custom_values_objects_on_update
421 field = IssueCustomField.find_by_name('Database')
442 field = IssueCustomField.find_by_name('Database')
422
443
423 issue = Issue.find(1)
444 issue = Issue.find(1)
424 issue.custom_field_values = { field.id => 'PostgreSQL' }
445 issue.custom_field_values = { field.id => 'PostgreSQL' }
425 assert issue.save
446 assert issue.save
426 custom_value = issue.custom_value_for(field)
447 custom_value = issue.custom_value_for(field)
427 issue.reload
448 issue.reload
428 issue.custom_field_values = { field.id => 'MySQL' }
449 issue.custom_field_values = { field.id => 'MySQL' }
429 assert issue.save
450 assert issue.save
430 issue.reload
451 issue.reload
431 assert_equal custom_value.id, issue.custom_value_for(field).id
452 assert_equal custom_value.id, issue.custom_value_for(field).id
432 end
453 end
433
454
434 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
455 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
435 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
456 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
436 :status_id => 1, :subject => 'Test',
457 :status_id => 1, :subject => 'Test',
437 :custom_field_values => {'2' => 'Test'})
458 :custom_field_values => {'2' => 'Test'})
438 assert !Tracker.find(2).custom_field_ids.include?(2)
459 assert !Tracker.find(2).custom_field_ids.include?(2)
439
460
440 issue = Issue.find(issue.id)
461 issue = Issue.find(issue.id)
441 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
462 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
442
463
443 issue = Issue.find(issue.id)
464 issue = Issue.find(issue.id)
444 custom_value = issue.custom_value_for(2)
465 custom_value = issue.custom_value_for(2)
445 assert_not_nil custom_value
466 assert_not_nil custom_value
446 assert_equal 'Test', custom_value.value
467 assert_equal 'Test', custom_value.value
447 end
468 end
448
469
449 def test_assigning_tracker_id_should_reload_custom_fields_values
470 def test_assigning_tracker_id_should_reload_custom_fields_values
450 issue = Issue.new(:project => Project.find(1))
471 issue = Issue.new(:project => Project.find(1))
451 assert issue.custom_field_values.empty?
472 assert issue.custom_field_values.empty?
452 issue.tracker_id = 1
473 issue.tracker_id = 1
453 assert issue.custom_field_values.any?
474 assert issue.custom_field_values.any?
454 end
475 end
455
476
456 def test_assigning_attributes_should_assign_project_and_tracker_first
477 def test_assigning_attributes_should_assign_project_and_tracker_first
457 seq = sequence('seq')
478 seq = sequence('seq')
458 issue = Issue.new
479 issue = Issue.new
459 issue.expects(:project_id=).in_sequence(seq)
480 issue.expects(:project_id=).in_sequence(seq)
460 issue.expects(:tracker_id=).in_sequence(seq)
481 issue.expects(:tracker_id=).in_sequence(seq)
461 issue.expects(:subject=).in_sequence(seq)
482 issue.expects(:subject=).in_sequence(seq)
462 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
483 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
463 end
484 end
464
485
465 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
486 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
466 attributes = ActiveSupport::OrderedHash.new
487 attributes = ActiveSupport::OrderedHash.new
467 attributes['custom_field_values'] = { '1' => 'MySQL' }
488 attributes['custom_field_values'] = { '1' => 'MySQL' }
468 attributes['tracker_id'] = '1'
489 attributes['tracker_id'] = '1'
469 issue = Issue.new(:project => Project.find(1))
490 issue = Issue.new(:project => Project.find(1))
470 issue.attributes = attributes
491 issue.attributes = attributes
471 assert_equal 'MySQL', issue.custom_field_value(1)
492 assert_equal 'MySQL', issue.custom_field_value(1)
472 end
493 end
473
494
474 def test_reload_should_reload_custom_field_values
495 def test_reload_should_reload_custom_field_values
475 issue = Issue.generate!
496 issue = Issue.generate!
476 issue.custom_field_values = {'2' => 'Foo'}
497 issue.custom_field_values = {'2' => 'Foo'}
477 issue.save!
498 issue.save!
478
499
479 issue = Issue.order('id desc').first
500 issue = Issue.order('id desc').first
480 assert_equal 'Foo', issue.custom_field_value(2)
501 assert_equal 'Foo', issue.custom_field_value(2)
481
502
482 issue.custom_field_values = {'2' => 'Bar'}
503 issue.custom_field_values = {'2' => 'Bar'}
483 assert_equal 'Bar', issue.custom_field_value(2)
504 assert_equal 'Bar', issue.custom_field_value(2)
484
505
485 issue.reload
506 issue.reload
486 assert_equal 'Foo', issue.custom_field_value(2)
507 assert_equal 'Foo', issue.custom_field_value(2)
487 end
508 end
488
509
489 def test_should_update_issue_with_disabled_tracker
510 def test_should_update_issue_with_disabled_tracker
490 p = Project.find(1)
511 p = Project.find(1)
491 issue = Issue.find(1)
512 issue = Issue.find(1)
492
513
493 p.trackers.delete(issue.tracker)
514 p.trackers.delete(issue.tracker)
494 assert !p.trackers.include?(issue.tracker)
515 assert !p.trackers.include?(issue.tracker)
495
516
496 issue.reload
517 issue.reload
497 issue.subject = 'New subject'
518 issue.subject = 'New subject'
498 assert issue.save
519 assert issue.save
499 end
520 end
500
521
501 def test_should_not_set_a_disabled_tracker
522 def test_should_not_set_a_disabled_tracker
502 p = Project.find(1)
523 p = Project.find(1)
503 p.trackers.delete(Tracker.find(2))
524 p.trackers.delete(Tracker.find(2))
504
525
505 issue = Issue.find(1)
526 issue = Issue.find(1)
506 issue.tracker_id = 2
527 issue.tracker_id = 2
507 issue.subject = 'New subject'
528 issue.subject = 'New subject'
508 assert !issue.save
529 assert !issue.save
509 assert_not_equal [], issue.errors[:tracker_id]
530 assert_not_equal [], issue.errors[:tracker_id]
510 end
531 end
511
532
512 def test_category_based_assignment
533 def test_category_based_assignment
513 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
534 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
514 :status_id => 1, :priority => IssuePriority.all.first,
535 :status_id => 1, :priority => IssuePriority.all.first,
515 :subject => 'Assignment test',
536 :subject => 'Assignment test',
516 :description => 'Assignment test', :category_id => 1)
537 :description => 'Assignment test', :category_id => 1)
517 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
538 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
518 end
539 end
519
540
520 def test_new_statuses_allowed_to
541 def test_new_statuses_allowed_to
521 WorkflowTransition.delete_all
542 WorkflowTransition.delete_all
522 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
543 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
523 :old_status_id => 1, :new_status_id => 2,
544 :old_status_id => 1, :new_status_id => 2,
524 :author => false, :assignee => false)
545 :author => false, :assignee => false)
525 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
546 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
526 :old_status_id => 1, :new_status_id => 3,
547 :old_status_id => 1, :new_status_id => 3,
527 :author => true, :assignee => false)
548 :author => true, :assignee => false)
528 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
549 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
529 :old_status_id => 1, :new_status_id => 4,
550 :old_status_id => 1, :new_status_id => 4,
530 :author => false, :assignee => true)
551 :author => false, :assignee => true)
531 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
552 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
532 :old_status_id => 1, :new_status_id => 5,
553 :old_status_id => 1, :new_status_id => 5,
533 :author => true, :assignee => true)
554 :author => true, :assignee => true)
534 status = IssueStatus.find(1)
555 status = IssueStatus.find(1)
535 role = Role.find(1)
556 role = Role.find(1)
536 tracker = Tracker.find(1)
557 tracker = Tracker.find(1)
537 user = User.find(2)
558 user = User.find(2)
538
559
539 issue = Issue.generate!(:tracker => tracker, :status => status,
560 issue = Issue.generate!(:tracker => tracker, :status => status,
540 :project_id => 1, :author_id => 1)
561 :project_id => 1, :author_id => 1)
541 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
562 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
542
563
543 issue = Issue.generate!(:tracker => tracker, :status => status,
564 issue = Issue.generate!(:tracker => tracker, :status => status,
544 :project_id => 1, :author => user)
565 :project_id => 1, :author => user)
545 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
566 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
546
567
547 issue = Issue.generate!(:tracker => tracker, :status => status,
568 issue = Issue.generate!(:tracker => tracker, :status => status,
548 :project_id => 1, :author_id => 1,
569 :project_id => 1, :author_id => 1,
549 :assigned_to => user)
570 :assigned_to => user)
550 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
571 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
551
572
552 issue = Issue.generate!(:tracker => tracker, :status => status,
573 issue = Issue.generate!(:tracker => tracker, :status => status,
553 :project_id => 1, :author => user,
574 :project_id => 1, :author => user,
554 :assigned_to => user)
575 :assigned_to => user)
555 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
576 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
556
577
557 group = Group.generate!
578 group = Group.generate!
558 group.users << user
579 group.users << user
559 issue = Issue.generate!(:tracker => tracker, :status => status,
580 issue = Issue.generate!(:tracker => tracker, :status => status,
560 :project_id => 1, :author => user,
581 :project_id => 1, :author => user,
561 :assigned_to => group)
582 :assigned_to => group)
562 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
583 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
563 end
584 end
564
585
565 def test_new_statuses_allowed_to_should_consider_group_assignment
586 def test_new_statuses_allowed_to_should_consider_group_assignment
566 WorkflowTransition.delete_all
587 WorkflowTransition.delete_all
567 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
588 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
568 :old_status_id => 1, :new_status_id => 4,
589 :old_status_id => 1, :new_status_id => 4,
569 :author => false, :assignee => true)
590 :author => false, :assignee => true)
570 user = User.find(2)
591 user = User.find(2)
571 group = Group.generate!
592 group = Group.generate!
572 group.users << user
593 group.users << user
573
594
574 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
595 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
575 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
596 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
576 end
597 end
577
598
578 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
599 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
579 admin = User.find(1)
600 admin = User.find(1)
580 issue = Issue.find(1)
601 issue = Issue.find(1)
581 assert !admin.member_of?(issue.project)
602 assert !admin.member_of?(issue.project)
582 expected_statuses = [issue.status] +
603 expected_statuses = [issue.status] +
583 WorkflowTransition.where(:old_status_id => issue.status_id).
604 WorkflowTransition.where(:old_status_id => issue.status_id).
584 map(&:new_status).uniq.sort
605 map(&:new_status).uniq.sort
585 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
606 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
586 end
607 end
587
608
588 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
609 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
589 issue = Issue.find(1).copy
610 issue = Issue.find(1).copy
590 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
611 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
591
612
592 issue = Issue.find(2).copy
613 issue = Issue.find(2).copy
593 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
614 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
594 end
615 end
595
616
596 def test_safe_attributes_names_should_not_include_disabled_field
617 def test_safe_attributes_names_should_not_include_disabled_field
597 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
618 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
598
619
599 issue = Issue.new(:tracker => tracker)
620 issue = Issue.new(:tracker => tracker)
600 assert_include 'tracker_id', issue.safe_attribute_names
621 assert_include 'tracker_id', issue.safe_attribute_names
601 assert_include 'status_id', issue.safe_attribute_names
622 assert_include 'status_id', issue.safe_attribute_names
602 assert_include 'subject', issue.safe_attribute_names
623 assert_include 'subject', issue.safe_attribute_names
603 assert_include 'description', issue.safe_attribute_names
624 assert_include 'description', issue.safe_attribute_names
604 assert_include 'custom_field_values', issue.safe_attribute_names
625 assert_include 'custom_field_values', issue.safe_attribute_names
605 assert_include 'custom_fields', issue.safe_attribute_names
626 assert_include 'custom_fields', issue.safe_attribute_names
606 assert_include 'lock_version', issue.safe_attribute_names
627 assert_include 'lock_version', issue.safe_attribute_names
607
628
608 tracker.core_fields.each do |field|
629 tracker.core_fields.each do |field|
609 assert_include field, issue.safe_attribute_names
630 assert_include field, issue.safe_attribute_names
610 end
631 end
611
632
612 tracker.disabled_core_fields.each do |field|
633 tracker.disabled_core_fields.each do |field|
613 assert_not_include field, issue.safe_attribute_names
634 assert_not_include field, issue.safe_attribute_names
614 end
635 end
615 end
636 end
616
637
617 def test_safe_attributes_should_ignore_disabled_fields
638 def test_safe_attributes_should_ignore_disabled_fields
618 tracker = Tracker.find(1)
639 tracker = Tracker.find(1)
619 tracker.core_fields = %w(assigned_to_id due_date)
640 tracker.core_fields = %w(assigned_to_id due_date)
620 tracker.save!
641 tracker.save!
621
642
622 issue = Issue.new(:tracker => tracker)
643 issue = Issue.new(:tracker => tracker)
623 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
644 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
624 assert_nil issue.start_date
645 assert_nil issue.start_date
625 assert_equal Date.parse('2012-07-14'), issue.due_date
646 assert_equal Date.parse('2012-07-14'), issue.due_date
626 end
647 end
627
648
628 def test_safe_attributes_should_accept_target_tracker_enabled_fields
649 def test_safe_attributes_should_accept_target_tracker_enabled_fields
629 source = Tracker.find(1)
650 source = Tracker.find(1)
630 source.core_fields = []
651 source.core_fields = []
631 source.save!
652 source.save!
632 target = Tracker.find(2)
653 target = Tracker.find(2)
633 target.core_fields = %w(assigned_to_id due_date)
654 target.core_fields = %w(assigned_to_id due_date)
634 target.save!
655 target.save!
635
656
636 issue = Issue.new(:tracker => source)
657 issue = Issue.new(:tracker => source)
637 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
658 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
638 assert_equal target, issue.tracker
659 assert_equal target, issue.tracker
639 assert_equal Date.parse('2012-07-14'), issue.due_date
660 assert_equal Date.parse('2012-07-14'), issue.due_date
640 end
661 end
641
662
642 def test_safe_attributes_should_not_include_readonly_fields
663 def test_safe_attributes_should_not_include_readonly_fields
643 WorkflowPermission.delete_all
664 WorkflowPermission.delete_all
644 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
665 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
645 :role_id => 1, :field_name => 'due_date',
666 :role_id => 1, :field_name => 'due_date',
646 :rule => 'readonly')
667 :rule => 'readonly')
647 user = User.find(2)
668 user = User.find(2)
648
669
649 issue = Issue.new(:project_id => 1, :tracker_id => 1)
670 issue = Issue.new(:project_id => 1, :tracker_id => 1)
650 assert_equal %w(due_date), issue.read_only_attribute_names(user)
671 assert_equal %w(due_date), issue.read_only_attribute_names(user)
651 assert_not_include 'due_date', issue.safe_attribute_names(user)
672 assert_not_include 'due_date', issue.safe_attribute_names(user)
652
673
653 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
674 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
654 assert_equal Date.parse('2012-07-14'), issue.start_date
675 assert_equal Date.parse('2012-07-14'), issue.start_date
655 assert_nil issue.due_date
676 assert_nil issue.due_date
656 end
677 end
657
678
658 def test_safe_attributes_should_not_include_readonly_custom_fields
679 def test_safe_attributes_should_not_include_readonly_custom_fields
659 cf1 = IssueCustomField.create!(:name => 'Writable field',
680 cf1 = IssueCustomField.create!(:name => 'Writable field',
660 :field_format => 'string',
681 :field_format => 'string',
661 :is_for_all => true, :tracker_ids => [1])
682 :is_for_all => true, :tracker_ids => [1])
662 cf2 = IssueCustomField.create!(:name => 'Readonly field',
683 cf2 = IssueCustomField.create!(:name => 'Readonly field',
663 :field_format => 'string',
684 :field_format => 'string',
664 :is_for_all => true, :tracker_ids => [1])
685 :is_for_all => true, :tracker_ids => [1])
665 WorkflowPermission.delete_all
686 WorkflowPermission.delete_all
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
687 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
667 :role_id => 1, :field_name => cf2.id.to_s,
688 :role_id => 1, :field_name => cf2.id.to_s,
668 :rule => 'readonly')
689 :rule => 'readonly')
669 user = User.find(2)
690 user = User.find(2)
670 issue = Issue.new(:project_id => 1, :tracker_id => 1)
691 issue = Issue.new(:project_id => 1, :tracker_id => 1)
671 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
692 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
672 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
693 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
673
694
674 issue.send :safe_attributes=, {'custom_field_values' => {
695 issue.send :safe_attributes=, {'custom_field_values' => {
675 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
696 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
676 }}, user
697 }}, user
677 assert_equal 'value1', issue.custom_field_value(cf1)
698 assert_equal 'value1', issue.custom_field_value(cf1)
678 assert_nil issue.custom_field_value(cf2)
699 assert_nil issue.custom_field_value(cf2)
679
700
680 issue.send :safe_attributes=, {'custom_fields' => [
701 issue.send :safe_attributes=, {'custom_fields' => [
681 {'id' => cf1.id.to_s, 'value' => 'valuea'},
702 {'id' => cf1.id.to_s, 'value' => 'valuea'},
682 {'id' => cf2.id.to_s, 'value' => 'valueb'}
703 {'id' => cf2.id.to_s, 'value' => 'valueb'}
683 ]}, user
704 ]}, user
684 assert_equal 'valuea', issue.custom_field_value(cf1)
705 assert_equal 'valuea', issue.custom_field_value(cf1)
685 assert_nil issue.custom_field_value(cf2)
706 assert_nil issue.custom_field_value(cf2)
686 end
707 end
687
708
688 def test_editable_custom_field_values_should_return_non_readonly_custom_values
709 def test_editable_custom_field_values_should_return_non_readonly_custom_values
689 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
710 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
690 :is_for_all => true, :tracker_ids => [1, 2])
711 :is_for_all => true, :tracker_ids => [1, 2])
691 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
712 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
692 :is_for_all => true, :tracker_ids => [1, 2])
713 :is_for_all => true, :tracker_ids => [1, 2])
693 WorkflowPermission.delete_all
714 WorkflowPermission.delete_all
694 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
715 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
695 :field_name => cf2.id.to_s, :rule => 'readonly')
716 :field_name => cf2.id.to_s, :rule => 'readonly')
696 user = User.find(2)
717 user = User.find(2)
697
718
698 issue = Issue.new(:project_id => 1, :tracker_id => 1)
719 issue = Issue.new(:project_id => 1, :tracker_id => 1)
699 values = issue.editable_custom_field_values(user)
720 values = issue.editable_custom_field_values(user)
700 assert values.detect {|value| value.custom_field == cf1}
721 assert values.detect {|value| value.custom_field == cf1}
701 assert_nil values.detect {|value| value.custom_field == cf2}
722 assert_nil values.detect {|value| value.custom_field == cf2}
702
723
703 issue.tracker_id = 2
724 issue.tracker_id = 2
704 values = issue.editable_custom_field_values(user)
725 values = issue.editable_custom_field_values(user)
705 assert values.detect {|value| value.custom_field == cf1}
726 assert values.detect {|value| value.custom_field == cf1}
706 assert values.detect {|value| value.custom_field == cf2}
727 assert values.detect {|value| value.custom_field == cf2}
707 end
728 end
708
729
709 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
730 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
710 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
731 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
711 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
732 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
712 user = User.find(2)
733 user = User.find(2)
713 issue = Issue.new(:project_id => 1, :tracker_id => 1)
734 issue = Issue.new(:project_id => 1, :tracker_id => 1)
714
735
715 assert_include enabled_cf, issue.editable_custom_fields(user)
736 assert_include enabled_cf, issue.editable_custom_fields(user)
716 assert_not_include disabled_cf, issue.editable_custom_fields(user)
737 assert_not_include disabled_cf, issue.editable_custom_fields(user)
717 end
738 end
718
739
719 def test_safe_attributes_should_accept_target_tracker_writable_fields
740 def test_safe_attributes_should_accept_target_tracker_writable_fields
720 WorkflowPermission.delete_all
741 WorkflowPermission.delete_all
721 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
742 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
722 :role_id => 1, :field_name => 'due_date',
743 :role_id => 1, :field_name => 'due_date',
723 :rule => 'readonly')
744 :rule => 'readonly')
724 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
745 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
725 :role_id => 1, :field_name => 'start_date',
746 :role_id => 1, :field_name => 'start_date',
726 :rule => 'readonly')
747 :rule => 'readonly')
727 user = User.find(2)
748 user = User.find(2)
728
749
729 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
750 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
730
751
731 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
752 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
732 'due_date' => '2012-07-14'}, user
753 'due_date' => '2012-07-14'}, user
733 assert_equal Date.parse('2012-07-12'), issue.start_date
754 assert_equal Date.parse('2012-07-12'), issue.start_date
734 assert_nil issue.due_date
755 assert_nil issue.due_date
735
756
736 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
757 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
737 'due_date' => '2012-07-16',
758 'due_date' => '2012-07-16',
738 'tracker_id' => 2}, user
759 'tracker_id' => 2}, user
739 assert_equal Date.parse('2012-07-12'), issue.start_date
760 assert_equal Date.parse('2012-07-12'), issue.start_date
740 assert_equal Date.parse('2012-07-16'), issue.due_date
761 assert_equal Date.parse('2012-07-16'), issue.due_date
741 end
762 end
742
763
743 def test_safe_attributes_should_accept_target_status_writable_fields
764 def test_safe_attributes_should_accept_target_status_writable_fields
744 WorkflowPermission.delete_all
765 WorkflowPermission.delete_all
745 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
766 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
746 :role_id => 1, :field_name => 'due_date',
767 :role_id => 1, :field_name => 'due_date',
747 :rule => 'readonly')
768 :rule => 'readonly')
748 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
769 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
749 :role_id => 1, :field_name => 'start_date',
770 :role_id => 1, :field_name => 'start_date',
750 :rule => 'readonly')
771 :rule => 'readonly')
751 user = User.find(2)
772 user = User.find(2)
752
773
753 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
774 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
754
775
755 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
776 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
756 'due_date' => '2012-07-14'},
777 'due_date' => '2012-07-14'},
757 user
778 user
758 assert_equal Date.parse('2012-07-12'), issue.start_date
779 assert_equal Date.parse('2012-07-12'), issue.start_date
759 assert_nil issue.due_date
780 assert_nil issue.due_date
760
781
761 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
782 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
762 'due_date' => '2012-07-16',
783 'due_date' => '2012-07-16',
763 'status_id' => 2},
784 'status_id' => 2},
764 user
785 user
765 assert_equal Date.parse('2012-07-12'), issue.start_date
786 assert_equal Date.parse('2012-07-12'), issue.start_date
766 assert_equal Date.parse('2012-07-16'), issue.due_date
787 assert_equal Date.parse('2012-07-16'), issue.due_date
767 end
788 end
768
789
769 def test_required_attributes_should_be_validated
790 def test_required_attributes_should_be_validated
770 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
791 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
771 :is_for_all => true, :tracker_ids => [1, 2])
792 :is_for_all => true, :tracker_ids => [1, 2])
772
793
773 WorkflowPermission.delete_all
794 WorkflowPermission.delete_all
774 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
795 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
775 :role_id => 1, :field_name => 'due_date',
796 :role_id => 1, :field_name => 'due_date',
776 :rule => 'required')
797 :rule => 'required')
777 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
798 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
778 :role_id => 1, :field_name => 'category_id',
799 :role_id => 1, :field_name => 'category_id',
779 :rule => 'required')
800 :rule => 'required')
780 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
801 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
781 :role_id => 1, :field_name => cf.id.to_s,
802 :role_id => 1, :field_name => cf.id.to_s,
782 :rule => 'required')
803 :rule => 'required')
783
804
784 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
805 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
785 :role_id => 1, :field_name => 'start_date',
806 :role_id => 1, :field_name => 'start_date',
786 :rule => 'required')
807 :rule => 'required')
787 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
808 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
788 :role_id => 1, :field_name => cf.id.to_s,
809 :role_id => 1, :field_name => cf.id.to_s,
789 :rule => 'required')
810 :rule => 'required')
790 user = User.find(2)
811 user = User.find(2)
791
812
792 issue = Issue.new(:project_id => 1, :tracker_id => 1,
813 issue = Issue.new(:project_id => 1, :tracker_id => 1,
793 :status_id => 1, :subject => 'Required fields',
814 :status_id => 1, :subject => 'Required fields',
794 :author => user)
815 :author => user)
795 assert_equal [cf.id.to_s, "category_id", "due_date"],
816 assert_equal [cf.id.to_s, "category_id", "due_date"],
796 issue.required_attribute_names(user).sort
817 issue.required_attribute_names(user).sort
797 assert !issue.save, "Issue was saved"
818 assert !issue.save, "Issue was saved"
798 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
819 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
799 issue.errors.full_messages.sort
820 issue.errors.full_messages.sort
800
821
801 issue.tracker_id = 2
822 issue.tracker_id = 2
802 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
823 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
803 assert !issue.save, "Issue was saved"
824 assert !issue.save, "Issue was saved"
804 assert_equal ["Foo can't be blank", "Start date can't be blank"],
825 assert_equal ["Foo can't be blank", "Start date can't be blank"],
805 issue.errors.full_messages.sort
826 issue.errors.full_messages.sort
806
827
807 issue.start_date = Date.today
828 issue.start_date = Date.today
808 issue.custom_field_values = {cf.id.to_s => 'bar'}
829 issue.custom_field_values = {cf.id.to_s => 'bar'}
809 assert issue.save
830 assert issue.save
810 end
831 end
811
832
812 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
833 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
813 WorkflowPermission.delete_all
834 WorkflowPermission.delete_all
814 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
835 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
815 :role_id => 1, :field_name => 'due_date',
836 :role_id => 1, :field_name => 'due_date',
816 :rule => 'required')
837 :rule => 'required')
817 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
838 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
818 :role_id => 1, :field_name => 'start_date',
839 :role_id => 1, :field_name => 'start_date',
819 :rule => 'required')
840 :rule => 'required')
820 user = User.find(2)
841 user = User.find(2)
821 member = Member.find(1)
842 member = Member.find(1)
822 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
843 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
823
844
824 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
845 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
825
846
826 member.role_ids = [1, 2]
847 member.role_ids = [1, 2]
827 member.save!
848 member.save!
828 assert_equal [], issue.required_attribute_names(user.reload)
849 assert_equal [], issue.required_attribute_names(user.reload)
829
850
830 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
851 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
831 :role_id => 2, :field_name => 'due_date',
852 :role_id => 2, :field_name => 'due_date',
832 :rule => 'required')
853 :rule => 'required')
833 assert_equal %w(due_date), issue.required_attribute_names(user)
854 assert_equal %w(due_date), issue.required_attribute_names(user)
834
855
835 member.role_ids = [1, 2, 3]
856 member.role_ids = [1, 2, 3]
836 member.save!
857 member.save!
837 assert_equal [], issue.required_attribute_names(user.reload)
858 assert_equal [], issue.required_attribute_names(user.reload)
838
859
839 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
860 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
840 :role_id => 2, :field_name => 'due_date',
861 :role_id => 2, :field_name => 'due_date',
841 :rule => 'readonly')
862 :rule => 'readonly')
842 # required + readonly => required
863 # required + readonly => required
843 assert_equal %w(due_date), issue.required_attribute_names(user)
864 assert_equal %w(due_date), issue.required_attribute_names(user)
844 end
865 end
845
866
846 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
867 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
847 WorkflowPermission.delete_all
868 WorkflowPermission.delete_all
848 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
869 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
849 :role_id => 1, :field_name => 'due_date',
870 :role_id => 1, :field_name => 'due_date',
850 :rule => 'readonly')
871 :rule => 'readonly')
851 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
872 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
852 :role_id => 1, :field_name => 'start_date',
873 :role_id => 1, :field_name => 'start_date',
853 :rule => 'readonly')
874 :rule => 'readonly')
854 user = User.find(2)
875 user = User.find(2)
855 member = Member.find(1)
876 member = Member.find(1)
856 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
877 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
857
878
858 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
879 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
859
880
860 member.role_ids = [1, 2]
881 member.role_ids = [1, 2]
861 member.save!
882 member.save!
862 assert_equal [], issue.read_only_attribute_names(user.reload)
883 assert_equal [], issue.read_only_attribute_names(user.reload)
863
884
864 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
885 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
865 :role_id => 2, :field_name => 'due_date',
886 :role_id => 2, :field_name => 'due_date',
866 :rule => 'readonly')
887 :rule => 'readonly')
867 assert_equal %w(due_date), issue.read_only_attribute_names(user)
888 assert_equal %w(due_date), issue.read_only_attribute_names(user)
868 end
889 end
869
890
870 def test_copy
891 def test_copy
871 issue = Issue.new.copy_from(1)
892 issue = Issue.new.copy_from(1)
872 assert issue.copy?
893 assert issue.copy?
873 assert issue.save
894 assert issue.save
874 issue.reload
895 issue.reload
875 orig = Issue.find(1)
896 orig = Issue.find(1)
876 assert_equal orig.subject, issue.subject
897 assert_equal orig.subject, issue.subject
877 assert_equal orig.tracker, issue.tracker
898 assert_equal orig.tracker, issue.tracker
878 assert_equal "125", issue.custom_value_for(2).value
899 assert_equal "125", issue.custom_value_for(2).value
879 end
900 end
880
901
881 def test_copy_should_copy_status
902 def test_copy_should_copy_status
882 orig = Issue.find(8)
903 orig = Issue.find(8)
883 assert orig.status != IssueStatus.default
904 assert orig.status != IssueStatus.default
884
905
885 issue = Issue.new.copy_from(orig)
906 issue = Issue.new.copy_from(orig)
886 assert issue.save
907 assert issue.save
887 issue.reload
908 issue.reload
888 assert_equal orig.status, issue.status
909 assert_equal orig.status, issue.status
889 end
910 end
890
911
891 def test_copy_should_add_relation_with_copied_issue
912 def test_copy_should_add_relation_with_copied_issue
892 copied = Issue.find(1)
913 copied = Issue.find(1)
893 issue = Issue.new.copy_from(copied)
914 issue = Issue.new.copy_from(copied)
894 assert issue.save
915 assert issue.save
895 issue.reload
916 issue.reload
896
917
897 assert_equal 1, issue.relations.size
918 assert_equal 1, issue.relations.size
898 relation = issue.relations.first
919 relation = issue.relations.first
899 assert_equal 'copied_to', relation.relation_type
920 assert_equal 'copied_to', relation.relation_type
900 assert_equal copied, relation.issue_from
921 assert_equal copied, relation.issue_from
901 assert_equal issue, relation.issue_to
922 assert_equal issue, relation.issue_to
902 end
923 end
903
924
904 def test_copy_should_copy_subtasks
925 def test_copy_should_copy_subtasks
905 issue = Issue.generate_with_descendants!
926 issue = Issue.generate_with_descendants!
906
927
907 copy = issue.reload.copy
928 copy = issue.reload.copy
908 copy.author = User.find(7)
929 copy.author = User.find(7)
909 assert_difference 'Issue.count', 1+issue.descendants.count do
930 assert_difference 'Issue.count', 1+issue.descendants.count do
910 assert copy.save
931 assert copy.save
911 end
932 end
912 copy.reload
933 copy.reload
913 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
934 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
914 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
935 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
915 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
936 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
916 assert_equal copy.author, child_copy.author
937 assert_equal copy.author, child_copy.author
917 end
938 end
918
939
919 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
940 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
920 parent = Issue.generate!
941 parent = Issue.generate!
921 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
942 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
922 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
943 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
923
944
924 copy = parent.reload.copy
945 copy = parent.reload.copy
925 copy.parent_issue_id = parent.id
946 copy.parent_issue_id = parent.id
926 copy.author = User.find(7)
947 copy.author = User.find(7)
927 assert_difference 'Issue.count', 3 do
948 assert_difference 'Issue.count', 3 do
928 assert copy.save
949 assert copy.save
929 end
950 end
930 parent.reload
951 parent.reload
931 copy.reload
952 copy.reload
932 assert_equal parent, copy.parent
953 assert_equal parent, copy.parent
933 assert_equal 3, parent.children.count
954 assert_equal 3, parent.children.count
934 assert_equal 5, parent.descendants.count
955 assert_equal 5, parent.descendants.count
935 assert_equal 2, copy.children.count
956 assert_equal 2, copy.children.count
936 assert_equal 2, copy.descendants.count
957 assert_equal 2, copy.descendants.count
937 end
958 end
938
959
939 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
960 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
940 parent = Issue.generate!
961 parent = Issue.generate!
941 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
962 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
942 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
963 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
943
964
944 copy = parent.reload.copy
965 copy = parent.reload.copy
945 copy.parent_issue_id = child1.id
966 copy.parent_issue_id = child1.id
946 copy.author = User.find(7)
967 copy.author = User.find(7)
947 assert_difference 'Issue.count', 3 do
968 assert_difference 'Issue.count', 3 do
948 assert copy.save
969 assert copy.save
949 end
970 end
950 parent.reload
971 parent.reload
951 child1.reload
972 child1.reload
952 copy.reload
973 copy.reload
953 assert_equal child1, copy.parent
974 assert_equal child1, copy.parent
954 assert_equal 2, parent.children.count
975 assert_equal 2, parent.children.count
955 assert_equal 5, parent.descendants.count
976 assert_equal 5, parent.descendants.count
956 assert_equal 1, child1.children.count
977 assert_equal 1, child1.children.count
957 assert_equal 3, child1.descendants.count
978 assert_equal 3, child1.descendants.count
958 assert_equal 2, copy.children.count
979 assert_equal 2, copy.children.count
959 assert_equal 2, copy.descendants.count
980 assert_equal 2, copy.descendants.count
960 end
981 end
961
982
962 def test_copy_should_copy_subtasks_to_target_project
983 def test_copy_should_copy_subtasks_to_target_project
963 issue = Issue.generate_with_descendants!
984 issue = Issue.generate_with_descendants!
964
985
965 copy = issue.copy(:project_id => 3)
986 copy = issue.copy(:project_id => 3)
966 assert_difference 'Issue.count', 1+issue.descendants.count do
987 assert_difference 'Issue.count', 1+issue.descendants.count do
967 assert copy.save
988 assert copy.save
968 end
989 end
969 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
990 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
970 end
991 end
971
992
972 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
993 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
973 issue = Issue.generate_with_descendants!
994 issue = Issue.generate_with_descendants!
974
995
975 copy = issue.reload.copy
996 copy = issue.reload.copy
976 assert_difference 'Issue.count', 1+issue.descendants.count do
997 assert_difference 'Issue.count', 1+issue.descendants.count do
977 assert copy.save
998 assert copy.save
978 assert copy.save
999 assert copy.save
979 end
1000 end
980 end
1001 end
981
1002
982 def test_should_not_call_after_project_change_on_creation
1003 def test_should_not_call_after_project_change_on_creation
983 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1004 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
984 :subject => 'Test', :author_id => 1)
1005 :subject => 'Test', :author_id => 1)
985 issue.expects(:after_project_change).never
1006 issue.expects(:after_project_change).never
986 issue.save!
1007 issue.save!
987 end
1008 end
988
1009
989 def test_should_not_call_after_project_change_on_update
1010 def test_should_not_call_after_project_change_on_update
990 issue = Issue.find(1)
1011 issue = Issue.find(1)
991 issue.project = Project.find(1)
1012 issue.project = Project.find(1)
992 issue.subject = 'No project change'
1013 issue.subject = 'No project change'
993 issue.expects(:after_project_change).never
1014 issue.expects(:after_project_change).never
994 issue.save!
1015 issue.save!
995 end
1016 end
996
1017
997 def test_should_call_after_project_change_on_project_change
1018 def test_should_call_after_project_change_on_project_change
998 issue = Issue.find(1)
1019 issue = Issue.find(1)
999 issue.project = Project.find(2)
1020 issue.project = Project.find(2)
1000 issue.expects(:after_project_change).once
1021 issue.expects(:after_project_change).once
1001 issue.save!
1022 issue.save!
1002 end
1023 end
1003
1024
1004 def test_adding_journal_should_update_timestamp
1025 def test_adding_journal_should_update_timestamp
1005 issue = Issue.find(1)
1026 issue = Issue.find(1)
1006 updated_on_was = issue.updated_on
1027 updated_on_was = issue.updated_on
1007
1028
1008 issue.init_journal(User.first, "Adding notes")
1029 issue.init_journal(User.first, "Adding notes")
1009 assert_difference 'Journal.count' do
1030 assert_difference 'Journal.count' do
1010 assert issue.save
1031 assert issue.save
1011 end
1032 end
1012 issue.reload
1033 issue.reload
1013
1034
1014 assert_not_equal updated_on_was, issue.updated_on
1035 assert_not_equal updated_on_was, issue.updated_on
1015 end
1036 end
1016
1037
1017 def test_should_close_duplicates
1038 def test_should_close_duplicates
1018 # Create 3 issues
1039 # Create 3 issues
1019 issue1 = Issue.generate!
1040 issue1 = Issue.generate!
1020 issue2 = Issue.generate!
1041 issue2 = Issue.generate!
1021 issue3 = Issue.generate!
1042 issue3 = Issue.generate!
1022
1043
1023 # 2 is a dupe of 1
1044 # 2 is a dupe of 1
1024 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1045 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1025 :relation_type => IssueRelation::TYPE_DUPLICATES)
1046 :relation_type => IssueRelation::TYPE_DUPLICATES)
1026 # And 3 is a dupe of 2
1047 # And 3 is a dupe of 2
1027 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1048 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1028 :relation_type => IssueRelation::TYPE_DUPLICATES)
1049 :relation_type => IssueRelation::TYPE_DUPLICATES)
1029 # And 3 is a dupe of 1 (circular duplicates)
1050 # And 3 is a dupe of 1 (circular duplicates)
1030 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1051 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1031 :relation_type => IssueRelation::TYPE_DUPLICATES)
1052 :relation_type => IssueRelation::TYPE_DUPLICATES)
1032
1053
1033 assert issue1.reload.duplicates.include?(issue2)
1054 assert issue1.reload.duplicates.include?(issue2)
1034
1055
1035 # Closing issue 1
1056 # Closing issue 1
1036 issue1.init_journal(User.first, "Closing issue1")
1057 issue1.init_journal(User.first, "Closing issue1")
1037 issue1.status = IssueStatus.where(:is_closed => true).first
1058 issue1.status = IssueStatus.where(:is_closed => true).first
1038 assert issue1.save
1059 assert issue1.save
1039 # 2 and 3 should be also closed
1060 # 2 and 3 should be also closed
1040 assert issue2.reload.closed?
1061 assert issue2.reload.closed?
1041 assert issue3.reload.closed?
1062 assert issue3.reload.closed?
1042 end
1063 end
1043
1064
1044 def test_should_not_close_duplicated_issue
1065 def test_should_not_close_duplicated_issue
1045 issue1 = Issue.generate!
1066 issue1 = Issue.generate!
1046 issue2 = Issue.generate!
1067 issue2 = Issue.generate!
1047
1068
1048 # 2 is a dupe of 1
1069 # 2 is a dupe of 1
1049 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1070 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1050 :relation_type => IssueRelation::TYPE_DUPLICATES)
1071 :relation_type => IssueRelation::TYPE_DUPLICATES)
1051 # 2 is a dup of 1 but 1 is not a duplicate of 2
1072 # 2 is a dup of 1 but 1 is not a duplicate of 2
1052 assert !issue2.reload.duplicates.include?(issue1)
1073 assert !issue2.reload.duplicates.include?(issue1)
1053
1074
1054 # Closing issue 2
1075 # Closing issue 2
1055 issue2.init_journal(User.first, "Closing issue2")
1076 issue2.init_journal(User.first, "Closing issue2")
1056 issue2.status = IssueStatus.where(:is_closed => true).first
1077 issue2.status = IssueStatus.where(:is_closed => true).first
1057 assert issue2.save
1078 assert issue2.save
1058 # 1 should not be also closed
1079 # 1 should not be also closed
1059 assert !issue1.reload.closed?
1080 assert !issue1.reload.closed?
1060 end
1081 end
1061
1082
1062 def test_assignable_versions
1083 def test_assignable_versions
1063 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1084 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1064 :status_id => 1, :fixed_version_id => 1,
1085 :status_id => 1, :fixed_version_id => 1,
1065 :subject => 'New issue')
1086 :subject => 'New issue')
1066 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1087 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1067 end
1088 end
1068
1089
1069 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1090 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1070 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1091 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1071 :status_id => 1, :fixed_version_id => 1,
1092 :status_id => 1, :fixed_version_id => 1,
1072 :subject => 'New issue')
1093 :subject => 'New issue')
1073 assert !issue.save
1094 assert !issue.save
1074 assert_not_equal [], issue.errors[:fixed_version_id]
1095 assert_not_equal [], issue.errors[:fixed_version_id]
1075 end
1096 end
1076
1097
1077 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1098 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1078 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1099 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1079 :status_id => 1, :fixed_version_id => 2,
1100 :status_id => 1, :fixed_version_id => 2,
1080 :subject => 'New issue')
1101 :subject => 'New issue')
1081 assert !issue.save
1102 assert !issue.save
1082 assert_not_equal [], issue.errors[:fixed_version_id]
1103 assert_not_equal [], issue.errors[:fixed_version_id]
1083 end
1104 end
1084
1105
1085 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1106 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1086 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1107 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1087 :status_id => 1, :fixed_version_id => 3,
1108 :status_id => 1, :fixed_version_id => 3,
1088 :subject => 'New issue')
1109 :subject => 'New issue')
1089 assert issue.save
1110 assert issue.save
1090 end
1111 end
1091
1112
1092 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1113 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1093 issue = Issue.find(11)
1114 issue = Issue.find(11)
1094 assert_equal 'closed', issue.fixed_version.status
1115 assert_equal 'closed', issue.fixed_version.status
1095 issue.subject = 'Subject changed'
1116 issue.subject = 'Subject changed'
1096 assert issue.save
1117 assert issue.save
1097 end
1118 end
1098
1119
1099 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1120 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1100 issue = Issue.find(11)
1121 issue = Issue.find(11)
1101 issue.status_id = 1
1122 issue.status_id = 1
1102 assert !issue.save
1123 assert !issue.save
1103 assert_not_equal [], issue.errors[:base]
1124 assert_not_equal [], issue.errors[:base]
1104 end
1125 end
1105
1126
1106 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1127 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1107 issue = Issue.find(11)
1128 issue = Issue.find(11)
1108 issue.status_id = 1
1129 issue.status_id = 1
1109 issue.fixed_version_id = 3
1130 issue.fixed_version_id = 3
1110 assert issue.save
1131 assert issue.save
1111 end
1132 end
1112
1133
1113 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1134 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1114 issue = Issue.find(12)
1135 issue = Issue.find(12)
1115 assert_equal 'locked', issue.fixed_version.status
1136 assert_equal 'locked', issue.fixed_version.status
1116 issue.status_id = 1
1137 issue.status_id = 1
1117 assert issue.save
1138 assert issue.save
1118 end
1139 end
1119
1140
1120 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1141 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1121 issue = Issue.find(2)
1142 issue = Issue.find(2)
1122 assert_equal 2, issue.fixed_version_id
1143 assert_equal 2, issue.fixed_version_id
1123 issue.project_id = 3
1144 issue.project_id = 3
1124 assert_nil issue.fixed_version_id
1145 assert_nil issue.fixed_version_id
1125 issue.fixed_version_id = 2
1146 issue.fixed_version_id = 2
1126 assert !issue.save
1147 assert !issue.save
1127 assert_include 'Target version is not included in the list', issue.errors.full_messages
1148 assert_include 'Target version is not included in the list', issue.errors.full_messages
1128 end
1149 end
1129
1150
1130 def test_should_keep_shared_version_when_changing_project
1151 def test_should_keep_shared_version_when_changing_project
1131 Version.find(2).update_attribute :sharing, 'tree'
1152 Version.find(2).update_attribute :sharing, 'tree'
1132
1153
1133 issue = Issue.find(2)
1154 issue = Issue.find(2)
1134 assert_equal 2, issue.fixed_version_id
1155 assert_equal 2, issue.fixed_version_id
1135 issue.project_id = 3
1156 issue.project_id = 3
1136 assert_equal 2, issue.fixed_version_id
1157 assert_equal 2, issue.fixed_version_id
1137 assert issue.save
1158 assert issue.save
1138 end
1159 end
1139
1160
1140 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1161 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1141 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1162 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1142 end
1163 end
1143
1164
1144 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1165 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1145 Project.find(2).disable_module! :issue_tracking
1166 Project.find(2).disable_module! :issue_tracking
1146 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1167 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1147 end
1168 end
1148
1169
1149 def test_move_to_another_project_with_same_category
1170 def test_move_to_another_project_with_same_category
1150 issue = Issue.find(1)
1171 issue = Issue.find(1)
1151 issue.project = Project.find(2)
1172 issue.project = Project.find(2)
1152 assert issue.save
1173 assert issue.save
1153 issue.reload
1174 issue.reload
1154 assert_equal 2, issue.project_id
1175 assert_equal 2, issue.project_id
1155 # Category changes
1176 # Category changes
1156 assert_equal 4, issue.category_id
1177 assert_equal 4, issue.category_id
1157 # Make sure time entries were move to the target project
1178 # Make sure time entries were move to the target project
1158 assert_equal 2, issue.time_entries.first.project_id
1179 assert_equal 2, issue.time_entries.first.project_id
1159 end
1180 end
1160
1181
1161 def test_move_to_another_project_without_same_category
1182 def test_move_to_another_project_without_same_category
1162 issue = Issue.find(2)
1183 issue = Issue.find(2)
1163 issue.project = Project.find(2)
1184 issue.project = Project.find(2)
1164 assert issue.save
1185 assert issue.save
1165 issue.reload
1186 issue.reload
1166 assert_equal 2, issue.project_id
1187 assert_equal 2, issue.project_id
1167 # Category cleared
1188 # Category cleared
1168 assert_nil issue.category_id
1189 assert_nil issue.category_id
1169 end
1190 end
1170
1191
1171 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1192 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1172 issue = Issue.find(1)
1193 issue = Issue.find(1)
1173 issue.update_attribute(:fixed_version_id, 1)
1194 issue.update_attribute(:fixed_version_id, 1)
1174 issue.project = Project.find(2)
1195 issue.project = Project.find(2)
1175 assert issue.save
1196 assert issue.save
1176 issue.reload
1197 issue.reload
1177 assert_equal 2, issue.project_id
1198 assert_equal 2, issue.project_id
1178 # Cleared fixed_version
1199 # Cleared fixed_version
1179 assert_equal nil, issue.fixed_version
1200 assert_equal nil, issue.fixed_version
1180 end
1201 end
1181
1202
1182 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1203 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1183 issue = Issue.find(1)
1204 issue = Issue.find(1)
1184 issue.update_attribute(:fixed_version_id, 4)
1205 issue.update_attribute(:fixed_version_id, 4)
1185 issue.project = Project.find(5)
1206 issue.project = Project.find(5)
1186 assert issue.save
1207 assert issue.save
1187 issue.reload
1208 issue.reload
1188 assert_equal 5, issue.project_id
1209 assert_equal 5, issue.project_id
1189 # Keep fixed_version
1210 # Keep fixed_version
1190 assert_equal 4, issue.fixed_version_id
1211 assert_equal 4, issue.fixed_version_id
1191 end
1212 end
1192
1213
1193 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1214 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1194 issue = Issue.find(1)
1215 issue = Issue.find(1)
1195 issue.update_attribute(:fixed_version_id, 1)
1216 issue.update_attribute(:fixed_version_id, 1)
1196 issue.project = Project.find(5)
1217 issue.project = Project.find(5)
1197 assert issue.save
1218 assert issue.save
1198 issue.reload
1219 issue.reload
1199 assert_equal 5, issue.project_id
1220 assert_equal 5, issue.project_id
1200 # Cleared fixed_version
1221 # Cleared fixed_version
1201 assert_equal nil, issue.fixed_version
1222 assert_equal nil, issue.fixed_version
1202 end
1223 end
1203
1224
1204 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1225 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1205 issue = Issue.find(1)
1226 issue = Issue.find(1)
1206 issue.update_attribute(:fixed_version_id, 7)
1227 issue.update_attribute(:fixed_version_id, 7)
1207 issue.project = Project.find(2)
1228 issue.project = Project.find(2)
1208 assert issue.save
1229 assert issue.save
1209 issue.reload
1230 issue.reload
1210 assert_equal 2, issue.project_id
1231 assert_equal 2, issue.project_id
1211 # Keep fixed_version
1232 # Keep fixed_version
1212 assert_equal 7, issue.fixed_version_id
1233 assert_equal 7, issue.fixed_version_id
1213 end
1234 end
1214
1235
1215 def test_move_to_another_project_should_keep_parent_if_valid
1236 def test_move_to_another_project_should_keep_parent_if_valid
1216 issue = Issue.find(1)
1237 issue = Issue.find(1)
1217 issue.update_attribute(:parent_issue_id, 2)
1238 issue.update_attribute(:parent_issue_id, 2)
1218 issue.project = Project.find(3)
1239 issue.project = Project.find(3)
1219 assert issue.save
1240 assert issue.save
1220 issue.reload
1241 issue.reload
1221 assert_equal 2, issue.parent_id
1242 assert_equal 2, issue.parent_id
1222 end
1243 end
1223
1244
1224 def test_move_to_another_project_should_clear_parent_if_not_valid
1245 def test_move_to_another_project_should_clear_parent_if_not_valid
1225 issue = Issue.find(1)
1246 issue = Issue.find(1)
1226 issue.update_attribute(:parent_issue_id, 2)
1247 issue.update_attribute(:parent_issue_id, 2)
1227 issue.project = Project.find(2)
1248 issue.project = Project.find(2)
1228 assert issue.save
1249 assert issue.save
1229 issue.reload
1250 issue.reload
1230 assert_nil issue.parent_id
1251 assert_nil issue.parent_id
1231 end
1252 end
1232
1253
1233 def test_move_to_another_project_with_disabled_tracker
1254 def test_move_to_another_project_with_disabled_tracker
1234 issue = Issue.find(1)
1255 issue = Issue.find(1)
1235 target = Project.find(2)
1256 target = Project.find(2)
1236 target.tracker_ids = [3]
1257 target.tracker_ids = [3]
1237 target.save
1258 target.save
1238 issue.project = target
1259 issue.project = target
1239 assert issue.save
1260 assert issue.save
1240 issue.reload
1261 issue.reload
1241 assert_equal 2, issue.project_id
1262 assert_equal 2, issue.project_id
1242 assert_equal 3, issue.tracker_id
1263 assert_equal 3, issue.tracker_id
1243 end
1264 end
1244
1265
1245 def test_copy_to_the_same_project
1266 def test_copy_to_the_same_project
1246 issue = Issue.find(1)
1267 issue = Issue.find(1)
1247 copy = issue.copy
1268 copy = issue.copy
1248 assert_difference 'Issue.count' do
1269 assert_difference 'Issue.count' do
1249 copy.save!
1270 copy.save!
1250 end
1271 end
1251 assert_kind_of Issue, copy
1272 assert_kind_of Issue, copy
1252 assert_equal issue.project, copy.project
1273 assert_equal issue.project, copy.project
1253 assert_equal "125", copy.custom_value_for(2).value
1274 assert_equal "125", copy.custom_value_for(2).value
1254 end
1275 end
1255
1276
1256 def test_copy_to_another_project_and_tracker
1277 def test_copy_to_another_project_and_tracker
1257 issue = Issue.find(1)
1278 issue = Issue.find(1)
1258 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1279 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1259 assert_difference 'Issue.count' do
1280 assert_difference 'Issue.count' do
1260 copy.save!
1281 copy.save!
1261 end
1282 end
1262 copy.reload
1283 copy.reload
1263 assert_kind_of Issue, copy
1284 assert_kind_of Issue, copy
1264 assert_equal Project.find(3), copy.project
1285 assert_equal Project.find(3), copy.project
1265 assert_equal Tracker.find(2), copy.tracker
1286 assert_equal Tracker.find(2), copy.tracker
1266 # Custom field #2 is not associated with target tracker
1287 # Custom field #2 is not associated with target tracker
1267 assert_nil copy.custom_value_for(2)
1288 assert_nil copy.custom_value_for(2)
1268 end
1289 end
1269
1290
1270 test "#copy should not create a journal" do
1291 test "#copy should not create a journal" do
1271 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1292 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1272 copy.save!
1293 copy.save!
1273 assert_equal 0, copy.reload.journals.size
1294 assert_equal 0, copy.reload.journals.size
1274 end
1295 end
1275
1296
1276 test "#copy should allow assigned_to changes" do
1297 test "#copy should allow assigned_to changes" do
1277 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1298 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1278 assert_equal 3, copy.assigned_to_id
1299 assert_equal 3, copy.assigned_to_id
1279 end
1300 end
1280
1301
1281 test "#copy should allow status changes" do
1302 test "#copy should allow status changes" do
1282 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1303 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1283 assert_equal 2, copy.status_id
1304 assert_equal 2, copy.status_id
1284 end
1305 end
1285
1306
1286 test "#copy should allow start date changes" do
1307 test "#copy should allow start date changes" do
1287 date = Date.today
1308 date = Date.today
1288 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1309 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1289 assert_equal date, copy.start_date
1310 assert_equal date, copy.start_date
1290 end
1311 end
1291
1312
1292 test "#copy should allow due date changes" do
1313 test "#copy should allow due date changes" do
1293 date = Date.today
1314 date = Date.today
1294 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1315 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1295 assert_equal date, copy.due_date
1316 assert_equal date, copy.due_date
1296 end
1317 end
1297
1318
1298 test "#copy should set current user as author" do
1319 test "#copy should set current user as author" do
1299 User.current = User.find(9)
1320 User.current = User.find(9)
1300 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1321 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1301 assert_equal User.current, copy.author
1322 assert_equal User.current, copy.author
1302 end
1323 end
1303
1324
1304 test "#copy should create a journal with notes" do
1325 test "#copy should create a journal with notes" do
1305 date = Date.today
1326 date = Date.today
1306 notes = "Notes added when copying"
1327 notes = "Notes added when copying"
1307 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1328 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1308 copy.init_journal(User.current, notes)
1329 copy.init_journal(User.current, notes)
1309 copy.save!
1330 copy.save!
1310
1331
1311 assert_equal 1, copy.journals.size
1332 assert_equal 1, copy.journals.size
1312 journal = copy.journals.first
1333 journal = copy.journals.first
1313 assert_equal 0, journal.details.size
1334 assert_equal 0, journal.details.size
1314 assert_equal notes, journal.notes
1335 assert_equal notes, journal.notes
1315 end
1336 end
1316
1337
1317 def test_valid_parent_project
1338 def test_valid_parent_project
1318 issue = Issue.find(1)
1339 issue = Issue.find(1)
1319 issue_in_same_project = Issue.find(2)
1340 issue_in_same_project = Issue.find(2)
1320 issue_in_child_project = Issue.find(5)
1341 issue_in_child_project = Issue.find(5)
1321 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1342 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1322 issue_in_other_child_project = Issue.find(6)
1343 issue_in_other_child_project = Issue.find(6)
1323 issue_in_different_tree = Issue.find(4)
1344 issue_in_different_tree = Issue.find(4)
1324
1345
1325 with_settings :cross_project_subtasks => '' do
1346 with_settings :cross_project_subtasks => '' do
1326 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1347 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1327 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1348 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1328 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1349 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1329 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1350 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1330 end
1351 end
1331
1352
1332 with_settings :cross_project_subtasks => 'system' do
1353 with_settings :cross_project_subtasks => 'system' do
1333 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1354 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1334 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1355 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1335 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1356 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1336 end
1357 end
1337
1358
1338 with_settings :cross_project_subtasks => 'tree' do
1359 with_settings :cross_project_subtasks => 'tree' do
1339 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1360 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1340 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1361 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1341 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1362 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1342 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1363 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1343
1364
1344 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1365 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1345 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1366 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1346 end
1367 end
1347
1368
1348 with_settings :cross_project_subtasks => 'descendants' do
1369 with_settings :cross_project_subtasks => 'descendants' do
1349 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1370 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1350 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1371 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1351 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1372 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1352 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1373 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1353
1374
1354 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1375 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1355 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1376 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1356 end
1377 end
1357 end
1378 end
1358
1379
1359 def test_recipients_should_include_previous_assignee
1380 def test_recipients_should_include_previous_assignee
1360 user = User.find(3)
1381 user = User.find(3)
1361 user.members.update_all ["mail_notification = ?", false]
1382 user.members.update_all ["mail_notification = ?", false]
1362 user.update_attribute :mail_notification, 'only_assigned'
1383 user.update_attribute :mail_notification, 'only_assigned'
1363
1384
1364 issue = Issue.find(2)
1385 issue = Issue.find(2)
1365 issue.assigned_to = nil
1386 issue.assigned_to = nil
1366 assert_include user.mail, issue.recipients
1387 assert_include user.mail, issue.recipients
1367 issue.save!
1388 issue.save!
1368 assert !issue.recipients.include?(user.mail)
1389 assert !issue.recipients.include?(user.mail)
1369 end
1390 end
1370
1391
1371 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1392 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1372 issue = Issue.find(12)
1393 issue = Issue.find(12)
1373 assert issue.recipients.include?(issue.author.mail)
1394 assert issue.recipients.include?(issue.author.mail)
1374 # copy the issue to a private project
1395 # copy the issue to a private project
1375 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1396 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1376 # author is not a member of project anymore
1397 # author is not a member of project anymore
1377 assert !copy.recipients.include?(copy.author.mail)
1398 assert !copy.recipients.include?(copy.author.mail)
1378 end
1399 end
1379
1400
1380 def test_recipients_should_include_the_assigned_group_members
1401 def test_recipients_should_include_the_assigned_group_members
1381 group_member = User.generate!
1402 group_member = User.generate!
1382 group = Group.generate!
1403 group = Group.generate!
1383 group.users << group_member
1404 group.users << group_member
1384
1405
1385 issue = Issue.find(12)
1406 issue = Issue.find(12)
1386 issue.assigned_to = group
1407 issue.assigned_to = group
1387 assert issue.recipients.include?(group_member.mail)
1408 assert issue.recipients.include?(group_member.mail)
1388 end
1409 end
1389
1410
1390 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1411 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1391 user = User.find(3)
1412 user = User.find(3)
1392 issue = Issue.find(9)
1413 issue = Issue.find(9)
1393 Watcher.create!(:user => user, :watchable => issue)
1414 Watcher.create!(:user => user, :watchable => issue)
1394 assert issue.watched_by?(user)
1415 assert issue.watched_by?(user)
1395 assert !issue.watcher_recipients.include?(user.mail)
1416 assert !issue.watcher_recipients.include?(user.mail)
1396 end
1417 end
1397
1418
1398 def test_issue_destroy
1419 def test_issue_destroy
1399 Issue.find(1).destroy
1420 Issue.find(1).destroy
1400 assert_nil Issue.find_by_id(1)
1421 assert_nil Issue.find_by_id(1)
1401 assert_nil TimeEntry.find_by_issue_id(1)
1422 assert_nil TimeEntry.find_by_issue_id(1)
1402 end
1423 end
1403
1424
1404 def test_destroy_should_delete_time_entries_custom_values
1425 def test_destroy_should_delete_time_entries_custom_values
1405 issue = Issue.generate!
1426 issue = Issue.generate!
1406 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1427 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1407
1428
1408 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1429 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1409 assert issue.destroy
1430 assert issue.destroy
1410 end
1431 end
1411 end
1432 end
1412
1433
1413 def test_destroying_a_deleted_issue_should_not_raise_an_error
1434 def test_destroying_a_deleted_issue_should_not_raise_an_error
1414 issue = Issue.find(1)
1435 issue = Issue.find(1)
1415 Issue.find(1).destroy
1436 Issue.find(1).destroy
1416
1437
1417 assert_nothing_raised do
1438 assert_nothing_raised do
1418 assert_no_difference 'Issue.count' do
1439 assert_no_difference 'Issue.count' do
1419 issue.destroy
1440 issue.destroy
1420 end
1441 end
1421 assert issue.destroyed?
1442 assert issue.destroyed?
1422 end
1443 end
1423 end
1444 end
1424
1445
1425 def test_destroying_a_stale_issue_should_not_raise_an_error
1446 def test_destroying_a_stale_issue_should_not_raise_an_error
1426 issue = Issue.find(1)
1447 issue = Issue.find(1)
1427 Issue.find(1).update_attribute :subject, "Updated"
1448 Issue.find(1).update_attribute :subject, "Updated"
1428
1449
1429 assert_nothing_raised do
1450 assert_nothing_raised do
1430 assert_difference 'Issue.count', -1 do
1451 assert_difference 'Issue.count', -1 do
1431 issue.destroy
1452 issue.destroy
1432 end
1453 end
1433 assert issue.destroyed?
1454 assert issue.destroyed?
1434 end
1455 end
1435 end
1456 end
1436
1457
1437 def test_blocked
1458 def test_blocked
1438 blocked_issue = Issue.find(9)
1459 blocked_issue = Issue.find(9)
1439 blocking_issue = Issue.find(10)
1460 blocking_issue = Issue.find(10)
1440
1461
1441 assert blocked_issue.blocked?
1462 assert blocked_issue.blocked?
1442 assert !blocking_issue.blocked?
1463 assert !blocking_issue.blocked?
1443 end
1464 end
1444
1465
1445 def test_blocked_issues_dont_allow_closed_statuses
1466 def test_blocked_issues_dont_allow_closed_statuses
1446 blocked_issue = Issue.find(9)
1467 blocked_issue = Issue.find(9)
1447
1468
1448 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1469 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1449 assert !allowed_statuses.empty?
1470 assert !allowed_statuses.empty?
1450 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1471 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1451 assert closed_statuses.empty?
1472 assert closed_statuses.empty?
1452 end
1473 end
1453
1474
1454 def test_unblocked_issues_allow_closed_statuses
1475 def test_unblocked_issues_allow_closed_statuses
1455 blocking_issue = Issue.find(10)
1476 blocking_issue = Issue.find(10)
1456
1477
1457 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1478 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1458 assert !allowed_statuses.empty?
1479 assert !allowed_statuses.empty?
1459 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1480 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1460 assert !closed_statuses.empty?
1481 assert !closed_statuses.empty?
1461 end
1482 end
1462
1483
1463 def test_reschedule_an_issue_without_dates
1484 def test_reschedule_an_issue_without_dates
1464 with_settings :non_working_week_days => [] do
1485 with_settings :non_working_week_days => [] do
1465 issue = Issue.new(:start_date => nil, :due_date => nil)
1486 issue = Issue.new(:start_date => nil, :due_date => nil)
1466 issue.reschedule_on '2012-10-09'.to_date
1487 issue.reschedule_on '2012-10-09'.to_date
1467 assert_equal '2012-10-09'.to_date, issue.start_date
1488 assert_equal '2012-10-09'.to_date, issue.start_date
1468 assert_equal '2012-10-09'.to_date, issue.due_date
1489 assert_equal '2012-10-09'.to_date, issue.due_date
1469 end
1490 end
1470
1491
1471 with_settings :non_working_week_days => %w(6 7) do
1492 with_settings :non_working_week_days => %w(6 7) do
1472 issue = Issue.new(:start_date => nil, :due_date => nil)
1493 issue = Issue.new(:start_date => nil, :due_date => nil)
1473 issue.reschedule_on '2012-10-09'.to_date
1494 issue.reschedule_on '2012-10-09'.to_date
1474 assert_equal '2012-10-09'.to_date, issue.start_date
1495 assert_equal '2012-10-09'.to_date, issue.start_date
1475 assert_equal '2012-10-09'.to_date, issue.due_date
1496 assert_equal '2012-10-09'.to_date, issue.due_date
1476
1497
1477 issue = Issue.new(:start_date => nil, :due_date => nil)
1498 issue = Issue.new(:start_date => nil, :due_date => nil)
1478 issue.reschedule_on '2012-10-13'.to_date
1499 issue.reschedule_on '2012-10-13'.to_date
1479 assert_equal '2012-10-15'.to_date, issue.start_date
1500 assert_equal '2012-10-15'.to_date, issue.start_date
1480 assert_equal '2012-10-15'.to_date, issue.due_date
1501 assert_equal '2012-10-15'.to_date, issue.due_date
1481 end
1502 end
1482 end
1503 end
1483
1504
1484 def test_reschedule_an_issue_with_start_date
1505 def test_reschedule_an_issue_with_start_date
1485 with_settings :non_working_week_days => [] do
1506 with_settings :non_working_week_days => [] do
1486 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1507 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1487 issue.reschedule_on '2012-10-13'.to_date
1508 issue.reschedule_on '2012-10-13'.to_date
1488 assert_equal '2012-10-13'.to_date, issue.start_date
1509 assert_equal '2012-10-13'.to_date, issue.start_date
1489 assert_equal '2012-10-13'.to_date, issue.due_date
1510 assert_equal '2012-10-13'.to_date, issue.due_date
1490 end
1511 end
1491
1512
1492 with_settings :non_working_week_days => %w(6 7) do
1513 with_settings :non_working_week_days => %w(6 7) do
1493 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1514 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1494 issue.reschedule_on '2012-10-11'.to_date
1515 issue.reschedule_on '2012-10-11'.to_date
1495 assert_equal '2012-10-11'.to_date, issue.start_date
1516 assert_equal '2012-10-11'.to_date, issue.start_date
1496 assert_equal '2012-10-11'.to_date, issue.due_date
1517 assert_equal '2012-10-11'.to_date, issue.due_date
1497
1518
1498 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1519 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1499 issue.reschedule_on '2012-10-13'.to_date
1520 issue.reschedule_on '2012-10-13'.to_date
1500 assert_equal '2012-10-15'.to_date, issue.start_date
1521 assert_equal '2012-10-15'.to_date, issue.start_date
1501 assert_equal '2012-10-15'.to_date, issue.due_date
1522 assert_equal '2012-10-15'.to_date, issue.due_date
1502 end
1523 end
1503 end
1524 end
1504
1525
1505 def test_reschedule_an_issue_with_start_and_due_dates
1526 def test_reschedule_an_issue_with_start_and_due_dates
1506 with_settings :non_working_week_days => [] do
1527 with_settings :non_working_week_days => [] do
1507 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1528 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1508 issue.reschedule_on '2012-10-13'.to_date
1529 issue.reschedule_on '2012-10-13'.to_date
1509 assert_equal '2012-10-13'.to_date, issue.start_date
1530 assert_equal '2012-10-13'.to_date, issue.start_date
1510 assert_equal '2012-10-19'.to_date, issue.due_date
1531 assert_equal '2012-10-19'.to_date, issue.due_date
1511 end
1532 end
1512
1533
1513 with_settings :non_working_week_days => %w(6 7) do
1534 with_settings :non_working_week_days => %w(6 7) do
1514 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1535 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1515 issue.reschedule_on '2012-10-11'.to_date
1536 issue.reschedule_on '2012-10-11'.to_date
1516 assert_equal '2012-10-11'.to_date, issue.start_date
1537 assert_equal '2012-10-11'.to_date, issue.start_date
1517 assert_equal '2012-10-23'.to_date, issue.due_date
1538 assert_equal '2012-10-23'.to_date, issue.due_date
1518
1539
1519 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1540 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1520 issue.reschedule_on '2012-10-13'.to_date
1541 issue.reschedule_on '2012-10-13'.to_date
1521 assert_equal '2012-10-15'.to_date, issue.start_date
1542 assert_equal '2012-10-15'.to_date, issue.start_date
1522 assert_equal '2012-10-25'.to_date, issue.due_date
1543 assert_equal '2012-10-25'.to_date, issue.due_date
1523 end
1544 end
1524 end
1545 end
1525
1546
1526 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1547 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1527 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1548 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1528 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1549 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1529 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1550 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1530 :relation_type => IssueRelation::TYPE_PRECEDES)
1551 :relation_type => IssueRelation::TYPE_PRECEDES)
1531 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1552 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1532
1553
1533 issue1.reload
1554 issue1.reload
1534 issue1.due_date = '2012-10-23'
1555 issue1.due_date = '2012-10-23'
1535 issue1.save!
1556 issue1.save!
1536 issue2.reload
1557 issue2.reload
1537 assert_equal Date.parse('2012-10-24'), issue2.start_date
1558 assert_equal Date.parse('2012-10-24'), issue2.start_date
1538 assert_equal Date.parse('2012-10-26'), issue2.due_date
1559 assert_equal Date.parse('2012-10-26'), issue2.due_date
1539 end
1560 end
1540
1561
1541 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1562 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1542 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1563 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1543 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1564 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1544 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1565 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1545 :relation_type => IssueRelation::TYPE_PRECEDES)
1566 :relation_type => IssueRelation::TYPE_PRECEDES)
1546 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1567 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1547
1568
1548 issue1.reload
1569 issue1.reload
1549 issue1.start_date = '2012-09-17'
1570 issue1.start_date = '2012-09-17'
1550 issue1.due_date = '2012-09-18'
1571 issue1.due_date = '2012-09-18'
1551 issue1.save!
1572 issue1.save!
1552 issue2.reload
1573 issue2.reload
1553 assert_equal Date.parse('2012-09-19'), issue2.start_date
1574 assert_equal Date.parse('2012-09-19'), issue2.start_date
1554 assert_equal Date.parse('2012-09-21'), issue2.due_date
1575 assert_equal Date.parse('2012-09-21'), issue2.due_date
1555 end
1576 end
1556
1577
1557 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1578 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1558 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1579 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1559 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1580 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1560 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1581 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1561 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1582 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1562 :relation_type => IssueRelation::TYPE_PRECEDES)
1583 :relation_type => IssueRelation::TYPE_PRECEDES)
1563 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1584 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1564 :relation_type => IssueRelation::TYPE_PRECEDES)
1585 :relation_type => IssueRelation::TYPE_PRECEDES)
1565 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1586 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1566
1587
1567 issue1.reload
1588 issue1.reload
1568 issue1.start_date = '2012-09-17'
1589 issue1.start_date = '2012-09-17'
1569 issue1.due_date = '2012-09-18'
1590 issue1.due_date = '2012-09-18'
1570 issue1.save!
1591 issue1.save!
1571 issue2.reload
1592 issue2.reload
1572 # Issue 2 must start after Issue 3
1593 # Issue 2 must start after Issue 3
1573 assert_equal Date.parse('2012-10-03'), issue2.start_date
1594 assert_equal Date.parse('2012-10-03'), issue2.start_date
1574 assert_equal Date.parse('2012-10-05'), issue2.due_date
1595 assert_equal Date.parse('2012-10-05'), issue2.due_date
1575 end
1596 end
1576
1597
1577 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1598 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1578 with_settings :non_working_week_days => [] do
1599 with_settings :non_working_week_days => [] do
1579 stale = Issue.find(1)
1600 stale = Issue.find(1)
1580 issue = Issue.find(1)
1601 issue = Issue.find(1)
1581 issue.subject = "Updated"
1602 issue.subject = "Updated"
1582 issue.save!
1603 issue.save!
1583 date = 10.days.from_now.to_date
1604 date = 10.days.from_now.to_date
1584 assert_nothing_raised do
1605 assert_nothing_raised do
1585 stale.reschedule_on!(date)
1606 stale.reschedule_on!(date)
1586 end
1607 end
1587 assert_equal date, stale.reload.start_date
1608 assert_equal date, stale.reload.start_date
1588 end
1609 end
1589 end
1610 end
1590
1611
1591 def test_child_issue_should_consider_parent_soonest_start_on_create
1612 def test_child_issue_should_consider_parent_soonest_start_on_create
1592 set_language_if_valid 'en'
1613 set_language_if_valid 'en'
1593 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1614 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1594 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1615 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1595 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1616 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1596 :relation_type => IssueRelation::TYPE_PRECEDES)
1617 :relation_type => IssueRelation::TYPE_PRECEDES)
1597 issue1.reload
1618 issue1.reload
1598 issue2.reload
1619 issue2.reload
1599 assert_equal Date.parse('2012-10-18'), issue2.start_date
1620 assert_equal Date.parse('2012-10-18'), issue2.start_date
1600
1621
1601 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1622 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1602 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1623 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1603 assert !child.valid?
1624 assert !child.valid?
1604 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1625 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1605 assert_equal Date.parse('2012-10-18'), child.soonest_start
1626 assert_equal Date.parse('2012-10-18'), child.soonest_start
1606 child.start_date = '2012-10-18'
1627 child.start_date = '2012-10-18'
1607 assert child.save
1628 assert child.save
1608 end
1629 end
1609
1630
1610 def test_setting_parent_to_a_dependent_issue_should_not_validate
1631 def test_setting_parent_to_a_dependent_issue_should_not_validate
1611 set_language_if_valid 'en'
1632 set_language_if_valid 'en'
1612 issue1 = Issue.generate!
1633 issue1 = Issue.generate!
1613 issue2 = Issue.generate!
1634 issue2 = Issue.generate!
1614 issue3 = Issue.generate!
1635 issue3 = Issue.generate!
1615 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1636 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1616 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1637 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1617 issue3.reload
1638 issue3.reload
1618 issue3.parent_issue_id = issue2.id
1639 issue3.parent_issue_id = issue2.id
1619 assert !issue3.valid?
1640 assert !issue3.valid?
1620 assert_include 'Parent task is invalid', issue3.errors.full_messages
1641 assert_include 'Parent task is invalid', issue3.errors.full_messages
1621 end
1642 end
1622
1643
1623 def test_setting_parent_should_not_allow_circular_dependency
1644 def test_setting_parent_should_not_allow_circular_dependency
1624 set_language_if_valid 'en'
1645 set_language_if_valid 'en'
1625 issue1 = Issue.generate!
1646 issue1 = Issue.generate!
1626 issue2 = Issue.generate!
1647 issue2 = Issue.generate!
1627 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1648 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1628 issue3 = Issue.generate!
1649 issue3 = Issue.generate!
1629 issue2.reload
1650 issue2.reload
1630 issue2.parent_issue_id = issue3.id
1651 issue2.parent_issue_id = issue3.id
1631 issue2.save!
1652 issue2.save!
1632 issue4 = Issue.generate!
1653 issue4 = Issue.generate!
1633 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1654 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1634 issue4.reload
1655 issue4.reload
1635 issue4.parent_issue_id = issue1.id
1656 issue4.parent_issue_id = issue1.id
1636 assert !issue4.valid?
1657 assert !issue4.valid?
1637 assert_include 'Parent task is invalid', issue4.errors.full_messages
1658 assert_include 'Parent task is invalid', issue4.errors.full_messages
1638 end
1659 end
1639
1660
1640 def test_overdue
1661 def test_overdue
1641 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1662 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1642 assert !Issue.new(:due_date => Date.today).overdue?
1663 assert !Issue.new(:due_date => Date.today).overdue?
1643 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1664 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1644 assert !Issue.new(:due_date => nil).overdue?
1665 assert !Issue.new(:due_date => nil).overdue?
1645 assert !Issue.new(:due_date => 1.day.ago.to_date,
1666 assert !Issue.new(:due_date => 1.day.ago.to_date,
1646 :status => IssueStatus.where(:is_closed => true).first
1667 :status => IssueStatus.where(:is_closed => true).first
1647 ).overdue?
1668 ).overdue?
1648 end
1669 end
1649
1670
1650 test "#behind_schedule? should be false if the issue has no start_date" do
1671 test "#behind_schedule? should be false if the issue has no start_date" do
1651 assert !Issue.new(:start_date => nil,
1672 assert !Issue.new(:start_date => nil,
1652 :due_date => 1.day.from_now.to_date,
1673 :due_date => 1.day.from_now.to_date,
1653 :done_ratio => 0).behind_schedule?
1674 :done_ratio => 0).behind_schedule?
1654 end
1675 end
1655
1676
1656 test "#behind_schedule? should be false if the issue has no end_date" do
1677 test "#behind_schedule? should be false if the issue has no end_date" do
1657 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1678 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1658 :due_date => nil,
1679 :due_date => nil,
1659 :done_ratio => 0).behind_schedule?
1680 :done_ratio => 0).behind_schedule?
1660 end
1681 end
1661
1682
1662 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1683 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1663 assert !Issue.new(:start_date => 50.days.ago.to_date,
1684 assert !Issue.new(:start_date => 50.days.ago.to_date,
1664 :due_date => 50.days.from_now.to_date,
1685 :due_date => 50.days.from_now.to_date,
1665 :done_ratio => 90).behind_schedule?
1686 :done_ratio => 90).behind_schedule?
1666 end
1687 end
1667
1688
1668 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1689 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1669 assert Issue.new(:start_date => 1.day.ago.to_date,
1690 assert Issue.new(:start_date => 1.day.ago.to_date,
1670 :due_date => 1.day.from_now.to_date,
1691 :due_date => 1.day.from_now.to_date,
1671 :done_ratio => 0).behind_schedule?
1692 :done_ratio => 0).behind_schedule?
1672 end
1693 end
1673
1694
1674 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1695 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1675 assert Issue.new(:start_date => 100.days.ago.to_date,
1696 assert Issue.new(:start_date => 100.days.ago.to_date,
1676 :due_date => Date.today,
1697 :due_date => Date.today,
1677 :done_ratio => 90).behind_schedule?
1698 :done_ratio => 90).behind_schedule?
1678 end
1699 end
1679
1700
1680 test "#assignable_users should be Users" do
1701 test "#assignable_users should be Users" do
1681 assert_kind_of User, Issue.find(1).assignable_users.first
1702 assert_kind_of User, Issue.find(1).assignable_users.first
1682 end
1703 end
1683
1704
1684 test "#assignable_users should include the issue author" do
1705 test "#assignable_users should include the issue author" do
1685 non_project_member = User.generate!
1706 non_project_member = User.generate!
1686 issue = Issue.generate!(:author => non_project_member)
1707 issue = Issue.generate!(:author => non_project_member)
1687
1708
1688 assert issue.assignable_users.include?(non_project_member)
1709 assert issue.assignable_users.include?(non_project_member)
1689 end
1710 end
1690
1711
1691 test "#assignable_users should include the current assignee" do
1712 test "#assignable_users should include the current assignee" do
1692 user = User.generate!
1713 user = User.generate!
1693 issue = Issue.generate!(:assigned_to => user)
1714 issue = Issue.generate!(:assigned_to => user)
1694 user.lock!
1715 user.lock!
1695
1716
1696 assert Issue.find(issue.id).assignable_users.include?(user)
1717 assert Issue.find(issue.id).assignable_users.include?(user)
1697 end
1718 end
1698
1719
1699 test "#assignable_users should not show the issue author twice" do
1720 test "#assignable_users should not show the issue author twice" do
1700 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1721 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1701 assert_equal 2, assignable_user_ids.length
1722 assert_equal 2, assignable_user_ids.length
1702
1723
1703 assignable_user_ids.each do |user_id|
1724 assignable_user_ids.each do |user_id|
1704 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1725 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1705 "User #{user_id} appears more or less than once"
1726 "User #{user_id} appears more or less than once"
1706 end
1727 end
1707 end
1728 end
1708
1729
1709 test "#assignable_users with issue_group_assignment should include groups" do
1730 test "#assignable_users with issue_group_assignment should include groups" do
1710 issue = Issue.new(:project => Project.find(2))
1731 issue = Issue.new(:project => Project.find(2))
1711
1732
1712 with_settings :issue_group_assignment => '1' do
1733 with_settings :issue_group_assignment => '1' do
1713 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1734 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1714 assert issue.assignable_users.include?(Group.find(11))
1735 assert issue.assignable_users.include?(Group.find(11))
1715 end
1736 end
1716 end
1737 end
1717
1738
1718 test "#assignable_users without issue_group_assignment should not include groups" do
1739 test "#assignable_users without issue_group_assignment should not include groups" do
1719 issue = Issue.new(:project => Project.find(2))
1740 issue = Issue.new(:project => Project.find(2))
1720
1741
1721 with_settings :issue_group_assignment => '0' do
1742 with_settings :issue_group_assignment => '0' do
1722 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1743 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1723 assert !issue.assignable_users.include?(Group.find(11))
1744 assert !issue.assignable_users.include?(Group.find(11))
1724 end
1745 end
1725 end
1746 end
1726
1747
1748 def test_assignable_users_should_not_include_builtin_groups
1749 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [1])
1750 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [1])
1751 issue = Issue.new(:project => Project.find(1))
1752
1753 with_settings :issue_group_assignment => '1' do
1754 assert_nil issue.assignable_users.detect {|u| u.is_a?(GroupBuiltin)}
1755 end
1756 end
1757
1727 def test_create_should_send_email_notification
1758 def test_create_should_send_email_notification
1728 ActionMailer::Base.deliveries.clear
1759 ActionMailer::Base.deliveries.clear
1729 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1760 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1730 :author_id => 3, :status_id => 1,
1761 :author_id => 3, :status_id => 1,
1731 :priority => IssuePriority.all.first,
1762 :priority => IssuePriority.all.first,
1732 :subject => 'test_create', :estimated_hours => '1:30')
1763 :subject => 'test_create', :estimated_hours => '1:30')
1733 with_settings :notified_events => %w(issue_added) do
1764 with_settings :notified_events => %w(issue_added) do
1734 assert issue.save
1765 assert issue.save
1735 assert_equal 1, ActionMailer::Base.deliveries.size
1766 assert_equal 1, ActionMailer::Base.deliveries.size
1736 end
1767 end
1737 end
1768 end
1738
1769
1739 def test_create_should_send_one_email_notification_with_both_settings
1770 def test_create_should_send_one_email_notification_with_both_settings
1740 ActionMailer::Base.deliveries.clear
1771 ActionMailer::Base.deliveries.clear
1741 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1772 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1742 :author_id => 3, :status_id => 1,
1773 :author_id => 3, :status_id => 1,
1743 :priority => IssuePriority.all.first,
1774 :priority => IssuePriority.all.first,
1744 :subject => 'test_create', :estimated_hours => '1:30')
1775 :subject => 'test_create', :estimated_hours => '1:30')
1745 with_settings :notified_events => %w(issue_added issue_updated) do
1776 with_settings :notified_events => %w(issue_added issue_updated) do
1746 assert issue.save
1777 assert issue.save
1747 assert_equal 1, ActionMailer::Base.deliveries.size
1778 assert_equal 1, ActionMailer::Base.deliveries.size
1748 end
1779 end
1749 end
1780 end
1750
1781
1751 def test_create_should_not_send_email_notification_with_no_setting
1782 def test_create_should_not_send_email_notification_with_no_setting
1752 ActionMailer::Base.deliveries.clear
1783 ActionMailer::Base.deliveries.clear
1753 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1784 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1754 :author_id => 3, :status_id => 1,
1785 :author_id => 3, :status_id => 1,
1755 :priority => IssuePriority.all.first,
1786 :priority => IssuePriority.all.first,
1756 :subject => 'test_create', :estimated_hours => '1:30')
1787 :subject => 'test_create', :estimated_hours => '1:30')
1757 with_settings :notified_events => [] do
1788 with_settings :notified_events => [] do
1758 assert issue.save
1789 assert issue.save
1759 assert_equal 0, ActionMailer::Base.deliveries.size
1790 assert_equal 0, ActionMailer::Base.deliveries.size
1760 end
1791 end
1761 end
1792 end
1762
1793
1763 def test_update_should_notify_previous_assignee
1794 def test_update_should_notify_previous_assignee
1764 ActionMailer::Base.deliveries.clear
1795 ActionMailer::Base.deliveries.clear
1765 user = User.find(3)
1796 user = User.find(3)
1766 user.members.update_all ["mail_notification = ?", false]
1797 user.members.update_all ["mail_notification = ?", false]
1767 user.update_attribute :mail_notification, 'only_assigned'
1798 user.update_attribute :mail_notification, 'only_assigned'
1768
1799
1769 issue = Issue.find(2)
1800 issue = Issue.find(2)
1770 issue.init_journal User.find(1)
1801 issue.init_journal User.find(1)
1771 issue.assigned_to = nil
1802 issue.assigned_to = nil
1772 issue.save!
1803 issue.save!
1773 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
1804 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
1774 end
1805 end
1775
1806
1776 def test_stale_issue_should_not_send_email_notification
1807 def test_stale_issue_should_not_send_email_notification
1777 ActionMailer::Base.deliveries.clear
1808 ActionMailer::Base.deliveries.clear
1778 issue = Issue.find(1)
1809 issue = Issue.find(1)
1779 stale = Issue.find(1)
1810 stale = Issue.find(1)
1780
1811
1781 issue.init_journal(User.find(1))
1812 issue.init_journal(User.find(1))
1782 issue.subject = 'Subjet update'
1813 issue.subject = 'Subjet update'
1783 with_settings :notified_events => %w(issue_updated) do
1814 with_settings :notified_events => %w(issue_updated) do
1784 assert issue.save
1815 assert issue.save
1785 assert_equal 1, ActionMailer::Base.deliveries.size
1816 assert_equal 1, ActionMailer::Base.deliveries.size
1786 ActionMailer::Base.deliveries.clear
1817 ActionMailer::Base.deliveries.clear
1787
1818
1788 stale.init_journal(User.find(1))
1819 stale.init_journal(User.find(1))
1789 stale.subject = 'Another subjet update'
1820 stale.subject = 'Another subjet update'
1790 assert_raise ActiveRecord::StaleObjectError do
1821 assert_raise ActiveRecord::StaleObjectError do
1791 stale.save
1822 stale.save
1792 end
1823 end
1793 assert ActionMailer::Base.deliveries.empty?
1824 assert ActionMailer::Base.deliveries.empty?
1794 end
1825 end
1795 end
1826 end
1796
1827
1797 def test_journalized_description
1828 def test_journalized_description
1798 IssueCustomField.delete_all
1829 IssueCustomField.delete_all
1799
1830
1800 i = Issue.first
1831 i = Issue.first
1801 old_description = i.description
1832 old_description = i.description
1802 new_description = "This is the new description"
1833 new_description = "This is the new description"
1803
1834
1804 i.init_journal(User.find(2))
1835 i.init_journal(User.find(2))
1805 i.description = new_description
1836 i.description = new_description
1806 assert_difference 'Journal.count', 1 do
1837 assert_difference 'Journal.count', 1 do
1807 assert_difference 'JournalDetail.count', 1 do
1838 assert_difference 'JournalDetail.count', 1 do
1808 i.save!
1839 i.save!
1809 end
1840 end
1810 end
1841 end
1811
1842
1812 detail = JournalDetail.order('id DESC').first
1843 detail = JournalDetail.order('id DESC').first
1813 assert_equal i, detail.journal.journalized
1844 assert_equal i, detail.journal.journalized
1814 assert_equal 'attr', detail.property
1845 assert_equal 'attr', detail.property
1815 assert_equal 'description', detail.prop_key
1846 assert_equal 'description', detail.prop_key
1816 assert_equal old_description, detail.old_value
1847 assert_equal old_description, detail.old_value
1817 assert_equal new_description, detail.value
1848 assert_equal new_description, detail.value
1818 end
1849 end
1819
1850
1820 def test_blank_descriptions_should_not_be_journalized
1851 def test_blank_descriptions_should_not_be_journalized
1821 IssueCustomField.delete_all
1852 IssueCustomField.delete_all
1822 Issue.where(:id => 1).update_all("description = NULL")
1853 Issue.where(:id => 1).update_all("description = NULL")
1823
1854
1824 i = Issue.find(1)
1855 i = Issue.find(1)
1825 i.init_journal(User.find(2))
1856 i.init_journal(User.find(2))
1826 i.subject = "blank description"
1857 i.subject = "blank description"
1827 i.description = "\r\n"
1858 i.description = "\r\n"
1828
1859
1829 assert_difference 'Journal.count', 1 do
1860 assert_difference 'Journal.count', 1 do
1830 assert_difference 'JournalDetail.count', 1 do
1861 assert_difference 'JournalDetail.count', 1 do
1831 i.save!
1862 i.save!
1832 end
1863 end
1833 end
1864 end
1834 end
1865 end
1835
1866
1836 def test_journalized_multi_custom_field
1867 def test_journalized_multi_custom_field
1837 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1868 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1838 :is_filter => true, :is_for_all => true,
1869 :is_filter => true, :is_for_all => true,
1839 :tracker_ids => [1],
1870 :tracker_ids => [1],
1840 :possible_values => ['value1', 'value2', 'value3'],
1871 :possible_values => ['value1', 'value2', 'value3'],
1841 :multiple => true)
1872 :multiple => true)
1842
1873
1843 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1874 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1844 :subject => 'Test', :author_id => 1)
1875 :subject => 'Test', :author_id => 1)
1845
1876
1846 assert_difference 'Journal.count' do
1877 assert_difference 'Journal.count' do
1847 assert_difference 'JournalDetail.count' do
1878 assert_difference 'JournalDetail.count' do
1848 issue.init_journal(User.first)
1879 issue.init_journal(User.first)
1849 issue.custom_field_values = {field.id => ['value1']}
1880 issue.custom_field_values = {field.id => ['value1']}
1850 issue.save!
1881 issue.save!
1851 end
1882 end
1852 assert_difference 'JournalDetail.count' do
1883 assert_difference 'JournalDetail.count' do
1853 issue.init_journal(User.first)
1884 issue.init_journal(User.first)
1854 issue.custom_field_values = {field.id => ['value1', 'value2']}
1885 issue.custom_field_values = {field.id => ['value1', 'value2']}
1855 issue.save!
1886 issue.save!
1856 end
1887 end
1857 assert_difference 'JournalDetail.count', 2 do
1888 assert_difference 'JournalDetail.count', 2 do
1858 issue.init_journal(User.first)
1889 issue.init_journal(User.first)
1859 issue.custom_field_values = {field.id => ['value3', 'value2']}
1890 issue.custom_field_values = {field.id => ['value3', 'value2']}
1860 issue.save!
1891 issue.save!
1861 end
1892 end
1862 assert_difference 'JournalDetail.count', 2 do
1893 assert_difference 'JournalDetail.count', 2 do
1863 issue.init_journal(User.first)
1894 issue.init_journal(User.first)
1864 issue.custom_field_values = {field.id => nil}
1895 issue.custom_field_values = {field.id => nil}
1865 issue.save!
1896 issue.save!
1866 end
1897 end
1867 end
1898 end
1868 end
1899 end
1869
1900
1870 def test_description_eol_should_be_normalized
1901 def test_description_eol_should_be_normalized
1871 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1902 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1872 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1903 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1873 end
1904 end
1874
1905
1875 def test_saving_twice_should_not_duplicate_journal_details
1906 def test_saving_twice_should_not_duplicate_journal_details
1876 i = Issue.first
1907 i = Issue.first
1877 i.init_journal(User.find(2), 'Some notes')
1908 i.init_journal(User.find(2), 'Some notes')
1878 # initial changes
1909 # initial changes
1879 i.subject = 'New subject'
1910 i.subject = 'New subject'
1880 i.done_ratio = i.done_ratio + 10
1911 i.done_ratio = i.done_ratio + 10
1881 assert_difference 'Journal.count' do
1912 assert_difference 'Journal.count' do
1882 assert i.save
1913 assert i.save
1883 end
1914 end
1884 # 1 more change
1915 # 1 more change
1885 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1916 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1886 assert_no_difference 'Journal.count' do
1917 assert_no_difference 'Journal.count' do
1887 assert_difference 'JournalDetail.count', 1 do
1918 assert_difference 'JournalDetail.count', 1 do
1888 i.save
1919 i.save
1889 end
1920 end
1890 end
1921 end
1891 # no more change
1922 # no more change
1892 assert_no_difference 'Journal.count' do
1923 assert_no_difference 'Journal.count' do
1893 assert_no_difference 'JournalDetail.count' do
1924 assert_no_difference 'JournalDetail.count' do
1894 i.save
1925 i.save
1895 end
1926 end
1896 end
1927 end
1897 end
1928 end
1898
1929
1899 def test_all_dependent_issues
1930 def test_all_dependent_issues
1900 IssueRelation.delete_all
1931 IssueRelation.delete_all
1901 assert IssueRelation.create!(:issue_from => Issue.find(1),
1932 assert IssueRelation.create!(:issue_from => Issue.find(1),
1902 :issue_to => Issue.find(2),
1933 :issue_to => Issue.find(2),
1903 :relation_type => IssueRelation::TYPE_PRECEDES)
1934 :relation_type => IssueRelation::TYPE_PRECEDES)
1904 assert IssueRelation.create!(:issue_from => Issue.find(2),
1935 assert IssueRelation.create!(:issue_from => Issue.find(2),
1905 :issue_to => Issue.find(3),
1936 :issue_to => Issue.find(3),
1906 :relation_type => IssueRelation::TYPE_PRECEDES)
1937 :relation_type => IssueRelation::TYPE_PRECEDES)
1907 assert IssueRelation.create!(:issue_from => Issue.find(3),
1938 assert IssueRelation.create!(:issue_from => Issue.find(3),
1908 :issue_to => Issue.find(8),
1939 :issue_to => Issue.find(8),
1909 :relation_type => IssueRelation::TYPE_PRECEDES)
1940 :relation_type => IssueRelation::TYPE_PRECEDES)
1910
1941
1911 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1942 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1912 end
1943 end
1913
1944
1914 def test_all_dependent_issues_with_subtask
1945 def test_all_dependent_issues_with_subtask
1915 IssueRelation.delete_all
1946 IssueRelation.delete_all
1916
1947
1917 project = Project.generate!(:name => "testproject")
1948 project = Project.generate!(:name => "testproject")
1918
1949
1919 parentIssue = Issue.generate!(:project => project)
1950 parentIssue = Issue.generate!(:project => project)
1920 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1951 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1921 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1952 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1922
1953
1923 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
1954 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
1924 end
1955 end
1925
1956
1926 def test_all_dependent_issues_does_not_include_self
1957 def test_all_dependent_issues_does_not_include_self
1927 IssueRelation.delete_all
1958 IssueRelation.delete_all
1928
1959
1929 project = Project.generate!(:name => "testproject")
1960 project = Project.generate!(:name => "testproject")
1930
1961
1931 parentIssue = Issue.generate!(:project => project)
1962 parentIssue = Issue.generate!(:project => project)
1932 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1963 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1933
1964
1934 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
1965 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
1935 end
1966 end
1936
1967
1937 def test_all_dependent_issues_with_parenttask_and_sibling
1968 def test_all_dependent_issues_with_parenttask_and_sibling
1938 IssueRelation.delete_all
1969 IssueRelation.delete_all
1939
1970
1940 project = Project.generate!(:name => "testproject")
1971 project = Project.generate!(:name => "testproject")
1941
1972
1942 parentIssue = Issue.generate!(:project => project)
1973 parentIssue = Issue.generate!(:project => project)
1943 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1974 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1944 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1975 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1945
1976
1946 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
1977 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
1947 end
1978 end
1948
1979
1949 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
1980 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
1950 IssueRelation.delete_all
1981 IssueRelation.delete_all
1951
1982
1952 project = Project.generate!(:name => "testproject")
1983 project = Project.generate!(:name => "testproject")
1953
1984
1954 parentIssue1 = Issue.generate!(:project => project)
1985 parentIssue1 = Issue.generate!(:project => project)
1955 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1986 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1956 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1987 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1957
1988
1958 parentIssue2 = Issue.generate!(:project => project)
1989 parentIssue2 = Issue.generate!(:project => project)
1959 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1990 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1960 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1991 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1961
1992
1962
1993
1963 assert IssueRelation.create(:issue_from => parentIssue1,
1994 assert IssueRelation.create(:issue_from => parentIssue1,
1964 :issue_to => childIssue2_2,
1995 :issue_to => childIssue2_2,
1965 :relation_type => IssueRelation::TYPE_BLOCKS)
1996 :relation_type => IssueRelation::TYPE_BLOCKS)
1966
1997
1967 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
1998 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
1968 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1999 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1969 end
2000 end
1970
2001
1971 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
2002 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
1972 IssueRelation.delete_all
2003 IssueRelation.delete_all
1973
2004
1974 project = Project.generate!(:name => "testproject")
2005 project = Project.generate!(:name => "testproject")
1975
2006
1976 parentIssue1 = Issue.generate!(:project => project)
2007 parentIssue1 = Issue.generate!(:project => project)
1977 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2008 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1978 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2009 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1979
2010
1980 parentIssue2 = Issue.generate!(:project => project)
2011 parentIssue2 = Issue.generate!(:project => project)
1981 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2012 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1982 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2013 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1983
2014
1984
2015
1985 assert IssueRelation.create(:issue_from => parentIssue1,
2016 assert IssueRelation.create(:issue_from => parentIssue1,
1986 :issue_to => parentIssue2,
2017 :issue_to => parentIssue2,
1987 :relation_type => IssueRelation::TYPE_BLOCKS)
2018 :relation_type => IssueRelation::TYPE_BLOCKS)
1988
2019
1989 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
2020 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
1990 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2021 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1991 end
2022 end
1992
2023
1993 def test_all_dependent_issues_with_transitive_relation
2024 def test_all_dependent_issues_with_transitive_relation
1994 IssueRelation.delete_all
2025 IssueRelation.delete_all
1995
2026
1996 project = Project.generate!(:name => "testproject")
2027 project = Project.generate!(:name => "testproject")
1997
2028
1998 parentIssue1 = Issue.generate!(:project => project)
2029 parentIssue1 = Issue.generate!(:project => project)
1999 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2030 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2000
2031
2001 parentIssue2 = Issue.generate!(:project => project)
2032 parentIssue2 = Issue.generate!(:project => project)
2002 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2033 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2003
2034
2004 independentIssue = Issue.generate!(:project => project)
2035 independentIssue = Issue.generate!(:project => project)
2005
2036
2006 assert IssueRelation.create(:issue_from => parentIssue1,
2037 assert IssueRelation.create(:issue_from => parentIssue1,
2007 :issue_to => childIssue2_1,
2038 :issue_to => childIssue2_1,
2008 :relation_type => IssueRelation::TYPE_RELATES)
2039 :relation_type => IssueRelation::TYPE_RELATES)
2009
2040
2010 assert IssueRelation.create(:issue_from => childIssue2_1,
2041 assert IssueRelation.create(:issue_from => childIssue2_1,
2011 :issue_to => independentIssue,
2042 :issue_to => independentIssue,
2012 :relation_type => IssueRelation::TYPE_RELATES)
2043 :relation_type => IssueRelation::TYPE_RELATES)
2013
2044
2014 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2045 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2015 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2046 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2016 end
2047 end
2017
2048
2018 def test_all_dependent_issues_with_transitive_relation2
2049 def test_all_dependent_issues_with_transitive_relation2
2019 IssueRelation.delete_all
2050 IssueRelation.delete_all
2020
2051
2021 project = Project.generate!(:name => "testproject")
2052 project = Project.generate!(:name => "testproject")
2022
2053
2023 parentIssue1 = Issue.generate!(:project => project)
2054 parentIssue1 = Issue.generate!(:project => project)
2024 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2055 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2025
2056
2026 parentIssue2 = Issue.generate!(:project => project)
2057 parentIssue2 = Issue.generate!(:project => project)
2027 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2058 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2028
2059
2029 independentIssue = Issue.generate!(:project => project)
2060 independentIssue = Issue.generate!(:project => project)
2030
2061
2031 assert IssueRelation.create(:issue_from => parentIssue1,
2062 assert IssueRelation.create(:issue_from => parentIssue1,
2032 :issue_to => independentIssue,
2063 :issue_to => independentIssue,
2033 :relation_type => IssueRelation::TYPE_RELATES)
2064 :relation_type => IssueRelation::TYPE_RELATES)
2034
2065
2035 assert IssueRelation.create(:issue_from => independentIssue,
2066 assert IssueRelation.create(:issue_from => independentIssue,
2036 :issue_to => childIssue2_1,
2067 :issue_to => childIssue2_1,
2037 :relation_type => IssueRelation::TYPE_RELATES)
2068 :relation_type => IssueRelation::TYPE_RELATES)
2038
2069
2039 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2070 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2040 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2071 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2041
2072
2042 end
2073 end
2043
2074
2044 def test_all_dependent_issues_with_persistent_circular_dependency
2075 def test_all_dependent_issues_with_persistent_circular_dependency
2045 IssueRelation.delete_all
2076 IssueRelation.delete_all
2046 assert IssueRelation.create!(:issue_from => Issue.find(1),
2077 assert IssueRelation.create!(:issue_from => Issue.find(1),
2047 :issue_to => Issue.find(2),
2078 :issue_to => Issue.find(2),
2048 :relation_type => IssueRelation::TYPE_PRECEDES)
2079 :relation_type => IssueRelation::TYPE_PRECEDES)
2049 assert IssueRelation.create!(:issue_from => Issue.find(2),
2080 assert IssueRelation.create!(:issue_from => Issue.find(2),
2050 :issue_to => Issue.find(3),
2081 :issue_to => Issue.find(3),
2051 :relation_type => IssueRelation::TYPE_PRECEDES)
2082 :relation_type => IssueRelation::TYPE_PRECEDES)
2052
2083
2053 r = IssueRelation.create!(:issue_from => Issue.find(3),
2084 r = IssueRelation.create!(:issue_from => Issue.find(3),
2054 :issue_to => Issue.find(7),
2085 :issue_to => Issue.find(7),
2055 :relation_type => IssueRelation::TYPE_PRECEDES)
2086 :relation_type => IssueRelation::TYPE_PRECEDES)
2056 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2087 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2057
2088
2058 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
2089 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
2059 end
2090 end
2060
2091
2061 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
2092 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
2062 IssueRelation.delete_all
2093 IssueRelation.delete_all
2063 assert IssueRelation.create!(:issue_from => Issue.find(1),
2094 assert IssueRelation.create!(:issue_from => Issue.find(1),
2064 :issue_to => Issue.find(2),
2095 :issue_to => Issue.find(2),
2065 :relation_type => IssueRelation::TYPE_RELATES)
2096 :relation_type => IssueRelation::TYPE_RELATES)
2066 assert IssueRelation.create!(:issue_from => Issue.find(2),
2097 assert IssueRelation.create!(:issue_from => Issue.find(2),
2067 :issue_to => Issue.find(3),
2098 :issue_to => Issue.find(3),
2068 :relation_type => IssueRelation::TYPE_RELATES)
2099 :relation_type => IssueRelation::TYPE_RELATES)
2069 assert IssueRelation.create!(:issue_from => Issue.find(3),
2100 assert IssueRelation.create!(:issue_from => Issue.find(3),
2070 :issue_to => Issue.find(8),
2101 :issue_to => Issue.find(8),
2071 :relation_type => IssueRelation::TYPE_RELATES)
2102 :relation_type => IssueRelation::TYPE_RELATES)
2072
2103
2073 r = IssueRelation.create!(:issue_from => Issue.find(8),
2104 r = IssueRelation.create!(:issue_from => Issue.find(8),
2074 :issue_to => Issue.find(7),
2105 :issue_to => Issue.find(7),
2075 :relation_type => IssueRelation::TYPE_RELATES)
2106 :relation_type => IssueRelation::TYPE_RELATES)
2076 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 2")
2107 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 2")
2077
2108
2078 r = IssueRelation.create!(:issue_from => Issue.find(3),
2109 r = IssueRelation.create!(:issue_from => Issue.find(3),
2079 :issue_to => Issue.find(7),
2110 :issue_to => Issue.find(7),
2080 :relation_type => IssueRelation::TYPE_RELATES)
2111 :relation_type => IssueRelation::TYPE_RELATES)
2081 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2112 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2082
2113
2083 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2114 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2084 end
2115 end
2085
2116
2086 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2117 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2087 @issue = Issue.find(1)
2118 @issue = Issue.find(1)
2088 @issue_status = IssueStatus.find(1)
2119 @issue_status = IssueStatus.find(1)
2089 @issue_status.update_attribute(:default_done_ratio, 50)
2120 @issue_status.update_attribute(:default_done_ratio, 50)
2090 @issue2 = Issue.find(2)
2121 @issue2 = Issue.find(2)
2091 @issue_status2 = IssueStatus.find(2)
2122 @issue_status2 = IssueStatus.find(2)
2092 @issue_status2.update_attribute(:default_done_ratio, 0)
2123 @issue_status2.update_attribute(:default_done_ratio, 0)
2093
2124
2094 with_settings :issue_done_ratio => 'issue_field' do
2125 with_settings :issue_done_ratio => 'issue_field' do
2095 assert_equal 0, @issue.done_ratio
2126 assert_equal 0, @issue.done_ratio
2096 assert_equal 30, @issue2.done_ratio
2127 assert_equal 30, @issue2.done_ratio
2097 end
2128 end
2098
2129
2099 with_settings :issue_done_ratio => 'issue_status' do
2130 with_settings :issue_done_ratio => 'issue_status' do
2100 assert_equal 50, @issue.done_ratio
2131 assert_equal 50, @issue.done_ratio
2101 assert_equal 0, @issue2.done_ratio
2132 assert_equal 0, @issue2.done_ratio
2102 end
2133 end
2103 end
2134 end
2104
2135
2105 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2136 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2106 @issue = Issue.find(1)
2137 @issue = Issue.find(1)
2107 @issue_status = IssueStatus.find(1)
2138 @issue_status = IssueStatus.find(1)
2108 @issue_status.update_attribute(:default_done_ratio, 50)
2139 @issue_status.update_attribute(:default_done_ratio, 50)
2109 @issue2 = Issue.find(2)
2140 @issue2 = Issue.find(2)
2110 @issue_status2 = IssueStatus.find(2)
2141 @issue_status2 = IssueStatus.find(2)
2111 @issue_status2.update_attribute(:default_done_ratio, 0)
2142 @issue_status2.update_attribute(:default_done_ratio, 0)
2112
2143
2113 with_settings :issue_done_ratio => 'issue_field' do
2144 with_settings :issue_done_ratio => 'issue_field' do
2114 @issue.update_done_ratio_from_issue_status
2145 @issue.update_done_ratio_from_issue_status
2115 @issue2.update_done_ratio_from_issue_status
2146 @issue2.update_done_ratio_from_issue_status
2116
2147
2117 assert_equal 0, @issue.read_attribute(:done_ratio)
2148 assert_equal 0, @issue.read_attribute(:done_ratio)
2118 assert_equal 30, @issue2.read_attribute(:done_ratio)
2149 assert_equal 30, @issue2.read_attribute(:done_ratio)
2119 end
2150 end
2120
2151
2121 with_settings :issue_done_ratio => 'issue_status' do
2152 with_settings :issue_done_ratio => 'issue_status' do
2122 @issue.update_done_ratio_from_issue_status
2153 @issue.update_done_ratio_from_issue_status
2123 @issue2.update_done_ratio_from_issue_status
2154 @issue2.update_done_ratio_from_issue_status
2124
2155
2125 assert_equal 50, @issue.read_attribute(:done_ratio)
2156 assert_equal 50, @issue.read_attribute(:done_ratio)
2126 assert_equal 0, @issue2.read_attribute(:done_ratio)
2157 assert_equal 0, @issue2.read_attribute(:done_ratio)
2127 end
2158 end
2128 end
2159 end
2129
2160
2130 test "#by_tracker" do
2161 test "#by_tracker" do
2131 User.current = User.anonymous
2162 User.current = User.anonymous
2132 groups = Issue.by_tracker(Project.find(1))
2163 groups = Issue.by_tracker(Project.find(1))
2133 assert_equal 3, groups.count
2164 assert_equal 3, groups.count
2134 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2165 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2135 end
2166 end
2136
2167
2137 test "#by_version" do
2168 test "#by_version" do
2138 User.current = User.anonymous
2169 User.current = User.anonymous
2139 groups = Issue.by_version(Project.find(1))
2170 groups = Issue.by_version(Project.find(1))
2140 assert_equal 3, groups.count
2171 assert_equal 3, groups.count
2141 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2172 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2142 end
2173 end
2143
2174
2144 test "#by_priority" do
2175 test "#by_priority" do
2145 User.current = User.anonymous
2176 User.current = User.anonymous
2146 groups = Issue.by_priority(Project.find(1))
2177 groups = Issue.by_priority(Project.find(1))
2147 assert_equal 4, groups.count
2178 assert_equal 4, groups.count
2148 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2179 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2149 end
2180 end
2150
2181
2151 test "#by_category" do
2182 test "#by_category" do
2152 User.current = User.anonymous
2183 User.current = User.anonymous
2153 groups = Issue.by_category(Project.find(1))
2184 groups = Issue.by_category(Project.find(1))
2154 assert_equal 2, groups.count
2185 assert_equal 2, groups.count
2155 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2186 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2156 end
2187 end
2157
2188
2158 test "#by_assigned_to" do
2189 test "#by_assigned_to" do
2159 User.current = User.anonymous
2190 User.current = User.anonymous
2160 groups = Issue.by_assigned_to(Project.find(1))
2191 groups = Issue.by_assigned_to(Project.find(1))
2161 assert_equal 2, groups.count
2192 assert_equal 2, groups.count
2162 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2193 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2163 end
2194 end
2164
2195
2165 test "#by_author" do
2196 test "#by_author" do
2166 User.current = User.anonymous
2197 User.current = User.anonymous
2167 groups = Issue.by_author(Project.find(1))
2198 groups = Issue.by_author(Project.find(1))
2168 assert_equal 4, groups.count
2199 assert_equal 4, groups.count
2169 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2200 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2170 end
2201 end
2171
2202
2172 test "#by_subproject" do
2203 test "#by_subproject" do
2173 User.current = User.anonymous
2204 User.current = User.anonymous
2174 groups = Issue.by_subproject(Project.find(1))
2205 groups = Issue.by_subproject(Project.find(1))
2175 # Private descendant not visible
2206 # Private descendant not visible
2176 assert_equal 1, groups.count
2207 assert_equal 1, groups.count
2177 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2208 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2178 end
2209 end
2179
2210
2180 def test_recently_updated_scope
2211 def test_recently_updated_scope
2181 #should return the last updated issue
2212 #should return the last updated issue
2182 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2213 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2183 end
2214 end
2184
2215
2185 def test_on_active_projects_scope
2216 def test_on_active_projects_scope
2186 assert Project.find(2).archive
2217 assert Project.find(2).archive
2187
2218
2188 before = Issue.on_active_project.length
2219 before = Issue.on_active_project.length
2189 # test inclusion to results
2220 # test inclusion to results
2190 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2221 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2191 assert_equal before + 1, Issue.on_active_project.length
2222 assert_equal before + 1, Issue.on_active_project.length
2192
2223
2193 # Move to an archived project
2224 # Move to an archived project
2194 issue.project = Project.find(2)
2225 issue.project = Project.find(2)
2195 assert issue.save
2226 assert issue.save
2196 assert_equal before, Issue.on_active_project.length
2227 assert_equal before, Issue.on_active_project.length
2197 end
2228 end
2198
2229
2199 test "Issue#recipients should include project recipients" do
2230 test "Issue#recipients should include project recipients" do
2200 issue = Issue.generate!
2231 issue = Issue.generate!
2201 assert issue.project.recipients.present?
2232 assert issue.project.recipients.present?
2202 issue.project.recipients.each do |project_recipient|
2233 issue.project.recipients.each do |project_recipient|
2203 assert issue.recipients.include?(project_recipient)
2234 assert issue.recipients.include?(project_recipient)
2204 end
2235 end
2205 end
2236 end
2206
2237
2207 test "Issue#recipients should include the author if the author is active" do
2238 test "Issue#recipients should include the author if the author is active" do
2208 issue = Issue.generate!(:author => User.generate!)
2239 issue = Issue.generate!(:author => User.generate!)
2209 assert issue.author, "No author set for Issue"
2240 assert issue.author, "No author set for Issue"
2210 assert issue.recipients.include?(issue.author.mail)
2241 assert issue.recipients.include?(issue.author.mail)
2211 end
2242 end
2212
2243
2213 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2244 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2214 issue = Issue.generate!(:assigned_to => User.generate!)
2245 issue = Issue.generate!(:assigned_to => User.generate!)
2215 assert issue.assigned_to, "No assigned_to set for Issue"
2246 assert issue.assigned_to, "No assigned_to set for Issue"
2216 assert issue.recipients.include?(issue.assigned_to.mail)
2247 assert issue.recipients.include?(issue.assigned_to.mail)
2217 end
2248 end
2218
2249
2219 test "Issue#recipients should not include users who opt out of all email" do
2250 test "Issue#recipients should not include users who opt out of all email" do
2220 issue = Issue.generate!(:author => User.generate!)
2251 issue = Issue.generate!(:author => User.generate!)
2221 issue.author.update_attribute(:mail_notification, :none)
2252 issue.author.update_attribute(:mail_notification, :none)
2222 assert !issue.recipients.include?(issue.author.mail)
2253 assert !issue.recipients.include?(issue.author.mail)
2223 end
2254 end
2224
2255
2225 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2256 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2226 issue = Issue.generate!(:author => User.generate!)
2257 issue = Issue.generate!(:author => User.generate!)
2227 issue.author.update_attribute(:mail_notification, :only_assigned)
2258 issue.author.update_attribute(:mail_notification, :only_assigned)
2228 assert !issue.recipients.include?(issue.author.mail)
2259 assert !issue.recipients.include?(issue.author.mail)
2229 end
2260 end
2230
2261
2231 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2262 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2232 issue = Issue.generate!(:assigned_to => User.generate!)
2263 issue = Issue.generate!(:assigned_to => User.generate!)
2233 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2264 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2234 assert !issue.recipients.include?(issue.assigned_to.mail)
2265 assert !issue.recipients.include?(issue.assigned_to.mail)
2235 end
2266 end
2236
2267
2237 def test_last_journal_id_with_journals_should_return_the_journal_id
2268 def test_last_journal_id_with_journals_should_return_the_journal_id
2238 assert_equal 2, Issue.find(1).last_journal_id
2269 assert_equal 2, Issue.find(1).last_journal_id
2239 end
2270 end
2240
2271
2241 def test_last_journal_id_without_journals_should_return_nil
2272 def test_last_journal_id_without_journals_should_return_nil
2242 assert_nil Issue.find(3).last_journal_id
2273 assert_nil Issue.find(3).last_journal_id
2243 end
2274 end
2244
2275
2245 def test_journals_after_should_return_journals_with_greater_id
2276 def test_journals_after_should_return_journals_with_greater_id
2246 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2277 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2247 assert_equal [], Issue.find(1).journals_after('2')
2278 assert_equal [], Issue.find(1).journals_after('2')
2248 end
2279 end
2249
2280
2250 def test_journals_after_with_blank_arg_should_return_all_journals
2281 def test_journals_after_with_blank_arg_should_return_all_journals
2251 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2282 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2252 end
2283 end
2253
2284
2254 def test_css_classes_should_include_tracker
2285 def test_css_classes_should_include_tracker
2255 issue = Issue.new(:tracker => Tracker.find(2))
2286 issue = Issue.new(:tracker => Tracker.find(2))
2256 classes = issue.css_classes.split(' ')
2287 classes = issue.css_classes.split(' ')
2257 assert_include 'tracker-2', classes
2288 assert_include 'tracker-2', classes
2258 end
2289 end
2259
2290
2260 def test_css_classes_should_include_priority
2291 def test_css_classes_should_include_priority
2261 issue = Issue.new(:priority => IssuePriority.find(8))
2292 issue = Issue.new(:priority => IssuePriority.find(8))
2262 classes = issue.css_classes.split(' ')
2293 classes = issue.css_classes.split(' ')
2263 assert_include 'priority-8', classes
2294 assert_include 'priority-8', classes
2264 assert_include 'priority-highest', classes
2295 assert_include 'priority-highest', classes
2265 end
2296 end
2266
2297
2267 def test_css_classes_should_include_user_and_group_assignment
2298 def test_css_classes_should_include_user_and_group_assignment
2268 project = Project.first
2299 project = Project.first
2269 user = User.generate!
2300 user = User.generate!
2270 group = Group.generate!
2301 group = Group.generate!
2271 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2302 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2272 group.users << user
2303 group.users << user
2273 assert user.member_of?(project)
2304 assert user.member_of?(project)
2274 issue1 = Issue.generate(:assigned_to_id => group.id)
2305 issue1 = Issue.generate(:assigned_to_id => group.id)
2275 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2306 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2276 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2307 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2277 issue2 = Issue.generate(:assigned_to_id => user.id)
2308 issue2 = Issue.generate(:assigned_to_id => user.id)
2278 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2309 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2279 assert_include 'assigned-to-me', issue2.css_classes(user)
2310 assert_include 'assigned-to-me', issue2.css_classes(user)
2280 end
2311 end
2281
2312
2282 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2313 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2283 set_tmp_attachments_directory
2314 set_tmp_attachments_directory
2284 issue = Issue.generate!
2315 issue = Issue.generate!
2285 issue.save_attachments({
2316 issue.save_attachments({
2286 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2317 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2287 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2318 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2288 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2319 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2289 })
2320 })
2290 issue.attach_saved_attachments
2321 issue.attach_saved_attachments
2291
2322
2292 assert_equal 3, issue.reload.attachments.count
2323 assert_equal 3, issue.reload.attachments.count
2293 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2324 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2294 end
2325 end
2295
2326
2296 def test_closed_on_should_be_nil_when_creating_an_open_issue
2327 def test_closed_on_should_be_nil_when_creating_an_open_issue
2297 issue = Issue.generate!(:status_id => 1).reload
2328 issue = Issue.generate!(:status_id => 1).reload
2298 assert !issue.closed?
2329 assert !issue.closed?
2299 assert_nil issue.closed_on
2330 assert_nil issue.closed_on
2300 end
2331 end
2301
2332
2302 def test_closed_on_should_be_set_when_creating_a_closed_issue
2333 def test_closed_on_should_be_set_when_creating_a_closed_issue
2303 issue = Issue.generate!(:status_id => 5).reload
2334 issue = Issue.generate!(:status_id => 5).reload
2304 assert issue.closed?
2335 assert issue.closed?
2305 assert_not_nil issue.closed_on
2336 assert_not_nil issue.closed_on
2306 assert_equal issue.updated_on, issue.closed_on
2337 assert_equal issue.updated_on, issue.closed_on
2307 assert_equal issue.created_on, issue.closed_on
2338 assert_equal issue.created_on, issue.closed_on
2308 end
2339 end
2309
2340
2310 def test_closed_on_should_be_nil_when_updating_an_open_issue
2341 def test_closed_on_should_be_nil_when_updating_an_open_issue
2311 issue = Issue.find(1)
2342 issue = Issue.find(1)
2312 issue.subject = 'Not closed yet'
2343 issue.subject = 'Not closed yet'
2313 issue.save!
2344 issue.save!
2314 issue.reload
2345 issue.reload
2315 assert_nil issue.closed_on
2346 assert_nil issue.closed_on
2316 end
2347 end
2317
2348
2318 def test_closed_on_should_be_set_when_closing_an_open_issue
2349 def test_closed_on_should_be_set_when_closing_an_open_issue
2319 issue = Issue.find(1)
2350 issue = Issue.find(1)
2320 issue.subject = 'Now closed'
2351 issue.subject = 'Now closed'
2321 issue.status_id = 5
2352 issue.status_id = 5
2322 issue.save!
2353 issue.save!
2323 issue.reload
2354 issue.reload
2324 assert_not_nil issue.closed_on
2355 assert_not_nil issue.closed_on
2325 assert_equal issue.updated_on, issue.closed_on
2356 assert_equal issue.updated_on, issue.closed_on
2326 end
2357 end
2327
2358
2328 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2359 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2329 issue = Issue.open(false).first
2360 issue = Issue.open(false).first
2330 was_closed_on = issue.closed_on
2361 was_closed_on = issue.closed_on
2331 assert_not_nil was_closed_on
2362 assert_not_nil was_closed_on
2332 issue.subject = 'Updating a closed issue'
2363 issue.subject = 'Updating a closed issue'
2333 issue.save!
2364 issue.save!
2334 issue.reload
2365 issue.reload
2335 assert_equal was_closed_on, issue.closed_on
2366 assert_equal was_closed_on, issue.closed_on
2336 end
2367 end
2337
2368
2338 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2369 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2339 issue = Issue.open(false).first
2370 issue = Issue.open(false).first
2340 was_closed_on = issue.closed_on
2371 was_closed_on = issue.closed_on
2341 assert_not_nil was_closed_on
2372 assert_not_nil was_closed_on
2342 issue.subject = 'Reopening a closed issue'
2373 issue.subject = 'Reopening a closed issue'
2343 issue.status_id = 1
2374 issue.status_id = 1
2344 issue.save!
2375 issue.save!
2345 issue.reload
2376 issue.reload
2346 assert !issue.closed?
2377 assert !issue.closed?
2347 assert_equal was_closed_on, issue.closed_on
2378 assert_equal was_closed_on, issue.closed_on
2348 end
2379 end
2349
2380
2350 def test_status_was_should_return_nil_for_new_issue
2381 def test_status_was_should_return_nil_for_new_issue
2351 issue = Issue.new
2382 issue = Issue.new
2352 assert_nil issue.status_was
2383 assert_nil issue.status_was
2353 end
2384 end
2354
2385
2355 def test_status_was_should_return_status_before_change
2386 def test_status_was_should_return_status_before_change
2356 issue = Issue.find(1)
2387 issue = Issue.find(1)
2357 issue.status = IssueStatus.find(2)
2388 issue.status = IssueStatus.find(2)
2358 assert_equal IssueStatus.find(1), issue.status_was
2389 assert_equal IssueStatus.find(1), issue.status_was
2359 end
2390 end
2360
2391
2361 def test_status_was_should_be_reset_on_save
2392 def test_status_was_should_be_reset_on_save
2362 issue = Issue.find(1)
2393 issue = Issue.find(1)
2363 issue.status = IssueStatus.find(2)
2394 issue.status = IssueStatus.find(2)
2364 assert_equal IssueStatus.find(1), issue.status_was
2395 assert_equal IssueStatus.find(1), issue.status_was
2365 assert issue.save!
2396 assert issue.save!
2366 assert_equal IssueStatus.find(2), issue.status_was
2397 assert_equal IssueStatus.find(2), issue.status_was
2367 end
2398 end
2368 end
2399 end
@@ -1,120 +1,114
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class PrincipalTest < ActiveSupport::TestCase
22 class PrincipalTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :members, :member_roles
23 fixtures :users, :projects, :members, :member_roles
24
24
25 def test_active_scope_should_return_groups_and_active_users
25 def test_active_scope_should_return_groups_and_active_users
26 result = Principal.active.all
26 result = Principal.active.all
27 assert_include Group.first, result
27 assert_include Group.first, result
28 assert_not_nil result.detect {|p| p.is_a?(User)}
28 assert_not_nil result.detect {|p| p.is_a?(User)}
29 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
29 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
30 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
30 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
31 end
31 end
32
32
33 def test_member_of_scope_should_return_the_union_of_all_members
33 def test_member_of_scope_should_return_the_union_of_all_members
34 projects = Project.find([1])
34 projects = Project.find([1])
35 assert_equal [3, 2], Principal.member_of(projects).sort.map(&:id)
35 assert_equal [3, 2], Principal.member_of(projects).sort.map(&:id)
36 projects = Project.find([1, 2])
36 projects = Project.find([1, 2])
37 assert_equal [3, 2, 8, 11], Principal.member_of(projects).sort.map(&:id)
37 assert_equal [3, 2, 8, 11], Principal.member_of(projects).sort.map(&:id)
38 end
38 end
39
39
40 def test_member_of_scope_should_be_empty_for_no_projects
40 def test_member_of_scope_should_be_empty_for_no_projects
41 assert_equal [], Principal.member_of([]).sort
41 assert_equal [], Principal.member_of([]).sort
42 end
42 end
43
43
44 def test_not_member_of_scope_should_return_users_that_have_no_memberships
44 def test_not_member_of_scope_should_return_users_that_have_no_memberships
45 [[1], [1, 2]].each do |ids|
45 [[1], [1, 2]].each do |ids|
46 projects = Project.find(ids)
46 projects = Project.find(ids)
47 assert_equal ids.size, projects.count
47 assert_equal ids.size, projects.count
48 expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort
48 expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort
49 assert_equal expected, Principal.not_member_of(projects).sort
49 assert_equal expected, Principal.not_member_of(projects).sort
50 end
50 end
51 end
51 end
52
52
53 def test_not_member_of_scope_should_be_empty_for_no_projects
53 def test_not_member_of_scope_should_be_empty_for_no_projects
54 assert_equal [], Principal.not_member_of([]).sort
54 assert_equal [], Principal.not_member_of([]).sort
55 end
55 end
56
56
57 def test_sorted_scope_should_sort_users_before_groups
57 def test_sorted_scope_should_sort_users_before_groups
58 scope = Principal.where("type <> ?", 'AnonymousUser')
58 scope = Principal.where(:type => ['User', 'Group'])
59 expected_order = scope.all.sort do |a, b|
59 users = scope.select {|p| p.is_a?(User)}.sort
60 if a.is_a?(User) && b.is_a?(Group)
60 groups = scope.select {|p| p.is_a?(Group)}.sort
61 -1
61
62 elsif a.is_a?(Group) && b.is_a?(User)
62 assert_equal (users + groups).map(&:name).map(&:downcase),
63 1
64 else
65 a.name.downcase <=> b.name.downcase
66 end
67 end
68 assert_equal expected_order.map(&:name).map(&:downcase),
69 scope.sorted.map(&:name).map(&:downcase)
63 scope.sorted.map(&:name).map(&:downcase)
70 end
64 end
71
65
72 test "like scope should search login" do
66 test "like scope should search login" do
73 results = Principal.like('jsmi')
67 results = Principal.like('jsmi')
74
68
75 assert results.any?
69 assert results.any?
76 assert results.all? {|u| u.login.match(/jsmi/i) }
70 assert results.all? {|u| u.login.match(/jsmi/i) }
77 end
71 end
78
72
79 test "like scope should search firstname" do
73 test "like scope should search firstname" do
80 results = Principal.like('john')
74 results = Principal.like('john')
81
75
82 assert results.any?
76 assert results.any?
83 assert results.all? {|u| u.firstname.match(/john/i) }
77 assert results.all? {|u| u.firstname.match(/john/i) }
84 end
78 end
85
79
86 test "like scope should search lastname" do
80 test "like scope should search lastname" do
87 results = Principal.like('smi')
81 results = Principal.like('smi')
88
82
89 assert results.any?
83 assert results.any?
90 assert results.all? {|u| u.lastname.match(/smi/i) }
84 assert results.all? {|u| u.lastname.match(/smi/i) }
91 end
85 end
92
86
93 test "like scope should search mail" do
87 test "like scope should search mail" do
94 results = Principal.like('somenet')
88 results = Principal.like('somenet')
95
89
96 assert results.any?
90 assert results.any?
97 assert results.all? {|u| u.mail.match(/somenet/i) }
91 assert results.all? {|u| u.mail.match(/somenet/i) }
98 end
92 end
99
93
100 test "like scope should search firstname and lastname" do
94 test "like scope should search firstname and lastname" do
101 results = Principal.like('john smi')
95 results = Principal.like('john smi')
102
96
103 assert_equal 1, results.count
97 assert_equal 1, results.count
104 assert_equal User.find(2), results.first
98 assert_equal User.find(2), results.first
105 end
99 end
106
100
107 test "like scope should search lastname and firstname" do
101 test "like scope should search lastname and firstname" do
108 results = Principal.like('smith joh')
102 results = Principal.like('smith joh')
109
103
110 assert_equal 1, results.count
104 assert_equal 1, results.count
111 assert_equal User.find(2), results.first
105 assert_equal User.find(2), results.first
112 end
106 end
113
107
114 def test_like_scope_with_cyrillic_name
108 def test_like_scope_with_cyrillic_name
115 user = User.generate!(:firstname => 'Соболев', :lastname => 'Денис')
109 user = User.generate!(:firstname => 'Соболев', :lastname => 'Денис')
116 results = Principal.like('Собо')
110 results = Principal.like('Собо')
117 assert_equal 1, results.count
111 assert_equal 1, results.count
118 assert_equal user, results.first
112 assert_equal user, results.first
119 end
113 end
120 end
114 end
@@ -1,1419 +1,1419
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers
29 :custom_fields_trackers
30
30
31 def test_query_with_roles_visibility_should_validate_roles
31 def test_query_with_roles_visibility_should_validate_roles
32 set_language_if_valid 'en'
32 set_language_if_valid 'en'
33 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
33 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
34 assert !query.save
34 assert !query.save
35 assert_include "Roles can't be blank", query.errors.full_messages
35 assert_include "Roles can't be blank", query.errors.full_messages
36 query.role_ids = [1, 2]
36 query.role_ids = [1, 2]
37 assert query.save
37 assert query.save
38 end
38 end
39
39
40 def test_changing_roles_visibility_should_clear_roles
40 def test_changing_roles_visibility_should_clear_roles
41 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
41 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
42 assert_equal 2, query.roles.count
42 assert_equal 2, query.roles.count
43
43
44 query.visibility = IssueQuery::VISIBILITY_PUBLIC
44 query.visibility = IssueQuery::VISIBILITY_PUBLIC
45 query.save!
45 query.save!
46 assert_equal 0, query.roles.count
46 assert_equal 0, query.roles.count
47 end
47 end
48
48
49 def test_available_filters_should_be_ordered
49 def test_available_filters_should_be_ordered
50 set_language_if_valid 'en'
50 set_language_if_valid 'en'
51 query = IssueQuery.new
51 query = IssueQuery.new
52 assert_equal 0, query.available_filters.keys.index('status_id')
52 assert_equal 0, query.available_filters.keys.index('status_id')
53 expected_order = [
53 expected_order = [
54 "Status",
54 "Status",
55 "Project",
55 "Project",
56 "Tracker",
56 "Tracker",
57 "Priority"
57 "Priority"
58 ]
58 ]
59 assert_equal expected_order,
59 assert_equal expected_order,
60 (query.available_filters.values.map{|v| v[:name]} & expected_order)
60 (query.available_filters.values.map{|v| v[:name]} & expected_order)
61 end
61 end
62
62
63 def test_available_filters_with_custom_fields_should_be_ordered
63 def test_available_filters_with_custom_fields_should_be_ordered
64 set_language_if_valid 'en'
64 set_language_if_valid 'en'
65 UserCustomField.create!(
65 UserCustomField.create!(
66 :name => 'order test', :field_format => 'string',
66 :name => 'order test', :field_format => 'string',
67 :is_for_all => true, :is_filter => true
67 :is_for_all => true, :is_filter => true
68 )
68 )
69 query = IssueQuery.new
69 query = IssueQuery.new
70 expected_order = [
70 expected_order = [
71 "Searchable field",
71 "Searchable field",
72 "Database",
72 "Database",
73 "Project's Development status",
73 "Project's Development status",
74 "Author's order test",
74 "Author's order test",
75 "Assignee's order test"
75 "Assignee's order test"
76 ]
76 ]
77 assert_equal expected_order,
77 assert_equal expected_order,
78 (query.available_filters.values.map{|v| v[:name]} & expected_order)
78 (query.available_filters.values.map{|v| v[:name]} & expected_order)
79 end
79 end
80
80
81 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
81 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
82 query = IssueQuery.new(:project => nil, :name => '_')
82 query = IssueQuery.new(:project => nil, :name => '_')
83 assert query.available_filters.has_key?('cf_1')
83 assert query.available_filters.has_key?('cf_1')
84 assert !query.available_filters.has_key?('cf_3')
84 assert !query.available_filters.has_key?('cf_3')
85 end
85 end
86
86
87 def test_system_shared_versions_should_be_available_in_global_queries
87 def test_system_shared_versions_should_be_available_in_global_queries
88 Version.find(2).update_attribute :sharing, 'system'
88 Version.find(2).update_attribute :sharing, 'system'
89 query = IssueQuery.new(:project => nil, :name => '_')
89 query = IssueQuery.new(:project => nil, :name => '_')
90 assert query.available_filters.has_key?('fixed_version_id')
90 assert query.available_filters.has_key?('fixed_version_id')
91 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
91 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
92 end
92 end
93
93
94 def test_project_filter_in_global_queries
94 def test_project_filter_in_global_queries
95 query = IssueQuery.new(:project => nil, :name => '_')
95 query = IssueQuery.new(:project => nil, :name => '_')
96 project_filter = query.available_filters["project_id"]
96 project_filter = query.available_filters["project_id"]
97 assert_not_nil project_filter
97 assert_not_nil project_filter
98 project_ids = project_filter[:values].map{|p| p[1]}
98 project_ids = project_filter[:values].map{|p| p[1]}
99 assert project_ids.include?("1") #public project
99 assert project_ids.include?("1") #public project
100 assert !project_ids.include?("2") #private project user cannot see
100 assert !project_ids.include?("2") #private project user cannot see
101 end
101 end
102
102
103 def find_issues_with_query(query)
103 def find_issues_with_query(query)
104 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
104 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
105 query.statement
105 query.statement
106 ).all
106 ).all
107 end
107 end
108
108
109 def assert_find_issues_with_query_is_successful(query)
109 def assert_find_issues_with_query_is_successful(query)
110 assert_nothing_raised do
110 assert_nothing_raised do
111 find_issues_with_query(query)
111 find_issues_with_query(query)
112 end
112 end
113 end
113 end
114
114
115 def assert_query_statement_includes(query, condition)
115 def assert_query_statement_includes(query, condition)
116 assert_include condition, query.statement
116 assert_include condition, query.statement
117 end
117 end
118
118
119 def assert_query_result(expected, query)
119 def assert_query_result(expected, query)
120 assert_nothing_raised do
120 assert_nothing_raised do
121 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
121 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
122 assert_equal expected.size, query.issue_count
122 assert_equal expected.size, query.issue_count
123 end
123 end
124 end
124 end
125
125
126 def test_query_should_allow_shared_versions_for_a_project_query
126 def test_query_should_allow_shared_versions_for_a_project_query
127 subproject_version = Version.find(4)
127 subproject_version = Version.find(4)
128 query = IssueQuery.new(:project => Project.find(1), :name => '_')
128 query = IssueQuery.new(:project => Project.find(1), :name => '_')
129 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
129 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
130
130
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
132 end
132 end
133
133
134 def test_query_with_multiple_custom_fields
134 def test_query_with_multiple_custom_fields
135 query = IssueQuery.find(1)
135 query = IssueQuery.find(1)
136 assert query.valid?
136 assert query.valid?
137 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
137 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
138 issues = find_issues_with_query(query)
138 issues = find_issues_with_query(query)
139 assert_equal 1, issues.length
139 assert_equal 1, issues.length
140 assert_equal Issue.find(3), issues.first
140 assert_equal Issue.find(3), issues.first
141 end
141 end
142
142
143 def test_operator_none
143 def test_operator_none
144 query = IssueQuery.new(:project => Project.find(1), :name => '_')
144 query = IssueQuery.new(:project => Project.find(1), :name => '_')
145 query.add_filter('fixed_version_id', '!*', [''])
145 query.add_filter('fixed_version_id', '!*', [''])
146 query.add_filter('cf_1', '!*', [''])
146 query.add_filter('cf_1', '!*', [''])
147 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
147 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
148 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
148 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
149 find_issues_with_query(query)
149 find_issues_with_query(query)
150 end
150 end
151
151
152 def test_operator_none_for_integer
152 def test_operator_none_for_integer
153 query = IssueQuery.new(:project => Project.find(1), :name => '_')
153 query = IssueQuery.new(:project => Project.find(1), :name => '_')
154 query.add_filter('estimated_hours', '!*', [''])
154 query.add_filter('estimated_hours', '!*', [''])
155 issues = find_issues_with_query(query)
155 issues = find_issues_with_query(query)
156 assert !issues.empty?
156 assert !issues.empty?
157 assert issues.all? {|i| !i.estimated_hours}
157 assert issues.all? {|i| !i.estimated_hours}
158 end
158 end
159
159
160 def test_operator_none_for_date
160 def test_operator_none_for_date
161 query = IssueQuery.new(:project => Project.find(1), :name => '_')
161 query = IssueQuery.new(:project => Project.find(1), :name => '_')
162 query.add_filter('start_date', '!*', [''])
162 query.add_filter('start_date', '!*', [''])
163 issues = find_issues_with_query(query)
163 issues = find_issues_with_query(query)
164 assert !issues.empty?
164 assert !issues.empty?
165 assert issues.all? {|i| i.start_date.nil?}
165 assert issues.all? {|i| i.start_date.nil?}
166 end
166 end
167
167
168 def test_operator_none_for_string_custom_field
168 def test_operator_none_for_string_custom_field
169 query = IssueQuery.new(:project => Project.find(1), :name => '_')
169 query = IssueQuery.new(:project => Project.find(1), :name => '_')
170 query.add_filter('cf_2', '!*', [''])
170 query.add_filter('cf_2', '!*', [''])
171 assert query.has_filter?('cf_2')
171 assert query.has_filter?('cf_2')
172 issues = find_issues_with_query(query)
172 issues = find_issues_with_query(query)
173 assert !issues.empty?
173 assert !issues.empty?
174 assert issues.all? {|i| i.custom_field_value(2).blank?}
174 assert issues.all? {|i| i.custom_field_value(2).blank?}
175 end
175 end
176
176
177 def test_operator_all
177 def test_operator_all
178 query = IssueQuery.new(:project => Project.find(1), :name => '_')
178 query = IssueQuery.new(:project => Project.find(1), :name => '_')
179 query.add_filter('fixed_version_id', '*', [''])
179 query.add_filter('fixed_version_id', '*', [''])
180 query.add_filter('cf_1', '*', [''])
180 query.add_filter('cf_1', '*', [''])
181 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
181 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
182 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
182 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
183 find_issues_with_query(query)
183 find_issues_with_query(query)
184 end
184 end
185
185
186 def test_operator_all_for_date
186 def test_operator_all_for_date
187 query = IssueQuery.new(:project => Project.find(1), :name => '_')
187 query = IssueQuery.new(:project => Project.find(1), :name => '_')
188 query.add_filter('start_date', '*', [''])
188 query.add_filter('start_date', '*', [''])
189 issues = find_issues_with_query(query)
189 issues = find_issues_with_query(query)
190 assert !issues.empty?
190 assert !issues.empty?
191 assert issues.all? {|i| i.start_date.present?}
191 assert issues.all? {|i| i.start_date.present?}
192 end
192 end
193
193
194 def test_operator_all_for_string_custom_field
194 def test_operator_all_for_string_custom_field
195 query = IssueQuery.new(:project => Project.find(1), :name => '_')
195 query = IssueQuery.new(:project => Project.find(1), :name => '_')
196 query.add_filter('cf_2', '*', [''])
196 query.add_filter('cf_2', '*', [''])
197 assert query.has_filter?('cf_2')
197 assert query.has_filter?('cf_2')
198 issues = find_issues_with_query(query)
198 issues = find_issues_with_query(query)
199 assert !issues.empty?
199 assert !issues.empty?
200 assert issues.all? {|i| i.custom_field_value(2).present?}
200 assert issues.all? {|i| i.custom_field_value(2).present?}
201 end
201 end
202
202
203 def test_numeric_filter_should_not_accept_non_numeric_values
203 def test_numeric_filter_should_not_accept_non_numeric_values
204 query = IssueQuery.new(:name => '_')
204 query = IssueQuery.new(:name => '_')
205 query.add_filter('estimated_hours', '=', ['a'])
205 query.add_filter('estimated_hours', '=', ['a'])
206
206
207 assert query.has_filter?('estimated_hours')
207 assert query.has_filter?('estimated_hours')
208 assert !query.valid?
208 assert !query.valid?
209 end
209 end
210
210
211 def test_operator_is_on_float
211 def test_operator_is_on_float
212 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
212 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
213 query = IssueQuery.new(:name => '_')
213 query = IssueQuery.new(:name => '_')
214 query.add_filter('estimated_hours', '=', ['171.20'])
214 query.add_filter('estimated_hours', '=', ['171.20'])
215 issues = find_issues_with_query(query)
215 issues = find_issues_with_query(query)
216 assert_equal 1, issues.size
216 assert_equal 1, issues.size
217 assert_equal 2, issues.first.id
217 assert_equal 2, issues.first.id
218 end
218 end
219
219
220 def test_operator_is_on_integer_custom_field
220 def test_operator_is_on_integer_custom_field
221 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
221 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
222 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
222 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
223 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
223 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
224 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
224 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
225
225
226 query = IssueQuery.new(:name => '_')
226 query = IssueQuery.new(:name => '_')
227 query.add_filter("cf_#{f.id}", '=', ['12'])
227 query.add_filter("cf_#{f.id}", '=', ['12'])
228 issues = find_issues_with_query(query)
228 issues = find_issues_with_query(query)
229 assert_equal 1, issues.size
229 assert_equal 1, issues.size
230 assert_equal 2, issues.first.id
230 assert_equal 2, issues.first.id
231 end
231 end
232
232
233 def test_operator_is_on_integer_custom_field_should_accept_negative_value
233 def test_operator_is_on_integer_custom_field_should_accept_negative_value
234 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
234 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
235 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
235 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
236 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
236 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
237 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
237 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
238
238
239 query = IssueQuery.new(:name => '_')
239 query = IssueQuery.new(:name => '_')
240 query.add_filter("cf_#{f.id}", '=', ['-12'])
240 query.add_filter("cf_#{f.id}", '=', ['-12'])
241 assert query.valid?
241 assert query.valid?
242 issues = find_issues_with_query(query)
242 issues = find_issues_with_query(query)
243 assert_equal 1, issues.size
243 assert_equal 1, issues.size
244 assert_equal 2, issues.first.id
244 assert_equal 2, issues.first.id
245 end
245 end
246
246
247 def test_operator_is_on_float_custom_field
247 def test_operator_is_on_float_custom_field
248 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
248 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
249 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
249 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
250 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
250 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
252
252
253 query = IssueQuery.new(:name => '_')
253 query = IssueQuery.new(:name => '_')
254 query.add_filter("cf_#{f.id}", '=', ['12.7'])
254 query.add_filter("cf_#{f.id}", '=', ['12.7'])
255 issues = find_issues_with_query(query)
255 issues = find_issues_with_query(query)
256 assert_equal 1, issues.size
256 assert_equal 1, issues.size
257 assert_equal 2, issues.first.id
257 assert_equal 2, issues.first.id
258 end
258 end
259
259
260 def test_operator_is_on_float_custom_field_should_accept_negative_value
260 def test_operator_is_on_float_custom_field_should_accept_negative_value
261 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
261 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
262 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
262 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
263 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
263 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
264 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
264 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
265
265
266 query = IssueQuery.new(:name => '_')
266 query = IssueQuery.new(:name => '_')
267 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
267 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
268 assert query.valid?
268 assert query.valid?
269 issues = find_issues_with_query(query)
269 issues = find_issues_with_query(query)
270 assert_equal 1, issues.size
270 assert_equal 1, issues.size
271 assert_equal 2, issues.first.id
271 assert_equal 2, issues.first.id
272 end
272 end
273
273
274 def test_operator_is_on_multi_list_custom_field
274 def test_operator_is_on_multi_list_custom_field
275 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
275 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
276 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
276 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
277 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
277 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
280
280
281 query = IssueQuery.new(:name => '_')
281 query = IssueQuery.new(:name => '_')
282 query.add_filter("cf_#{f.id}", '=', ['value1'])
282 query.add_filter("cf_#{f.id}", '=', ['value1'])
283 issues = find_issues_with_query(query)
283 issues = find_issues_with_query(query)
284 assert_equal [1, 3], issues.map(&:id).sort
284 assert_equal [1, 3], issues.map(&:id).sort
285
285
286 query = IssueQuery.new(:name => '_')
286 query = IssueQuery.new(:name => '_')
287 query.add_filter("cf_#{f.id}", '=', ['value2'])
287 query.add_filter("cf_#{f.id}", '=', ['value2'])
288 issues = find_issues_with_query(query)
288 issues = find_issues_with_query(query)
289 assert_equal [1], issues.map(&:id).sort
289 assert_equal [1], issues.map(&:id).sort
290 end
290 end
291
291
292 def test_operator_is_not_on_multi_list_custom_field
292 def test_operator_is_not_on_multi_list_custom_field
293 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
293 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
294 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
294 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
295 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
295 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
296 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
296 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
297 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
297 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
298
298
299 query = IssueQuery.new(:name => '_')
299 query = IssueQuery.new(:name => '_')
300 query.add_filter("cf_#{f.id}", '!', ['value1'])
300 query.add_filter("cf_#{f.id}", '!', ['value1'])
301 issues = find_issues_with_query(query)
301 issues = find_issues_with_query(query)
302 assert !issues.map(&:id).include?(1)
302 assert !issues.map(&:id).include?(1)
303 assert !issues.map(&:id).include?(3)
303 assert !issues.map(&:id).include?(3)
304
304
305 query = IssueQuery.new(:name => '_')
305 query = IssueQuery.new(:name => '_')
306 query.add_filter("cf_#{f.id}", '!', ['value2'])
306 query.add_filter("cf_#{f.id}", '!', ['value2'])
307 issues = find_issues_with_query(query)
307 issues = find_issues_with_query(query)
308 assert !issues.map(&:id).include?(1)
308 assert !issues.map(&:id).include?(1)
309 assert issues.map(&:id).include?(3)
309 assert issues.map(&:id).include?(3)
310 end
310 end
311
311
312 def test_operator_is_on_is_private_field
312 def test_operator_is_on_is_private_field
313 # is_private filter only available for those who can set issues private
313 # is_private filter only available for those who can set issues private
314 User.current = User.find(2)
314 User.current = User.find(2)
315
315
316 query = IssueQuery.new(:name => '_')
316 query = IssueQuery.new(:name => '_')
317 assert query.available_filters.key?('is_private')
317 assert query.available_filters.key?('is_private')
318
318
319 query.add_filter("is_private", '=', ['1'])
319 query.add_filter("is_private", '=', ['1'])
320 issues = find_issues_with_query(query)
320 issues = find_issues_with_query(query)
321 assert issues.any?
321 assert issues.any?
322 assert_nil issues.detect {|issue| !issue.is_private?}
322 assert_nil issues.detect {|issue| !issue.is_private?}
323 ensure
323 ensure
324 User.current = nil
324 User.current = nil
325 end
325 end
326
326
327 def test_operator_is_not_on_is_private_field
327 def test_operator_is_not_on_is_private_field
328 # is_private filter only available for those who can set issues private
328 # is_private filter only available for those who can set issues private
329 User.current = User.find(2)
329 User.current = User.find(2)
330
330
331 query = IssueQuery.new(:name => '_')
331 query = IssueQuery.new(:name => '_')
332 assert query.available_filters.key?('is_private')
332 assert query.available_filters.key?('is_private')
333
333
334 query.add_filter("is_private", '!', ['1'])
334 query.add_filter("is_private", '!', ['1'])
335 issues = find_issues_with_query(query)
335 issues = find_issues_with_query(query)
336 assert issues.any?
336 assert issues.any?
337 assert_nil issues.detect {|issue| issue.is_private?}
337 assert_nil issues.detect {|issue| issue.is_private?}
338 ensure
338 ensure
339 User.current = nil
339 User.current = nil
340 end
340 end
341
341
342 def test_operator_greater_than
342 def test_operator_greater_than
343 query = IssueQuery.new(:project => Project.find(1), :name => '_')
343 query = IssueQuery.new(:project => Project.find(1), :name => '_')
344 query.add_filter('done_ratio', '>=', ['40'])
344 query.add_filter('done_ratio', '>=', ['40'])
345 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
345 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
346 find_issues_with_query(query)
346 find_issues_with_query(query)
347 end
347 end
348
348
349 def test_operator_greater_than_a_float
349 def test_operator_greater_than_a_float
350 query = IssueQuery.new(:project => Project.find(1), :name => '_')
350 query = IssueQuery.new(:project => Project.find(1), :name => '_')
351 query.add_filter('estimated_hours', '>=', ['40.5'])
351 query.add_filter('estimated_hours', '>=', ['40.5'])
352 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
352 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
353 find_issues_with_query(query)
353 find_issues_with_query(query)
354 end
354 end
355
355
356 def test_operator_greater_than_on_int_custom_field
356 def test_operator_greater_than_on_int_custom_field
357 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
357 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
358 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
358 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
359 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
359 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
360 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
360 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
361
361
362 query = IssueQuery.new(:project => Project.find(1), :name => '_')
362 query = IssueQuery.new(:project => Project.find(1), :name => '_')
363 query.add_filter("cf_#{f.id}", '>=', ['8'])
363 query.add_filter("cf_#{f.id}", '>=', ['8'])
364 issues = find_issues_with_query(query)
364 issues = find_issues_with_query(query)
365 assert_equal 1, issues.size
365 assert_equal 1, issues.size
366 assert_equal 2, issues.first.id
366 assert_equal 2, issues.first.id
367 end
367 end
368
368
369 def test_operator_lesser_than
369 def test_operator_lesser_than
370 query = IssueQuery.new(:project => Project.find(1), :name => '_')
370 query = IssueQuery.new(:project => Project.find(1), :name => '_')
371 query.add_filter('done_ratio', '<=', ['30'])
371 query.add_filter('done_ratio', '<=', ['30'])
372 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
372 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
373 find_issues_with_query(query)
373 find_issues_with_query(query)
374 end
374 end
375
375
376 def test_operator_lesser_than_on_custom_field
376 def test_operator_lesser_than_on_custom_field
377 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
377 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
378 query = IssueQuery.new(:project => Project.find(1), :name => '_')
378 query = IssueQuery.new(:project => Project.find(1), :name => '_')
379 query.add_filter("cf_#{f.id}", '<=', ['30'])
379 query.add_filter("cf_#{f.id}", '<=', ['30'])
380 assert_match /CAST.+ <= 30\.0/, query.statement
380 assert_match /CAST.+ <= 30\.0/, query.statement
381 find_issues_with_query(query)
381 find_issues_with_query(query)
382 end
382 end
383
383
384 def test_operator_lesser_than_on_date_custom_field
384 def test_operator_lesser_than_on_date_custom_field
385 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
385 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
386 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
386 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
387 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
387 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
388 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
388 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
389
389
390 query = IssueQuery.new(:project => Project.find(1), :name => '_')
390 query = IssueQuery.new(:project => Project.find(1), :name => '_')
391 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
391 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
392 issue_ids = find_issues_with_query(query).map(&:id)
392 issue_ids = find_issues_with_query(query).map(&:id)
393 assert_include 1, issue_ids
393 assert_include 1, issue_ids
394 assert_not_include 2, issue_ids
394 assert_not_include 2, issue_ids
395 assert_not_include 3, issue_ids
395 assert_not_include 3, issue_ids
396 end
396 end
397
397
398 def test_operator_between
398 def test_operator_between
399 query = IssueQuery.new(:project => Project.find(1), :name => '_')
399 query = IssueQuery.new(:project => Project.find(1), :name => '_')
400 query.add_filter('done_ratio', '><', ['30', '40'])
400 query.add_filter('done_ratio', '><', ['30', '40'])
401 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
401 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
402 find_issues_with_query(query)
402 find_issues_with_query(query)
403 end
403 end
404
404
405 def test_operator_between_on_custom_field
405 def test_operator_between_on_custom_field
406 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
406 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
407 query = IssueQuery.new(:project => Project.find(1), :name => '_')
407 query = IssueQuery.new(:project => Project.find(1), :name => '_')
408 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
408 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
409 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
409 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
410 find_issues_with_query(query)
410 find_issues_with_query(query)
411 end
411 end
412
412
413 def test_date_filter_should_not_accept_non_date_values
413 def test_date_filter_should_not_accept_non_date_values
414 query = IssueQuery.new(:name => '_')
414 query = IssueQuery.new(:name => '_')
415 query.add_filter('created_on', '=', ['a'])
415 query.add_filter('created_on', '=', ['a'])
416
416
417 assert query.has_filter?('created_on')
417 assert query.has_filter?('created_on')
418 assert !query.valid?
418 assert !query.valid?
419 end
419 end
420
420
421 def test_date_filter_should_not_accept_invalid_date_values
421 def test_date_filter_should_not_accept_invalid_date_values
422 query = IssueQuery.new(:name => '_')
422 query = IssueQuery.new(:name => '_')
423 query.add_filter('created_on', '=', ['2011-01-34'])
423 query.add_filter('created_on', '=', ['2011-01-34'])
424
424
425 assert query.has_filter?('created_on')
425 assert query.has_filter?('created_on')
426 assert !query.valid?
426 assert !query.valid?
427 end
427 end
428
428
429 def test_relative_date_filter_should_not_accept_non_integer_values
429 def test_relative_date_filter_should_not_accept_non_integer_values
430 query = IssueQuery.new(:name => '_')
430 query = IssueQuery.new(:name => '_')
431 query.add_filter('created_on', '>t-', ['a'])
431 query.add_filter('created_on', '>t-', ['a'])
432
432
433 assert query.has_filter?('created_on')
433 assert query.has_filter?('created_on')
434 assert !query.valid?
434 assert !query.valid?
435 end
435 end
436
436
437 def test_operator_date_equals
437 def test_operator_date_equals
438 query = IssueQuery.new(:name => '_')
438 query = IssueQuery.new(:name => '_')
439 query.add_filter('due_date', '=', ['2011-07-10'])
439 query.add_filter('due_date', '=', ['2011-07-10'])
440 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
440 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
441 find_issues_with_query(query)
441 find_issues_with_query(query)
442 end
442 end
443
443
444 def test_operator_date_lesser_than
444 def test_operator_date_lesser_than
445 query = IssueQuery.new(:name => '_')
445 query = IssueQuery.new(:name => '_')
446 query.add_filter('due_date', '<=', ['2011-07-10'])
446 query.add_filter('due_date', '<=', ['2011-07-10'])
447 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
447 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
448 find_issues_with_query(query)
448 find_issues_with_query(query)
449 end
449 end
450
450
451 def test_operator_date_lesser_than_with_timestamp
451 def test_operator_date_lesser_than_with_timestamp
452 query = IssueQuery.new(:name => '_')
452 query = IssueQuery.new(:name => '_')
453 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
453 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
454 assert_match /issues\.updated_on <= '2011-07-10 19:13:52/, query.statement
454 assert_match /issues\.updated_on <= '2011-07-10 19:13:52/, query.statement
455 find_issues_with_query(query)
455 find_issues_with_query(query)
456 end
456 end
457
457
458 def test_operator_date_greater_than
458 def test_operator_date_greater_than
459 query = IssueQuery.new(:name => '_')
459 query = IssueQuery.new(:name => '_')
460 query.add_filter('due_date', '>=', ['2011-07-10'])
460 query.add_filter('due_date', '>=', ['2011-07-10'])
461 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
461 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
462 find_issues_with_query(query)
462 find_issues_with_query(query)
463 end
463 end
464
464
465 def test_operator_date_greater_than_with_timestamp
465 def test_operator_date_greater_than_with_timestamp
466 query = IssueQuery.new(:name => '_')
466 query = IssueQuery.new(:name => '_')
467 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
467 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
468 assert_match /issues\.updated_on > '2011-07-10 19:13:51(\.0+)?'/, query.statement
468 assert_match /issues\.updated_on > '2011-07-10 19:13:51(\.0+)?'/, query.statement
469 find_issues_with_query(query)
469 find_issues_with_query(query)
470 end
470 end
471
471
472 def test_operator_date_between
472 def test_operator_date_between
473 query = IssueQuery.new(:name => '_')
473 query = IssueQuery.new(:name => '_')
474 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
474 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
475 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?'/, query.statement
475 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?'/, query.statement
476 find_issues_with_query(query)
476 find_issues_with_query(query)
477 end
477 end
478
478
479 def test_operator_in_more_than
479 def test_operator_in_more_than
480 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
480 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
481 query = IssueQuery.new(:project => Project.find(1), :name => '_')
481 query = IssueQuery.new(:project => Project.find(1), :name => '_')
482 query.add_filter('due_date', '>t+', ['15'])
482 query.add_filter('due_date', '>t+', ['15'])
483 issues = find_issues_with_query(query)
483 issues = find_issues_with_query(query)
484 assert !issues.empty?
484 assert !issues.empty?
485 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
485 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
486 end
486 end
487
487
488 def test_operator_in_less_than
488 def test_operator_in_less_than
489 query = IssueQuery.new(:project => Project.find(1), :name => '_')
489 query = IssueQuery.new(:project => Project.find(1), :name => '_')
490 query.add_filter('due_date', '<t+', ['15'])
490 query.add_filter('due_date', '<t+', ['15'])
491 issues = find_issues_with_query(query)
491 issues = find_issues_with_query(query)
492 assert !issues.empty?
492 assert !issues.empty?
493 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
493 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
494 end
494 end
495
495
496 def test_operator_in_the_next_days
496 def test_operator_in_the_next_days
497 query = IssueQuery.new(:project => Project.find(1), :name => '_')
497 query = IssueQuery.new(:project => Project.find(1), :name => '_')
498 query.add_filter('due_date', '><t+', ['15'])
498 query.add_filter('due_date', '><t+', ['15'])
499 issues = find_issues_with_query(query)
499 issues = find_issues_with_query(query)
500 assert !issues.empty?
500 assert !issues.empty?
501 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
501 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
502 end
502 end
503
503
504 def test_operator_less_than_ago
504 def test_operator_less_than_ago
505 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
505 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
506 query = IssueQuery.new(:project => Project.find(1), :name => '_')
506 query = IssueQuery.new(:project => Project.find(1), :name => '_')
507 query.add_filter('due_date', '>t-', ['3'])
507 query.add_filter('due_date', '>t-', ['3'])
508 issues = find_issues_with_query(query)
508 issues = find_issues_with_query(query)
509 assert !issues.empty?
509 assert !issues.empty?
510 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
510 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
511 end
511 end
512
512
513 def test_operator_in_the_past_days
513 def test_operator_in_the_past_days
514 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
514 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
515 query = IssueQuery.new(:project => Project.find(1), :name => '_')
515 query = IssueQuery.new(:project => Project.find(1), :name => '_')
516 query.add_filter('due_date', '><t-', ['3'])
516 query.add_filter('due_date', '><t-', ['3'])
517 issues = find_issues_with_query(query)
517 issues = find_issues_with_query(query)
518 assert !issues.empty?
518 assert !issues.empty?
519 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
519 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
520 end
520 end
521
521
522 def test_operator_more_than_ago
522 def test_operator_more_than_ago
523 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
523 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
524 query = IssueQuery.new(:project => Project.find(1), :name => '_')
524 query = IssueQuery.new(:project => Project.find(1), :name => '_')
525 query.add_filter('due_date', '<t-', ['10'])
525 query.add_filter('due_date', '<t-', ['10'])
526 assert query.statement.include?("#{Issue.table_name}.due_date <=")
526 assert query.statement.include?("#{Issue.table_name}.due_date <=")
527 issues = find_issues_with_query(query)
527 issues = find_issues_with_query(query)
528 assert !issues.empty?
528 assert !issues.empty?
529 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
529 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
530 end
530 end
531
531
532 def test_operator_in
532 def test_operator_in
533 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
533 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
534 query = IssueQuery.new(:project => Project.find(1), :name => '_')
534 query = IssueQuery.new(:project => Project.find(1), :name => '_')
535 query.add_filter('due_date', 't+', ['2'])
535 query.add_filter('due_date', 't+', ['2'])
536 issues = find_issues_with_query(query)
536 issues = find_issues_with_query(query)
537 assert !issues.empty?
537 assert !issues.empty?
538 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
538 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
539 end
539 end
540
540
541 def test_operator_ago
541 def test_operator_ago
542 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
542 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
543 query = IssueQuery.new(:project => Project.find(1), :name => '_')
543 query = IssueQuery.new(:project => Project.find(1), :name => '_')
544 query.add_filter('due_date', 't-', ['3'])
544 query.add_filter('due_date', 't-', ['3'])
545 issues = find_issues_with_query(query)
545 issues = find_issues_with_query(query)
546 assert !issues.empty?
546 assert !issues.empty?
547 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
547 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
548 end
548 end
549
549
550 def test_operator_today
550 def test_operator_today
551 query = IssueQuery.new(:project => Project.find(1), :name => '_')
551 query = IssueQuery.new(:project => Project.find(1), :name => '_')
552 query.add_filter('due_date', 't', [''])
552 query.add_filter('due_date', 't', [''])
553 issues = find_issues_with_query(query)
553 issues = find_issues_with_query(query)
554 assert !issues.empty?
554 assert !issues.empty?
555 issues.each {|issue| assert_equal Date.today, issue.due_date}
555 issues.each {|issue| assert_equal Date.today, issue.due_date}
556 end
556 end
557
557
558 def test_operator_this_week_on_date
558 def test_operator_this_week_on_date
559 query = IssueQuery.new(:project => Project.find(1), :name => '_')
559 query = IssueQuery.new(:project => Project.find(1), :name => '_')
560 query.add_filter('due_date', 'w', [''])
560 query.add_filter('due_date', 'w', [''])
561 find_issues_with_query(query)
561 find_issues_with_query(query)
562 end
562 end
563
563
564 def test_operator_this_week_on_datetime
564 def test_operator_this_week_on_datetime
565 query = IssueQuery.new(:project => Project.find(1), :name => '_')
565 query = IssueQuery.new(:project => Project.find(1), :name => '_')
566 query.add_filter('created_on', 'w', [''])
566 query.add_filter('created_on', 'w', [''])
567 find_issues_with_query(query)
567 find_issues_with_query(query)
568 end
568 end
569
569
570 def test_operator_contains
570 def test_operator_contains
571 query = IssueQuery.new(:project => Project.find(1), :name => '_')
571 query = IssueQuery.new(:project => Project.find(1), :name => '_')
572 query.add_filter('subject', '~', ['uNable'])
572 query.add_filter('subject', '~', ['uNable'])
573 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
573 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
574 result = find_issues_with_query(query)
574 result = find_issues_with_query(query)
575 assert result.empty?
575 assert result.empty?
576 result.each {|issue| assert issue.subject.downcase.include?('unable') }
576 result.each {|issue| assert issue.subject.downcase.include?('unable') }
577 end
577 end
578
578
579 def test_range_for_this_week_with_week_starting_on_monday
579 def test_range_for_this_week_with_week_starting_on_monday
580 I18n.locale = :fr
580 I18n.locale = :fr
581 assert_equal '1', I18n.t(:general_first_day_of_week)
581 assert_equal '1', I18n.t(:general_first_day_of_week)
582
582
583 Date.stubs(:today).returns(Date.parse('2011-04-29'))
583 Date.stubs(:today).returns(Date.parse('2011-04-29'))
584
584
585 query = IssueQuery.new(:project => Project.find(1), :name => '_')
585 query = IssueQuery.new(:project => Project.find(1), :name => '_')
586 query.add_filter('due_date', 'w', [''])
586 query.add_filter('due_date', 'w', [''])
587 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
587 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
588 I18n.locale = :en
588 I18n.locale = :en
589 end
589 end
590
590
591 def test_range_for_this_week_with_week_starting_on_sunday
591 def test_range_for_this_week_with_week_starting_on_sunday
592 I18n.locale = :en
592 I18n.locale = :en
593 assert_equal '7', I18n.t(:general_first_day_of_week)
593 assert_equal '7', I18n.t(:general_first_day_of_week)
594
594
595 Date.stubs(:today).returns(Date.parse('2011-04-29'))
595 Date.stubs(:today).returns(Date.parse('2011-04-29'))
596
596
597 query = IssueQuery.new(:project => Project.find(1), :name => '_')
597 query = IssueQuery.new(:project => Project.find(1), :name => '_')
598 query.add_filter('due_date', 'w', [''])
598 query.add_filter('due_date', 'w', [''])
599 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
599 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
600 end
600 end
601
601
602 def test_operator_does_not_contains
602 def test_operator_does_not_contains
603 query = IssueQuery.new(:project => Project.find(1), :name => '_')
603 query = IssueQuery.new(:project => Project.find(1), :name => '_')
604 query.add_filter('subject', '!~', ['uNable'])
604 query.add_filter('subject', '!~', ['uNable'])
605 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
605 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
606 find_issues_with_query(query)
606 find_issues_with_query(query)
607 end
607 end
608
608
609 def test_filter_assigned_to_me
609 def test_filter_assigned_to_me
610 user = User.find(2)
610 user = User.find(2)
611 group = Group.find(10)
611 group = Group.find(10)
612 User.current = user
612 User.current = user
613 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
613 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
614 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
614 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
615 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
615 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
616 group.users << user
616 group.users << user
617
617
618 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
618 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
619 result = query.issues
619 result = query.issues
620 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
620 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
621
621
622 assert result.include?(i1)
622 assert result.include?(i1)
623 assert result.include?(i2)
623 assert result.include?(i2)
624 assert !result.include?(i3)
624 assert !result.include?(i3)
625 end
625 end
626
626
627 def test_user_custom_field_filtered_on_me
627 def test_user_custom_field_filtered_on_me
628 User.current = User.find(2)
628 User.current = User.find(2)
629 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
629 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
630 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
630 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
631 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
631 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
632
632
633 query = IssueQuery.new(:name => '_', :project => Project.find(1))
633 query = IssueQuery.new(:name => '_', :project => Project.find(1))
634 filter = query.available_filters["cf_#{cf.id}"]
634 filter = query.available_filters["cf_#{cf.id}"]
635 assert_not_nil filter
635 assert_not_nil filter
636 assert_include 'me', filter[:values].map{|v| v[1]}
636 assert_include 'me', filter[:values].map{|v| v[1]}
637
637
638 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
638 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
639 result = query.issues
639 result = query.issues
640 assert_equal 1, result.size
640 assert_equal 1, result.size
641 assert_equal issue1, result.first
641 assert_equal issue1, result.first
642 end
642 end
643
643
644 def test_filter_my_projects
644 def test_filter_my_projects
645 User.current = User.find(2)
645 User.current = User.find(2)
646 query = IssueQuery.new(:name => '_')
646 query = IssueQuery.new(:name => '_')
647 filter = query.available_filters['project_id']
647 filter = query.available_filters['project_id']
648 assert_not_nil filter
648 assert_not_nil filter
649 assert_include 'mine', filter[:values].map{|v| v[1]}
649 assert_include 'mine', filter[:values].map{|v| v[1]}
650
650
651 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
651 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
652 result = query.issues
652 result = query.issues
653 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
653 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
654 end
654 end
655
655
656 def test_filter_watched_issues
656 def test_filter_watched_issues
657 User.current = User.find(1)
657 User.current = User.find(1)
658 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
658 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
659 result = find_issues_with_query(query)
659 result = find_issues_with_query(query)
660 assert_not_nil result
660 assert_not_nil result
661 assert !result.empty?
661 assert !result.empty?
662 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
662 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
663 User.current = nil
663 User.current = nil
664 end
664 end
665
665
666 def test_filter_unwatched_issues
666 def test_filter_unwatched_issues
667 User.current = User.find(1)
667 User.current = User.find(1)
668 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
668 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
669 result = find_issues_with_query(query)
669 result = find_issues_with_query(query)
670 assert_not_nil result
670 assert_not_nil result
671 assert !result.empty?
671 assert !result.empty?
672 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
672 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
673 User.current = nil
673 User.current = nil
674 end
674 end
675
675
676 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
676 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
677 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_filter => true)
677 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_filter => true)
678 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
678 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
679 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
679 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
680
680
681 query = IssueQuery.new(:name => '_', :project => Project.find(1))
681 query = IssueQuery.new(:name => '_', :project => Project.find(1))
682 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
682 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
683 assert_equal 2, find_issues_with_query(query).size
683 assert_equal 2, find_issues_with_query(query).size
684
684
685 field.project_ids = [1, 3] # Disable the field for project 4
685 field.project_ids = [1, 3] # Disable the field for project 4
686 field.save!
686 field.save!
687 assert_equal 1, find_issues_with_query(query).size
687 assert_equal 1, find_issues_with_query(query).size
688 end
688 end
689
689
690 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
690 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
691 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
691 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
692 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
692 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
693 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
693 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
694
694
695 query = IssueQuery.new(:name => '_', :project => Project.find(1))
695 query = IssueQuery.new(:name => '_', :project => Project.find(1))
696 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
696 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
697 assert_equal 2, find_issues_with_query(query).size
697 assert_equal 2, find_issues_with_query(query).size
698
698
699 field.tracker_ids = [1] # Disable the field for tracker 2
699 field.tracker_ids = [1] # Disable the field for tracker 2
700 field.save!
700 field.save!
701 assert_equal 1, find_issues_with_query(query).size
701 assert_equal 1, find_issues_with_query(query).size
702 end
702 end
703
703
704 def test_filter_on_project_custom_field
704 def test_filter_on_project_custom_field
705 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
705 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
706 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
706 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
707 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
707 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
708
708
709 query = IssueQuery.new(:name => '_')
709 query = IssueQuery.new(:name => '_')
710 filter_name = "project.cf_#{field.id}"
710 filter_name = "project.cf_#{field.id}"
711 assert_include filter_name, query.available_filters.keys
711 assert_include filter_name, query.available_filters.keys
712 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
712 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
713 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
713 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
714 end
714 end
715
715
716 def test_filter_on_author_custom_field
716 def test_filter_on_author_custom_field
717 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
717 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
718 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
718 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
719
719
720 query = IssueQuery.new(:name => '_')
720 query = IssueQuery.new(:name => '_')
721 filter_name = "author.cf_#{field.id}"
721 filter_name = "author.cf_#{field.id}"
722 assert_include filter_name, query.available_filters.keys
722 assert_include filter_name, query.available_filters.keys
723 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
723 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
724 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
724 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
725 end
725 end
726
726
727 def test_filter_on_assigned_to_custom_field
727 def test_filter_on_assigned_to_custom_field
728 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
728 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
729 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
729 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
730
730
731 query = IssueQuery.new(:name => '_')
731 query = IssueQuery.new(:name => '_')
732 filter_name = "assigned_to.cf_#{field.id}"
732 filter_name = "assigned_to.cf_#{field.id}"
733 assert_include filter_name, query.available_filters.keys
733 assert_include filter_name, query.available_filters.keys
734 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
734 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
735 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
735 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
736 end
736 end
737
737
738 def test_filter_on_fixed_version_custom_field
738 def test_filter_on_fixed_version_custom_field
739 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
739 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
740 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
740 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
741
741
742 query = IssueQuery.new(:name => '_')
742 query = IssueQuery.new(:name => '_')
743 filter_name = "fixed_version.cf_#{field.id}"
743 filter_name = "fixed_version.cf_#{field.id}"
744 assert_include filter_name, query.available_filters.keys
744 assert_include filter_name, query.available_filters.keys
745 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
745 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
746 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
746 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
747 end
747 end
748
748
749 def test_filter_on_relations_with_a_specific_issue
749 def test_filter_on_relations_with_a_specific_issue
750 IssueRelation.delete_all
750 IssueRelation.delete_all
751 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
751 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
752 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
752 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
753
753
754 query = IssueQuery.new(:name => '_')
754 query = IssueQuery.new(:name => '_')
755 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
755 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
756 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
756 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
757
757
758 query = IssueQuery.new(:name => '_')
758 query = IssueQuery.new(:name => '_')
759 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
759 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
760 assert_equal [1], find_issues_with_query(query).map(&:id).sort
760 assert_equal [1], find_issues_with_query(query).map(&:id).sort
761 end
761 end
762
762
763 def test_filter_on_relations_with_any_issues_in_a_project
763 def test_filter_on_relations_with_any_issues_in_a_project
764 IssueRelation.delete_all
764 IssueRelation.delete_all
765 with_settings :cross_project_issue_relations => '1' do
765 with_settings :cross_project_issue_relations => '1' do
766 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
766 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
767 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
767 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
768 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
768 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
769 end
769 end
770
770
771 query = IssueQuery.new(:name => '_')
771 query = IssueQuery.new(:name => '_')
772 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
772 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
773 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
773 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
774
774
775 query = IssueQuery.new(:name => '_')
775 query = IssueQuery.new(:name => '_')
776 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
776 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
777 assert_equal [1], find_issues_with_query(query).map(&:id).sort
777 assert_equal [1], find_issues_with_query(query).map(&:id).sort
778
778
779 query = IssueQuery.new(:name => '_')
779 query = IssueQuery.new(:name => '_')
780 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
780 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
781 assert_equal [], find_issues_with_query(query).map(&:id).sort
781 assert_equal [], find_issues_with_query(query).map(&:id).sort
782 end
782 end
783
783
784 def test_filter_on_relations_with_any_issues_not_in_a_project
784 def test_filter_on_relations_with_any_issues_not_in_a_project
785 IssueRelation.delete_all
785 IssueRelation.delete_all
786 with_settings :cross_project_issue_relations => '1' do
786 with_settings :cross_project_issue_relations => '1' do
787 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
787 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
788 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
788 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
789 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
789 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
790 end
790 end
791
791
792 query = IssueQuery.new(:name => '_')
792 query = IssueQuery.new(:name => '_')
793 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
793 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
794 assert_equal [1], find_issues_with_query(query).map(&:id).sort
794 assert_equal [1], find_issues_with_query(query).map(&:id).sort
795 end
795 end
796
796
797 def test_filter_on_relations_with_no_issues_in_a_project
797 def test_filter_on_relations_with_no_issues_in_a_project
798 IssueRelation.delete_all
798 IssueRelation.delete_all
799 with_settings :cross_project_issue_relations => '1' do
799 with_settings :cross_project_issue_relations => '1' do
800 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
800 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
801 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
801 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
802 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
802 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
803 end
803 end
804
804
805 query = IssueQuery.new(:name => '_')
805 query = IssueQuery.new(:name => '_')
806 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
806 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
807 ids = find_issues_with_query(query).map(&:id).sort
807 ids = find_issues_with_query(query).map(&:id).sort
808 assert_include 2, ids
808 assert_include 2, ids
809 assert_not_include 1, ids
809 assert_not_include 1, ids
810 assert_not_include 3, ids
810 assert_not_include 3, ids
811 end
811 end
812
812
813 def test_filter_on_relations_with_no_issues
813 def test_filter_on_relations_with_no_issues
814 IssueRelation.delete_all
814 IssueRelation.delete_all
815 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
815 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
816 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
816 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
817
817
818 query = IssueQuery.new(:name => '_')
818 query = IssueQuery.new(:name => '_')
819 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
819 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
820 ids = find_issues_with_query(query).map(&:id)
820 ids = find_issues_with_query(query).map(&:id)
821 assert_equal [], ids & [1, 2, 3]
821 assert_equal [], ids & [1, 2, 3]
822 assert_include 4, ids
822 assert_include 4, ids
823 end
823 end
824
824
825 def test_filter_on_relations_with_any_issues
825 def test_filter_on_relations_with_any_issues
826 IssueRelation.delete_all
826 IssueRelation.delete_all
827 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
827 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
828 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
828 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
829
829
830 query = IssueQuery.new(:name => '_')
830 query = IssueQuery.new(:name => '_')
831 query.filters = {"relates" => {:operator => '*', :values => ['']}}
831 query.filters = {"relates" => {:operator => '*', :values => ['']}}
832 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
832 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
833 end
833 end
834
834
835 def test_filter_on_relations_should_not_ignore_other_filter
835 def test_filter_on_relations_should_not_ignore_other_filter
836 issue = Issue.generate!
836 issue = Issue.generate!
837 issue1 = Issue.generate!(:status_id => 1)
837 issue1 = Issue.generate!(:status_id => 1)
838 issue2 = Issue.generate!(:status_id => 2)
838 issue2 = Issue.generate!(:status_id => 2)
839 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
839 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
840 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
840 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
841
841
842 query = IssueQuery.new(:name => '_')
842 query = IssueQuery.new(:name => '_')
843 query.filters = {
843 query.filters = {
844 "status_id" => {:operator => '=', :values => ['1']},
844 "status_id" => {:operator => '=', :values => ['1']},
845 "relates" => {:operator => '=', :values => [issue.id.to_s]}
845 "relates" => {:operator => '=', :values => [issue.id.to_s]}
846 }
846 }
847 assert_equal [issue1], find_issues_with_query(query)
847 assert_equal [issue1], find_issues_with_query(query)
848 end
848 end
849
849
850 def test_statement_should_be_nil_with_no_filters
850 def test_statement_should_be_nil_with_no_filters
851 q = IssueQuery.new(:name => '_')
851 q = IssueQuery.new(:name => '_')
852 q.filters = {}
852 q.filters = {}
853
853
854 assert q.valid?
854 assert q.valid?
855 assert_nil q.statement
855 assert_nil q.statement
856 end
856 end
857
857
858 def test_default_columns
858 def test_default_columns
859 q = IssueQuery.new
859 q = IssueQuery.new
860 assert q.columns.any?
860 assert q.columns.any?
861 assert q.inline_columns.any?
861 assert q.inline_columns.any?
862 assert q.block_columns.empty?
862 assert q.block_columns.empty?
863 end
863 end
864
864
865 def test_set_column_names
865 def test_set_column_names
866 q = IssueQuery.new
866 q = IssueQuery.new
867 q.column_names = ['tracker', :subject, '', 'unknonw_column']
867 q.column_names = ['tracker', :subject, '', 'unknonw_column']
868 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
868 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
869 end
869 end
870
870
871 def test_has_column_should_accept_a_column_name
871 def test_has_column_should_accept_a_column_name
872 q = IssueQuery.new
872 q = IssueQuery.new
873 q.column_names = ['tracker', :subject]
873 q.column_names = ['tracker', :subject]
874 assert q.has_column?(:tracker)
874 assert q.has_column?(:tracker)
875 assert !q.has_column?(:category)
875 assert !q.has_column?(:category)
876 end
876 end
877
877
878 def test_has_column_should_accept_a_column
878 def test_has_column_should_accept_a_column
879 q = IssueQuery.new
879 q = IssueQuery.new
880 q.column_names = ['tracker', :subject]
880 q.column_names = ['tracker', :subject]
881
881
882 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
882 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
883 assert_kind_of QueryColumn, tracker_column
883 assert_kind_of QueryColumn, tracker_column
884 category_column = q.available_columns.detect {|c| c.name==:category}
884 category_column = q.available_columns.detect {|c| c.name==:category}
885 assert_kind_of QueryColumn, category_column
885 assert_kind_of QueryColumn, category_column
886
886
887 assert q.has_column?(tracker_column)
887 assert q.has_column?(tracker_column)
888 assert !q.has_column?(category_column)
888 assert !q.has_column?(category_column)
889 end
889 end
890
890
891 def test_inline_and_block_columns
891 def test_inline_and_block_columns
892 q = IssueQuery.new
892 q = IssueQuery.new
893 q.column_names = ['subject', 'description', 'tracker']
893 q.column_names = ['subject', 'description', 'tracker']
894
894
895 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
895 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
896 assert_equal [:description], q.block_columns.map(&:name)
896 assert_equal [:description], q.block_columns.map(&:name)
897 end
897 end
898
898
899 def test_custom_field_columns_should_be_inline
899 def test_custom_field_columns_should_be_inline
900 q = IssueQuery.new
900 q = IssueQuery.new
901 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
901 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
902 assert columns.any?
902 assert columns.any?
903 assert_nil columns.detect {|column| !column.inline?}
903 assert_nil columns.detect {|column| !column.inline?}
904 end
904 end
905
905
906 def test_query_should_preload_spent_hours
906 def test_query_should_preload_spent_hours
907 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
907 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
908 assert q.has_column?(:spent_hours)
908 assert q.has_column?(:spent_hours)
909 issues = q.issues
909 issues = q.issues
910 assert_not_nil issues.first.instance_variable_get("@spent_hours")
910 assert_not_nil issues.first.instance_variable_get("@spent_hours")
911 end
911 end
912
912
913 def test_groupable_columns_should_include_custom_fields
913 def test_groupable_columns_should_include_custom_fields
914 q = IssueQuery.new
914 q = IssueQuery.new
915 column = q.groupable_columns.detect {|c| c.name == :cf_1}
915 column = q.groupable_columns.detect {|c| c.name == :cf_1}
916 assert_not_nil column
916 assert_not_nil column
917 assert_kind_of QueryCustomFieldColumn, column
917 assert_kind_of QueryCustomFieldColumn, column
918 end
918 end
919
919
920 def test_groupable_columns_should_not_include_multi_custom_fields
920 def test_groupable_columns_should_not_include_multi_custom_fields
921 field = CustomField.find(1)
921 field = CustomField.find(1)
922 field.update_attribute :multiple, true
922 field.update_attribute :multiple, true
923
923
924 q = IssueQuery.new
924 q = IssueQuery.new
925 column = q.groupable_columns.detect {|c| c.name == :cf_1}
925 column = q.groupable_columns.detect {|c| c.name == :cf_1}
926 assert_nil column
926 assert_nil column
927 end
927 end
928
928
929 def test_groupable_columns_should_include_user_custom_fields
929 def test_groupable_columns_should_include_user_custom_fields
930 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
930 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
931
931
932 q = IssueQuery.new
932 q = IssueQuery.new
933 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
933 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
934 end
934 end
935
935
936 def test_groupable_columns_should_include_version_custom_fields
936 def test_groupable_columns_should_include_version_custom_fields
937 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
937 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
938
938
939 q = IssueQuery.new
939 q = IssueQuery.new
940 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
940 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
941 end
941 end
942
942
943 def test_grouped_with_valid_column
943 def test_grouped_with_valid_column
944 q = IssueQuery.new(:group_by => 'status')
944 q = IssueQuery.new(:group_by => 'status')
945 assert q.grouped?
945 assert q.grouped?
946 assert_not_nil q.group_by_column
946 assert_not_nil q.group_by_column
947 assert_equal :status, q.group_by_column.name
947 assert_equal :status, q.group_by_column.name
948 assert_not_nil q.group_by_statement
948 assert_not_nil q.group_by_statement
949 assert_equal 'status', q.group_by_statement
949 assert_equal 'status', q.group_by_statement
950 end
950 end
951
951
952 def test_grouped_with_invalid_column
952 def test_grouped_with_invalid_column
953 q = IssueQuery.new(:group_by => 'foo')
953 q = IssueQuery.new(:group_by => 'foo')
954 assert !q.grouped?
954 assert !q.grouped?
955 assert_nil q.group_by_column
955 assert_nil q.group_by_column
956 assert_nil q.group_by_statement
956 assert_nil q.group_by_statement
957 end
957 end
958
958
959 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
959 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
960 with_settings :user_format => 'lastname_coma_firstname' do
960 with_settings :user_format => 'lastname_coma_firstname' do
961 q = IssueQuery.new
961 q = IssueQuery.new
962 assert q.sortable_columns.has_key?('assigned_to')
962 assert q.sortable_columns.has_key?('assigned_to')
963 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
963 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
964 end
964 end
965 end
965 end
966
966
967 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
967 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
968 with_settings :user_format => 'lastname_coma_firstname' do
968 with_settings :user_format => 'lastname_coma_firstname' do
969 q = IssueQuery.new
969 q = IssueQuery.new
970 assert q.sortable_columns.has_key?('author')
970 assert q.sortable_columns.has_key?('author')
971 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
971 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
972 end
972 end
973 end
973 end
974
974
975 def test_sortable_columns_should_include_custom_field
975 def test_sortable_columns_should_include_custom_field
976 q = IssueQuery.new
976 q = IssueQuery.new
977 assert q.sortable_columns['cf_1']
977 assert q.sortable_columns['cf_1']
978 end
978 end
979
979
980 def test_sortable_columns_should_not_include_multi_custom_field
980 def test_sortable_columns_should_not_include_multi_custom_field
981 field = CustomField.find(1)
981 field = CustomField.find(1)
982 field.update_attribute :multiple, true
982 field.update_attribute :multiple, true
983
983
984 q = IssueQuery.new
984 q = IssueQuery.new
985 assert !q.sortable_columns['cf_1']
985 assert !q.sortable_columns['cf_1']
986 end
986 end
987
987
988 def test_default_sort
988 def test_default_sort
989 q = IssueQuery.new
989 q = IssueQuery.new
990 assert_equal [], q.sort_criteria
990 assert_equal [], q.sort_criteria
991 end
991 end
992
992
993 def test_set_sort_criteria_with_hash
993 def test_set_sort_criteria_with_hash
994 q = IssueQuery.new
994 q = IssueQuery.new
995 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
995 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
996 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
996 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
997 end
997 end
998
998
999 def test_set_sort_criteria_with_array
999 def test_set_sort_criteria_with_array
1000 q = IssueQuery.new
1000 q = IssueQuery.new
1001 q.sort_criteria = [['priority', 'desc'], 'tracker']
1001 q.sort_criteria = [['priority', 'desc'], 'tracker']
1002 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1002 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1003 end
1003 end
1004
1004
1005 def test_create_query_with_sort
1005 def test_create_query_with_sort
1006 q = IssueQuery.new(:name => 'Sorted')
1006 q = IssueQuery.new(:name => 'Sorted')
1007 q.sort_criteria = [['priority', 'desc'], 'tracker']
1007 q.sort_criteria = [['priority', 'desc'], 'tracker']
1008 assert q.save
1008 assert q.save
1009 q.reload
1009 q.reload
1010 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1010 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1011 end
1011 end
1012
1012
1013 def test_sort_by_string_custom_field_asc
1013 def test_sort_by_string_custom_field_asc
1014 q = IssueQuery.new
1014 q = IssueQuery.new
1015 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1015 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1016 assert c
1016 assert c
1017 assert c.sortable
1017 assert c.sortable
1018 issues = q.issues(:order => "#{c.sortable} ASC")
1018 issues = q.issues(:order => "#{c.sortable} ASC")
1019 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1019 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1020 assert !values.empty?
1020 assert !values.empty?
1021 assert_equal values.sort, values
1021 assert_equal values.sort, values
1022 end
1022 end
1023
1023
1024 def test_sort_by_string_custom_field_desc
1024 def test_sort_by_string_custom_field_desc
1025 q = IssueQuery.new
1025 q = IssueQuery.new
1026 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1026 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1027 assert c
1027 assert c
1028 assert c.sortable
1028 assert c.sortable
1029 issues = q.issues(:order => "#{c.sortable} DESC")
1029 issues = q.issues(:order => "#{c.sortable} DESC")
1030 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1030 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1031 assert !values.empty?
1031 assert !values.empty?
1032 assert_equal values.sort.reverse, values
1032 assert_equal values.sort.reverse, values
1033 end
1033 end
1034
1034
1035 def test_sort_by_float_custom_field_asc
1035 def test_sort_by_float_custom_field_asc
1036 q = IssueQuery.new
1036 q = IssueQuery.new
1037 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1037 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1038 assert c
1038 assert c
1039 assert c.sortable
1039 assert c.sortable
1040 issues = q.issues(:order => "#{c.sortable} ASC")
1040 issues = q.issues(:order => "#{c.sortable} ASC")
1041 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1041 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1042 assert !values.empty?
1042 assert !values.empty?
1043 assert_equal values.sort, values
1043 assert_equal values.sort, values
1044 end
1044 end
1045
1045
1046 def test_invalid_query_should_raise_query_statement_invalid_error
1046 def test_invalid_query_should_raise_query_statement_invalid_error
1047 q = IssueQuery.new
1047 q = IssueQuery.new
1048 assert_raise Query::StatementInvalid do
1048 assert_raise Query::StatementInvalid do
1049 q.issues(:conditions => "foo = 1")
1049 q.issues(:conditions => "foo = 1")
1050 end
1050 end
1051 end
1051 end
1052
1052
1053 def test_issue_count
1053 def test_issue_count
1054 q = IssueQuery.new(:name => '_')
1054 q = IssueQuery.new(:name => '_')
1055 issue_count = q.issue_count
1055 issue_count = q.issue_count
1056 assert_equal q.issues.size, issue_count
1056 assert_equal q.issues.size, issue_count
1057 end
1057 end
1058
1058
1059 def test_issue_count_with_archived_issues
1059 def test_issue_count_with_archived_issues
1060 p = Project.generate! do |project|
1060 p = Project.generate! do |project|
1061 project.status = Project::STATUS_ARCHIVED
1061 project.status = Project::STATUS_ARCHIVED
1062 end
1062 end
1063 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1063 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1064 assert !i.visible?
1064 assert !i.visible?
1065
1065
1066 test_issue_count
1066 test_issue_count
1067 end
1067 end
1068
1068
1069 def test_issue_count_by_association_group
1069 def test_issue_count_by_association_group
1070 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1070 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1071 count_by_group = q.issue_count_by_group
1071 count_by_group = q.issue_count_by_group
1072 assert_kind_of Hash, count_by_group
1072 assert_kind_of Hash, count_by_group
1073 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1073 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1074 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1074 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1075 assert count_by_group.has_key?(User.find(3))
1075 assert count_by_group.has_key?(User.find(3))
1076 end
1076 end
1077
1077
1078 def test_issue_count_by_list_custom_field_group
1078 def test_issue_count_by_list_custom_field_group
1079 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1079 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1080 count_by_group = q.issue_count_by_group
1080 count_by_group = q.issue_count_by_group
1081 assert_kind_of Hash, count_by_group
1081 assert_kind_of Hash, count_by_group
1082 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1082 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1083 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1083 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1084 assert count_by_group.has_key?('MySQL')
1084 assert count_by_group.has_key?('MySQL')
1085 end
1085 end
1086
1086
1087 def test_issue_count_by_date_custom_field_group
1087 def test_issue_count_by_date_custom_field_group
1088 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1088 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1089 count_by_group = q.issue_count_by_group
1089 count_by_group = q.issue_count_by_group
1090 assert_kind_of Hash, count_by_group
1090 assert_kind_of Hash, count_by_group
1091 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1091 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1092 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1092 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1093 end
1093 end
1094
1094
1095 def test_issue_count_with_nil_group_only
1095 def test_issue_count_with_nil_group_only
1096 Issue.update_all("assigned_to_id = NULL")
1096 Issue.update_all("assigned_to_id = NULL")
1097
1097
1098 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1098 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1099 count_by_group = q.issue_count_by_group
1099 count_by_group = q.issue_count_by_group
1100 assert_kind_of Hash, count_by_group
1100 assert_kind_of Hash, count_by_group
1101 assert_equal 1, count_by_group.keys.size
1101 assert_equal 1, count_by_group.keys.size
1102 assert_nil count_by_group.keys.first
1102 assert_nil count_by_group.keys.first
1103 end
1103 end
1104
1104
1105 def test_issue_ids
1105 def test_issue_ids
1106 q = IssueQuery.new(:name => '_')
1106 q = IssueQuery.new(:name => '_')
1107 order = "issues.subject, issues.id"
1107 order = "issues.subject, issues.id"
1108 issues = q.issues(:order => order)
1108 issues = q.issues(:order => order)
1109 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1109 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1110 end
1110 end
1111
1111
1112 def test_label_for
1112 def test_label_for
1113 set_language_if_valid 'en'
1113 set_language_if_valid 'en'
1114 q = IssueQuery.new
1114 q = IssueQuery.new
1115 assert_equal 'Assignee', q.label_for('assigned_to_id')
1115 assert_equal 'Assignee', q.label_for('assigned_to_id')
1116 end
1116 end
1117
1117
1118 def test_label_for_fr
1118 def test_label_for_fr
1119 set_language_if_valid 'fr'
1119 set_language_if_valid 'fr'
1120 q = IssueQuery.new
1120 q = IssueQuery.new
1121 s = "Assign\xc3\xa9 \xc3\xa0"
1121 s = "Assign\xc3\xa9 \xc3\xa0"
1122 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
1122 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
1123 assert_equal s, q.label_for('assigned_to_id')
1123 assert_equal s, q.label_for('assigned_to_id')
1124 end
1124 end
1125
1125
1126 def test_editable_by
1126 def test_editable_by
1127 admin = User.find(1)
1127 admin = User.find(1)
1128 manager = User.find(2)
1128 manager = User.find(2)
1129 developer = User.find(3)
1129 developer = User.find(3)
1130
1130
1131 # Public query on project 1
1131 # Public query on project 1
1132 q = IssueQuery.find(1)
1132 q = IssueQuery.find(1)
1133 assert q.editable_by?(admin)
1133 assert q.editable_by?(admin)
1134 assert q.editable_by?(manager)
1134 assert q.editable_by?(manager)
1135 assert !q.editable_by?(developer)
1135 assert !q.editable_by?(developer)
1136
1136
1137 # Private query on project 1
1137 # Private query on project 1
1138 q = IssueQuery.find(2)
1138 q = IssueQuery.find(2)
1139 assert q.editable_by?(admin)
1139 assert q.editable_by?(admin)
1140 assert !q.editable_by?(manager)
1140 assert !q.editable_by?(manager)
1141 assert q.editable_by?(developer)
1141 assert q.editable_by?(developer)
1142
1142
1143 # Private query for all projects
1143 # Private query for all projects
1144 q = IssueQuery.find(3)
1144 q = IssueQuery.find(3)
1145 assert q.editable_by?(admin)
1145 assert q.editable_by?(admin)
1146 assert !q.editable_by?(manager)
1146 assert !q.editable_by?(manager)
1147 assert q.editable_by?(developer)
1147 assert q.editable_by?(developer)
1148
1148
1149 # Public query for all projects
1149 # Public query for all projects
1150 q = IssueQuery.find(4)
1150 q = IssueQuery.find(4)
1151 assert q.editable_by?(admin)
1151 assert q.editable_by?(admin)
1152 assert !q.editable_by?(manager)
1152 assert !q.editable_by?(manager)
1153 assert !q.editable_by?(developer)
1153 assert !q.editable_by?(developer)
1154 end
1154 end
1155
1155
1156 def test_visible_scope
1156 def test_visible_scope
1157 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1157 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1158
1158
1159 assert query_ids.include?(1), 'public query on public project was not visible'
1159 assert query_ids.include?(1), 'public query on public project was not visible'
1160 assert query_ids.include?(4), 'public query for all projects was not visible'
1160 assert query_ids.include?(4), 'public query for all projects was not visible'
1161 assert !query_ids.include?(2), 'private query on public project was visible'
1161 assert !query_ids.include?(2), 'private query on public project was visible'
1162 assert !query_ids.include?(3), 'private query for all projects was visible'
1162 assert !query_ids.include?(3), 'private query for all projects was visible'
1163 assert !query_ids.include?(7), 'public query on private project was visible'
1163 assert !query_ids.include?(7), 'public query on private project was visible'
1164 end
1164 end
1165
1165
1166 def test_query_with_public_visibility_should_be_visible_to_anyone
1166 def test_query_with_public_visibility_should_be_visible_to_anyone
1167 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1167 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1168
1168
1169 assert q.visible?(User.anonymous)
1169 assert q.visible?(User.anonymous)
1170 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1170 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1171
1171
1172 assert q.visible?(User.find(7))
1172 assert q.visible?(User.find(7))
1173 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1173 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1174
1174
1175 assert q.visible?(User.find(2))
1175 assert q.visible?(User.find(2))
1176 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1176 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1177
1177
1178 assert q.visible?(User.find(1))
1178 assert q.visible?(User.find(1))
1179 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1179 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1180 end
1180 end
1181
1181
1182 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1182 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1183 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1183 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1184
1184
1185 assert !q.visible?(User.anonymous)
1185 assert !q.visible?(User.anonymous)
1186 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1186 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1187
1187
1188 assert !q.visible?(User.find(7))
1188 assert !q.visible?(User.find(7))
1189 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1189 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1190
1190
1191 assert q.visible?(User.find(2))
1191 assert q.visible?(User.find(2))
1192 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1192 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1193
1193
1194 assert q.visible?(User.find(1))
1194 assert q.visible?(User.find(1))
1195 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1195 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1196 end
1196 end
1197
1197
1198 def test_query_with_private_visibility_should_be_visible_to_owner
1198 def test_query_with_private_visibility_should_be_visible_to_owner
1199 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1199 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1200
1200
1201 assert !q.visible?(User.anonymous)
1201 assert !q.visible?(User.anonymous)
1202 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1202 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1203
1203
1204 assert q.visible?(User.find(7))
1204 assert q.visible?(User.find(7))
1205 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1205 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1206
1206
1207 assert !q.visible?(User.find(2))
1207 assert !q.visible?(User.find(2))
1208 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1208 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1209
1209
1210 assert q.visible?(User.find(1))
1210 assert q.visible?(User.find(1))
1211 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1211 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1212 end
1212 end
1213
1213
1214 test "#available_filters should include users of visible projects in cross-project view" do
1214 test "#available_filters should include users of visible projects in cross-project view" do
1215 users = IssueQuery.new.available_filters["assigned_to_id"]
1215 users = IssueQuery.new.available_filters["assigned_to_id"]
1216 assert_not_nil users
1216 assert_not_nil users
1217 assert users[:values].map{|u|u[1]}.include?("3")
1217 assert users[:values].map{|u|u[1]}.include?("3")
1218 end
1218 end
1219
1219
1220 test "#available_filters should include users of subprojects" do
1220 test "#available_filters should include users of subprojects" do
1221 user1 = User.generate!
1221 user1 = User.generate!
1222 user2 = User.generate!
1222 user2 = User.generate!
1223 project = Project.find(1)
1223 project = Project.find(1)
1224 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1224 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1225
1225
1226 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1226 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1227 assert_not_nil users
1227 assert_not_nil users
1228 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1228 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1229 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1229 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1230 end
1230 end
1231
1231
1232 test "#available_filters should include visible projects in cross-project view" do
1232 test "#available_filters should include visible projects in cross-project view" do
1233 projects = IssueQuery.new.available_filters["project_id"]
1233 projects = IssueQuery.new.available_filters["project_id"]
1234 assert_not_nil projects
1234 assert_not_nil projects
1235 assert projects[:values].map{|u|u[1]}.include?("1")
1235 assert projects[:values].map{|u|u[1]}.include?("1")
1236 end
1236 end
1237
1237
1238 test "#available_filters should include 'member_of_group' filter" do
1238 test "#available_filters should include 'member_of_group' filter" do
1239 query = IssueQuery.new
1239 query = IssueQuery.new
1240 assert query.available_filters.keys.include?("member_of_group")
1240 assert query.available_filters.keys.include?("member_of_group")
1241 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1241 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1242 assert query.available_filters["member_of_group"][:values].present?
1242 assert query.available_filters["member_of_group"][:values].present?
1243 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1243 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1244 query.available_filters["member_of_group"][:values].sort
1244 query.available_filters["member_of_group"][:values].sort
1245 end
1245 end
1246
1246
1247 test "#available_filters should include 'assigned_to_role' filter" do
1247 test "#available_filters should include 'assigned_to_role' filter" do
1248 query = IssueQuery.new
1248 query = IssueQuery.new
1249 assert query.available_filters.keys.include?("assigned_to_role")
1249 assert query.available_filters.keys.include?("assigned_to_role")
1250 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1250 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1251
1251
1252 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1252 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1253 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1253 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1254 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1254 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1255
1255
1256 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1256 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1257 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1257 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1258 end
1258 end
1259
1259
1260 def test_available_filters_should_include_custom_field_according_to_user_visibility
1260 def test_available_filters_should_include_custom_field_according_to_user_visibility
1261 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1261 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1262 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1262 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1263
1263
1264 with_current_user User.find(3) do
1264 with_current_user User.find(3) do
1265 query = IssueQuery.new
1265 query = IssueQuery.new
1266 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1266 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1267 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1267 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1268 end
1268 end
1269 end
1269 end
1270
1270
1271 def test_available_columns_should_include_custom_field_according_to_user_visibility
1271 def test_available_columns_should_include_custom_field_according_to_user_visibility
1272 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1272 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1273 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1273 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1274
1274
1275 with_current_user User.find(3) do
1275 with_current_user User.find(3) do
1276 query = IssueQuery.new
1276 query = IssueQuery.new
1277 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1277 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1278 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1278 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1279 end
1279 end
1280 end
1280 end
1281
1281
1282 context "#statement" do
1282 context "#statement" do
1283 context "with 'member_of_group' filter" do
1283 context "with 'member_of_group' filter" do
1284 setup do
1284 setup do
1285 Group.destroy_all # No fixtures
1285 Group.destroy_all # No fixtures
1286 @user_in_group = User.generate!
1286 @user_in_group = User.generate!
1287 @second_user_in_group = User.generate!
1287 @second_user_in_group = User.generate!
1288 @user_in_group2 = User.generate!
1288 @user_in_group2 = User.generate!
1289 @user_not_in_group = User.generate!
1289 @user_not_in_group = User.generate!
1290
1290
1291 @group = Group.generate!.reload
1291 @group = Group.generate!.reload
1292 @group.users << @user_in_group
1292 @group.users << @user_in_group
1293 @group.users << @second_user_in_group
1293 @group.users << @second_user_in_group
1294
1294
1295 @group2 = Group.generate!.reload
1295 @group2 = Group.generate!.reload
1296 @group2.users << @user_in_group2
1296 @group2.users << @user_in_group2
1297
1297
1298 end
1298 end
1299
1299
1300 should "search assigned to for users in the group" do
1300 should "search assigned to for users in the group" do
1301 @query = IssueQuery.new(:name => '_')
1301 @query = IssueQuery.new(:name => '_')
1302 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1302 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1303
1303
1304 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1304 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1305 assert_find_issues_with_query_is_successful @query
1305 assert_find_issues_with_query_is_successful @query
1306 end
1306 end
1307
1307
1308 should "search not assigned to any group member (none)" do
1308 should "search not assigned to any group member (none)" do
1309 @query = IssueQuery.new(:name => '_')
1309 @query = IssueQuery.new(:name => '_')
1310 @query.add_filter('member_of_group', '!*', [''])
1310 @query.add_filter('member_of_group', '!*', [''])
1311
1311
1312 # Users not in a group
1312 # Users not in a group
1313 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1313 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1314 assert_find_issues_with_query_is_successful @query
1314 assert_find_issues_with_query_is_successful @query
1315 end
1315 end
1316
1316
1317 should "search assigned to any group member (all)" do
1317 should "search assigned to any group member (all)" do
1318 @query = IssueQuery.new(:name => '_')
1318 @query = IssueQuery.new(:name => '_')
1319 @query.add_filter('member_of_group', '*', [''])
1319 @query.add_filter('member_of_group', '*', [''])
1320
1320
1321 # Only users in a group
1321 # Only users in a group
1322 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1322 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1323 assert_find_issues_with_query_is_successful @query
1323 assert_find_issues_with_query_is_successful @query
1324 end
1324 end
1325
1325
1326 should "return an empty set with = empty group" do
1326 should "return an empty set with = empty group" do
1327 @empty_group = Group.generate!
1327 @empty_group = Group.generate!
1328 @query = IssueQuery.new(:name => '_')
1328 @query = IssueQuery.new(:name => '_')
1329 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1329 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1330
1330
1331 assert_equal [], find_issues_with_query(@query)
1331 assert_equal [], find_issues_with_query(@query)
1332 end
1332 end
1333
1333
1334 should "return issues with ! empty group" do
1334 should "return issues with ! empty group" do
1335 @empty_group = Group.generate!
1335 @empty_group = Group.generate!
1336 @query = IssueQuery.new(:name => '_')
1336 @query = IssueQuery.new(:name => '_')
1337 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1337 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1338
1338
1339 assert_find_issues_with_query_is_successful @query
1339 assert_find_issues_with_query_is_successful @query
1340 end
1340 end
1341 end
1341 end
1342
1342
1343 context "with 'assigned_to_role' filter" do
1343 context "with 'assigned_to_role' filter" do
1344 setup do
1344 setup do
1345 @manager_role = Role.find_by_name('Manager')
1345 @manager_role = Role.find_by_name('Manager')
1346 @developer_role = Role.find_by_name('Developer')
1346 @developer_role = Role.find_by_name('Developer')
1347
1347
1348 @project = Project.generate!
1348 @project = Project.generate!
1349 @manager = User.generate!
1349 @manager = User.generate!
1350 @developer = User.generate!
1350 @developer = User.generate!
1351 @boss = User.generate!
1351 @boss = User.generate!
1352 @guest = User.generate!
1352 @guest = User.generate!
1353 User.add_to_project(@manager, @project, @manager_role)
1353 User.add_to_project(@manager, @project, @manager_role)
1354 User.add_to_project(@developer, @project, @developer_role)
1354 User.add_to_project(@developer, @project, @developer_role)
1355 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1355 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1356
1356
1357 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1357 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1358 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1358 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1359 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1359 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1360 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1360 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1361 @issue5 = Issue.generate!(:project => @project)
1361 @issue5 = Issue.generate!(:project => @project)
1362 end
1362 end
1363
1363
1364 should "search assigned to for users with the Role" do
1364 should "search assigned to for users with the Role" do
1365 @query = IssueQuery.new(:name => '_', :project => @project)
1365 @query = IssueQuery.new(:name => '_', :project => @project)
1366 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1366 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1367
1367
1368 assert_query_result [@issue1, @issue3], @query
1368 assert_query_result [@issue1, @issue3], @query
1369 end
1369 end
1370
1370
1371 should "search assigned to for users with the Role on the issue project" do
1371 should "search assigned to for users with the Role on the issue project" do
1372 other_project = Project.generate!
1372 other_project = Project.generate!
1373 User.add_to_project(@developer, other_project, @manager_role)
1373 User.add_to_project(@developer, other_project, @manager_role)
1374
1374
1375 @query = IssueQuery.new(:name => '_', :project => @project)
1375 @query = IssueQuery.new(:name => '_', :project => @project)
1376 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1376 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1377
1377
1378 assert_query_result [@issue1, @issue3], @query
1378 assert_query_result [@issue1, @issue3], @query
1379 end
1379 end
1380
1380
1381 should "return an empty set with empty role" do
1381 should "return an empty set with empty role" do
1382 @empty_role = Role.generate!
1382 @empty_role = Role.generate!
1383 @query = IssueQuery.new(:name => '_', :project => @project)
1383 @query = IssueQuery.new(:name => '_', :project => @project)
1384 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1384 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1385
1385
1386 assert_query_result [], @query
1386 assert_query_result [], @query
1387 end
1387 end
1388
1388
1389 should "search assigned to for users without the Role" do
1389 should "search assigned to for users without the Role" do
1390 @query = IssueQuery.new(:name => '_', :project => @project)
1390 @query = IssueQuery.new(:name => '_', :project => @project)
1391 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1391 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1392
1392
1393 assert_query_result [@issue2, @issue4, @issue5], @query
1393 assert_query_result [@issue2, @issue4, @issue5], @query
1394 end
1394 end
1395
1395
1396 should "search assigned to for users not assigned to any Role (none)" do
1396 should "search assigned to for users not assigned to any Role (none)" do
1397 @query = IssueQuery.new(:name => '_', :project => @project)
1397 @query = IssueQuery.new(:name => '_', :project => @project)
1398 @query.add_filter('assigned_to_role', '!*', [''])
1398 @query.add_filter('assigned_to_role', '!*', [''])
1399
1399
1400 assert_query_result [@issue4, @issue5], @query
1400 assert_query_result [@issue4, @issue5], @query
1401 end
1401 end
1402
1402
1403 should "search assigned to for users assigned to any Role (all)" do
1403 should "search assigned to for users assigned to any Role (all)" do
1404 @query = IssueQuery.new(:name => '_', :project => @project)
1404 @query = IssueQuery.new(:name => '_', :project => @project)
1405 @query.add_filter('assigned_to_role', '*', [''])
1405 @query.add_filter('assigned_to_role', '*', [''])
1406
1406
1407 assert_query_result [@issue1, @issue2, @issue3], @query
1407 assert_query_result [@issue1, @issue2, @issue3], @query
1408 end
1408 end
1409
1409
1410 should "return issues with ! empty role" do
1410 should "return issues with ! empty role" do
1411 @empty_role = Role.generate!
1411 @empty_role = Role.generate!
1412 @query = IssueQuery.new(:name => '_', :project => @project)
1412 @query = IssueQuery.new(:name => '_', :project => @project)
1413 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1413 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1414
1414
1415 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1415 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1416 end
1416 end
1417 end
1417 end
1418 end
1418 end
1419 end
1419 end
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now