##// END OF EJS Templates
User groups branch merged....
Jean-Philippe Lang -
r2755:770745714544
parent child
Show More
@@ -0,0 +1,162
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 GroupsController < ApplicationController
19 layout 'base'
20 before_filter :require_admin
21
22 helper :custom_fields
23
24 # GET /groups
25 # GET /groups.xml
26 def index
27 @groups = Group.find(:all, :order => 'lastname')
28
29 respond_to do |format|
30 format.html # index.html.erb
31 format.xml { render :xml => @groups }
32 end
33 end
34
35 # GET /groups/1
36 # GET /groups/1.xml
37 def show
38 @group = Group.find(params[:id])
39
40 respond_to do |format|
41 format.html # show.html.erb
42 format.xml { render :xml => @group }
43 end
44 end
45
46 # GET /groups/new
47 # GET /groups/new.xml
48 def new
49 @group = Group.new
50
51 respond_to do |format|
52 format.html # new.html.erb
53 format.xml { render :xml => @group }
54 end
55 end
56
57 # GET /groups/1/edit
58 def edit
59 @group = Group.find(params[:id])
60 end
61
62 # POST /groups
63 # POST /groups.xml
64 def create
65 @group = Group.new(params[:group])
66
67 respond_to do |format|
68 if @group.save
69 flash[:notice] = l(:notice_successful_create)
70 format.html { redirect_to(groups_path) }
71 format.xml { render :xml => @group, :status => :created, :location => @group }
72 else
73 format.html { render :action => "new" }
74 format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
75 end
76 end
77 end
78
79 # PUT /groups/1
80 # PUT /groups/1.xml
81 def update
82 @group = Group.find(params[:id])
83
84 respond_to do |format|
85 if @group.update_attributes(params[:group])
86 flash[:notice] = l(:notice_successful_update)
87 format.html { redirect_to(groups_path) }
88 format.xml { head :ok }
89 else
90 format.html { render :action => "edit" }
91 format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
92 end
93 end
94 end
95
96 # DELETE /groups/1
97 # DELETE /groups/1.xml
98 def destroy
99 @group = Group.find(params[:id])
100 @group.destroy
101
102 respond_to do |format|
103 format.html { redirect_to(groups_url) }
104 format.xml { head :ok }
105 end
106 end
107
108 def add_users
109 @group = Group.find(params[:id])
110 users = User.find_all_by_id(params[:user_ids])
111 @group.users << users if request.post?
112 respond_to do |format|
113 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
114 format.js {
115 render(:update) {|page|
116 page.replace_html "tab-content-users", :partial => 'groups/users'
117 users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
118 }
119 }
120 end
121 end
122
123 def remove_user
124 @group = Group.find(params[:id])
125 @group.users.delete(User.find(params[:user_id])) if request.post?
126 respond_to do |format|
127 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
128 format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
129 end
130 end
131
132 def autocomplete_for_user
133 @group = Group.find(params[:id])
134 @users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
135 render :layout => false
136 end
137
138 def edit_membership
139 @group = Group.find(params[:id])
140 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @group)
141 @membership.attributes = params[:membership]
142 @membership.save if request.post?
143 respond_to do |format|
144 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
145 format.js {
146 render(:update) {|page|
147 page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
148 page.visual_effect(:highlight, "member-#{@membership.id}")
149 }
150 }
151 end
152 end
153
154 def destroy_membership
155 @group = Group.find(params[:id])
156 Member.find(params[:membership_id]).destroy if request.post?
157 respond_to do |format|
158 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
159 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
160 end
161 end
162 end
@@ -0,0 +1,34
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 module GroupsHelper
19 # Options for the new membership projects combo-box
20 def options_for_membership_project_select(user, projects)
21 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
22 options << project_tree_options_for_select(projects) do |p|
23 {:disabled => (user.projects.include?(p))}
24 end
25 options
26 end
27
28 def group_settings_tabs
29 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
30 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
31 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
32 ]
33 end
34 end
@@ -0,0 +1,48
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 Group < Principal
19 has_and_belongs_to_many :users, :after_add => :user_added,
20 :after_remove => :user_removed
21
22 acts_as_customizable
23
24 validates_presence_of :lastname
25 validates_uniqueness_of :lastname, :case_sensitive => false
26 validates_length_of :lastname, :maximum => 30
27
28 def to_s
29 lastname.to_s
30 end
31
32 def user_added(user)
33 members.each do |member|
34 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)
35 member.member_roles.each do |member_role|
36 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
37 end
38 user_member.save!
39 end
40 end
41
42 def user_removed(user)
43 members.each do |member|
44 MemberRole.find(:all, :include => :member,
45 :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
46 end
47 end
48 end
@@ -0,0 +1,22
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 GroupCustomField < CustomField
19 def type_name
20 :label_group_plural
21 end
22 end
@@ -0,0 +1,38
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 Principal < ActiveRecord::Base
19 set_table_name 'users'
20
21 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
22 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
23 has_many :projects, :through => :memberships
24
25 # Groups and active users
26 named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)"
27
28 named_scope :like, lambda {|q|
29 s = "%#{q.to_s.strip.downcase}%"
30 {:conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", s, s, s],
31 :order => 'type, login, lastname, firstname'
32 }
33 }
34
35 def <=>(principal)
36 self.to_s.downcase <=> principal.to_s.downcase
37 end
38 end
@@ -0,0 +1,8
1 <%= error_messages_for :group %>
2
3 <div class="box tabular">
4 <p><%= f.text_field :lastname, :label => :field_name %></p>
5 <% @group.custom_field_values.each do |value| %>
6 <p><%= custom_field_tag_with_label :group, value %></p>
7 <% end %>
8 </div>
@@ -0,0 +1,4
1 <% labelled_tabular_form_for :group, @group, :url => { :controller => 'group', :action => 'update', :tab => nil } do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
@@ -0,0 +1,56
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
3
4 <div class="splitcontentleft">
5 <% if @group.memberships.any? %>
6 <table class="list memberships">
7 <thead>
8 <th><%= l(:label_project) %></th>
9 <th><%= l(:label_role_plural) %></th>
10 <th style="width:15%"></th>
11 </thead>
12 <tbody>
13 <% @group.memberships.each do |membership| %>
14 <% next if membership.new_record? %>
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <td class="project"><%=h membership.project %></td>
17 <td class="roles">
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
19 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
20 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 <p><% roles.each do |role| %>
22 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <% end %></p>
24 <p><%= submit_tag l(:button_change) %>
25 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
26 <% end %>
27 </td>
28 <td class="buttons">
29 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
30 <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership },
31 :method => :post },
32 :class => 'icon icon-del' %>
33 </td>
34 </tr>
35 </tbody>
36 <% end; reset_cycle %>
37 </table>
38 <% else %>
39 <p class="nodata"><%= l(:label_no_data) %></p>
40 <% end %>
41 </div>
42
43 <div class="splitcontentright">
44 <% if projects.any? %>
45 <fieldset><legend><%=l(:label_project_new)%></legend>
46 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
47 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
48 <p><%= l(:label_role_plural) %>:
49 <% roles.each do |role| %>
50 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
51 <% end %></p>
52 <p><%= submit_tag l(:button_add) %></p>
53 <% end %>
54 </fieldset>
55 <% end %>
56 </div>
@@ -0,0 +1,49
1 <div class="splitcontentleft">
2 <% if @group.users.any? %>
3 <table class="list users">
4 <thead>
5 <th><%= l(:label_user) %></th>
6 <th style="width:15%"></th>
7 </thead>
8 <tbody>
9 <% @group.users.sort.each do |user| %>
10 <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
11 <td class="user"><%= link_to_user user %></td>
12 <td class="buttons">
13 <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'remove_user', :id => @group, :user_id => user },
14 :method => :post },
15 :class => 'icon icon-del' %>
16 </td>
17 </tr>
18 <% end %>
19 </tbody>
20 </table>
21 <% else %>
22 <p class="nodata"><%= l(:label_no_data) %></p>
23 <% end %>
24 </div>
25
26 <div class="splitcontentright">
27 <% users = User.active.find(:all, :limit => 100) - @group.users %>
28 <% if users.any? %>
29 <% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
30 <fieldset><legend><%=l(:label_user_new)%></legend>
31
32 <p><%= text_field_tag 'user_search', nil, :size => "40" %></p>
33 <%= observe_field(:user_search,
34 :frequency => 0.5,
35 :update => :users,
36 :url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
37 :with => 'q')
38 %>
39
40 <div id="users">
41 <%= principals_check_box_tags 'user_ids[]', users %>
42 </div>
43
44 <p><%= submit_tag l(:button_add) %></p>
45 </fieldset>
46 <% end %>
47 <% end %>
48
49 </div>
@@ -0,0 +1,1
1 <%= principals_check_box_tags 'user_ids[]', @users %>
@@ -0,0 +1,23
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= h(@group) %></h2>
2
3 <% selected_tab = params[:tab] ? params[:tab].to_s : group_settings_tabs.first[:name] %>
4
5 <div class="tabs">
6 <ul>
7 <% group_settings_tabs.each do |tab| -%>
8 <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
9 :id => "tab-#{tab[:name]}",
10 :class => (tab[:name] != selected_tab ? nil : 'selected'),
11 :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
12 <% end -%>
13 </ul>
14 </div>
15
16 <% group_settings_tabs.each do |tab| -%>
17 <%= content_tag('div', render(:partial => tab[:partial]),
18 :id => "tab-content-#{tab[:name]}",
19 :style => (tab[:name] != selected_tab ? 'display:none' : nil),
20 :class => 'tab-content') %>
21 <% end -%>
22
23 <% html_title(l(:label_group), @group, l(:label_administration)) -%>
@@ -0,0 +1,25
1 <div class="contextual">
2 <%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
3 </div>
4
5 <h2><%= l(:label_group_plural) %></h2>
6
7 <% if @groups.any? %>
8 <table class="list groups">
9 <thead><tr>
10 <th><%=l(:label_group)%></th>
11 <th><%=l(:label_user_plural)%></th>
12 <th></th>
13 </tr></thead>
14 <tbody>
15 <% @groups.each do |group| %>
16 <tr class="<%= cycle 'odd', 'even' %>">
17 <td><%= link_to h(group), :action => 'edit', :id => group %></td>
18 <td align="center"><%= group.users.size %></td>
19 <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
20 </tr>
21 <% end %>
22 </table>
23 <% else %>
24 <p class="nodata"><%= l(:label_no_data) %></p>
25 <% end %>
@@ -0,0 +1,8
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
2
3 <%= error_messages_for :group %>
4
5 <% form_for(@group, :builder => TabularFormBuilder, :lang => current_language) do |f| %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
7 <p><%= f.submit l(:button_create) %></p>
8 <% end %>
@@ -0,0 +1,7
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%=h @group %></h2>
2
3 <ul>
4 <% @group.users.each do |user| %>
5 <li><%=h user %></li>
6 <% end %>
7 </ul>
@@ -0,0 +1,1
1 <%= principals_check_box_tags 'member[user_ids][]', @principals %> No newline at end of file
@@ -0,0 +1,9
1 <% form_for(:user, :url => { :action => 'edit' }) do %>
2 <div class="box">
3 <% Group.all.each do |group| %>
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
5 <% end %>
6 <%= hidden_field_tag 'user[group_ids][]', '' %>
7 </div>
8 <%= submit_tag l(:button_save) %>
9 <% end %>
@@ -0,0 +1,8
1 class PopulateUsersType < ActiveRecord::Migration
2 def self.up
3 Principal.update_all("type = 'User'", "type IS NULL")
4 end
5
6 def self.down
7 end
8 end
@@ -0,0 +1,13
1 class CreateGroupsUsers < ActiveRecord::Migration
2 def self.up
3 create_table :groups_users, :id => false do |t|
4 t.column :group_id, :integer, :null => false
5 t.column :user_id, :integer, :null => false
6 end
7 add_index :groups_users, [:group_id, :user_id], :unique => true, :name => :groups_users_ids
8 end
9
10 def self.down
11 drop_table :groups_users
12 end
13 end
@@ -0,0 +1,9
1 class AddMemberRolesInheritedFrom < ActiveRecord::Migration
2 def self.up
3 add_column :member_roles, :inherited_from, :integer
4 end
5
6 def self.down
7 remove_column :member_roles, :inherited_from
8 end
9 end
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,5
1 ---
2 groups_users_001:
3 group_id: 10
4 user_id: 8
5 No newline at end of file
@@ -0,0 +1,107
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 require File.dirname(__FILE__) + '/../test_helper'
19 require 'groups_controller'
20
21 # Re-raise errors caught by the controller.
22 class GroupsController; def rescue_action(e) raise e end; end
23
24 class GroupsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :member_roles
26
27 def setup
28 @controller = GroupsController.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
32 @request.session[:user_id] = 1
33 end
34
35 def test_index
36 get :index
37 assert_response :success
38 assert_template 'index'
39 end
40
41 def test_show
42 get :show, :id => 10
43 assert_response :success
44 assert_template 'show'
45 end
46
47 def test_new
48 get :new
49 assert_response :success
50 assert_template 'new'
51 end
52
53 def test_create
54 assert_difference 'Group.count' do
55 post :create, :group => {:lastname => 'New group'}
56 end
57 assert_redirected_to 'groups'
58 end
59
60 def test_edit
61 get :edit, :id => 10
62 assert_response :success
63 assert_template 'edit'
64 end
65
66 def test_update
67 post :update, :id => 10
68 assert_redirected_to 'groups'
69 end
70
71 def test_destroy
72 assert_difference 'Group.count', -1 do
73 post :destroy, :id => 10
74 end
75 assert_redirected_to 'groups'
76 end
77
78 def test_add_users
79 assert_difference 'Group.find(10).users.count', 2 do
80 post :add_users, :id => 10, :user_ids => ['2', '3']
81 end
82 end
83
84 def test_remove_user
85 assert_difference 'Group.find(10).users.count', -1 do
86 post :remove_user, :id => 10, :user_id => '8'
87 end
88 end
89
90 def test_new_membership
91 assert_difference 'Group.find(10).members.count' do
92 post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']}
93 end
94 end
95
96 def test_edit_membership
97 assert_no_difference 'Group.find(10).members.count' do
98 post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']}
99 end
100 end
101
102 def test_destroy_membership
103 assert_difference 'Group.find(10).members.count', -1 do
104 post :destroy_membership, :id => 10, :membership_id => 6
105 end
106 end
107 end
@@ -0,0 +1,77
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 require File.dirname(__FILE__) + '/../test_helper'
19
20 class GroupTest < Test::Unit::TestCase
21 fixtures :all
22
23 def test_create
24 g = Group.new(:lastname => 'New group')
25 assert g.save
26 end
27
28 def test_roles_given_to_new_user
29 group = Group.find(11)
30 user = User.find(9)
31 project = Project.first
32
33 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
34 group.users << user
35 assert user.member_of?(project)
36 end
37
38 def test_roles_given_to_existing_user
39 group = Group.find(11)
40 user = User.find(9)
41 project = Project.first
42
43 group.users << user
44 m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
45 assert user.member_of?(project)
46 end
47
48 def test_roles_updated
49 group = Group.find(11)
50 user = User.find(9)
51 project = Project.first
52 group.users << user
53 m = Member.create!(:principal => group, :project => project, :role_ids => [1])
54 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
55
56 m.role_ids = [1, 2]
57 assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort
58
59 m.role_ids = [2]
60 assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort
61
62 m.role_ids = [1]
63 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
64 end
65
66 def test_roles_removed_when_removing_group_membership
67 assert User.find(8).member_of?(Project.find(5))
68 Member.find_by_project_id_and_user_id(5, 10).destroy
69 assert !User.find(8).member_of?(Project.find(5))
70 end
71
72 def test_roles_removed_when_removing_user_from_group
73 assert User.find(8).member_of?(Project.find(5))
74 User.find(8).groups.clear
75 assert !User.find(8).member_of?(Project.find(5))
76 end
77 end
@@ -1,89 +1,89
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MembersController < ApplicationController
19 before_filter :find_member, :except => [:new, :autocomplete_for_member_login]
20 before_filter :find_project, :only => [:new, :autocomplete_for_member_login]
19 before_filter :find_member, :except => [:new, :autocomplete_for_member]
20 before_filter :find_project, :only => [:new, :autocomplete_for_member]
21 21 before_filter :authorize
22 22
23 23 def new
24 24 members = []
25 25 if params[:member] && request.post?
26 26 attrs = params[:member].dup
27 27 if (user_ids = attrs.delete(:user_ids))
28 28 user_ids.each do |user_id|
29 29 members << Member.new(attrs.merge(:user_id => user_id))
30 30 end
31 31 else
32 32 members << Member.new(attrs)
33 33 end
34 34 @project.members << members
35 35 end
36 36 respond_to do |format|
37 37 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
38 38 format.js {
39 39 render(:update) {|page|
40 40 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
41 41 members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
42 42 }
43 43 }
44 44 end
45 45 end
46 46
47 47 def edit
48 48 if request.post? and @member.update_attributes(params[:member])
49 49 respond_to do |format|
50 50 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
51 51 format.js {
52 52 render(:update) {|page|
53 53 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
54 54 page.visual_effect(:highlight, "member-#{@member.id}")
55 55 }
56 56 }
57 57 end
58 58 end
59 59 end
60 60
61 61 def destroy
62 @member.destroy
63 respond_to do |format|
62 if request.post? && @member.deletable?
63 @member.destroy
64 end
65 respond_to do |format|
64 66 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
65 67 format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
66 68 end
67 69 end
68 70
69 def autocomplete_for_member_login
70 @users = User.active.find(:all, :conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", "#{params[:user]}%", "#{params[:user]}%", "#{params[:user]}%"],
71 :limit => 10,
72 :order => 'login ASC') - @project.users
71 def autocomplete_for_member
72 @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
73 73 render :layout => false
74 74 end
75 75
76 76 private
77 77 def find_project
78 78 @project = Project.find(params[:id])
79 79 rescue ActiveRecord::RecordNotFound
80 80 render_404
81 81 end
82 82
83 83 def find_member
84 84 @member = Member.find(params[:id])
85 85 @project = @member.project
86 86 rescue ActiveRecord::RecordNotFound
87 87 render_404
88 88 end
89 89 end
@@ -1,120 +1,125
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 before_filter :require_admin
20 20
21 21 helper :sort
22 22 include SortHelper
23 23 helper :custom_fields
24 24 include CustomFieldsHelper
25 25
26 26 def index
27 27 list
28 28 render :action => 'list' unless request.xhr?
29 29 end
30 30
31 31 def list
32 32 sort_init 'login', 'asc'
33 33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
34 34
35 35 @status = params[:status] ? params[:status].to_i : 1
36 36 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
37 37
38 38 unless params[:name].blank?
39 39 name = "%#{params[:name].strip.downcase}%"
40 40 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
41 41 end
42 42
43 43 @user_count = User.count(:conditions => c.conditions)
44 44 @user_pages = Paginator.new self, @user_count,
45 45 per_page_option,
46 46 params['page']
47 47 @users = User.find :all,:order => sort_clause,
48 48 :conditions => c.conditions,
49 49 :limit => @user_pages.items_per_page,
50 50 :offset => @user_pages.current.offset
51 51
52 52 render :action => "list", :layout => false if request.xhr?
53 53 end
54 54
55 55 def add
56 56 if request.get?
57 57 @user = User.new(:language => Setting.default_language)
58 58 else
59 59 @user = User.new(params[:user])
60 60 @user.admin = params[:user][:admin] || false
61 61 @user.login = params[:user][:login]
62 62 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
63 63 if @user.save
64 64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
65 65 flash[:notice] = l(:notice_successful_create)
66 redirect_to :action => 'list'
66 redirect_to :controller => 'users', :action => 'edit', :id => @user
67 67 end
68 68 end
69 69 @auth_sources = AuthSource.find(:all)
70 70 end
71 71
72 72 def edit
73 73 @user = User.find(params[:id])
74 74 if request.post?
75 75 @user.admin = params[:user][:admin] if params[:user][:admin]
76 76 @user.login = params[:user][:login] if params[:user][:login]
77 77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
78 @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
78 79 @user.attributes = params[:user]
79 80 # Was the account actived ? (do it before User#save clears the change)
80 81 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
81 82 if @user.save
82 83 if was_activated
83 84 Mailer.deliver_account_activated(@user)
84 85 elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
85 86 Mailer.deliver_account_information(@user, params[:password])
86 87 end
87 88 flash[:notice] = l(:notice_successful_update)
88 # Give a string to redirect_to otherwise it would use status param as the response code
89 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
89 redirect_to :back
90 90 end
91 91 end
92 92 @auth_sources = AuthSource.find(:all)
93 93 @membership ||= Member.new
94 rescue ::ActionController::RedirectBackError
95 redirect_to :controller => 'users', :action => 'edit', :id => @user
94 96 end
95 97
96 98 def edit_membership
97 99 @user = User.find(params[:id])
98 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
100 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user)
99 101 @membership.attributes = params[:membership]
100 102 @membership.save if request.post?
101 103 respond_to do |format|
102 104 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
103 105 format.js {
104 106 render(:update) {|page|
105 107 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
106 108 page.visual_effect(:highlight, "member-#{@membership.id}")
107 109 }
108 110 }
109 111 end
110 112 end
111 113
112 114 def destroy_membership
113 115 @user = User.find(params[:id])
114 Member.find(params[:membership_id]).destroy if request.post?
116 @membership = Member.find(params[:membership_id])
117 if request.post? && @membership.deletable?
118 @membership.destroy
119 end
115 120 respond_to do |format|
116 121 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
117 122 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
118 123 end
119 124 end
120 125 end
@@ -1,655 +1,667
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
38 38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
39 39 end
40 40
41 41 # Display a link to remote if user is authorized
42 42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
43 43 url = options[:url] || {}
44 44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
45 45 end
46 46
47 47 # Display a link to user's account page
48 48 def link_to_user(user, options={})
49 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
49 if user.is_a?(User)
50 !user.anonymous? ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
51 else
52 user.to_s
53 end
50 54 end
51 55
52 56 def link_to_issue(issue, options={})
53 57 options[:class] ||= issue.css_classes
54 58 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
55 59 end
56 60
57 61 # Generates a link to an attachment.
58 62 # Options:
59 63 # * :text - Link text (default to attachment filename)
60 64 # * :download - Force download (default: false)
61 65 def link_to_attachment(attachment, options={})
62 66 text = options.delete(:text) || attachment.filename
63 67 action = options.delete(:download) ? 'download' : 'show'
64 68
65 69 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
66 70 end
67 71
68 72 def toggle_link(name, id, options={})
69 73 onclick = "Element.toggle('#{id}'); "
70 74 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
71 75 onclick << "return false;"
72 76 link_to(name, "#", :onclick => onclick)
73 77 end
74 78
75 79 def image_to_function(name, function, html_options = {})
76 80 html_options.symbolize_keys!
77 81 tag(:input, html_options.merge({
78 82 :type => "image", :src => image_path(name),
79 83 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
80 84 }))
81 85 end
82 86
83 87 def prompt_to_remote(name, text, param, url, html_options = {})
84 88 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
85 89 link_to name, {}, html_options
86 90 end
87 91
88 92 def format_activity_title(text)
89 93 h(truncate_single_line(text, :length => 100))
90 94 end
91 95
92 96 def format_activity_day(date)
93 97 date == Date.today ? l(:label_today).titleize : format_date(date)
94 98 end
95 99
96 100 def format_activity_description(text)
97 101 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
98 102 end
99 103
100 104 def due_date_distance_in_words(date)
101 105 if date
102 106 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
103 107 end
104 108 end
105 109
106 110 def render_page_hierarchy(pages, node=nil)
107 111 content = ''
108 112 if pages[node]
109 113 content << "<ul class=\"pages-hierarchy\">\n"
110 114 pages[node].each do |page|
111 115 content << "<li>"
112 116 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
113 117 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
114 118 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
115 119 content << "</li>\n"
116 120 end
117 121 content << "</ul>\n"
118 122 end
119 123 content
120 124 end
121 125
122 126 # Renders flash messages
123 127 def render_flash_messages
124 128 s = ''
125 129 flash.each do |k,v|
126 130 s << content_tag('div', v, :class => "flash #{k}")
127 131 end
128 132 s
129 133 end
130 134
131 135 # Renders the project quick-jump box
132 136 def render_project_jump_box
133 137 # Retrieve them now to avoid a COUNT query
134 138 projects = User.current.projects.all
135 139 if projects.any?
136 140 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
137 141 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
138 142 '<option disabled="disabled">---</option>'
139 143 s << project_tree_options_for_select(projects) do |p|
140 144 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
141 145 end
142 146 s << '</select>'
143 147 s
144 148 end
145 149 end
146 150
147 151 def project_tree_options_for_select(projects, options = {})
148 152 s = ''
149 153 project_tree(projects) do |project, level|
150 154 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
151 155 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
152 156 tag_options.merge!(yield(project)) if block_given?
153 157 s << content_tag('option', name_prefix + h(project), tag_options)
154 158 end
155 159 s
156 160 end
157 161
158 162 # Yields the given block for each project with its level in the tree
159 163 def project_tree(projects, &block)
160 164 ancestors = []
161 165 projects.sort_by(&:lft).each do |project|
162 166 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
163 167 ancestors.pop
164 168 end
165 169 yield project, ancestors.size
166 170 ancestors << project
167 171 end
168 172 end
169 173
170 174 def project_nested_ul(projects, &block)
171 175 s = ''
172 176 if projects.any?
173 177 ancestors = []
174 178 projects.sort_by(&:lft).each do |project|
175 179 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
176 180 s << "<ul>\n"
177 181 else
178 182 ancestors.pop
179 183 s << "</li>"
180 184 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
181 185 ancestors.pop
182 186 s << "</ul></li>\n"
183 187 end
184 188 end
185 189 s << "<li>"
186 190 s << yield(project).to_s
187 191 ancestors << project
188 192 end
189 193 s << ("</li></ul>\n" * ancestors.size)
190 194 end
191 195 s
192 196 end
197
198 def principals_check_box_tags(name, principals)
199 s = ''
200 principals.each do |principal|
201 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
202 end
203 s
204 end
193 205
194 206 # Truncates and returns the string as a single line
195 207 def truncate_single_line(string, *args)
196 208 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
197 209 end
198 210
199 211 def html_hours(text)
200 212 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
201 213 end
202 214
203 215 def authoring(created, author, options={})
204 216 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
205 217 l(options[:label] || :label_added_time_by, :author => author_tag, :age => time_tag(created))
206 218 end
207 219
208 220 def time_tag(time)
209 221 text = distance_of_time_in_words(Time.now, time)
210 222 if @project
211 223 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
212 224 else
213 225 content_tag('acronym', text, :title => format_time(time))
214 226 end
215 227 end
216 228
217 229 def syntax_highlight(name, content)
218 230 type = CodeRay::FileType[name]
219 231 type ? CodeRay.scan(content, type).html : h(content)
220 232 end
221 233
222 234 def to_path_param(path)
223 235 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
224 236 end
225 237
226 238 def pagination_links_full(paginator, count=nil, options={})
227 239 page_param = options.delete(:page_param) || :page
228 240 url_param = params.dup
229 241 # don't reuse query params if filters are present
230 242 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
231 243
232 244 html = ''
233 245 if paginator.current.previous
234 246 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
235 247 end
236 248
237 249 html << (pagination_links_each(paginator, options) do |n|
238 250 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
239 251 end || '')
240 252
241 253 if paginator.current.next
242 254 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
243 255 end
244 256
245 257 unless count.nil?
246 258 html << [
247 259 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
248 260 per_page_links(paginator.items_per_page)
249 261 ].compact.join(' | ')
250 262 end
251 263
252 264 html
253 265 end
254 266
255 267 def per_page_links(selected=nil)
256 268 url_param = params.dup
257 269 url_param.clear if url_param.has_key?(:set_filter)
258 270
259 271 links = Setting.per_page_options_array.collect do |n|
260 272 n == selected ? n : link_to_remote(n, {:update => "content",
261 273 :url => params.dup.merge(:per_page => n),
262 274 :method => :get},
263 275 {:href => url_for(url_param.merge(:per_page => n))})
264 276 end
265 277 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
266 278 end
267 279
268 280 def reorder_links(name, url)
269 281 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
270 282 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
271 283 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
272 284 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
273 285 end
274 286
275 287 def breadcrumb(*args)
276 288 elements = args.flatten
277 289 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
278 290 end
279 291
280 292 def other_formats_links(&block)
281 293 concat('<p class="other-formats">' + l(:label_export_to))
282 294 yield Redmine::Views::OtherFormatsBuilder.new(self)
283 295 concat('</p>')
284 296 end
285 297
286 298 def page_header_title
287 299 if @project.nil? || @project.new_record?
288 300 h(Setting.app_title)
289 301 else
290 302 b = []
291 303 ancestors = (@project.root? ? [] : @project.ancestors.visible)
292 304 if ancestors.any?
293 305 root = ancestors.shift
294 306 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
295 307 if ancestors.size > 2
296 308 b << '&#8230;'
297 309 ancestors = ancestors[-2, 2]
298 310 end
299 311 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
300 312 end
301 313 b << h(@project)
302 314 b.join(' &#187; ')
303 315 end
304 316 end
305 317
306 318 def html_title(*args)
307 319 if args.empty?
308 320 title = []
309 321 title << @project.name if @project
310 322 title += @html_title if @html_title
311 323 title << Setting.app_title
312 324 title.compact.join(' - ')
313 325 else
314 326 @html_title ||= []
315 327 @html_title += args
316 328 end
317 329 end
318 330
319 331 def accesskey(s)
320 332 Redmine::AccessKeys.key_for s
321 333 end
322 334
323 335 # Formats text according to system settings.
324 336 # 2 ways to call this method:
325 337 # * with a String: textilizable(text, options)
326 338 # * with an object and one of its attribute: textilizable(issue, :description, options)
327 339 def textilizable(*args)
328 340 options = args.last.is_a?(Hash) ? args.pop : {}
329 341 case args.size
330 342 when 1
331 343 obj = options[:object]
332 344 text = args.shift
333 345 when 2
334 346 obj = args.shift
335 347 text = obj.send(args.shift).to_s
336 348 else
337 349 raise ArgumentError, 'invalid arguments to textilizable'
338 350 end
339 351 return '' if text.blank?
340 352
341 353 only_path = options.delete(:only_path) == false ? false : true
342 354
343 355 # when using an image link, try to use an attachment, if possible
344 356 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
345 357
346 358 if attachments
347 359 attachments = attachments.sort_by(&:created_on).reverse
348 360 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
349 361 style = $1
350 362 filename = $6.downcase
351 363 # search for the picture in attachments
352 364 if found = attachments.detect { |att| att.filename.downcase == filename }
353 365 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
354 366 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
355 367 alt = desc.blank? ? nil : "(#{desc})"
356 368 "!#{style}#{image_url}#{alt}!"
357 369 else
358 370 m
359 371 end
360 372 end
361 373 end
362 374
363 375 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
364 376
365 377 # different methods for formatting wiki links
366 378 case options[:wiki_links]
367 379 when :local
368 380 # used for local links to html files
369 381 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
370 382 when :anchor
371 383 # used for single-file wiki export
372 384 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
373 385 else
374 386 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
375 387 end
376 388
377 389 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
378 390
379 391 # Wiki links
380 392 #
381 393 # Examples:
382 394 # [[mypage]]
383 395 # [[mypage|mytext]]
384 396 # wiki links can refer other project wikis, using project name or identifier:
385 397 # [[project:]] -> wiki starting page
386 398 # [[project:|mytext]]
387 399 # [[project:mypage]]
388 400 # [[project:mypage|mytext]]
389 401 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
390 402 link_project = project
391 403 esc, all, page, title = $1, $2, $3, $5
392 404 if esc.nil?
393 405 if page =~ /^([^\:]+)\:(.*)$/
394 406 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
395 407 page = $2
396 408 title ||= $1 if page.blank?
397 409 end
398 410
399 411 if link_project && link_project.wiki
400 412 # extract anchor
401 413 anchor = nil
402 414 if page =~ /^(.+?)\#(.+)$/
403 415 page, anchor = $1, $2
404 416 end
405 417 # check if page exists
406 418 wiki_page = link_project.wiki.find_page(page)
407 419 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
408 420 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
409 421 else
410 422 # project or wiki doesn't exist
411 423 all
412 424 end
413 425 else
414 426 all
415 427 end
416 428 end
417 429
418 430 # Redmine links
419 431 #
420 432 # Examples:
421 433 # Issues:
422 434 # #52 -> Link to issue #52
423 435 # Changesets:
424 436 # r52 -> Link to revision 52
425 437 # commit:a85130f -> Link to scmid starting with a85130f
426 438 # Documents:
427 439 # document#17 -> Link to document with id 17
428 440 # document:Greetings -> Link to the document with title "Greetings"
429 441 # document:"Some document" -> Link to the document with title "Some document"
430 442 # Versions:
431 443 # version#3 -> Link to version with id 3
432 444 # version:1.0.0 -> Link to version named "1.0.0"
433 445 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
434 446 # Attachments:
435 447 # attachment:file.zip -> Link to the attachment of the current object named file.zip
436 448 # Source files:
437 449 # source:some/file -> Link to the file located at /some/file in the project's repository
438 450 # source:some/file@52 -> Link to the file's revision 52
439 451 # source:some/file#L120 -> Link to line 120 of the file
440 452 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
441 453 # export:some/file -> Force the download of the file
442 454 # Forum messages:
443 455 # message#1218 -> Link to message with id 1218
444 456 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
445 457 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
446 458 link = nil
447 459 if esc.nil?
448 460 if prefix.nil? && sep == 'r'
449 461 if project && (changeset = project.changesets.find_by_revision(oid))
450 462 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
451 463 :class => 'changeset',
452 464 :title => truncate_single_line(changeset.comments, :length => 100))
453 465 end
454 466 elsif sep == '#'
455 467 oid = oid.to_i
456 468 case prefix
457 469 when nil
458 470 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
459 471 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
460 472 :class => (issue.closed? ? 'issue closed' : 'issue'),
461 473 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
462 474 link = content_tag('del', link) if issue.closed?
463 475 end
464 476 when 'document'
465 477 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
466 478 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
467 479 :class => 'document'
468 480 end
469 481 when 'version'
470 482 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
471 483 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
472 484 :class => 'version'
473 485 end
474 486 when 'message'
475 487 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
476 488 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
477 489 :controller => 'messages',
478 490 :action => 'show',
479 491 :board_id => message.board,
480 492 :id => message.root,
481 493 :anchor => (message.parent ? "message-#{message.id}" : nil)},
482 494 :class => 'message'
483 495 end
484 496 end
485 497 elsif sep == ':'
486 498 # removes the double quotes if any
487 499 name = oid.gsub(%r{^"(.*)"$}, "\\1")
488 500 case prefix
489 501 when 'document'
490 502 if project && document = project.documents.find_by_title(name)
491 503 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
492 504 :class => 'document'
493 505 end
494 506 when 'version'
495 507 if project && version = project.versions.find_by_name(name)
496 508 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
497 509 :class => 'version'
498 510 end
499 511 when 'commit'
500 512 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
501 513 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
502 514 :class => 'changeset',
503 515 :title => truncate_single_line(changeset.comments, :length => 100)
504 516 end
505 517 when 'source', 'export'
506 518 if project && project.repository
507 519 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
508 520 path, rev, anchor = $1, $3, $5
509 521 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
510 522 :path => to_path_param(path),
511 523 :rev => rev,
512 524 :anchor => anchor,
513 525 :format => (prefix == 'export' ? 'raw' : nil)},
514 526 :class => (prefix == 'export' ? 'source download' : 'source')
515 527 end
516 528 when 'attachment'
517 529 if attachments && attachment = attachments.detect {|a| a.filename == name }
518 530 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
519 531 :class => 'attachment'
520 532 end
521 533 end
522 534 end
523 535 end
524 536 leading + (link || "#{prefix}#{sep}#{oid}")
525 537 end
526 538
527 539 text
528 540 end
529 541
530 542 # Same as Rails' simple_format helper without using paragraphs
531 543 def simple_format_without_paragraph(text)
532 544 text.to_s.
533 545 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
534 546 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
535 547 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
536 548 end
537 549
538 550 def lang_options_for_select(blank=true)
539 551 (blank ? [["(auto)", ""]] : []) +
540 552 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
541 553 end
542 554
543 555 def label_tag_for(name, option_tags = nil, options = {})
544 556 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
545 557 content_tag("label", label_text)
546 558 end
547 559
548 560 def labelled_tabular_form_for(name, object, options, &proc)
549 561 options[:html] ||= {}
550 562 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
551 563 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
552 564 end
553 565
554 566 def back_url_hidden_field_tag
555 567 back_url = params[:back_url] || request.env['HTTP_REFERER']
556 568 back_url = CGI.unescape(back_url.to_s)
557 569 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
558 570 end
559 571
560 572 def check_all_links(form_name)
561 573 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
562 574 " | " +
563 575 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
564 576 end
565 577
566 578 def progress_bar(pcts, options={})
567 579 pcts = [pcts, pcts] unless pcts.is_a?(Array)
568 580 pcts[1] = pcts[1] - pcts[0]
569 581 pcts << (100 - pcts[1] - pcts[0])
570 582 width = options[:width] || '100px;'
571 583 legend = options[:legend] || ''
572 584 content_tag('table',
573 585 content_tag('tr',
574 586 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
575 587 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
576 588 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
577 589 ), :class => 'progress', :style => "width: #{width};") +
578 590 content_tag('p', legend, :class => 'pourcent')
579 591 end
580 592
581 593 def context_menu_link(name, url, options={})
582 594 options[:class] ||= ''
583 595 if options.delete(:selected)
584 596 options[:class] << ' icon-checked disabled'
585 597 options[:disabled] = true
586 598 end
587 599 if options.delete(:disabled)
588 600 options.delete(:method)
589 601 options.delete(:confirm)
590 602 options.delete(:onclick)
591 603 options[:class] << ' disabled'
592 604 url = '#'
593 605 end
594 606 link_to name, url, options
595 607 end
596 608
597 609 def calendar_for(field_id)
598 610 include_calendar_headers_tags
599 611 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
600 612 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
601 613 end
602 614
603 615 def include_calendar_headers_tags
604 616 unless @calendar_headers_tags_included
605 617 @calendar_headers_tags_included = true
606 618 content_for :header_tags do
607 619 javascript_include_tag('calendar/calendar') +
608 620 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
609 621 javascript_include_tag('calendar/calendar-setup') +
610 622 stylesheet_link_tag('calendar')
611 623 end
612 624 end
613 625 end
614 626
615 627 def content_for(name, content = nil, &block)
616 628 @has_content ||= {}
617 629 @has_content[name] = true
618 630 super(name, content, &block)
619 631 end
620 632
621 633 def has_content?(name)
622 634 (@has_content && @has_content[name]) || false
623 635 end
624 636
625 637 # Returns the avatar image tag for the given +user+ if avatars are enabled
626 638 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
627 639 def avatar(user, options = { })
628 640 if Setting.gravatar_enabled?
629 641 options.merge!({:ssl => Setting.protocol == 'https'})
630 642 email = nil
631 643 if user.respond_to?(:mail)
632 644 email = user.mail
633 645 elsif user.to_s =~ %r{<(.+?)>}
634 646 email = $1
635 647 end
636 648 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
637 649 end
638 650 end
639 651
640 652 private
641 653
642 654 def wiki_helper
643 655 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
644 656 extend helper
645 657 return self
646 658 end
647 659
648 660 def link_to_remote_content_update(text, url_params)
649 661 link_to_remote(text,
650 662 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
651 663 {:href => url_for(:params => url_params)}
652 664 )
653 665 end
654 666
655 667 end
@@ -1,88 +1,89
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module CustomFieldsHelper
19 19
20 20 def custom_fields_tabs
21 21 tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
22 22 {:name => 'TimeEntryCustomField', :label => :label_spent_time},
23 23 {:name => 'ProjectCustomField', :label => :label_project_plural},
24 {:name => 'UserCustomField', :label => :label_user_plural}
24 {:name => 'UserCustomField', :label => :label_user_plural},
25 {:name => 'GroupCustomField', :label => :label_group_plural}
25 26 ]
26 27 end
27 28
28 29 # Return custom field html tag corresponding to its format
29 30 def custom_field_tag(name, custom_value)
30 31 custom_field = custom_value.custom_field
31 32 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
32 33 field_id = "#{name}_custom_field_values_#{custom_field.id}"
33 34
34 35 case custom_field.field_format
35 36 when "date"
36 37 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
37 38 calendar_for(field_id)
38 39 when "text"
39 40 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
40 41 when "bool"
41 42 check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0')
42 43 when "list"
43 44 blank_option = custom_field.is_required? ?
44 45 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
45 46 '<option></option>'
46 47 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
47 48 else
48 49 text_field_tag(field_name, custom_value.value, :id => field_id)
49 50 end
50 51 end
51 52
52 53 # Return custom field label tag
53 54 def custom_field_label_tag(name, custom_value)
54 55 content_tag "label", custom_value.custom_field.name +
55 56 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
56 57 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
57 58 :class => (custom_value.errors.empty? ? nil : "error" )
58 59 end
59 60
60 61 # Return custom field tag with its label tag
61 62 def custom_field_tag_with_label(name, custom_value)
62 63 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
63 64 end
64 65
65 66 # Return a string used to display a custom value
66 67 def show_value(custom_value)
67 68 return "" unless custom_value
68 69 format_value(custom_value.value, custom_value.custom_field.field_format)
69 70 end
70 71
71 72 # Return a string used to display a custom value
72 73 def format_value(value, field_format)
73 74 return "" unless value && !value.empty?
74 75 case field_format
75 76 when "date"
76 77 begin; format_date(value.to_date); rescue; value end
77 78 when "bool"
78 79 l(value == "1" ? :general_text_Yes : :general_text_No)
79 80 else
80 81 value
81 82 end
82 83 end
83 84
84 85 # Return an array of custom field formats which can be used in select_tag
85 86 def custom_field_formats_for_select
86 87 CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
87 88 end
88 89 end
@@ -1,53 +1,54
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module UsersHelper
19 19 def users_status_options_for_select(selected)
20 20 user_count_by_status = User.count(:group => 'status').to_hash
21 21 options_for_select([[l(:label_all), ''],
22 22 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1],
23 23 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2],
24 24 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected)
25 25 end
26 26
27 27 # Options for the new membership projects combo-box
28 28 def options_for_membership_project_select(user, projects)
29 29 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
30 30 options << project_tree_options_for_select(projects) do |p|
31 31 {:disabled => (user.projects.include?(p))}
32 32 end
33 33 options
34 34 end
35 35
36 36 def change_status_link(user)
37 37 url = {:controller => 'users', :action => 'edit', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
38 38
39 39 if user.locked?
40 40 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
41 41 elsif user.registered?
42 42 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
43 43 elsif user != User.current
44 44 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
45 45 end
46 46 end
47 47
48 48 def user_settings_tabs
49 49 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
50 {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural},
50 51 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
51 52 ]
52 53 end
53 54 end
@@ -1,56 +1,66
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Member < ActiveRecord::Base
19 19 belongs_to :user
20 has_many :member_roles, :dependent => :delete_all
20 belongs_to :principal, :foreign_key => 'user_id'
21 has_many :member_roles, :dependent => :destroy
21 22 has_many :roles, :through => :member_roles
22 23 belongs_to :project
23 24
24 validates_presence_of :user, :project
25 validates_presence_of :principal, :project
25 26 validates_uniqueness_of :user_id, :scope => :project_id
26 27
27 28 def name
28 29 self.user.name
29 30 end
30 31
31 # Sets user by login
32 def user_login=(login)
33 login = login.to_s
34 unless login.blank?
35 if (u = User.find_by_login(login))
36 self.user = u
37 end
38 end
32 alias :base_role_ids= :role_ids=
33 def role_ids=(arg)
34 ids = (arg || []).collect(&:to_i) - [0]
35 # Keep inherited roles
36 ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
37
38 new_role_ids = ids - role_ids
39 # Add new roles
40 new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
41 # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
42 member_roles.select {|mr| !ids.include?(mr.role_id)}.each(&:destroy)
39 43 end
40 44
41 45 def <=>(member)
42 46 a, b = roles.sort.first, member.roles.sort.first
43 a == b ? (user <=> member.user) : (a <=> b)
47 a == b ? (principal <=> member.principal) : (a <=> b)
48 end
49
50 def deletable?
51 member_roles.detect {|mr| mr.inherited_from}.nil?
44 52 end
45 53
46 54 def before_destroy
47 # remove category based auto assignments for this member
48 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
55 if user
56 # remove category based auto assignments for this member
57 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
58 end
49 59 end
50 60
51 61 protected
52 62
53 63 def validate
54 errors.add_to_base "Role can't be blank" if roles.empty?
64 errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
55 65 end
56 66 end
@@ -1,27 +1,54
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MemberRole < ActiveRecord::Base
19 19 belongs_to :member
20 20 belongs_to :role
21 21
22 after_destroy :remove_member_if_empty
23
24 after_create :add_role_to_group_users
25 after_destroy :remove_role_from_group_users
26
22 27 validates_presence_of :role
23 28
24 29 def validate
25 30 errors.add :role_id, :invalid if role && !role.member?
26 31 end
32
33 private
34
35 def remove_member_if_empty
36 if member.roles.empty?
37 member.destroy
38 end
39 end
40
41 def add_role_to_group_users
42 if member.principal.is_a?(Group)
43 member.principal.users.each do |user|
44 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)
45 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
46 user_member.save!
47 end
48 end
49 end
50
51 def remove_role_from_group_users
52 MemberRole.find(:all, :conditions => { :inherited_from => id }).each(&:destroy)
53 end
27 54 end
@@ -1,409 +1,414
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :member_principals, :class_name => 'Member',
25 :include => :principal,
26 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
24 27 has_many :users, :through => :members
28 has_many :principals, :through => :member_principals, :source => :principal
29
25 30 has_many :enabled_modules, :dependent => :delete_all
26 31 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 32 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 33 has_many :issue_changes, :through => :issues, :source => :journals
29 34 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 35 has_many :time_entries, :dependent => :delete_all
31 36 has_many :queries, :dependent => :delete_all
32 37 has_many :documents, :dependent => :destroy
33 38 has_many :news, :dependent => :delete_all, :include => :author
34 39 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 40 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 41 has_one :repository, :dependent => :destroy
37 42 has_many :changesets, :through => :repository
38 43 has_one :wiki, :dependent => :destroy
39 44 # Custom field for the project issues
40 45 has_and_belongs_to_many :issue_custom_fields,
41 46 :class_name => 'IssueCustomField',
42 47 :order => "#{CustomField.table_name}.position",
43 48 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 49 :association_foreign_key => 'custom_field_id'
45 50
46 51 acts_as_nested_set :order => 'name', :dependent => :destroy
47 52 acts_as_attachable :view_permission => :view_files,
48 53 :delete_permission => :manage_files
49 54
50 55 acts_as_customizable
51 56 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 57 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 58 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 59 :author => nil
55 60
56 61 attr_protected :status, :enabled_module_names
57 62
58 63 validates_presence_of :name, :identifier
59 64 validates_uniqueness_of :name, :identifier
60 65 validates_associated :repository, :wiki
61 66 validates_length_of :name, :maximum => 30
62 67 validates_length_of :homepage, :maximum => 255
63 68 validates_length_of :identifier, :in => 1..20
64 69 # donwcase letters, digits, dashes but not digits only
65 70 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
66 71 # reserved words
67 72 validates_exclusion_of :identifier, :in => %w( new )
68 73
69 74 before_destroy :delete_all_members
70 75
71 76 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
72 77 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
73 78 named_scope :public, { :conditions => { :is_public => true } }
74 79 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
75 80
76 81 def identifier=(identifier)
77 82 super unless identifier_frozen?
78 83 end
79 84
80 85 def identifier_frozen?
81 86 errors[:identifier].nil? && !(new_record? || identifier.blank?)
82 87 end
83 88
84 89 def issues_with_subprojects(include_subprojects=false)
85 90 conditions = nil
86 91 if include_subprojects
87 92 ids = [id] + descendants.collect(&:id)
88 93 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
89 94 end
90 95 conditions ||= ["#{Project.table_name}.id = ?", id]
91 96 # Quick and dirty fix for Rails 2 compatibility
92 97 Issue.send(:with_scope, :find => { :conditions => conditions }) do
93 98 Version.send(:with_scope, :find => { :conditions => conditions }) do
94 99 yield
95 100 end
96 101 end
97 102 end
98 103
99 104 # returns latest created projects
100 105 # non public projects will be returned only if user is a member of those
101 106 def self.latest(user=nil, count=5)
102 107 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
103 108 end
104 109
105 110 # Returns a SQL :conditions string used to find all active projects for the specified user.
106 111 #
107 112 # Examples:
108 113 # Projects.visible_by(admin) => "projects.status = 1"
109 114 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
110 115 def self.visible_by(user=nil)
111 116 user ||= User.current
112 117 if user && user.admin?
113 118 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
114 119 elsif user && user.memberships.any?
115 120 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
116 121 else
117 122 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
118 123 end
119 124 end
120 125
121 126 def self.allowed_to_condition(user, permission, options={})
122 127 statements = []
123 128 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
124 129 if perm = Redmine::AccessControl.permission(permission)
125 130 unless perm.project_module.nil?
126 131 # If the permission belongs to a project module, make sure the module is enabled
127 132 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
128 133 end
129 134 end
130 135 if options[:project]
131 136 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
132 137 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
133 138 base_statement = "(#{project_statement}) AND (#{base_statement})"
134 139 end
135 140 if user.admin?
136 141 # no restriction
137 142 else
138 143 statements << "1=0"
139 144 if user.logged?
140 145 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
141 146 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
142 147 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
143 148 elsif Role.anonymous.allowed_to?(permission)
144 149 # anonymous user allowed on public project
145 150 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
146 151 else
147 152 # anonymous user is not authorized
148 153 end
149 154 end
150 155 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
151 156 end
152 157
153 158 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
154 159 #
155 160 # Examples:
156 161 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
157 162 # project.project_condition(false) => "projects.id = 1"
158 163 def project_condition(with_subprojects)
159 164 cond = "#{Project.table_name}.id = #{id}"
160 165 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
161 166 cond
162 167 end
163 168
164 169 def self.find(*args)
165 170 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
166 171 project = find_by_identifier(*args)
167 172 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
168 173 project
169 174 else
170 175 super
171 176 end
172 177 end
173 178
174 179 def to_param
175 180 # id is used for projects with a numeric identifier (compatibility)
176 181 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
177 182 end
178 183
179 184 def active?
180 185 self.status == STATUS_ACTIVE
181 186 end
182 187
183 188 # Archives the project and its descendants recursively
184 189 def archive
185 190 # Archive subprojects if any
186 191 children.each do |subproject|
187 192 subproject.archive
188 193 end
189 194 update_attribute :status, STATUS_ARCHIVED
190 195 end
191 196
192 197 # Unarchives the project
193 198 # All its ancestors must be active
194 199 def unarchive
195 200 return false if ancestors.detect {|a| !a.active?}
196 201 update_attribute :status, STATUS_ACTIVE
197 202 end
198 203
199 204 # Returns an array of projects the project can be moved to
200 205 def possible_parents
201 206 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
202 207 end
203 208
204 209 # Sets the parent of the project
205 210 # Argument can be either a Project, a String, a Fixnum or nil
206 211 def set_parent!(p)
207 212 unless p.nil? || p.is_a?(Project)
208 213 if p.to_s.blank?
209 214 p = nil
210 215 else
211 216 p = Project.find_by_id(p)
212 217 return false unless p
213 218 end
214 219 end
215 220 if p == parent && !p.nil?
216 221 # Nothing to do
217 222 true
218 223 elsif p.nil? || (p.active? && move_possible?(p))
219 224 # Insert the project so that target's children or root projects stay alphabetically sorted
220 225 sibs = (p.nil? ? self.class.roots : p.children)
221 226 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
222 227 if to_be_inserted_before
223 228 move_to_left_of(to_be_inserted_before)
224 229 elsif p.nil?
225 230 if sibs.empty?
226 231 # move_to_root adds the project in first (ie. left) position
227 232 move_to_root
228 233 else
229 234 move_to_right_of(sibs.last) unless self == sibs.last
230 235 end
231 236 else
232 237 # move_to_child_of adds the project in last (ie.right) position
233 238 move_to_child_of(p)
234 239 end
235 240 true
236 241 else
237 242 # Can not move to the given target
238 243 false
239 244 end
240 245 end
241 246
242 247 # Returns an array of the trackers used by the project and its active sub projects
243 248 def rolled_up_trackers
244 249 @rolled_up_trackers ||=
245 250 Tracker.find(:all, :include => :projects,
246 251 :select => "DISTINCT #{Tracker.table_name}.*",
247 252 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
248 253 :order => "#{Tracker.table_name}.position")
249 254 end
250 255
251 256 # Returns a hash of project users grouped by role
252 257 def users_by_role
253 258 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
254 259 m.roles.each do |r|
255 260 h[r] ||= []
256 261 h[r] << m.user
257 262 end
258 263 h
259 264 end
260 265 end
261 266
262 267 # Deletes all project's members
263 268 def delete_all_members
264 269 me, mr = Member.table_name, MemberRole.table_name
265 270 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
266 271 Member.delete_all(['project_id = ?', id])
267 272 end
268 273
269 274 # Users issues can be assigned to
270 275 def assignable_users
271 276 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
272 277 end
273 278
274 279 # Returns the mail adresses of users that should be always notified on project events
275 280 def recipients
276 281 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
277 282 end
278 283
279 284 # Returns an array of all custom fields enabled for project issues
280 285 # (explictly associated custom fields and custom fields enabled for all projects)
281 286 def all_issue_custom_fields
282 287 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
283 288 end
284 289
285 290 def project
286 291 self
287 292 end
288 293
289 294 def <=>(project)
290 295 name.downcase <=> project.name.downcase
291 296 end
292 297
293 298 def to_s
294 299 name
295 300 end
296 301
297 302 # Returns a short description of the projects (first lines)
298 303 def short_description(length = 255)
299 304 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
300 305 end
301 306
302 307 # Return true if this project is allowed to do the specified action.
303 308 # action can be:
304 309 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
305 310 # * a permission Symbol (eg. :edit_project)
306 311 def allows_to?(action)
307 312 if action.is_a? Hash
308 313 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
309 314 else
310 315 allowed_permissions.include? action
311 316 end
312 317 end
313 318
314 319 def module_enabled?(module_name)
315 320 module_name = module_name.to_s
316 321 enabled_modules.detect {|m| m.name == module_name}
317 322 end
318 323
319 324 def enabled_module_names=(module_names)
320 325 if module_names && module_names.is_a?(Array)
321 326 module_names = module_names.collect(&:to_s)
322 327 # remove disabled modules
323 328 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
324 329 # add new modules
325 330 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
326 331 else
327 332 enabled_modules.clear
328 333 end
329 334 end
330 335
331 336 # Returns an auto-generated project identifier based on the last identifier used
332 337 def self.next_identifier
333 338 p = Project.find(:first, :order => 'created_on DESC')
334 339 p.nil? ? nil : p.identifier.to_s.succ
335 340 end
336 341
337 342 # Copies and saves the Project instance based on the +project+.
338 343 # Will duplicate the source project's:
339 344 # * Issues
340 345 # * Members
341 346 # * Queries
342 347 def copy(project)
343 348 project = project.is_a?(Project) ? project : Project.find(project)
344 349
345 350 Project.transaction do
346 351 # Issues
347 352 project.issues.each do |issue|
348 353 new_issue = Issue.new
349 354 new_issue.copy_from(issue)
350 355 self.issues << new_issue
351 356 end
352 357
353 358 # Members
354 359 project.members.each do |member|
355 360 new_member = Member.new
356 361 new_member.attributes = member.attributes.dup.except("project_id")
357 362 new_member.role_ids = member.role_ids.dup
358 363 new_member.project = self
359 364 self.members << new_member
360 365 end
361 366
362 367 # Queries
363 368 project.queries.each do |query|
364 369 new_query = Query.new
365 370 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
366 371 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
367 372 new_query.project = self
368 373 self.queries << new_query
369 374 end
370 375
371 376 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
372 377 self.save
373 378 end
374 379 end
375 380
376 381
377 382 # Copies +project+ and returns the new instance. This will not save
378 383 # the copy
379 384 def self.copy_from(project)
380 385 begin
381 386 project = project.is_a?(Project) ? project : Project.find(project)
382 387 if project
383 388 # clear unique attributes
384 389 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
385 390 copy = Project.new(attributes)
386 391 copy.enabled_modules = project.enabled_modules
387 392 copy.trackers = project.trackers
388 393 copy.custom_values = project.custom_values.collect {|v| v.clone}
389 394 return copy
390 395 else
391 396 return nil
392 397 end
393 398 rescue ActiveRecord::RecordNotFound
394 399 return nil
395 400 end
396 401 end
397 402
398 403 private
399 404 def allowed_permissions
400 405 @allowed_permissions ||= begin
401 406 module_names = enabled_modules.collect {|m| m.name}
402 407 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
403 408 end
404 409 end
405 410
406 411 def allowed_actions
407 412 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
408 413 end
409 414 end
@@ -1,345 +1,344
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 class User < ActiveRecord::Base
20 class User < Principal
21 21
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 has_many :members, :dependent => :delete_all
38 has_many :projects, :through => :memberships
36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
39 38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 39 has_many :changesets, :dependent => :nullify
41 40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
42 41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
43 42 belongs_to :auth_source
44 43
45 44 # Active non-anonymous users scope
46 45 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47 46
48 47 acts_as_customizable
49 48
50 49 attr_accessor :password, :password_confirmation
51 50 attr_accessor :last_before_login_on
52 51 # Prevents unauthorized assignments
53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
52 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54 53
55 54 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 55 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
57 56 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 57 # Login must contain lettres, numbers, underscores only
59 58 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 59 validates_length_of :login, :maximum => 30
61 60 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 61 validates_length_of :firstname, :lastname, :maximum => 30
63 62 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 63 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 64 validates_confirmation_of :password, :allow_nil => true
66 65
67 66 def before_create
68 67 self.mail_notification = false
69 68 true
70 69 end
71 70
72 71 def before_save
73 72 # update hashed_password if password was set
74 73 self.hashed_password = User.hash_password(self.password) if self.password
75 74 end
76 75
77 76 def reload(*args)
78 77 @name = nil
79 78 super
80 79 end
81 80
82 81 def identity_url=(url)
83 82 if url.blank?
84 83 write_attribute(:identity_url, '')
85 84 else
86 85 begin
87 86 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
88 87 rescue OpenIdAuthentication::InvalidOpenId
89 88 # Invlaid url, don't save
90 89 end
91 90 end
92 91 self.read_attribute(:identity_url)
93 92 end
94 93
95 94 # Returns the user that matches provided login and password, or nil
96 95 def self.try_to_login(login, password)
97 96 # Make sure no one can sign in with an empty password
98 97 return nil if password.to_s.empty?
99 98 user = find(:first, :conditions => ["login=?", login])
100 99 if user
101 100 # user is already in local database
102 101 return nil if !user.active?
103 102 if user.auth_source
104 103 # user has an external authentication method
105 104 return nil unless user.auth_source.authenticate(login, password)
106 105 else
107 106 # authentication with local password
108 107 return nil unless User.hash_password(password) == user.hashed_password
109 108 end
110 109 else
111 110 # user is not yet registered, try to authenticate with available sources
112 111 attrs = AuthSource.authenticate(login, password)
113 112 if attrs
114 113 user = new(*attrs)
115 114 user.login = login
116 115 user.language = Setting.default_language
117 116 if user.save
118 117 user.reload
119 118 logger.info("User '#{user.login}' created from the LDAP") if logger
120 119 end
121 120 end
122 121 end
123 122 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
124 123 user
125 124 rescue => text
126 125 raise text
127 126 end
128 127
129 128 # Returns the user who matches the given autologin +key+ or nil
130 129 def self.try_to_autologin(key)
131 130 tokens = Token.find_all_by_action_and_value('autologin', key)
132 131 # Make sure there's only 1 token that matches the key
133 132 if tokens.size == 1
134 133 token = tokens.first
135 134 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
136 135 token.user.update_attribute(:last_login_on, Time.now)
137 136 token.user
138 137 end
139 138 end
140 139 end
141 140
142 141 # Return user's full name for display
143 142 def name(formatter = nil)
144 143 if formatter
145 144 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
146 145 else
147 146 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
148 147 end
149 148 end
150 149
151 150 def active?
152 151 self.status == STATUS_ACTIVE
153 152 end
154 153
155 154 def registered?
156 155 self.status == STATUS_REGISTERED
157 156 end
158 157
159 158 def locked?
160 159 self.status == STATUS_LOCKED
161 160 end
162 161
163 162 def check_password?(clear_password)
164 163 User.hash_password(clear_password) == self.hashed_password
165 164 end
166 165
167 166 # Generate and set a random password. Useful for automated user creation
168 167 # Based on Token#generate_token_value
169 168 #
170 169 def random_password
171 170 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
172 171 password = ''
173 172 40.times { |i| password << chars[rand(chars.size-1)] }
174 173 self.password = password
175 174 self.password_confirmation = password
176 175 self
177 176 end
178 177
179 178 def pref
180 179 self.preference ||= UserPreference.new(:user => self)
181 180 end
182 181
183 182 def time_zone
184 183 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
185 184 end
186 185
187 186 def wants_comments_in_reverse_order?
188 187 self.pref[:comments_sorting] == 'desc'
189 188 end
190 189
191 190 # Return user's RSS key (a 40 chars long string), used to access feeds
192 191 def rss_key
193 192 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
194 193 token.value
195 194 end
196 195
197 196 # Return an array of project ids for which the user has explicitly turned mail notifications on
198 197 def notified_projects_ids
199 198 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
200 199 end
201 200
202 201 def notified_project_ids=(ids)
203 202 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
204 203 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
205 204 @notified_projects_ids = nil
206 205 notified_projects_ids
207 206 end
208 207
209 208 def self.find_by_rss_key(key)
210 209 token = Token.find_by_value(key)
211 210 token && token.user.active? ? token.user : nil
212 211 end
213 212
214 213 # Makes find_by_mail case-insensitive
215 214 def self.find_by_mail(mail)
216 215 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
217 216 end
218 217
219 218 # Sort users by their display names
220 219 def <=>(user)
221 220 self.to_s.downcase <=> user.to_s.downcase
222 221 end
223 222
224 223 def to_s
225 224 name
226 225 end
227 226
228 227 def logged?
229 228 true
230 229 end
231 230
232 231 def anonymous?
233 232 !logged?
234 233 end
235 234
236 235 # Return user's roles for project
237 236 def roles_for_project(project)
238 237 roles = []
239 238 # No role on archived projects
240 239 return roles unless project && project.active?
241 240 if logged?
242 241 # Find project membership
243 242 membership = memberships.detect {|m| m.project_id == project.id}
244 243 if membership
245 244 roles = membership.roles
246 245 else
247 246 @role_non_member ||= Role.non_member
248 247 roles << @role_non_member
249 248 end
250 249 else
251 250 @role_anonymous ||= Role.anonymous
252 251 roles << @role_anonymous
253 252 end
254 253 roles
255 254 end
256 255
257 256 # Return true if the user is a member of project
258 257 def member_of?(project)
259 258 !roles_for_project(project).detect {|role| role.member?}.nil?
260 259 end
261 260
262 261 # Return true if the user is allowed to do the specified action on project
263 262 # action can be:
264 263 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
265 264 # * a permission Symbol (eg. :edit_project)
266 265 def allowed_to?(action, project, options={})
267 266 if project
268 267 # No action allowed on archived projects
269 268 return false unless project.active?
270 269 # No action allowed on disabled modules
271 270 return false unless project.allows_to?(action)
272 271 # Admin users are authorized for anything else
273 272 return true if admin?
274 273
275 274 roles = roles_for_project(project)
276 275 return false unless roles
277 276 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
278 277
279 278 elsif options[:global]
280 279 # Admin users are always authorized
281 280 return true if admin?
282 281
283 282 # authorize if user has at least one role that has this permission
284 283 roles = memberships.collect {|m| m.roles}.flatten.uniq
285 284 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
286 285 else
287 286 false
288 287 end
289 288 end
290 289
291 290 def self.current=(user)
292 291 @current_user = user
293 292 end
294 293
295 294 def self.current
296 295 @current_user ||= User.anonymous
297 296 end
298 297
299 298 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
300 299 # one anonymous user per database.
301 300 def self.anonymous
302 301 anonymous_user = AnonymousUser.find(:first)
303 302 if anonymous_user.nil?
304 303 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
305 304 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
306 305 end
307 306 anonymous_user
308 307 end
309 308
310 309 protected
311 310
312 311 def validate
313 312 # Password length validation based on setting
314 313 if !password.nil? && password.size < Setting.password_min_length.to_i
315 314 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
316 315 end
317 316 end
318 317
319 318 private
320
319
321 320 # Return password digest
322 321 def self.hash_password(clear_password)
323 322 Digest::SHA1.hexdigest(clear_password || "")
324 323 end
325 324 end
326 325
327 326 class AnonymousUser < User
328 327
329 328 def validate_on_create
330 329 # There should be only one AnonymousUser in the database
331 330 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
332 331 end
333 332
334 333 def available_custom_fields
335 334 []
336 335 end
337 336
338 337 # Overrides a few properties
339 338 def logged?; false end
340 339 def admin; false end
341 340 def name; 'Anonymous' end
342 341 def mail; nil end
343 342 def time_zone; nil end
344 343 def rss_key; nil end
345 344 end
@@ -1,51 +1,56
1 1 <h2><%=l(:label_administration)%></h2>
2 2
3 3 <%= render :partial => 'no_data' if @no_configuration_data %>
4 4
5 5 <p class="icon22 icon22-projects">
6 6 <%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> |
7 7 <%= link_to l(:label_new), :controller => 'projects', :action => 'add' %>
8 8 </p>
9 9
10 10 <p class="icon22 icon22-users">
11 11 <%= link_to l(:label_user_plural), :controller => 'users' %> |
12 12 <%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
13 13 </p>
14 14
15 <p class="icon22 icon22-groups">
16 <%= link_to l(:label_group_plural), :controller => 'groups' %> |
17 <%= link_to l(:label_new), :controller => 'groups', :action => 'new' %>
18 </p>
19
15 20 <p class="icon22 icon22-role">
16 21 <%= link_to l(:label_role_and_permissions), :controller => 'roles' %>
17 22 </p>
18 23
19 24 <p class="icon22 icon22-tracker">
20 25 <%= link_to l(:label_tracker_plural), :controller => 'trackers' %> |
21 26 <%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> |
22 27 <%= link_to l(:label_workflow), :controller => 'workflows', :action => 'edit' %>
23 28 </p>
24 29
25 30 <p class="icon22 icon22-workflow">
26 31 <%= link_to l(:label_custom_field_plural), :controller => 'custom_fields' %>
27 32 </p>
28 33
29 34 <p class="icon22 icon22-options">
30 35 <%= link_to l(:label_enumerations), :controller => 'enumerations' %>
31 36 </p>
32 37
33 38 <p class="icon22 icon22-settings">
34 39 <%= link_to l(:label_settings), :controller => 'settings' %>
35 40 </p>
36 41
37 42 <% menu_items_for(:admin_menu) do |item, caption, url, selected| -%>
38 43 <%= content_tag 'p',
39 44 link_to(h(caption), item.url, item.html_options),
40 45 :class => ["icon22", "icon22-#{item.name}"].join(' ') %>
41 46 <% end -%>
42 47
43 48 <p class="icon22 icon22-plugin">
44 49 <%= link_to l(:label_plugins), :controller => 'admin', :action => 'plugins' %>
45 50 </p>
46 51
47 52 <p class="icon22 icon22-info">
48 53 <%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %>
49 54 </p>
50 55
51 56 <% html_title(l(:label_administration)) -%>
@@ -1,76 +1,81
1 1 <%= error_messages_for 'member' %>
2 2 <% roles = Role.find_all_givable
3 members = @project.members.find(:all, :include => [:roles, :user]).sort %>
3 members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %>
4 4
5 5 <div class="splitcontentleft">
6 6 <% if members.any? %>
7 7 <table class="list members">
8 8 <thead>
9 <th><%= l(:label_user) %></th>
9 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
10 10 <th><%= l(:label_role_plural) %></th>
11 11 <th style="width:15%"></th>
12 12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
13 13 </thead>
14 14 <tbody>
15 15 <% members.each do |member| %>
16 16 <% next if member.new_record? %>
17 17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
18 <td class="user"><%= link_to_user member.user %></td>
18 <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
19 19 <td class="roles">
20 20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
21 21 <% if authorize_for('members', 'edit') %>
22 22 <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member},
23 23 :method => :post,
24 24 :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
25 25 <p><% roles.each do |role| %>
26 <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role) %> <%=h role %></label><br />
26 <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
27 :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 28 <% end %></p>
29 <%= hidden_field_tag 'member[role_ids][]', '' %>
28 30 <p><%= submit_tag l(:button_change), :class => "small" %>
29 31 <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
30 32 <% end %>
31 33 <% end %>
32 34 </td>
33 35 <td class="buttons">
34 36 <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
35 <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
37 <%= link_to_remote(l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
36 38 :method => :post
37 39 }, :title => l(:button_delete),
38 :class => 'icon icon-del' %>
40 :class => 'icon icon-del') if member.deletable? %>
39 41 </td>
40 42 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
41 43 </tr>
42 44 </tbody>
43 45 <% end; reset_cycle %>
44 46 </table>
45 47 <% else %>
46 48 <p class="nodata"><%= l(:label_no_data) %></p>
47 49 <% end %>
48 50 </div>
49 51
50 52
51 <% users_count = User.active.count - @project.users.count
52 users = (users_count < 300) ? User.active.find(:all, :limit => 200).sort - @project.users : [] %>
53 <% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %>
53 54
54 55 <div class="splitcontentright">
55 <% if roles.any? && users_count > 0 %>
56 <% if roles.any? && principals.any? %>
56 57 <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
57 58 <fieldset><legend><%=l(:label_member_new)%></legend>
58 <p><%= text_field_tag 'member[user_login]', nil, :size => "40" %></p>
59 <div id="member_user_login_choices" class="autocomplete">sqd</div>
60 <%= javascript_tag "new Ajax.Autocompleter('member_user_login', 'member_user_login_choices', '#{ url_for(:controller => 'members', :action => 'autocomplete_for_member_login', :id => @project) }', { minChars: 1, frequency: 0.5, paramName: 'user' });" %>
61 <% unless users.empty? %>
62 <div>
63 <% users.each do |user| -%>
64 <label><%= check_box_tag 'member[user_ids][]', user.id, false %> <%= user %></label>
65 <% end -%>
66 </div>
67 <% end %>
59
60 <p><%= text_field_tag 'principal_search', nil, :size => "40" %></p>
61 <%= observe_field(:principal_search,
62 :frequency => 0.5,
63 :update => :principals,
64 :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project },
65 :with => 'q')
66 %>
67
68 <div id="principals">
69 <%= principals_check_box_tags 'member[user_ids][]', principals %>
70 </div>
71
68 72 <p><%= l(:label_role_plural) %>:
69 73 <% roles.each do |role| %>
70 74 <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
71 75 <% end %></p>
76
72 77 <p><%= submit_tag l(:button_add) %></p>
73 78 </fieldset>
74 79 <% end %>
75 80 <% end %>
76 81 </div>
@@ -1,58 +1,60
1 1 <% roles = Role.find_all_givable %>
2 2 <% projects = Project.active.find(:all, :order => 'lft') %>
3 3
4 4 <div class="splitcontentleft">
5 5 <% if @user.memberships.any? %>
6 6 <table class="list memberships">
7 7 <thead>
8 8 <th><%= l(:label_project) %></th>
9 9 <th><%= l(:label_role_plural) %></th>
10 10 <th style="width:15%"></th>
11 11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
12 12 </thead>
13 13 <tbody>
14 14 <% @user.memberships.each do |membership| %>
15 15 <% next if membership.new_record? %>
16 16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
17 17 <td class="project"><%=h membership.project %></td>
18 18 <td class="roles">
19 19 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
20 20 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
21 21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
22 22 <p><% roles.each do |role| %>
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
24 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
24 25 <% end %></p>
26 <%= hidden_field_tag 'membership[role_ids][]', '' %>
25 27 <p><%= submit_tag l(:button_change) %>
26 28 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
27 29 <% end %>
28 30 </td>
29 31 <td class="buttons">
30 32 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
31 <%= link_to_remote l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
33 <%= link_to_remote(l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
32 34 :method => :post },
33 :class => 'icon icon-del' %>
35 :class => 'icon icon-del') if membership.deletable? %>
34 36 </td>
35 37 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
36 38 </tr>
37 39 </tbody>
38 40 <% end; reset_cycle %>
39 41 </table>
40 42 <% else %>
41 43 <p class="nodata"><%= l(:label_no_data) %></p>
42 44 <% end %>
43 45 </div>
44 46
45 47 <div class="splitcontentright">
46 48 <% if projects.any? %>
47 49 <fieldset><legend><%=l(:label_project_new)%></legend>
48 50 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user }) do %>
49 51 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
50 52 <p><%= l(:label_role_plural) %>:
51 53 <% roles.each do |role| %>
52 54 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
53 55 <% end %></p>
54 56 <p><%= submit_tag l(:button_add) %></p>
55 57 <% end %>
56 58 </fieldset>
57 59 <% end %>
58 60 </div>
@@ -1,808 +1,811
1 1 en:
2 2 date:
3 3 formats:
4 4 # Use the strftime parameters for formats.
5 5 # When no format has been given, it uses default.
6 6 # You can provide other formats here if you like!
7 7 default: "%m/%d/%Y"
8 8 short: "%b %d"
9 9 long: "%B %d, %Y"
10 10
11 11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
12 12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
13 13
14 14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
15 15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
16 16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
17 17 # Used in date_select and datime_select.
18 18 order: [ :year, :month, :day ]
19 19
20 20 time:
21 21 formats:
22 22 default: "%m/%d/%Y %I:%M %p"
23 23 time: "%I:%M %p"
24 24 short: "%d %b %H:%M"
25 25 long: "%B %d, %Y %H:%M"
26 26 am: "am"
27 27 pm: "pm"
28 28
29 29 datetime:
30 30 distance_in_words:
31 31 half_a_minute: "half a minute"
32 32 less_than_x_seconds:
33 33 one: "less than 1 second"
34 34 other: "less than {{count}} seconds"
35 35 x_seconds:
36 36 one: "1 second"
37 37 other: "{{count}} seconds"
38 38 less_than_x_minutes:
39 39 one: "less than a minute"
40 40 other: "less than {{count}} minutes"
41 41 x_minutes:
42 42 one: "1 minute"
43 43 other: "{{count}} minutes"
44 44 about_x_hours:
45 45 one: "about 1 hour"
46 46 other: "about {{count}} hours"
47 47 x_days:
48 48 one: "1 day"
49 49 other: "{{count}} days"
50 50 about_x_months:
51 51 one: "about 1 month"
52 52 other: "about {{count}} months"
53 53 x_months:
54 54 one: "1 month"
55 55 other: "{{count}} months"
56 56 about_x_years:
57 57 one: "about 1 year"
58 58 other: "about {{count}} years"
59 59 over_x_years:
60 60 one: "over 1 year"
61 61 other: "over {{count}} years"
62 62
63 63 # Used in array.to_sentence.
64 64 support:
65 65 array:
66 66 sentence_connector: "and"
67 67 skip_last_comma: false
68 68
69 69 activerecord:
70 70 errors:
71 71 messages:
72 72 inclusion: "is not included in the list"
73 73 exclusion: "is reserved"
74 74 invalid: "is invalid"
75 75 confirmation: "doesn't match confirmation"
76 76 accepted: "must be accepted"
77 77 empty: "can't be empty"
78 78 blank: "can't be blank"
79 79 too_long: "is too long (maximum is {{count}} characters)"
80 80 too_short: "is too short (minimum is {{count}} characters)"
81 81 wrong_length: "is the wrong length (should be {{count}} characters)"
82 82 taken: "has already been taken"
83 83 not_a_number: "is not a number"
84 84 not_a_date: "is not a valid date"
85 85 greater_than: "must be greater than {{count}}"
86 86 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
87 87 equal_to: "must be equal to {{count}}"
88 88 less_than: "must be less than {{count}}"
89 89 less_than_or_equal_to: "must be less than or equal to {{count}}"
90 90 odd: "must be odd"
91 91 even: "must be even"
92 92 greater_than_start_date: "must be greater than start date"
93 93 not_same_project: "doesn't belong to the same project"
94 94 circular_dependency: "This relation would create a circular dependency"
95 95
96 96 actionview_instancetag_blank_option: Please select
97 97
98 98 general_text_No: 'No'
99 99 general_text_Yes: 'Yes'
100 100 general_text_no: 'no'
101 101 general_text_yes: 'yes'
102 102 general_lang_name: 'English'
103 103 general_csv_separator: ','
104 104 general_csv_decimal_separator: '.'
105 105 general_csv_encoding: ISO-8859-1
106 106 general_pdf_encoding: ISO-8859-1
107 107 general_first_day_of_week: '7'
108 108
109 109 notice_account_updated: Account was successfully updated.
110 110 notice_account_invalid_creditentials: Invalid user or password
111 111 notice_account_password_updated: Password was successfully updated.
112 112 notice_account_wrong_password: Wrong password
113 113 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
114 114 notice_account_unknown_email: Unknown user.
115 115 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
116 116 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
117 117 notice_account_activated: Your account has been activated. You can now log in.
118 118 notice_successful_create: Successful creation.
119 119 notice_successful_update: Successful update.
120 120 notice_successful_delete: Successful deletion.
121 121 notice_successful_connection: Successful connection.
122 122 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
123 123 notice_locking_conflict: Data has been updated by another user.
124 124 notice_not_authorized: You are not authorized to access this page.
125 125 notice_email_sent: "An email was sent to {{value}}"
126 126 notice_email_error: "An error occurred while sending mail ({{value}})"
127 127 notice_feeds_access_key_reseted: Your RSS access key was reset.
128 128 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
129 129 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
130 130 notice_account_pending: "Your account was created and is now pending administrator approval."
131 131 notice_default_data_loaded: Default configuration successfully loaded.
132 132 notice_unable_delete_version: Unable to delete version.
133 133
134 134 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
135 135 error_scm_not_found: "The entry or revision was not found in the repository."
136 136 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
137 137 error_scm_annotate: "The entry does not exist or can not be annotated."
138 138 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
139 139 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
140 140 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
141 141
142 142 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
143 143
144 144 mail_subject_lost_password: "Your {{value}} password"
145 145 mail_body_lost_password: 'To change your password, click on the following link:'
146 146 mail_subject_register: "Your {{value}} account activation"
147 147 mail_body_register: 'To activate your account, click on the following link:'
148 148 mail_body_account_information_external: "You can use your {{value}} account to log in."
149 149 mail_body_account_information: Your account information
150 150 mail_subject_account_activation_request: "{{value}} account activation request"
151 151 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
152 152 mail_subject_reminder: "{{count}} issue(s) due in the next days"
153 153 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
154 154 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
155 155 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
156 156 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
157 157 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
158 158
159 159 gui_validation_error: 1 error
160 160 gui_validation_error_plural: "{{count}} errors"
161 161
162 162 field_name: Name
163 163 field_description: Description
164 164 field_summary: Summary
165 165 field_is_required: Required
166 166 field_firstname: Firstname
167 167 field_lastname: Lastname
168 168 field_mail: Email
169 169 field_filename: File
170 170 field_filesize: Size
171 171 field_downloads: Downloads
172 172 field_author: Author
173 173 field_created_on: Created
174 174 field_updated_on: Updated
175 175 field_field_format: Format
176 176 field_is_for_all: For all projects
177 177 field_possible_values: Possible values
178 178 field_regexp: Regular expression
179 179 field_min_length: Minimum length
180 180 field_max_length: Maximum length
181 181 field_value: Value
182 182 field_category: Category
183 183 field_title: Title
184 184 field_project: Project
185 185 field_issue: Issue
186 186 field_status: Status
187 187 field_notes: Notes
188 188 field_is_closed: Issue closed
189 189 field_is_default: Default value
190 190 field_tracker: Tracker
191 191 field_subject: Subject
192 192 field_due_date: Due date
193 193 field_assigned_to: Assigned to
194 194 field_priority: Priority
195 195 field_fixed_version: Target version
196 196 field_user: User
197 197 field_role: Role
198 198 field_homepage: Homepage
199 199 field_is_public: Public
200 200 field_parent: Subproject of
201 201 field_is_in_chlog: Issues displayed in changelog
202 202 field_is_in_roadmap: Issues displayed in roadmap
203 203 field_login: Login
204 204 field_mail_notification: Email notifications
205 205 field_admin: Administrator
206 206 field_last_login_on: Last connection
207 207 field_language: Language
208 208 field_effective_date: Date
209 209 field_password: Password
210 210 field_new_password: New password
211 211 field_password_confirmation: Confirmation
212 212 field_version: Version
213 213 field_type: Type
214 214 field_host: Host
215 215 field_port: Port
216 216 field_account: Account
217 217 field_base_dn: Base DN
218 218 field_attr_login: Login attribute
219 219 field_attr_firstname: Firstname attribute
220 220 field_attr_lastname: Lastname attribute
221 221 field_attr_mail: Email attribute
222 222 field_onthefly: On-the-fly user creation
223 223 field_start_date: Start
224 224 field_done_ratio: % Done
225 225 field_auth_source: Authentication mode
226 226 field_hide_mail: Hide my email address
227 227 field_comments: Comment
228 228 field_url: URL
229 229 field_start_page: Start page
230 230 field_subproject: Subproject
231 231 field_hours: Hours
232 232 field_activity: Activity
233 233 field_spent_on: Date
234 234 field_identifier: Identifier
235 235 field_is_filter: Used as a filter
236 236 field_issue_to: Related issue
237 237 field_delay: Delay
238 238 field_assignable: Issues can be assigned to this role
239 239 field_redirect_existing_links: Redirect existing links
240 240 field_estimated_hours: Estimated time
241 241 field_column_names: Columns
242 242 field_time_zone: Time zone
243 243 field_searchable: Searchable
244 244 field_default_value: Default value
245 245 field_comments_sorting: Display comments
246 246 field_parent_title: Parent page
247 247 field_editable: Editable
248 248 field_watcher: Watcher
249 249 field_identity_url: OpenID URL
250 250 field_content: Content
251 251 field_group_by: Group results by
252 252
253 253 setting_app_title: Application title
254 254 setting_app_subtitle: Application subtitle
255 255 setting_welcome_text: Welcome text
256 256 setting_default_language: Default language
257 257 setting_login_required: Authentication required
258 258 setting_self_registration: Self-registration
259 259 setting_attachment_max_size: Attachment max. size
260 260 setting_issues_export_limit: Issues export limit
261 261 setting_mail_from: Emission email address
262 262 setting_bcc_recipients: Blind carbon copy recipients (bcc)
263 263 setting_plain_text_mail: Plain text mail (no HTML)
264 264 setting_host_name: Host name and path
265 265 setting_text_formatting: Text formatting
266 266 setting_wiki_compression: Wiki history compression
267 267 setting_feeds_limit: Feed content limit
268 268 setting_default_projects_public: New projects are public by default
269 269 setting_autofetch_changesets: Autofetch commits
270 270 setting_sys_api_enabled: Enable WS for repository management
271 271 setting_commit_ref_keywords: Referencing keywords
272 272 setting_commit_fix_keywords: Fixing keywords
273 273 setting_autologin: Autologin
274 274 setting_date_format: Date format
275 275 setting_time_format: Time format
276 276 setting_cross_project_issue_relations: Allow cross-project issue relations
277 277 setting_issue_list_default_columns: Default columns displayed on the issue list
278 278 setting_repositories_encodings: Repositories encodings
279 279 setting_commit_logs_encoding: Commit messages encoding
280 280 setting_emails_footer: Emails footer
281 281 setting_protocol: Protocol
282 282 setting_per_page_options: Objects per page options
283 283 setting_user_format: Users display format
284 284 setting_activity_days_default: Days displayed on project activity
285 285 setting_display_subprojects_issues: Display subprojects issues on main projects by default
286 286 setting_enabled_scm: Enabled SCM
287 287 setting_mail_handler_api_enabled: Enable WS for incoming emails
288 288 setting_mail_handler_api_key: API key
289 289 setting_sequential_project_identifiers: Generate sequential project identifiers
290 290 setting_gravatar_enabled: Use Gravatar user icons
291 291 setting_diff_max_lines_displayed: Max number of diff lines displayed
292 292 setting_file_max_size_displayed: Max size of text files displayed inline
293 293 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
294 294 setting_openid: Allow OpenID login and registration
295 295 setting_password_min_length: Minimum password length
296 296 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
297 297
298 298 permission_add_project: Create project
299 299 permission_edit_project: Edit project
300 300 permission_select_project_modules: Select project modules
301 301 permission_manage_members: Manage members
302 302 permission_manage_versions: Manage versions
303 303 permission_manage_categories: Manage issue categories
304 304 permission_add_issues: Add issues
305 305 permission_edit_issues: Edit issues
306 306 permission_manage_issue_relations: Manage issue relations
307 307 permission_add_issue_notes: Add notes
308 308 permission_edit_issue_notes: Edit notes
309 309 permission_edit_own_issue_notes: Edit own notes
310 310 permission_move_issues: Move issues
311 311 permission_delete_issues: Delete issues
312 312 permission_manage_public_queries: Manage public queries
313 313 permission_save_queries: Save queries
314 314 permission_view_gantt: View gantt chart
315 315 permission_view_calendar: View calender
316 316 permission_view_issue_watchers: View watchers list
317 317 permission_add_issue_watchers: Add watchers
318 318 permission_log_time: Log spent time
319 319 permission_view_time_entries: View spent time
320 320 permission_edit_time_entries: Edit time logs
321 321 permission_edit_own_time_entries: Edit own time logs
322 322 permission_manage_news: Manage news
323 323 permission_comment_news: Comment news
324 324 permission_manage_documents: Manage documents
325 325 permission_view_documents: View documents
326 326 permission_manage_files: Manage files
327 327 permission_view_files: View files
328 328 permission_manage_wiki: Manage wiki
329 329 permission_rename_wiki_pages: Rename wiki pages
330 330 permission_delete_wiki_pages: Delete wiki pages
331 331 permission_view_wiki_pages: View wiki
332 332 permission_view_wiki_edits: View wiki history
333 333 permission_edit_wiki_pages: Edit wiki pages
334 334 permission_delete_wiki_pages_attachments: Delete attachments
335 335 permission_protect_wiki_pages: Protect wiki pages
336 336 permission_manage_repository: Manage repository
337 337 permission_browse_repository: Browse repository
338 338 permission_view_changesets: View changesets
339 339 permission_commit_access: Commit access
340 340 permission_manage_boards: Manage boards
341 341 permission_view_messages: View messages
342 342 permission_add_messages: Post messages
343 343 permission_edit_messages: Edit messages
344 344 permission_edit_own_messages: Edit own messages
345 345 permission_delete_messages: Delete messages
346 346 permission_delete_own_messages: Delete own messages
347 347
348 348 project_module_issue_tracking: Issue tracking
349 349 project_module_time_tracking: Time tracking
350 350 project_module_news: News
351 351 project_module_documents: Documents
352 352 project_module_files: Files
353 353 project_module_wiki: Wiki
354 354 project_module_repository: Repository
355 355 project_module_boards: Boards
356 356
357 357 label_user: User
358 358 label_user_plural: Users
359 359 label_user_new: New user
360 360 label_project: Project
361 361 label_project_new: New project
362 362 label_project_plural: Projects
363 363 label_x_projects:
364 364 zero: no projects
365 365 one: 1 project
366 366 other: "{{count}} projects"
367 367 label_project_all: All Projects
368 368 label_project_latest: Latest projects
369 369 label_issue: Issue
370 370 label_issue_new: New issue
371 371 label_issue_plural: Issues
372 372 label_issue_view_all: View all issues
373 373 label_issues_by: "Issues by {{value}}"
374 374 label_issue_added: Issue added
375 375 label_issue_updated: Issue updated
376 376 label_document: Document
377 377 label_document_new: New document
378 378 label_document_plural: Documents
379 379 label_document_added: Document added
380 380 label_role: Role
381 381 label_role_plural: Roles
382 382 label_role_new: New role
383 383 label_role_and_permissions: Roles and permissions
384 384 label_member: Member
385 385 label_member_new: New member
386 386 label_member_plural: Members
387 387 label_tracker: Tracker
388 388 label_tracker_plural: Trackers
389 389 label_tracker_new: New tracker
390 390 label_workflow: Workflow
391 391 label_issue_status: Issue status
392 392 label_issue_status_plural: Issue statuses
393 393 label_issue_status_new: New status
394 394 label_issue_category: Issue category
395 395 label_issue_category_plural: Issue categories
396 396 label_issue_category_new: New category
397 397 label_custom_field: Custom field
398 398 label_custom_field_plural: Custom fields
399 399 label_custom_field_new: New custom field
400 400 label_enumerations: Enumerations
401 401 label_enumeration_new: New value
402 402 label_information: Information
403 403 label_information_plural: Information
404 404 label_please_login: Please log in
405 405 label_register: Register
406 406 label_login_with_open_id_option: or login with OpenID
407 407 label_password_lost: Lost password
408 408 label_home: Home
409 409 label_my_page: My page
410 410 label_my_account: My account
411 411 label_my_projects: My projects
412 412 label_administration: Administration
413 413 label_login: Sign in
414 414 label_logout: Sign out
415 415 label_help: Help
416 416 label_reported_issues: Reported issues
417 417 label_assigned_to_me_issues: Issues assigned to me
418 418 label_last_login: Last connection
419 419 label_registered_on: Registered on
420 420 label_activity: Activity
421 421 label_overall_activity: Overall activity
422 422 label_user_activity: "{{value}}'s activity"
423 423 label_new: New
424 424 label_logged_as: Logged in as
425 425 label_environment: Environment
426 426 label_authentication: Authentication
427 427 label_auth_source: Authentication mode
428 428 label_auth_source_new: New authentication mode
429 429 label_auth_source_plural: Authentication modes
430 430 label_subproject_plural: Subprojects
431 431 label_and_its_subprojects: "{{value}} and its subprojects"
432 432 label_min_max_length: Min - Max length
433 433 label_list: List
434 434 label_date: Date
435 435 label_integer: Integer
436 436 label_float: Float
437 437 label_boolean: Boolean
438 438 label_string: Text
439 439 label_text: Long text
440 440 label_attribute: Attribute
441 441 label_attribute_plural: Attributes
442 442 label_download: "{{count}} Download"
443 443 label_download_plural: "{{count}} Downloads"
444 444 label_no_data: No data to display
445 445 label_change_status: Change status
446 446 label_history: History
447 447 label_attachment: File
448 448 label_attachment_new: New file
449 449 label_attachment_delete: Delete file
450 450 label_attachment_plural: Files
451 451 label_file_added: File added
452 452 label_report: Report
453 453 label_report_plural: Reports
454 454 label_news: News
455 455 label_news_new: Add news
456 456 label_news_plural: News
457 457 label_news_latest: Latest news
458 458 label_news_view_all: View all news
459 459 label_news_added: News added
460 460 label_change_log: Change log
461 461 label_settings: Settings
462 462 label_overview: Overview
463 463 label_version: Version
464 464 label_version_new: New version
465 465 label_version_plural: Versions
466 466 label_confirmation: Confirmation
467 467 label_export_to: 'Also available in:'
468 468 label_read: Read...
469 469 label_public_projects: Public projects
470 470 label_open_issues: open
471 471 label_open_issues_plural: open
472 472 label_closed_issues: closed
473 473 label_closed_issues_plural: closed
474 474 label_x_open_issues_abbr_on_total:
475 475 zero: 0 open / {{total}}
476 476 one: 1 open / {{total}}
477 477 other: "{{count}} open / {{total}}"
478 478 label_x_open_issues_abbr:
479 479 zero: 0 open
480 480 one: 1 open
481 481 other: "{{count}} open"
482 482 label_x_closed_issues_abbr:
483 483 zero: 0 closed
484 484 one: 1 closed
485 485 other: "{{count}} closed"
486 486 label_total: Total
487 487 label_permissions: Permissions
488 488 label_current_status: Current status
489 489 label_new_statuses_allowed: New statuses allowed
490 490 label_all: all
491 491 label_none: none
492 492 label_nobody: nobody
493 493 label_next: Next
494 494 label_previous: Previous
495 495 label_used_by: Used by
496 496 label_details: Details
497 497 label_add_note: Add a note
498 498 label_per_page: Per page
499 499 label_calendar: Calendar
500 500 label_months_from: months from
501 501 label_gantt: Gantt
502 502 label_internal: Internal
503 503 label_last_changes: "last {{count}} changes"
504 504 label_change_view_all: View all changes
505 505 label_personalize_page: Personalize this page
506 506 label_comment: Comment
507 507 label_comment_plural: Comments
508 508 label_x_comments:
509 509 zero: no comments
510 510 one: 1 comment
511 511 other: "{{count}} comments"
512 512 label_comment_add: Add a comment
513 513 label_comment_added: Comment added
514 514 label_comment_delete: Delete comments
515 515 label_query: Custom query
516 516 label_query_plural: Custom queries
517 517 label_query_new: New query
518 518 label_filter_add: Add filter
519 519 label_filter_plural: Filters
520 520 label_equals: is
521 521 label_not_equals: is not
522 522 label_in_less_than: in less than
523 523 label_in_more_than: in more than
524 524 label_greater_or_equal: '>='
525 525 label_less_or_equal: '<='
526 526 label_in: in
527 527 label_today: today
528 528 label_all_time: all time
529 529 label_yesterday: yesterday
530 530 label_this_week: this week
531 531 label_last_week: last week
532 532 label_last_n_days: "last {{count}} days"
533 533 label_this_month: this month
534 534 label_last_month: last month
535 535 label_this_year: this year
536 536 label_date_range: Date range
537 537 label_less_than_ago: less than days ago
538 538 label_more_than_ago: more than days ago
539 539 label_ago: days ago
540 540 label_contains: contains
541 541 label_not_contains: doesn't contain
542 542 label_day_plural: days
543 543 label_repository: Repository
544 544 label_repository_plural: Repositories
545 545 label_browse: Browse
546 546 label_modification: "{{count}} change"
547 547 label_modification_plural: "{{count}} changes"
548 548 label_branch: Branch
549 549 label_tag: Tag
550 550 label_revision: Revision
551 551 label_revision_plural: Revisions
552 552 label_associated_revisions: Associated revisions
553 553 label_added: added
554 554 label_modified: modified
555 555 label_copied: copied
556 556 label_renamed: renamed
557 557 label_deleted: deleted
558 558 label_latest_revision: Latest revision
559 559 label_latest_revision_plural: Latest revisions
560 560 label_view_revisions: View revisions
561 561 label_view_all_revisions: View all revisions
562 562 label_max_size: Maximum size
563 563 label_sort_highest: Move to top
564 564 label_sort_higher: Move up
565 565 label_sort_lower: Move down
566 566 label_sort_lowest: Move to bottom
567 567 label_roadmap: Roadmap
568 568 label_roadmap_due_in: "Due in {{value}}"
569 569 label_roadmap_overdue: "{{value}} late"
570 570 label_roadmap_no_issues: No issues for this version
571 571 label_search: Search
572 572 label_result_plural: Results
573 573 label_all_words: All words
574 574 label_wiki: Wiki
575 575 label_wiki_edit: Wiki edit
576 576 label_wiki_edit_plural: Wiki edits
577 577 label_wiki_page: Wiki page
578 578 label_wiki_page_plural: Wiki pages
579 579 label_index_by_title: Index by title
580 580 label_index_by_date: Index by date
581 581 label_current_version: Current version
582 582 label_preview: Preview
583 583 label_feed_plural: Feeds
584 584 label_changes_details: Details of all changes
585 585 label_issue_tracking: Issue tracking
586 586 label_spent_time: Spent time
587 587 label_f_hour: "{{value}} hour"
588 588 label_f_hour_plural: "{{value}} hours"
589 589 label_time_tracking: Time tracking
590 590 label_change_plural: Changes
591 591 label_statistics: Statistics
592 592 label_commits_per_month: Commits per month
593 593 label_commits_per_author: Commits per author
594 594 label_view_diff: View differences
595 595 label_diff_inline: inline
596 596 label_diff_side_by_side: side by side
597 597 label_options: Options
598 598 label_copy_workflow_from: Copy workflow from
599 599 label_permissions_report: Permissions report
600 600 label_watched_issues: Watched issues
601 601 label_related_issues: Related issues
602 602 label_applied_status: Applied status
603 603 label_loading: Loading...
604 604 label_relation_new: New relation
605 605 label_relation_delete: Delete relation
606 606 label_relates_to: related to
607 607 label_duplicates: duplicates
608 608 label_duplicated_by: duplicated by
609 609 label_blocks: blocks
610 610 label_blocked_by: blocked by
611 611 label_precedes: precedes
612 612 label_follows: follows
613 613 label_end_to_start: end to start
614 614 label_end_to_end: end to end
615 615 label_start_to_start: start to start
616 616 label_start_to_end: start to end
617 617 label_stay_logged_in: Stay logged in
618 618 label_disabled: disabled
619 619 label_show_completed_versions: Show completed versions
620 620 label_me: me
621 621 label_board: Forum
622 622 label_board_new: New forum
623 623 label_board_plural: Forums
624 624 label_topic_plural: Topics
625 625 label_message_plural: Messages
626 626 label_message_last: Last message
627 627 label_message_new: New message
628 628 label_message_posted: Message added
629 629 label_reply_plural: Replies
630 630 label_send_information: Send account information to the user
631 631 label_year: Year
632 632 label_month: Month
633 633 label_week: Week
634 634 label_date_from: From
635 635 label_date_to: To
636 636 label_language_based: Based on user's language
637 637 label_sort_by: "Sort by {{value}}"
638 638 label_send_test_email: Send a test email
639 639 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
640 640 label_module_plural: Modules
641 641 label_added_time_by: "Added by {{author}} {{age}} ago"
642 642 label_updated_time_by: "Updated by {{author}} {{age}} ago"
643 643 label_updated_time: "Updated {{value}} ago"
644 644 label_jump_to_a_project: Jump to a project...
645 645 label_file_plural: Files
646 646 label_changeset_plural: Changesets
647 647 label_default_columns: Default columns
648 648 label_no_change_option: (No change)
649 649 label_bulk_edit_selected_issues: Bulk edit selected issues
650 650 label_theme: Theme
651 651 label_default: Default
652 652 label_search_titles_only: Search titles only
653 653 label_user_mail_option_all: "For any event on all my projects"
654 654 label_user_mail_option_selected: "For any event on the selected projects only..."
655 655 label_user_mail_option_none: "Only for things I watch or I'm involved in"
656 656 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
657 657 label_registration_activation_by_email: account activation by email
658 658 label_registration_manual_activation: manual account activation
659 659 label_registration_automatic_activation: automatic account activation
660 660 label_display_per_page: "Per page: {{value}}"
661 661 label_age: Age
662 662 label_change_properties: Change properties
663 663 label_general: General
664 664 label_more: More
665 665 label_scm: SCM
666 666 label_plugins: Plugins
667 667 label_ldap_authentication: LDAP authentication
668 668 label_downloads_abbr: D/L
669 669 label_optional_description: Optional description
670 670 label_add_another_file: Add another file
671 671 label_preferences: Preferences
672 672 label_chronological_order: In chronological order
673 673 label_reverse_chronological_order: In reverse chronological order
674 674 label_planning: Planning
675 675 label_incoming_emails: Incoming emails
676 676 label_generate_key: Generate a key
677 677 label_issue_watchers: Watchers
678 678 label_example: Example
679 679 label_display: Display
680 680 label_sort: Sort
681 681 label_ascending: Ascending
682 682 label_descending: Descending
683 683 label_date_from_to: From {{start}} to {{end}}
684 684 label_wiki_content_added: Wiki page added
685 685 label_wiki_content_updated: Wiki page updated
686 label_group: Group
687 label_group_plural: Groups
688 label_group_new: New group
686 689
687 690 button_login: Login
688 691 button_submit: Submit
689 692 button_save: Save
690 693 button_check_all: Check all
691 694 button_uncheck_all: Uncheck all
692 695 button_delete: Delete
693 696 button_create: Create
694 697 button_create_and_continue: Create and continue
695 698 button_test: Test
696 699 button_edit: Edit
697 700 button_add: Add
698 701 button_change: Change
699 702 button_apply: Apply
700 703 button_clear: Clear
701 704 button_lock: Lock
702 705 button_unlock: Unlock
703 706 button_download: Download
704 707 button_list: List
705 708 button_view: View
706 709 button_move: Move
707 710 button_back: Back
708 711 button_cancel: Cancel
709 712 button_activate: Activate
710 713 button_sort: Sort
711 714 button_log_time: Log time
712 715 button_rollback: Rollback to this version
713 716 button_watch: Watch
714 717 button_unwatch: Unwatch
715 718 button_reply: Reply
716 719 button_archive: Archive
717 720 button_unarchive: Unarchive
718 721 button_reset: Reset
719 722 button_rename: Rename
720 723 button_change_password: Change password
721 724 button_copy: Copy
722 725 button_annotate: Annotate
723 726 button_update: Update
724 727 button_configure: Configure
725 728 button_quote: Quote
726 729
727 730 status_active: active
728 731 status_registered: registered
729 732 status_locked: locked
730 733
731 734 text_select_mail_notifications: Select actions for which email notifications should be sent.
732 735 text_regexp_info: eg. ^[A-Z0-9]+$
733 736 text_min_max_length_info: 0 means no restriction
734 737 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
735 738 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
736 739 text_workflow_edit: Select a role and a tracker to edit the workflow
737 740 text_are_you_sure: Are you sure ?
738 741 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
739 742 text_journal_set_to: "{{label}} set to {{value}}"
740 743 text_journal_deleted: "{{label}} deleted"
741 744 text_tip_task_begin_day: task beginning this day
742 745 text_tip_task_end_day: task ending this day
743 746 text_tip_task_begin_end_day: task beginning and ending this day
744 747 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
745 748 text_caracters_maximum: "{{count}} characters maximum."
746 749 text_caracters_minimum: "Must be at least {{count}} characters long."
747 750 text_length_between: "Length between {{min}} and {{max}} characters."
748 751 text_tracker_no_workflow: No workflow defined for this tracker
749 752 text_unallowed_characters: Unallowed characters
750 753 text_comma_separated: Multiple values allowed (comma separated).
751 754 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
752 755 text_issue_added: "Issue {{id}} has been reported by {{author}}."
753 756 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
754 757 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
755 758 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
756 759 text_issue_category_destroy_assignments: Remove category assignments
757 760 text_issue_category_reassign_to: Reassign issues to this category
758 761 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)."
759 762 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."
760 763 text_load_default_configuration: Load the default configuration
761 764 text_status_changed_by_changeset: "Applied in changeset {{value}}."
762 765 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
763 766 text_select_project_modules: 'Select modules to enable for this project:'
764 767 text_default_administrator_account_changed: Default administrator account changed
765 768 text_file_repository_writable: Attachments directory writable
766 769 text_plugin_assets_writable: Plugin assets directory writable
767 770 text_rmagick_available: RMagick available (optional)
768 771 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
769 772 text_destroy_time_entries: Delete reported hours
770 773 text_assign_time_entries_to_project: Assign reported hours to the project
771 774 text_reassign_time_entries: 'Reassign reported hours to this issue:'
772 775 text_user_wrote: "{{value}} wrote:"
773 776 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
774 777 text_enumeration_category_reassign_to: 'Reassign them to this value:'
775 778 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
776 779 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."
777 780 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
778 781 text_custom_field_possible_values_info: 'One line for each value'
779 782 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
780 783 text_wiki_page_nullify_children: "Keep child pages as root pages"
781 784 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
782 785 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
783 786
784 787 default_role_manager: Manager
785 788 default_role_developper: Developer
786 789 default_role_reporter: Reporter
787 790 default_tracker_bug: Bug
788 791 default_tracker_feature: Feature
789 792 default_tracker_support: Support
790 793 default_issue_status_new: New
791 794 default_issue_status_assigned: Assigned
792 795 default_issue_status_resolved: Resolved
793 796 default_issue_status_feedback: Feedback
794 797 default_issue_status_closed: Closed
795 798 default_issue_status_rejected: Rejected
796 799 default_doc_category_user: User documentation
797 800 default_doc_category_tech: Technical documentation
798 801 default_priority_low: Low
799 802 default_priority_normal: Normal
800 803 default_priority_high: High
801 804 default_priority_urgent: Urgent
802 805 default_priority_immediate: Immediate
803 806 default_activity_design: Design
804 807 default_activity_development: Development
805 808
806 809 enumeration_issue_priorities: Issue priorities
807 810 enumeration_doc_categories: Document categories
808 811 enumeration_activities: Activities (time tracking)
@@ -1,261 +1,262
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 # Allow Redmine plugins to map routes and potentially override them
10 10 Rails.plugins.each do |plugin|
11 11 map.from_plugin plugin.name.to_sym
12 12 end
13 13
14 14 map.home '', :controller => 'welcome'
15 15
16 16 map.signin 'login', :controller => 'account', :action => 'login'
17 17 map.signout 'logout', :controller => 'account', :action => 'logout'
18 18
19 19 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
20 20 map.connect 'help/:ctrl/:page', :controller => 'help'
21 21
22 22 map.connect 'time_entries/:id/edit', :action => 'edit', :controller => 'timelog'
23 23 map.connect 'projects/:project_id/time_entries/new', :action => 'edit', :controller => 'timelog'
24 24 map.connect 'projects/:project_id/issues/:issue_id/time_entries/new', :action => 'edit', :controller => 'timelog'
25 25
26 26 map.with_options :controller => 'timelog' do |timelog|
27 27 timelog.connect 'projects/:project_id/time_entries', :action => 'details'
28 28
29 29 timelog.with_options :action => 'details', :conditions => {:method => :get} do |time_details|
30 30 time_details.connect 'time_entries'
31 31 time_details.connect 'time_entries.:format'
32 32 time_details.connect 'issues/:issue_id/time_entries'
33 33 time_details.connect 'issues/:issue_id/time_entries.:format'
34 34 time_details.connect 'projects/:project_id/time_entries.:format'
35 35 time_details.connect 'projects/:project_id/issues/:issue_id/time_entries'
36 36 time_details.connect 'projects/:project_id/issues/:issue_id/time_entries.:format'
37 37 end
38 38 timelog.connect 'projects/:project_id/time_entries/report', :action => 'report'
39 39 timelog.with_options :action => 'report',:conditions => {:method => :get} do |time_report|
40 40 time_report.connect 'time_entries/report'
41 41 time_report.connect 'time_entries/report.:format'
42 42 time_report.connect 'projects/:project_id/time_entries/report.:format'
43 43 end
44 44
45 45 timelog.with_options :action => 'edit', :conditions => {:method => :get} do |time_edit|
46 46 time_edit.connect 'issues/:issue_id/time_entries/new'
47 47 end
48 48
49 49 timelog.connect 'time_entries/:id/destroy', :action => 'destroy', :conditions => {:method => :post}
50 50 end
51 51
52 52 map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
53 53 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
54 54 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
55 55 map.with_options :controller => 'wiki' do |wiki_routes|
56 56 wiki_routes.with_options :conditions => {:method => :get} do |wiki_views|
57 57 wiki_views.connect 'projects/:id/wiki/:page', :action => 'special', :page => /page_index|date_index|export/i
58 58 wiki_views.connect 'projects/:id/wiki/:page', :action => 'index', :page => nil
59 59 wiki_views.connect 'projects/:id/wiki/:page/edit', :action => 'edit'
60 60 wiki_views.connect 'projects/:id/wiki/:page/rename', :action => 'rename'
61 61 wiki_views.connect 'projects/:id/wiki/:page/history', :action => 'history'
62 62 wiki_views.connect 'projects/:id/wiki/:page/diff/:version/vs/:version_from', :action => 'diff'
63 63 wiki_views.connect 'projects/:id/wiki/:page/annotate/:version', :action => 'annotate'
64 64 end
65 65
66 66 wiki_routes.connect 'projects/:id/wiki/:page/:action',
67 67 :action => /edit|rename|destroy|preview|protect/,
68 68 :conditions => {:method => :post}
69 69 end
70 70
71 71 map.with_options :controller => 'messages' do |messages_routes|
72 72 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
73 73 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
74 74 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
75 75 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
76 76 end
77 77 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
78 78 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
79 79 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
80 80 messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
81 81 end
82 82 end
83 83
84 84 map.with_options :controller => 'boards' do |board_routes|
85 85 board_routes.with_options :conditions => {:method => :get} do |board_views|
86 86 board_views.connect 'projects/:project_id/boards', :action => 'index'
87 87 board_views.connect 'projects/:project_id/boards/new', :action => 'new'
88 88 board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
89 89 board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
90 90 board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
91 91 end
92 92 board_routes.with_options :conditions => {:method => :post} do |board_actions|
93 93 board_actions.connect 'projects/:project_id/boards', :action => 'new'
94 94 board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
95 95 end
96 96 end
97 97
98 98 map.with_options :controller => 'documents' do |document_routes|
99 99 document_routes.with_options :conditions => {:method => :get} do |document_views|
100 100 document_views.connect 'projects/:project_id/documents', :action => 'index'
101 101 document_views.connect 'projects/:project_id/documents/new', :action => 'new'
102 102 document_views.connect 'documents/:id', :action => 'show'
103 103 document_views.connect 'documents/:id/edit', :action => 'edit'
104 104 end
105 105 document_routes.with_options :conditions => {:method => :post} do |document_actions|
106 106 document_actions.connect 'projects/:project_id/documents', :action => 'new'
107 107 document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
108 108 end
109 109 end
110 110
111 111 map.with_options :controller => 'issues' do |issues_routes|
112 112 issues_routes.with_options :conditions => {:method => :get} do |issues_views|
113 113 issues_views.connect 'issues', :action => 'index'
114 114 issues_views.connect 'issues.:format', :action => 'index'
115 115 issues_views.connect 'projects/:project_id/issues', :action => 'index'
116 116 issues_views.connect 'projects/:project_id/issues.:format', :action => 'index'
117 117 issues_views.connect 'projects/:project_id/issues/new', :action => 'new'
118 118 issues_views.connect 'projects/:project_id/issues/gantt', :action => 'gantt'
119 119 issues_views.connect 'projects/:project_id/issues/calendar', :action => 'calendar'
120 120 issues_views.connect 'projects/:project_id/issues/:copy_from/copy', :action => 'new'
121 121 issues_views.connect 'issues/:id', :action => 'show', :id => /\d+/
122 122 issues_views.connect 'issues/:id.:format', :action => 'show', :id => /\d+/
123 123 issues_views.connect 'issues/:id/edit', :action => 'edit', :id => /\d+/
124 124 issues_views.connect 'issues/:id/move', :action => 'move', :id => /\d+/
125 125 end
126 126 issues_routes.with_options :conditions => {:method => :post} do |issues_actions|
127 127 issues_actions.connect 'projects/:project_id/issues', :action => 'new'
128 128 issues_actions.connect 'issues/:id/quoted', :action => 'reply', :id => /\d+/
129 129 issues_actions.connect 'issues/:id/:action', :action => /edit|move|destroy/, :id => /\d+/
130 130 end
131 131 issues_routes.connect 'issues/:action'
132 132 end
133 133
134 134 map.with_options :controller => 'issue_relations', :conditions => {:method => :post} do |relations|
135 135 relations.connect 'issues/:issue_id/relations/:id', :action => 'new'
136 136 relations.connect 'issues/:issue_id/relations/:id/destroy', :action => 'destroy'
137 137 end
138 138
139 139 map.with_options :controller => 'reports', :action => 'issue_report', :conditions => {:method => :get} do |reports|
140 140 reports.connect 'projects/:id/issues/report'
141 141 reports.connect 'projects/:id/issues/report/:detail'
142 142 end
143 143
144 144 map.with_options :controller => 'news' do |news_routes|
145 145 news_routes.with_options :conditions => {:method => :get} do |news_views|
146 146 news_views.connect 'news', :action => 'index'
147 147 news_views.connect 'projects/:project_id/news', :action => 'index'
148 148 news_views.connect 'projects/:project_id/news.:format', :action => 'index'
149 149 news_views.connect 'news.:format', :action => 'index'
150 150 news_views.connect 'projects/:project_id/news/new', :action => 'new'
151 151 news_views.connect 'news/:id', :action => 'show'
152 152 news_views.connect 'news/:id/edit', :action => 'edit'
153 153 end
154 154 news_routes.with_options do |news_actions|
155 155 news_actions.connect 'projects/:project_id/news', :action => 'new'
156 156 news_actions.connect 'news/:id/edit', :action => 'edit'
157 157 news_actions.connect 'news/:id/destroy', :action => 'destroy'
158 158 end
159 159 end
160 160
161 161 map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
162 162
163 163 map.with_options :controller => 'users' do |users|
164 164 users.with_options :conditions => {:method => :get} do |user_views|
165 165 user_views.connect 'users', :action => 'list'
166 166 user_views.connect 'users', :action => 'index'
167 167 user_views.connect 'users/new', :action => 'add'
168 168 user_views.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil
169 169 end
170 170 users.with_options :conditions => {:method => :post} do |user_actions|
171 171 user_actions.connect 'users', :action => 'add'
172 172 user_actions.connect 'users/new', :action => 'add'
173 173 user_actions.connect 'users/:id/edit', :action => 'edit'
174 174 user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
175 175 user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
176 176 user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
177 177 end
178 178 end
179 179
180 180 map.with_options :controller => 'projects' do |projects|
181 181 projects.with_options :conditions => {:method => :get} do |project_views|
182 182 project_views.connect 'projects', :action => 'index'
183 183 project_views.connect 'projects.:format', :action => 'index'
184 184 project_views.connect 'projects/new', :action => 'add'
185 185 project_views.connect 'projects/:id', :action => 'show'
186 186 project_views.connect 'projects/:id/:action', :action => /roadmap|changelog|destroy|settings/
187 187 project_views.connect 'projects/:id/files', :action => 'list_files'
188 188 project_views.connect 'projects/:id/files/new', :action => 'add_file'
189 189 project_views.connect 'projects/:id/versions/new', :action => 'add_version'
190 190 project_views.connect 'projects/:id/categories/new', :action => 'add_issue_category'
191 191 project_views.connect 'projects/:id/settings/:tab', :action => 'settings'
192 192 end
193 193
194 194 projects.with_options :action => 'activity', :conditions => {:method => :get} do |activity|
195 195 activity.connect 'projects/:id/activity'
196 196 activity.connect 'projects/:id/activity.:format'
197 197 activity.connect 'activity', :id => nil
198 198 activity.connect 'activity.:format', :id => nil
199 199 end
200 200
201 201 projects.with_options :conditions => {:method => :post} do |project_actions|
202 202 project_actions.connect 'projects/new', :action => 'add'
203 203 project_actions.connect 'projects', :action => 'add'
204 204 project_actions.connect 'projects/:id/:action', :action => /destroy|archive|unarchive/
205 205 project_actions.connect 'projects/:id/files/new', :action => 'add_file'
206 206 project_actions.connect 'projects/:id/versions/new', :action => 'add_version'
207 207 project_actions.connect 'projects/:id/categories/new', :action => 'add_issue_category'
208 208 end
209 209 end
210 210
211 211 map.with_options :controller => 'repositories' do |repositories|
212 212 repositories.with_options :conditions => {:method => :get} do |repository_views|
213 213 repository_views.connect 'projects/:id/repository', :action => 'show'
214 214 repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
215 215 repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
216 216 repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
217 217 repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
218 218 repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
219 219 repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
220 220 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
221 221 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
222 222 repository_views.connect 'projects/:id/repository/:action/*path'
223 223 end
224 224
225 225 repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
226 226 end
227 227
228 228 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
229 229 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
230 230 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
231 231
232
232 map.resources :groups
233
233 234 #left old routes at the bottom for backwards compat
234 235 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
235 236 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
236 237 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
237 238 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
238 239 map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
239 240 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
240 241 map.connect 'projects/:project_id/news/:action', :controller => 'news'
241 242 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
242 243 map.with_options :controller => 'repositories' do |omap|
243 244 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
244 245 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
245 246 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
246 247 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
247 248 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
248 249 omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
249 250 end
250 251
251 252 map.with_options :controller => 'sys' do |sys|
252 253 sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
253 254 sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
254 255 end
255 256
256 257 # Install the default route as the lowest priority.
257 258 map.connect ':controller/:action/:id'
258 259 map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
259 260 # Used for OpenID
260 261 map.root :controller => 'account', :action => 'login'
261 262 end
@@ -1,164 +1,164
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/activity'
4 4 require 'redmine/mime_type'
5 5 require 'redmine/core_ext'
6 6 require 'redmine/themes'
7 7 require 'redmine/hook'
8 8 require 'redmine/plugin'
9 9 require 'redmine/wiki_formatting'
10 10
11 11 begin
12 12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
13 13 rescue LoadError
14 14 # RMagick is not available
15 15 end
16 16
17 17 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
18 18
19 19 # Permissions
20 20 Redmine::AccessControl.map do |map|
21 21 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
22 22 map.permission :search_project, {:search => :index}, :public => true
23 23 map.permission :add_project, {:projects => :add}, :require => :loggedin
24 24 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
25 25 map.permission :select_project_modules, {:projects => :modules}, :require => :member
26 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member_login]}, :require => :member
26 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
27 27 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
28 28
29 29 map.project_module :issue_tracking do |map|
30 30 # Issue categories
31 31 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
32 32 # Issues
33 33 map.permission :view_issues, {:projects => [:changelog, :roadmap],
34 34 :issues => [:index, :changes, :show, :context_menu],
35 35 :versions => [:show, :status_by],
36 36 :queries => :index,
37 37 :reports => :issue_report}, :public => true
38 38 map.permission :add_issues, {:issues => :new}
39 39 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
40 40 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
41 41 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
42 42 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
43 43 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
44 44 map.permission :move_issues, {:issues => :move}, :require => :loggedin
45 45 map.permission :delete_issues, {:issues => :destroy}, :require => :member
46 46 # Queries
47 47 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
48 48 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
49 49 # Gantt & calendar
50 50 map.permission :view_gantt, :issues => :gantt
51 51 map.permission :view_calendar, :issues => :calendar
52 52 # Watchers
53 53 map.permission :view_issue_watchers, {}
54 54 map.permission :add_issue_watchers, {:watchers => :new}
55 55 end
56 56
57 57 map.project_module :time_tracking do |map|
58 58 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
59 59 map.permission :view_time_entries, :timelog => [:details, :report]
60 60 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
61 61 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
62 62 end
63 63
64 64 map.project_module :news do |map|
65 65 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
66 66 map.permission :view_news, {:news => [:index, :show]}, :public => true
67 67 map.permission :comment_news, {:news => :add_comment}
68 68 end
69 69
70 70 map.project_module :documents do |map|
71 71 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
72 72 map.permission :view_documents, :documents => [:index, :show, :download]
73 73 end
74 74
75 75 map.project_module :files do |map|
76 76 map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
77 77 map.permission :view_files, :projects => :list_files, :versions => :download
78 78 end
79 79
80 80 map.project_module :wiki do |map|
81 81 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
82 82 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
83 83 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
84 84 map.permission :view_wiki_pages, :wiki => [:index, :special]
85 85 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
86 86 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
87 87 map.permission :delete_wiki_pages_attachments, {}
88 88 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
89 89 end
90 90
91 91 map.project_module :repository do |map|
92 92 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
93 93 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
94 94 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
95 95 map.permission :commit_access, {}
96 96 end
97 97
98 98 map.project_module :boards do |map|
99 99 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
100 100 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
101 101 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
102 102 map.permission :edit_messages, {:messages => :edit}, :require => :member
103 103 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
104 104 map.permission :delete_messages, {:messages => :destroy}, :require => :member
105 105 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
106 106 end
107 107 end
108 108
109 109 Redmine::MenuManager.map :top_menu do |menu|
110 110 menu.push :home, :home_path
111 111 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
112 112 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
113 113 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
114 114 menu.push :help, Redmine::Info.help_url, :last => true
115 115 end
116 116
117 117 Redmine::MenuManager.map :account_menu do |menu|
118 118 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
119 119 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
120 120 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
121 121 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
122 122 end
123 123
124 124 Redmine::MenuManager.map :application_menu do |menu|
125 125 # Empty
126 126 end
127 127
128 128 Redmine::MenuManager.map :admin_menu do |menu|
129 129 # Empty
130 130 end
131 131
132 132 Redmine::MenuManager.map :project_menu do |menu|
133 133 menu.push :overview, { :controller => 'projects', :action => 'show' }
134 134 menu.push :activity, { :controller => 'projects', :action => 'activity' }
135 135 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
136 136 :if => Proc.new { |p| p.versions.any? }
137 137 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
138 138 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
139 139 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
140 140 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
141 141 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
142 142 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
143 143 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
144 144 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
145 145 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
146 146 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
147 147 menu.push :repository, { :controller => 'repositories', :action => 'show' },
148 148 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
149 149 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
150 150 end
151 151
152 152 Redmine::Activity.map do |activity|
153 153 activity.register :issues, :class_name => %w(Issue Journal)
154 154 activity.register :changesets
155 155 activity.register :news
156 156 activity.register :documents, :class_name => %w(Document Attachment)
157 157 activity.register :files, :class_name => 'Attachment'
158 158 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
159 159 activity.register :messages, :default => false
160 160 end
161 161
162 162 Redmine::WikiFormatting.map do |format|
163 163 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
164 164 end
1 NO CONTENT: modified file, binary diff hidden
@@ -1,794 +1,797
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 13 #top-menu ul {margin: 0; padding: 0;}
14 14 #top-menu li {
15 15 float:left;
16 16 list-style-type:none;
17 17 margin: 0px 0px 0px 0px;
18 18 padding: 0px 0px 0px 0px;
19 19 white-space:nowrap;
20 20 }
21 21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 23
24 24 #account {float:right;}
25 25
26 26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 27 #header a {color:#f8f8f8;}
28 28 #header h1 a.ancestor { font-size: 80%; }
29 29 #quick-search {float:right;}
30 30
31 31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 32 #main-menu ul {margin: 0; padding: 0;}
33 33 #main-menu li {
34 34 float:left;
35 35 list-style-type:none;
36 36 margin: 0px 2px 0px 0px;
37 37 padding: 0px 0px 0px 0px;
38 38 white-space:nowrap;
39 39 }
40 40 #main-menu li a {
41 41 display: block;
42 42 color: #fff;
43 43 text-decoration: none;
44 44 font-weight: bold;
45 45 margin: 0;
46 46 padding: 4px 10px 4px 10px;
47 47 }
48 48 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50 50
51 51 #main {background-color:#EEEEEE;}
52 52
53 53 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
54 54 * html #sidebar{ width: 17%; }
55 55 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
56 56 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
57 57 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
58 58
59 59 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
60 60 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
61 61 html>body #content { min-height: 600px; }
62 62 * html body #content { height: 600px; } /* IE */
63 63
64 64 #main.nosidebar #sidebar{ display: none; }
65 65 #main.nosidebar #content{ width: auto; border-right: 0; }
66 66
67 67 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
68 68
69 69 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
70 70 #login-form table td {padding: 6px;}
71 71 #login-form label {font-weight: bold;}
72 72 #login-form input#username, #login-form input#password { width: 300px; }
73 73
74 74 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
75 75
76 76 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
77 77
78 78 /***** Links *****/
79 79 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
80 80 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
81 81 a img{ border: 0; }
82 82
83 83 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
84 84
85 85 /***** Tables *****/
86 86 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
87 87 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
88 88 table.list td { vertical-align: top; }
89 89 table.list td.id { width: 2%; text-align: center;}
90 90 table.list td.checkbox { width: 15px; padding: 0px;}
91 91 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
92 92 table.list td.buttons a { padding-right: 0.6em; }
93 93
94 94 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
95 95 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
96 96
97 97 tr.issue { text-align: center; white-space: nowrap; }
98 98 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
99 99 tr.issue td.subject { text-align: left; }
100 100 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
101 101
102 102 tr.entry { border: 1px solid #f8f8f8; }
103 103 tr.entry td { white-space: nowrap; }
104 104 tr.entry td.filename { width: 30%; }
105 105 tr.entry td.size { text-align: right; font-size: 90%; }
106 106 tr.entry td.revision, tr.entry td.author { text-align: center; }
107 107 tr.entry td.age { text-align: right; }
108 108 tr.entry.file td.filename a { margin-left: 16px; }
109 109
110 110 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
111 111 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
112 112
113 113 tr.changeset td.author { text-align: center; width: 15%; }
114 114 tr.changeset td.committed_on { text-align: center; width: 15%; }
115 115
116 116 table.files tr.file td { text-align: center; }
117 117 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
118 118 table.files tr.file td.digest { font-size: 80%; }
119 119
120 120 table.members td.roles, table.memberships td.roles { width: 45%; }
121 121
122 122 tr.message { height: 2.6em; }
123 123 tr.message td.last_message { font-size: 80%; }
124 124 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
125 125 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
126 126
127 127 tr.user td { width:13%; }
128 128 tr.user td.email { width:18%; }
129 129 tr.user td { white-space: nowrap; }
130 130 tr.user.locked, tr.user.registered { color: #aaa; }
131 131 tr.user.locked a, tr.user.registered a { color: #aaa; }
132 132
133 133 tr.time-entry { text-align: center; white-space: nowrap; }
134 134 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
135 135 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
136 136 td.hours .hours-dec { font-size: 0.9em; }
137 137
138 138 table.plugins td { vertical-align: middle; }
139 139 table.plugins td.configure { text-align: right; padding-right: 1em; }
140 140 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
141 141 table.plugins span.description { display: block; font-size: 0.9em; }
142 142 table.plugins span.url { display: block; font-size: 0.9em; }
143 143
144 144 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
145 145 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
146 146
147 147 table.list tbody tr:hover { background-color:#ffffdd; }
148 148 table.list tbody tr.group:hover { background-color:inherit; }
149 149 table td {padding:2px;}
150 150 table p {margin:0;}
151 151 .odd {background-color:#f6f7f8;}
152 152 .even {background-color: #fff;}
153 153
154 154 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
155 155 a.sort.asc { background-image: url(../images/sort_asc.png); }
156 156 a.sort.desc { background-image: url(../images/sort_desc.png); }
157 157
158 158 table.attributes { width: 100% }
159 159 table.attributes th { vertical-align: top; text-align: left; }
160 160 table.attributes td { vertical-align: top; }
161 161
162 162 .highlight { background-color: #FCFD8D;}
163 163 .highlight.token-1 { background-color: #faa;}
164 164 .highlight.token-2 { background-color: #afa;}
165 165 .highlight.token-3 { background-color: #aaf;}
166 166
167 167 .box{
168 168 padding:6px;
169 169 margin-bottom: 10px;
170 170 background-color:#f6f6f6;
171 171 color:#505050;
172 172 line-height:1.5em;
173 173 border: 1px solid #e4e4e4;
174 174 }
175 175
176 176 div.square {
177 177 border: 1px solid #999;
178 178 float: left;
179 179 margin: .3em .4em 0 .4em;
180 180 overflow: hidden;
181 181 width: .6em; height: .6em;
182 182 }
183 183 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
184 184 .contextual input, .contextual select {font-size:0.9em;}
185 185 .message .contextual { margin-top: 0; }
186 186
187 187 .splitcontentleft{float:left; width:49%;}
188 188 .splitcontentright{float:right; width:49%;}
189 189 form {display: inline;}
190 190 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
191 191 fieldset {border: 1px solid #e4e4e4; margin:0;}
192 192 legend {color: #484848;}
193 193 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
194 194 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
195 195 blockquote blockquote { margin-left: 0;}
196 196 textarea.wiki-edit { width: 99%; }
197 197 li p {margin-top: 0;}
198 198 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
199 199 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
200 200 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
201 201 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
202 202
203 203 #query_form_content { font-size: 0.9em; padding: 4px; background: #f6f6f6; border: 1px solid #e4e4e4; }
204 204 #query_form_content fieldset#filters { border-left: 0; border-right: 0; }
205 205 #query_form_content p { margin-top: 0.5em; margin-bottom: 0.5em; }
206 206
207 207 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
208 208 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
209 209 fieldset#filters table { border-collapse: collapse; }
210 210 fieldset#filters table td { padding: 0; vertical-align: middle; }
211 211 fieldset#filters tr.filter { height: 2em; }
212 212 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
213 213 .buttons { font-size: 0.9em; margin-bottom: 1.4em; }
214 214
215 215 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
216 216 div#issue-changesets .changeset { padding: 4px;}
217 217 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
218 218 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
219 219
220 220 div#activity dl, #search-results { margin-left: 2em; }
221 221 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
222 222 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
223 223 div#activity dt.me .time { border-bottom: 1px solid #999; }
224 224 div#activity dt .time { color: #777; font-size: 80%; }
225 225 div#activity dd .description, #search-results dd .description { font-style: italic; }
226 226 div#activity span.project:after, #search-results span.project:after { content: " -"; }
227 227 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
228 228
229 229 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
230 230
231 231 div#search-results-counts {float:right;}
232 232 div#search-results-counts ul { margin-top: 0.5em; }
233 233 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
234 234
235 235 dt.issue { background-image: url(../images/ticket.png); }
236 236 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
237 237 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
238 238 dt.issue-note { background-image: url(../images/ticket_note.png); }
239 239 dt.changeset { background-image: url(../images/changeset.png); }
240 240 dt.news { background-image: url(../images/news.png); }
241 241 dt.message { background-image: url(../images/message.png); }
242 242 dt.reply { background-image: url(../images/comments.png); }
243 243 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
244 244 dt.attachment { background-image: url(../images/attachment.png); }
245 245 dt.document { background-image: url(../images/document.png); }
246 246 dt.project { background-image: url(../images/projects.png); }
247 247
248 248 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
249 249
250 250 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
251 251 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
252 252 div#roadmap .wiki h1:first-child { display: none; }
253 253 div#roadmap .wiki h1 { font-size: 120%; }
254 254 div#roadmap .wiki h2 { font-size: 110%; }
255 255
256 256 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
257 257 div#version-summary fieldset { margin-bottom: 1em; }
258 258 div#version-summary .total-hours { text-align: right; }
259 259
260 260 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
261 261 table#time-report tbody tr { font-style: italic; color: #777; }
262 262 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
263 263 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
264 264 table#time-report .hours-dec { font-size: 0.9em; }
265 265
266 266 form#issue-form .attributes { margin-bottom: 8px; }
267 267 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
268 268 form#issue-form .attributes select { min-width: 30%; }
269 269
270 270 ul.projects { margin: 0; padding-left: 1em; }
271 271 ul.projects.root { margin: 0; padding: 0; }
272 272 ul.projects ul { border-left: 3px solid #e0e0e0; }
273 273 ul.projects li { list-style-type:none; }
274 274 ul.projects li.root { margin-bottom: 1em; }
275 275 ul.projects li.child { margin-top: 1em;}
276 276 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
277 277 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
278 278
279 279 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
280 280 #tracker_project_ids li { list-style-type:none; }
281 281
282 282 ul.properties {padding:0; font-size: 0.9em; color: #777;}
283 283 ul.properties li {list-style-type:none;}
284 284 ul.properties li span {font-style:italic;}
285 285
286 286 .total-hours { font-size: 110%; font-weight: bold; }
287 287 .total-hours span.hours-int { font-size: 120%; }
288 288
289 289 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
290 290 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
291 291
292 292 .pagination {font-size: 90%}
293 293 p.pagination {margin-top:8px;}
294 294
295 295 /***** Tabular forms ******/
296 296 .tabular p{
297 297 margin: 0;
298 298 padding: 5px 0 8px 0;
299 299 padding-left: 180px; /*width of left column containing the label elements*/
300 300 height: 1%;
301 301 clear:left;
302 302 }
303 303
304 304 html>body .tabular p {overflow:hidden;}
305 305
306 306 .tabular label{
307 307 font-weight: bold;
308 308 float: left;
309 309 text-align: right;
310 310 margin-left: -180px; /*width of left column*/
311 311 width: 175px; /*width of labels. Should be smaller than left column to create some right
312 312 margin*/
313 313 }
314 314
315 315 .tabular label.floating{
316 316 font-weight: normal;
317 317 margin-left: 0px;
318 318 text-align: left;
319 319 width: 270px;
320 320 }
321 321
322 322 input#time_entry_comments { width: 90%;}
323 323
324 324 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
325 325
326 326 .tabular.settings p{ padding-left: 300px; }
327 327 .tabular.settings label{ margin-left: -300px; width: 295px; }
328 328
329 329 .required {color: #bb0000;}
330 330 .summary {font-style: italic;}
331 331
332 332 #attachments_fields input[type=text] {margin-left: 8px; }
333 333
334 334 div.attachments { margin-top: 12px; }
335 335 div.attachments p { margin:4px 0 2px 0; }
336 336 div.attachments img { vertical-align: middle; }
337 337 div.attachments span.author { font-size: 0.9em; color: #888; }
338 338
339 339 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
340 340 .other-formats span + span:before { content: "| "; }
341 341
342 342 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
343 343
344 344 /* Project members tab */
345 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft { width: 64% }
346 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright { width: 34% }
347 div#tab-content-members fieldset, div#tab-content-memberships fieldset { padding:1em; margin-bottom: 1em; }
348 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend { font-weight: bold; }
349 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label { display: block; }
350 div#tab-content-members fieldset div { max-height: 400px; overflow:auto; }
345 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
346 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
347 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
348 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
349 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
350 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
351
352 table.members td.group { padding-left: 20px; background: url(../images/users.png) no-repeat 0% 0%; }
351 353
352 354 * html div#tab-content-members fieldset div { height: 450px; }
353 355
354 356 /***** Flash & error messages ****/
355 357 #errorExplanation, div.flash, .nodata, .warning {
356 358 padding: 4px 4px 4px 30px;
357 359 margin-bottom: 12px;
358 360 font-size: 1.1em;
359 361 border: 2px solid;
360 362 }
361 363
362 364 div.flash {margin-top: 8px;}
363 365
364 366 div.flash.error, #errorExplanation {
365 367 background: url(../images/false.png) 8px 5px no-repeat;
366 368 background-color: #ffe3e3;
367 369 border-color: #dd0000;
368 370 color: #550000;
369 371 }
370 372
371 373 div.flash.notice {
372 374 background: url(../images/true.png) 8px 5px no-repeat;
373 375 background-color: #dfffdf;
374 376 border-color: #9fcf9f;
375 377 color: #005f00;
376 378 }
377 379
378 380 div.flash.warning {
379 381 background: url(../images/warning.png) 8px 5px no-repeat;
380 382 background-color: #FFEBC1;
381 383 border-color: #FDBF3B;
382 384 color: #A6750C;
383 385 text-align: left;
384 386 }
385 387
386 388 .nodata, .warning {
387 389 text-align: center;
388 390 background-color: #FFEBC1;
389 391 border-color: #FDBF3B;
390 392 color: #A6750C;
391 393 }
392 394
393 395 #errorExplanation ul { font-size: 0.9em;}
394 396 #errorExplanation h2, #errorExplanation p { display: none; }
395 397
396 398 /***** Ajax indicator ******/
397 399 #ajax-indicator {
398 400 position: absolute; /* fixed not supported by IE */
399 401 background-color:#eee;
400 402 border: 1px solid #bbb;
401 403 top:35%;
402 404 left:40%;
403 405 width:20%;
404 406 font-weight:bold;
405 407 text-align:center;
406 408 padding:0.6em;
407 409 z-index:100;
408 410 filter:alpha(opacity=50);
409 411 opacity: 0.5;
410 412 }
411 413
412 414 html>body #ajax-indicator { position: fixed; }
413 415
414 416 #ajax-indicator span {
415 417 background-position: 0% 40%;
416 418 background-repeat: no-repeat;
417 419 background-image: url(../images/loading.gif);
418 420 padding-left: 26px;
419 421 vertical-align: bottom;
420 422 }
421 423
422 424 /***** Calendar *****/
423 425 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
424 426 table.cal thead th {width: 14%;}
425 427 table.cal tbody tr {height: 100px;}
426 428 table.cal th { background-color:#EEEEEE; padding: 4px; }
427 429 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
428 430 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
429 431 table.cal td.odd p.day-num {color: #bbb;}
430 432 table.cal td.today {background:#ffffdd;}
431 433 table.cal td.today p.day-num {font-weight: bold;}
432 434
433 435 /***** Tooltips ******/
434 436 .tooltip{position:relative;z-index:24;}
435 437 .tooltip:hover{z-index:25;color:#000;}
436 438 .tooltip span.tip{display: none; text-align:left;}
437 439
438 440 div.tooltip:hover span.tip{
439 441 display:block;
440 442 position:absolute;
441 443 top:12px; left:24px; width:270px;
442 444 border:1px solid #555;
443 445 background-color:#fff;
444 446 padding: 4px;
445 447 font-size: 0.8em;
446 448 color:#505050;
447 449 }
448 450
449 451 /***** Progress bar *****/
450 452 table.progress {
451 453 border: 1px solid #D7D7D7;
452 454 border-collapse: collapse;
453 455 border-spacing: 0pt;
454 456 empty-cells: show;
455 457 text-align: center;
456 458 float:left;
457 459 margin: 1px 6px 1px 0px;
458 460 }
459 461
460 462 table.progress td { height: 0.9em; }
461 463 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
462 464 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
463 465 table.progress td.open { background: #FFF none repeat scroll 0%; }
464 466 p.pourcent {font-size: 80%;}
465 467 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
466 468
467 469 /***** Tabs *****/
468 470 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
469 471 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
470 472 #content .tabs>ul { bottom:-1px; } /* others */
471 473 #content .tabs ul li {
472 474 float:left;
473 475 list-style-type:none;
474 476 white-space:nowrap;
475 477 margin-right:8px;
476 478 background:#fff;
477 479 }
478 480 #content .tabs ul li a{
479 481 display:block;
480 482 font-size: 0.9em;
481 483 text-decoration:none;
482 484 line-height:1.3em;
483 485 padding:4px 6px 4px 6px;
484 486 border: 1px solid #ccc;
485 487 border-bottom: 1px solid #bbbbbb;
486 488 background-color: #eeeeee;
487 489 color:#777;
488 490 font-weight:bold;
489 491 }
490 492
491 493 #content .tabs ul li a:hover {
492 494 background-color: #ffffdd;
493 495 text-decoration:none;
494 496 }
495 497
496 498 #content .tabs ul li a.selected {
497 499 background-color: #fff;
498 500 border: 1px solid #bbbbbb;
499 501 border-bottom: 1px solid #fff;
500 502 }
501 503
502 504 #content .tabs ul li a.selected:hover {
503 505 background-color: #fff;
504 506 }
505 507
506 508 /***** Auto-complete *****/
507 509 div.autocomplete {
508 510 position:absolute;
509 511 width:250px;
510 512 background-color:white;
511 513 margin:0;
512 514 padding:0;
513 515 }
514 516 div.autocomplete ul {
515 517 list-style-type:none;
516 518 margin:0;
517 519 padding:0;
518 520 }
519 521 div.autocomplete ul li.selected { background-color: #ffb;}
520 522 div.autocomplete ul li {
521 523 list-style-type:none;
522 524 display:block;
523 525 margin:0;
524 526 padding:2px;
525 527 cursor:pointer;
526 528 font-size: 90%;
527 529 border-bottom: 1px solid #ccc;
528 530 border-left: 1px solid #ccc;
529 531 border-right: 1px solid #ccc;
530 532 }
531 533 div.autocomplete ul li span.informal {
532 534 font-size: 80%;
533 535 color: #aaa;
534 536 }
535 537
536 538 /***** Diff *****/
537 539 .diff_out { background: #fcc; }
538 540 .diff_in { background: #cfc; }
539 541
540 542 /***** Wiki *****/
541 543 div.wiki table {
542 544 border: 1px solid #505050;
543 545 border-collapse: collapse;
544 546 margin-bottom: 1em;
545 547 }
546 548
547 549 div.wiki table, div.wiki td, div.wiki th {
548 550 border: 1px solid #bbb;
549 551 padding: 4px;
550 552 }
551 553
552 554 div.wiki .external {
553 555 background-position: 0% 60%;
554 556 background-repeat: no-repeat;
555 557 padding-left: 12px;
556 558 background-image: url(../images/external.png);
557 559 }
558 560
559 561 div.wiki a.new {
560 562 color: #b73535;
561 563 }
562 564
563 565 div.wiki pre {
564 566 margin: 1em 1em 1em 1.6em;
565 567 padding: 2px;
566 568 background-color: #fafafa;
567 569 border: 1px solid #dadada;
568 570 width:95%;
569 571 overflow-x: auto;
570 572 }
571 573
572 574 div.wiki ul.toc {
573 575 background-color: #ffffdd;
574 576 border: 1px solid #e4e4e4;
575 577 padding: 4px;
576 578 line-height: 1.2em;
577 579 margin-bottom: 12px;
578 580 margin-right: 12px;
579 581 margin-left: 0;
580 582 display: table
581 583 }
582 584 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
583 585
584 586 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
585 587 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
586 588 div.wiki ul.toc li { list-style-type:none;}
587 589 div.wiki ul.toc li.heading2 { margin-left: 6px; }
588 590 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
589 591
590 592 div.wiki ul.toc a {
591 593 font-size: 0.9em;
592 594 font-weight: normal;
593 595 text-decoration: none;
594 596 color: #606060;
595 597 }
596 598 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
597 599
598 600 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
599 601 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
600 602 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
601 603
602 604 /***** My page layout *****/
603 605 .block-receiver {
604 606 border:1px dashed #c0c0c0;
605 607 margin-bottom: 20px;
606 608 padding: 15px 0 15px 0;
607 609 }
608 610
609 611 .mypage-box {
610 612 margin:0 0 20px 0;
611 613 color:#505050;
612 614 line-height:1.5em;
613 615 }
614 616
615 617 .handle {
616 618 cursor: move;
617 619 }
618 620
619 621 a.close-icon {
620 622 display:block;
621 623 margin-top:3px;
622 624 overflow:hidden;
623 625 width:12px;
624 626 height:12px;
625 627 background-repeat: no-repeat;
626 628 cursor:pointer;
627 629 background-image:url('../images/close.png');
628 630 }
629 631
630 632 a.close-icon:hover {
631 633 background-image:url('../images/close_hl.png');
632 634 }
633 635
634 636 /***** Gantt chart *****/
635 637 .gantt_hdr {
636 638 position:absolute;
637 639 top:0;
638 640 height:16px;
639 641 border-top: 1px solid #c0c0c0;
640 642 border-bottom: 1px solid #c0c0c0;
641 643 border-right: 1px solid #c0c0c0;
642 644 text-align: center;
643 645 overflow: hidden;
644 646 }
645 647
646 648 .task {
647 649 position: absolute;
648 650 height:8px;
649 651 font-size:0.8em;
650 652 color:#888;
651 653 padding:0;
652 654 margin:0;
653 655 line-height:0.8em;
654 656 }
655 657
656 658 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
657 659 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
658 660 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
659 661 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
660 662
661 663 /***** Icons *****/
662 664 .icon {
663 665 background-position: 0% 40%;
664 666 background-repeat: no-repeat;
665 667 padding-left: 20px;
666 668 padding-top: 2px;
667 669 padding-bottom: 3px;
668 670 }
669 671
670 672 .icon22 {
671 673 background-position: 0% 40%;
672 674 background-repeat: no-repeat;
673 675 padding-left: 26px;
674 676 line-height: 22px;
675 677 vertical-align: middle;
676 678 }
677 679
678 680 .icon-add { background-image: url(../images/add.png); }
679 681 .icon-edit { background-image: url(../images/edit.png); }
680 682 .icon-copy { background-image: url(../images/copy.png); }
681 683 .icon-del { background-image: url(../images/delete.png); }
682 684 .icon-move { background-image: url(../images/move.png); }
683 685 .icon-save { background-image: url(../images/save.png); }
684 686 .icon-cancel { background-image: url(../images/cancel.png); }
685 687 .icon-folder { background-image: url(../images/folder.png); }
686 688 .open .icon-folder { background-image: url(../images/folder_open.png); }
687 689 .icon-package { background-image: url(../images/package.png); }
688 690 .icon-home { background-image: url(../images/home.png); }
689 691 .icon-user { background-image: url(../images/user.png); }
690 692 .icon-mypage { background-image: url(../images/user_page.png); }
691 693 .icon-admin { background-image: url(../images/admin.png); }
692 694 .icon-projects { background-image: url(../images/projects.png); }
693 695 .icon-help { background-image: url(../images/help.png); }
694 696 .icon-attachment { background-image: url(../images/attachment.png); }
695 697 .icon-index { background-image: url(../images/index.png); }
696 698 .icon-history { background-image: url(../images/history.png); }
697 699 .icon-time { background-image: url(../images/time.png); }
698 700 .icon-time-add { background-image: url(../images/time_add.png); }
699 701 .icon-stats { background-image: url(../images/stats.png); }
700 702 .icon-warning { background-image: url(../images/warning.png); }
701 703 .icon-fav { background-image: url(../images/fav.png); }
702 704 .icon-fav-off { background-image: url(../images/fav_off.png); }
703 705 .icon-reload { background-image: url(../images/reload.png); }
704 706 .icon-lock { background-image: url(../images/locked.png); }
705 707 .icon-unlock { background-image: url(../images/unlock.png); }
706 708 .icon-checked { background-image: url(../images/true.png); }
707 709 .icon-details { background-image: url(../images/zoom_in.png); }
708 710 .icon-report { background-image: url(../images/report.png); }
709 711 .icon-comment { background-image: url(../images/comment.png); }
710 712
711 713 .icon-file { background-image: url(../images/files/default.png); }
712 714 .icon-file.text-plain { background-image: url(../images/files/text.png); }
713 715 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
714 716 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
715 717 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
716 718 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
717 719 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
718 720 .icon-file.image-gif { background-image: url(../images/files/image.png); }
719 721 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
720 722 .icon-file.image-png { background-image: url(../images/files/image.png); }
721 723 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
722 724 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
723 725 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
724 726 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
725 727
726 728 .icon22-projects { background-image: url(../images/22x22/projects.png); }
727 729 .icon22-users { background-image: url(../images/22x22/users.png); }
730 .icon22-groups { background-image: url(../images/22x22/groups.png); }
728 731 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
729 732 .icon22-role { background-image: url(../images/22x22/role.png); }
730 733 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
731 734 .icon22-options { background-image: url(../images/22x22/options.png); }
732 735 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
733 736 .icon22-authent { background-image: url(../images/22x22/authent.png); }
734 737 .icon22-info { background-image: url(../images/22x22/info.png); }
735 738 .icon22-comment { background-image: url(../images/22x22/comment.png); }
736 739 .icon22-package { background-image: url(../images/22x22/package.png); }
737 740 .icon22-settings { background-image: url(../images/22x22/settings.png); }
738 741 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
739 742
740 743 img.gravatar {
741 744 padding: 2px;
742 745 border: solid 1px #d5d5d5;
743 746 background: #fff;
744 747 }
745 748
746 749 div.issue img.gravatar {
747 750 float: right;
748 751 margin: 0 0 0 1em;
749 752 padding: 5px;
750 753 }
751 754
752 755 div.issue table img.gravatar {
753 756 height: 14px;
754 757 width: 14px;
755 758 padding: 2px;
756 759 float: left;
757 760 margin: 0 0.5em 0 0;
758 761 }
759 762
760 763 #history img.gravatar {
761 764 padding: 3px;
762 765 margin: 0 1.5em 1em 0;
763 766 float: left;
764 767 }
765 768
766 769 td.username img.gravatar {
767 770 float: left;
768 771 margin: 0 1em 0 0;
769 772 }
770 773
771 774 #activity dt img.gravatar {
772 775 float: left;
773 776 margin: 0 1em 1em 0;
774 777 }
775 778
776 779 #activity dt,
777 780 .journal {
778 781 clear: left;
779 782 }
780 783
781 784 .gravatar-margin {
782 785 margin-left: 40px;
783 786 }
784 787
785 788 h2 img { vertical-align:middle; }
786 789
787 790
788 791 /***** Media print specific styles *****/
789 792 @media print {
790 793 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
791 794 #main { background: #fff; }
792 795 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
793 796 #wiki_add_attachment { display:none; }
794 797 }
@@ -1,23 +1,39
1 1 ---
2 2 member_roles_001:
3 3 id: 1
4 4 role_id: 1
5 5 member_id: 1
6 6 member_roles_002:
7 7 id: 2
8 8 role_id: 2
9 9 member_id: 2
10 10 member_roles_003:
11 11 id: 3
12 12 role_id: 2
13 13 member_id: 3
14 14 member_roles_004:
15 15 id: 4
16 16 role_id: 2
17 17 member_id: 4
18 18 member_roles_005:
19 19 id: 5
20 20 role_id: 1
21 21 member_id: 5
22
23 No newline at end of file
22 member_roles_006:
23 id: 6
24 role_id: 1
25 member_id: 6
26 member_roles_007:
27 id: 7
28 role_id: 2
29 member_id: 6
30 member_roles_008:
31 id: 8
32 role_id: 1
33 member_id: 7
34 inherited_from: 6
35 member_roles_009:
36 id: 9
37 role_id: 2
38 member_id: 7
39 inherited_from: 7
@@ -1,33 +1,45
1 1 ---
2 2 members_001:
3 3 created_on: 2006-07-19 19:35:33 +02:00
4 4 project_id: 1
5 5 id: 1
6 6 user_id: 2
7 7 mail_notification: true
8 8 members_002:
9 9 created_on: 2006-07-19 19:35:36 +02:00
10 10 project_id: 1
11 11 id: 2
12 12 user_id: 3
13 13 mail_notification: true
14 14 members_003:
15 15 created_on: 2006-07-19 19:35:36 +02:00
16 16 project_id: 2
17 17 id: 3
18 18 user_id: 2
19 19 mail_notification: true
20 20 members_004:
21 21 id: 4
22 22 created_on: 2006-07-19 19:35:36 +02:00
23 23 project_id: 1
24 24 # Locked user
25 25 user_id: 5
26 26 mail_notification: true
27 27 members_005:
28 28 id: 5
29 29 created_on: 2006-07-19 19:35:33 +02:00
30 30 project_id: 5
31 31 user_id: 2
32 32 mail_notification: true
33 members_006:
34 id: 6
35 created_on: 2006-07-19 19:35:33 +02:00
36 project_id: 5
37 user_id: 10
38 mail_notification: false
39 members_007:
40 id: 7
41 created_on: 2006-07-19 19:35:33 +02:00
42 project_id: 5
43 user_id: 8
44 mail_notification: false
33 45 No newline at end of file
@@ -1,148 +1,156
1 1 ---
2 2 users_004:
3 3 created_on: 2006-07-19 19:34:07 +02:00
4 4 status: 1
5 5 last_login_on:
6 6 language: en
7 7 hashed_password: 4e4aeb7baaf0706bd670263fef42dad15763b608
8 8 updated_on: 2006-07-19 19:34:07 +02:00
9 9 admin: false
10 10 mail: rhill@somenet.foo
11 11 lastname: Hill
12 12 firstname: Robert
13 13 id: 4
14 14 auth_source_id:
15 15 mail_notification: true
16 16 login: rhill
17 17 type: User
18 18 users_001:
19 19 created_on: 2006-07-19 19:12:21 +02:00
20 20 status: 1
21 21 last_login_on: 2006-07-19 22:57:52 +02:00
22 22 language: en
23 23 hashed_password: d033e22ae348aeb5660fc2140aec35850c4da997
24 24 updated_on: 2006-07-19 22:57:52 +02:00
25 25 admin: true
26 26 mail: admin@somenet.foo
27 27 lastname: Admin
28 28 firstname: redMine
29 29 id: 1
30 30 auth_source_id:
31 31 mail_notification: true
32 32 login: admin
33 33 type: User
34 34 users_002:
35 35 created_on: 2006-07-19 19:32:09 +02:00
36 36 status: 1
37 37 last_login_on: 2006-07-19 22:42:15 +02:00
38 38 language: en
39 39 hashed_password: a9a653d4151fa2c081ba1ffc2c2726f3b80b7d7d
40 40 updated_on: 2006-07-19 22:42:15 +02:00
41 41 admin: false
42 42 mail: jsmith@somenet.foo
43 43 lastname: Smith
44 44 firstname: John
45 45 id: 2
46 46 auth_source_id:
47 47 mail_notification: true
48 48 login: jsmith
49 49 type: User
50 50 users_003:
51 51 created_on: 2006-07-19 19:33:19 +02:00
52 52 status: 1
53 53 last_login_on:
54 54 language: en
55 55 hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
56 56 updated_on: 2006-07-19 19:33:19 +02:00
57 57 admin: false
58 58 mail: dlopper@somenet.foo
59 59 lastname: Lopper
60 60 firstname: Dave
61 61 id: 3
62 62 auth_source_id:
63 63 mail_notification: true
64 64 login: dlopper
65 65 type: User
66 66 users_005:
67 67 id: 5
68 68 created_on: 2006-07-19 19:33:19 +02:00
69 69 # Locked
70 70 status: 3
71 71 last_login_on:
72 72 language: en
73 73 hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
74 74 updated_on: 2006-07-19 19:33:19 +02:00
75 75 admin: false
76 76 mail: dlopper2@somenet.foo
77 77 lastname: Lopper2
78 78 firstname: Dave2
79 79 auth_source_id:
80 80 mail_notification: true
81 81 login: dlopper2
82 82 type: User
83 83 users_006:
84 84 id: 6
85 85 created_on: 2006-07-19 19:33:19 +02:00
86 86 status: 1
87 87 last_login_on:
88 88 language: ''
89 89 hashed_password: 1
90 90 updated_on: 2006-07-19 19:33:19 +02:00
91 91 admin: false
92 92 mail: ''
93 93 lastname: Anonymous
94 94 firstname: ''
95 95 auth_source_id:
96 96 mail_notification: false
97 97 login: ''
98 98 type: AnonymousUser
99 99 users_007:
100 100 id: 7
101 101 created_on: 2006-07-19 19:33:19 +02:00
102 102 status: 1
103 103 last_login_on:
104 104 language: ''
105 105 hashed_password: 1
106 106 updated_on: 2006-07-19 19:33:19 +02:00
107 107 admin: false
108 108 mail: someone@foo.bar
109 109 lastname: One
110 110 firstname: Some
111 111 auth_source_id:
112 112 mail_notification: false
113 113 login: someone
114 114 type: User
115 115 users_008:
116 116 id: 8
117 117 created_on: 2006-07-19 19:33:19 +02:00
118 118 status: 1
119 119 last_login_on:
120 120 language: 'it'
121 121 hashed_password: 1
122 122 updated_on: 2006-07-19 19:33:19 +02:00
123 123 admin: false
124 124 mail: miscuser8@foo.bar
125 125 lastname: Misc
126 126 firstname: User
127 127 auth_source_id:
128 128 mail_notification: false
129 129 login: miscuser8
130 130 type: User
131 131 users_009:
132 132 id: 9
133 133 created_on: 2006-07-19 19:33:19 +02:00
134 134 status: 1
135 135 last_login_on:
136 136 language: 'it'
137 137 hashed_password: 1
138 138 updated_on: 2006-07-19 19:33:19 +02:00
139 139 admin: false
140 140 mail: miscuser9@foo.bar
141 141 lastname: Misc
142 142 firstname: User
143 143 auth_source_id:
144 144 mail_notification: false
145 145 login: miscuser9
146 146 type: User
147 groups_010:
148 id: 10
149 lastname: A Team
150 type: Group
151 groups_011:
152 id: 11
153 lastname: B Team
154 type: Group
147 155
148 156 No newline at end of file
@@ -1,89 +1,82
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'members_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class MembersController; def rescue_action(e) raise e end; end
23 23
24 24
25 25 class MembersControllerTest < Test::Unit::TestCase
26 26 fixtures :projects, :members, :member_roles, :roles, :users
27 27
28 28 def setup
29 29 @controller = MembersController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 User.current = nil
33 33 @request.session[:user_id] = 2
34 34 end
35 35
36 36 def test_members_routing
37 37 assert_routing(
38 38 {:method => :post, :path => 'projects/5234/members/new'},
39 39 :controller => 'members', :action => 'new', :id => '5234'
40 40 )
41 41 end
42 42
43 43 def test_create
44 44 assert_difference 'Member.count' do
45 45 post :new, :id => 1, :member => {:role_ids => [1], :user_id => 7}
46 46 end
47 47 assert_redirected_to '/projects/ecookbook/settings/members'
48 48 assert User.find(7).member_of?(Project.find(1))
49 49 end
50 50
51 def test_create_by_user_login
52 assert_difference 'Member.count' do
53 post :new, :id => 1, :member => {:role_ids => [1], :user_login => 'someone'}
54 end
55 assert_redirected_to '/projects/ecookbook/settings/members'
56 assert User.find(7).member_of?(Project.find(1))
57 end
58
59 51 def test_create_multiple
60 52 assert_difference 'Member.count', 3 do
61 53 post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]}
62 54 end
63 55 assert_redirected_to '/projects/ecookbook/settings/members'
64 56 assert User.find(7).member_of?(Project.find(1))
65 57 end
66 58
67 59 def test_edit
68 60 assert_no_difference 'Member.count' do
69 61 post :edit, :id => 2, :member => {:role_ids => [1], :user_id => 3}
70 62 end
71 63 assert_redirected_to '/projects/ecookbook/settings/members'
72 64 end
73 65
74 66 def test_destroy
75 67 assert_difference 'Member.count', -1 do
76 68 post :destroy, :id => 2
77 69 end
78 70 assert_redirected_to '/projects/ecookbook/settings/members'
79 71 assert !User.find(3).member_of?(Project.find(1))
80 72 end
81 73
82 def test_autocomplete_for_member_login
83 get :autocomplete_for_member_login, :id => 1, :user => 'mis'
74 def test_autocomplete_for_member
75 get :autocomplete_for_member, :id => 1, :q => 'mis'
84 76 assert_response :success
85 assert_template 'autocomplete_for_member_login'
77 assert_template 'autocomplete_for_member'
86 78
87 assert_tag :ul, :child => {:tag => 'li', :content => /miscuser8/}
79 assert_tag :label, :content => /User Misc/,
80 :child => { :tag => 'input', :attributes => { :name => 'member[user_ids][]', :value => '8' } }
88 81 end
89 82 end
@@ -1,42 +1,43
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "#{File.dirname(__FILE__)}/../test_helper"
19 19
20 20 class AdminTest < ActionController::IntegrationTest
21 fixtures :users
21 fixtures :all
22 22
23 23 def test_add_user
24 24 log_user("admin", "admin")
25 25 get "/users/add"
26 26 assert_response :success
27 27 assert_template "users/add"
28 28 post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09"
29 assert_redirected_to "/users"
30 29
31 30 user = User.find_by_login("psmith")
32 31 assert_kind_of User, user
32 assert_redirected_to "/users/#{ user.id }/edit"
33
33 34 logged_user = User.try_to_login("psmith", "psmith09")
34 35 assert_kind_of User, logged_user
35 36 assert_equal "Paul", logged_user.firstname
36 37
37 38 post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED }
38 assert_redirected_to "/users"
39 assert_redirected_to "/users/#{ user.id }/edit"
39 40 locked_user = User.try_to_login("psmith", "psmith09")
40 41 assert_equal nil, locked_user
41 42 end
42 43 end
@@ -1,343 +1,355
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < Test::Unit::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :issues, :issue_statuses, :journals, :journal_details,
23 23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 24 :queries
25 25
26 26 def setup
27 27 @ecookbook = Project.find(1)
28 28 @ecookbook_sub1 = Project.find(3)
29 29 end
30 30
31 31 def test_truth
32 32 assert_kind_of Project, @ecookbook
33 33 assert_equal "eCookbook", @ecookbook.name
34 34 end
35 35
36 36 def test_update
37 37 assert_equal "eCookbook", @ecookbook.name
38 38 @ecookbook.name = "eCook"
39 39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
40 40 @ecookbook.reload
41 41 assert_equal "eCook", @ecookbook.name
42 42 end
43 43
44 44 def test_validate
45 45 @ecookbook.name = ""
46 46 assert !@ecookbook.save
47 47 assert_equal 1, @ecookbook.errors.count
48 48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
49 49 end
50 50
51 51 def test_validate_identifier
52 52 to_test = {"abc" => true,
53 53 "ab12" => true,
54 54 "ab-12" => true,
55 55 "12" => false,
56 56 "new" => false}
57 57
58 58 to_test.each do |identifier, valid|
59 59 p = Project.new
60 60 p.identifier = identifier
61 61 p.valid?
62 62 assert_equal valid, p.errors.on('identifier').nil?
63 63 end
64 64 end
65 65
66 def test_members_should_be_active_users
67 Project.all.each do |project|
68 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
69 end
70 end
71
72 def test_users_should_be_active_users
73 Project.all.each do |project|
74 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
75 end
76 end
77
66 78 def test_archive
67 79 user = @ecookbook.members.first.user
68 80 @ecookbook.archive
69 81 @ecookbook.reload
70 82
71 83 assert !@ecookbook.active?
72 84 assert !user.projects.include?(@ecookbook)
73 85 # Subproject are also archived
74 86 assert !@ecookbook.children.empty?
75 87 assert @ecookbook.descendants.active.empty?
76 88 end
77 89
78 90 def test_unarchive
79 91 user = @ecookbook.members.first.user
80 92 @ecookbook.archive
81 93 # A subproject of an archived project can not be unarchived
82 94 assert !@ecookbook_sub1.unarchive
83 95
84 96 # Unarchive project
85 97 assert @ecookbook.unarchive
86 98 @ecookbook.reload
87 99 assert @ecookbook.active?
88 100 assert user.projects.include?(@ecookbook)
89 101 # Subproject can now be unarchived
90 102 @ecookbook_sub1.reload
91 103 assert @ecookbook_sub1.unarchive
92 104 end
93 105
94 106 def test_destroy
95 107 # 2 active members
96 108 assert_equal 2, @ecookbook.members.size
97 109 # and 1 is locked
98 110 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
99 111 # some boards
100 112 assert @ecookbook.boards.any?
101 113
102 114 @ecookbook.destroy
103 115 # make sure that the project non longer exists
104 116 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
105 117 # make sure related data was removed
106 118 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
107 119 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
108 120 end
109 121
110 122 def test_move_an_orphan_project_to_a_root_project
111 123 sub = Project.find(2)
112 124 sub.set_parent! @ecookbook
113 125 assert_equal @ecookbook.id, sub.parent.id
114 126 @ecookbook.reload
115 127 assert_equal 4, @ecookbook.children.size
116 128 end
117 129
118 130 def test_move_an_orphan_project_to_a_subproject
119 131 sub = Project.find(2)
120 132 assert sub.set_parent!(@ecookbook_sub1)
121 133 end
122 134
123 135 def test_move_a_root_project_to_a_project
124 136 sub = @ecookbook
125 137 assert sub.set_parent!(Project.find(2))
126 138 end
127 139
128 140 def test_should_not_move_a_project_to_its_children
129 141 sub = @ecookbook
130 142 assert !(sub.set_parent!(Project.find(3)))
131 143 end
132 144
133 145 def test_set_parent_should_add_roots_in_alphabetical_order
134 146 ProjectCustomField.delete_all
135 147 Project.delete_all
136 148 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
137 149 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
138 150 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
139 151 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
140 152
141 153 assert_equal 4, Project.count
142 154 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
143 155 end
144 156
145 157 def test_set_parent_should_add_children_in_alphabetical_order
146 158 ProjectCustomField.delete_all
147 159 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
148 160 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
149 161 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
150 162 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
151 163 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
152 164
153 165 parent.reload
154 166 assert_equal 4, parent.children.size
155 167 assert_equal parent.children.sort_by(&:name), parent.children
156 168 end
157 169
158 170 def test_rebuild_should_sort_children_alphabetically
159 171 ProjectCustomField.delete_all
160 172 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
161 173 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
162 174 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
163 175 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
164 176 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
165 177
166 178 Project.update_all("lft = NULL, rgt = NULL")
167 179 Project.rebuild!
168 180
169 181 parent.reload
170 182 assert_equal 4, parent.children.size
171 183 assert_equal parent.children.sort_by(&:name), parent.children
172 184 end
173 185
174 186 def test_parent
175 187 p = Project.find(6).parent
176 188 assert p.is_a?(Project)
177 189 assert_equal 5, p.id
178 190 end
179 191
180 192 def test_ancestors
181 193 a = Project.find(6).ancestors
182 194 assert a.first.is_a?(Project)
183 195 assert_equal [1, 5], a.collect(&:id)
184 196 end
185 197
186 198 def test_root
187 199 r = Project.find(6).root
188 200 assert r.is_a?(Project)
189 201 assert_equal 1, r.id
190 202 end
191 203
192 204 def test_children
193 205 c = Project.find(1).children
194 206 assert c.first.is_a?(Project)
195 207 assert_equal [5, 3, 4], c.collect(&:id)
196 208 end
197 209
198 210 def test_descendants
199 211 d = Project.find(1).descendants
200 212 assert d.first.is_a?(Project)
201 213 assert_equal [5, 6, 3, 4], d.collect(&:id)
202 214 end
203 215
204 216 def test_users_by_role
205 217 users_by_role = Project.find(1).users_by_role
206 218 assert_kind_of Hash, users_by_role
207 219 role = Role.find(1)
208 220 assert_kind_of Array, users_by_role[role]
209 221 assert users_by_role[role].include?(User.find(2))
210 222 end
211 223
212 224 def test_rolled_up_trackers
213 225 parent = Project.find(1)
214 226 parent.trackers = Tracker.find([1,2])
215 227 child = parent.children.find(3)
216 228
217 229 assert_equal [1, 2], parent.tracker_ids
218 230 assert_equal [2, 3], child.trackers.collect(&:id)
219 231
220 232 assert_kind_of Tracker, parent.rolled_up_trackers.first
221 233 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
222 234
223 235 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
224 236 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
225 237 end
226 238
227 239 def test_rolled_up_trackers_should_ignore_archived_subprojects
228 240 parent = Project.find(1)
229 241 parent.trackers = Tracker.find([1,2])
230 242 child = parent.children.find(3)
231 243 child.trackers = Tracker.find([1,3])
232 244 parent.children.each(&:archive)
233 245
234 246 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
235 247 end
236 248
237 249 def test_next_identifier
238 250 ProjectCustomField.delete_all
239 251 Project.create!(:name => 'last', :identifier => 'p2008040')
240 252 assert_equal 'p2008041', Project.next_identifier
241 253 end
242 254
243 255 def test_next_identifier_first_project
244 256 Project.delete_all
245 257 assert_nil Project.next_identifier
246 258 end
247 259
248 260
249 261 def test_enabled_module_names_should_not_recreate_enabled_modules
250 262 project = Project.find(1)
251 263 # Remove one module
252 264 modules = project.enabled_modules.slice(0..-2)
253 265 assert modules.any?
254 266 assert_difference 'EnabledModule.count', -1 do
255 267 project.enabled_module_names = modules.collect(&:name)
256 268 end
257 269 project.reload
258 270 # Ids should be preserved
259 271 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
260 272 end
261 273
262 274 def test_copy_from_existing_project
263 275 source_project = Project.find(1)
264 276 copied_project = Project.copy_from(1)
265 277
266 278 assert copied_project
267 279 # Cleared attributes
268 280 assert copied_project.id.blank?
269 281 assert copied_project.name.blank?
270 282 assert copied_project.identifier.blank?
271 283
272 284 # Duplicated attributes
273 285 assert_equal source_project.description, copied_project.description
274 286 assert_equal source_project.enabled_modules, copied_project.enabled_modules
275 287 assert_equal source_project.trackers, copied_project.trackers
276 288
277 289 # Default attributes
278 290 assert_equal 1, copied_project.status
279 291 end
280 292
281 293 # Context: Project#copy
282 294 def test_copy_should_copy_issues
283 295 # Setup
284 296 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
285 297 source_project = Project.find(2)
286 298 Project.destroy_all :identifier => "copy-test"
287 299 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
288 300 project.trackers = source_project.trackers
289 301 assert project.valid?
290 302
291 303 assert project.issues.empty?
292 304 assert project.copy(source_project)
293 305
294 306 # Tests
295 307 assert_equal source_project.issues.size, project.issues.size
296 308 project.issues.each do |issue|
297 309 assert issue.valid?
298 310 assert ! issue.assigned_to.blank?
299 311 assert_equal project, issue.project
300 312 end
301 313 end
302 314
303 315 def test_copy_should_copy_members
304 316 # Setup
305 317 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
306 318 source_project = Project.find(2)
307 319 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
308 320 project.trackers = source_project.trackers
309 321 project.enabled_modules = source_project.enabled_modules
310 322 assert project.valid?
311 323
312 324 assert project.members.empty?
313 325 assert project.copy(source_project)
314 326
315 327 # Tests
316 328 assert_equal source_project.members.size, project.members.size
317 329 project.members.each do |member|
318 330 assert member
319 331 assert_equal project, member.project
320 332 end
321 333 end
322 334
323 335 def test_copy_should_copy_project_level_queries
324 336 # Setup
325 337 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
326 338 source_project = Project.find(2)
327 339 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
328 340 project.trackers = source_project.trackers
329 341 project.enabled_modules = source_project.enabled_modules
330 342 assert project.valid?
331 343
332 344 assert project.queries.empty?
333 345 assert project.copy(source_project)
334 346
335 347 # Tests
336 348 assert_equal source_project.queries.size, project.queries.size
337 349 project.queries.each do |query|
338 350 assert query
339 351 assert_equal project, query.project
340 352 end
341 353 end
342 354
343 355 end
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now