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