##// END OF EJS Templates
Highlight the current item of the main menu....
Jean-Philippe Lang -
r1062:0faa4568a0ab
parent child
Show More
@@ -1,204 +1,207
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ApplicationController < ActionController::Base
19 19 before_filter :user_setup, :check_if_login_required, :set_localization
20 20 filter_parameter_logging :password
21 21
22 include Redmine::MenuManager::MenuController
23 helper Redmine::MenuManager::MenuHelper
24
22 25 REDMINE_SUPPORTED_SCM.each do |scm|
23 26 require_dependency "repository/#{scm.underscore}"
24 27 end
25 28
26 29 def current_role
27 30 @current_role ||= User.current.role_for_project(@project)
28 31 end
29 32
30 33 def user_setup
31 34 # Check the settings cache for each request
32 35 Setting.check_cache
33 36 # Find the current user
34 37 User.current = find_current_user
35 38 end
36 39
37 40 # Returns the current user or nil if no user is logged in
38 41 def find_current_user
39 42 if session[:user_id]
40 43 # existing session
41 44 (User.find_active(session[:user_id]) rescue nil)
42 45 elsif cookies[:autologin] && Setting.autologin?
43 46 # auto-login feature
44 47 User.find_by_autologin_key(cookies[:autologin])
45 48 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
46 49 # RSS key authentication
47 50 User.find_by_rss_key(params[:key])
48 51 end
49 52 end
50 53
51 54 # check if login is globally required to access the application
52 55 def check_if_login_required
53 56 # no check needed if user is already logged in
54 57 return true if User.current.logged?
55 58 require_login if Setting.login_required?
56 59 end
57 60
58 61 def set_localization
59 62 lang = begin
60 63 if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
61 64 User.current.language
62 65 elsif request.env['HTTP_ACCEPT_LANGUAGE']
63 66 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
64 67 if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
65 68 accept_lang
66 69 end
67 70 end
68 71 rescue
69 72 nil
70 73 end || Setting.default_language
71 74 set_language_if_valid(lang)
72 75 end
73 76
74 77 def require_login
75 78 if !User.current.logged?
76 79 store_location
77 80 redirect_to :controller => "account", :action => "login"
78 81 return false
79 82 end
80 83 true
81 84 end
82 85
83 86 def require_admin
84 87 return unless require_login
85 88 if !User.current.admin?
86 89 render_403
87 90 return false
88 91 end
89 92 true
90 93 end
91 94
92 95 # Authorize the user for the requested action
93 96 def authorize(ctrl = params[:controller], action = params[:action])
94 97 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
95 98 allowed ? true : (User.current.logged? ? render_403 : require_login)
96 99 end
97 100
98 101 # make sure that the user is a member of the project (or admin) if project is private
99 102 # used as a before_filter for actions that do not require any particular permission on the project
100 103 def check_project_privacy
101 104 unless @project.active?
102 105 @project = nil
103 106 render_404
104 107 return false
105 108 end
106 109 return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
107 110 User.current.logged? ? render_403 : require_login
108 111 end
109 112
110 113 # store current uri in session.
111 114 # return to this location by calling redirect_back_or_default
112 115 def store_location
113 116 session[:return_to_params] = params
114 117 end
115 118
116 119 # move to the last store_location call or to the passed default one
117 120 def redirect_back_or_default(default)
118 121 if session[:return_to_params].nil?
119 122 redirect_to default
120 123 else
121 124 redirect_to session[:return_to_params]
122 125 session[:return_to_params] = nil
123 126 end
124 127 end
125 128
126 129 def render_403
127 130 @project = nil
128 131 render :template => "common/403", :layout => !request.xhr?, :status => 403
129 132 return false
130 133 end
131 134
132 135 def render_404
133 136 render :template => "common/404", :layout => !request.xhr?, :status => 404
134 137 return false
135 138 end
136 139
137 140 def render_feed(items, options={})
138 141 @items = items || []
139 142 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
140 143 @title = options[:title] || Setting.app_title
141 144 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
142 145 end
143 146
144 147 def self.accept_key_auth(*actions)
145 148 actions = actions.flatten.map(&:to_s)
146 149 write_inheritable_attribute('accept_key_auth_actions', actions)
147 150 end
148 151
149 152 def accept_key_auth_actions
150 153 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
151 154 end
152 155
153 156 # TODO: move to model
154 157 def attach_files(obj, files)
155 158 attachments = []
156 159 if files && files.is_a?(Array)
157 160 files.each do |file|
158 161 next unless file.size > 0
159 162 a = Attachment.create(:container => obj, :file => file, :author => User.current)
160 163 attachments << a unless a.new_record?
161 164 end
162 165 end
163 166 attachments
164 167 end
165 168
166 169 # Returns the number of objects that should be displayed
167 170 # on the paginated list
168 171 def per_page_option
169 172 per_page = nil
170 173 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
171 174 per_page = params[:per_page].to_s.to_i
172 175 session[:per_page] = per_page
173 176 elsif session[:per_page]
174 177 per_page = session[:per_page]
175 178 else
176 179 per_page = Setting.per_page_options_array.first || 25
177 180 end
178 181 per_page
179 182 end
180 183
181 184 # qvalues http header parser
182 185 # code taken from webrick
183 186 def parse_qvalues(value)
184 187 tmp = []
185 188 if value
186 189 parts = value.split(/,\s*/)
187 190 parts.each {|part|
188 191 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
189 192 val = m[1]
190 193 q = (m[2] or 1).to_f
191 194 tmp.push([val, q])
192 195 end
193 196 }
194 197 tmp = tmp.sort_by{|val, q| -q}
195 198 tmp.collect!{|val, q| val}
196 199 end
197 200 return tmp
198 201 end
199 202
200 203 # Returns a string that can be used as filename value in Content-Disposition header
201 204 def filename_for_content_disposition(name)
202 205 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
203 206 end
204 207 end
@@ -1,52 +1,53
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueCategoriesController < ApplicationController
19 19 layout 'base'
20 menu_item :settings
20 21 before_filter :find_project, :authorize
21 22
22 23 verify :method => :post, :only => :destroy
23 24
24 25 def edit
25 26 if request.post? and @category.update_attributes(params[:category])
26 27 flash[:notice] = l(:notice_successful_update)
27 28 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
28 29 end
29 30 end
30 31
31 32 def destroy
32 33 @issue_count = @category.issues.size
33 34 if @issue_count == 0
34 35 # No issue assigned to this category
35 36 @category.destroy
36 37 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
37 38 elsif params[:todo]
38 39 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
39 40 @category.destroy(reassign_to)
40 41 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
41 42 end
42 43 @categories = @project.issue_categories - [@category]
43 44 end
44 45
45 46 private
46 47 def find_project
47 48 @category = IssueCategory.find(params[:id])
48 49 @project = @category.project
49 50 rescue ActiveRecord::RecordNotFound
50 51 render_404
51 52 end
52 53 end
@@ -1,98 +1,99
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MessagesController < ApplicationController
19 19 layout 'base'
20 menu_item :boards
20 21 before_filter :find_board, :only => :new
21 22 before_filter :find_message, :except => :new
22 23 before_filter :authorize
23 24
24 25 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
25 26
26 27 helper :attachments
27 28 include AttachmentsHelper
28 29
29 30 # Show a topic and its replies
30 31 def show
31 32 @reply = Message.new(:subject => "RE: #{@message.subject}")
32 33 render :action => "show", :layout => false if request.xhr?
33 34 end
34 35
35 36 # Create a new topic
36 37 def new
37 38 @message = Message.new(params[:message])
38 39 @message.author = User.current
39 40 @message.board = @board
40 41 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
41 42 @message.locked = params[:message]['locked']
42 43 @message.sticky = params[:message]['sticky']
43 44 end
44 45 if request.post? && @message.save
45 46 attach_files(@message, params[:attachments])
46 47 redirect_to :action => 'show', :id => @message
47 48 end
48 49 end
49 50
50 51 # Reply to a topic
51 52 def reply
52 53 @reply = Message.new(params[:reply])
53 54 @reply.author = User.current
54 55 @reply.board = @board
55 56 @topic.children << @reply
56 57 if !@reply.new_record?
57 58 attach_files(@reply, params[:attachments])
58 59 end
59 60 redirect_to :action => 'show', :id => @topic
60 61 end
61 62
62 63 # Edit a message
63 64 def edit
64 65 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
65 66 @message.locked = params[:message]['locked']
66 67 @message.sticky = params[:message]['sticky']
67 68 end
68 69 if request.post? && @message.update_attributes(params[:message])
69 70 attach_files(@message, params[:attachments])
70 71 flash[:notice] = l(:notice_successful_update)
71 72 redirect_to :action => 'show', :id => @topic
72 73 end
73 74 end
74 75
75 76 # Delete a messages
76 77 def destroy
77 78 @message.destroy
78 79 redirect_to @message.parent.nil? ?
79 80 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
80 81 { :action => 'show', :id => @message.parent }
81 82 end
82 83
83 84 private
84 85 def find_message
85 86 find_board
86 87 @message = @board.messages.find(params[:id], :include => :parent)
87 88 @topic = @message.root
88 89 rescue ActiveRecord::RecordNotFound
89 90 render_404
90 91 end
91 92
92 93 def find_board
93 94 @board = Board.find(params[:board_id], :include => :project)
94 95 @project = @board.project
95 96 rescue ActiveRecord::RecordNotFound
96 97 render_404
97 98 end
98 99 end
@@ -1,518 +1,525
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 layout 'base'
20 menu_item :overview
21 menu_item :activity, :only => :activity
22 menu_item :roadmap, :only => :roadmap
23 menu_item :files, :only => [:list_files, :add_file]
24 menu_item :settings, :only => :settings
25 menu_item :issues, :only => [:add_issue, :bulk_edit_issues, :changelog, :move_issues]
26
20 27 before_filter :find_project, :except => [ :index, :list, :add ]
21 28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
22 29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
23 30 accept_key_auth :activity, :calendar
24 31
25 32 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
26 33 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
27 34 cache_sweeper :version_sweeper, :only => [ :add_version ]
28 35
29 36 helper :sort
30 37 include SortHelper
31 38 helper :custom_fields
32 39 include CustomFieldsHelper
33 40 helper :ifpdf
34 41 include IfpdfHelper
35 42 helper :issues
36 43 helper IssuesHelper
37 44 helper :queries
38 45 include QueriesHelper
39 46 helper :repositories
40 47 include RepositoriesHelper
41 48 include ProjectsHelper
42 49
43 50 def index
44 51 list
45 52 render :action => 'list' unless request.xhr?
46 53 end
47 54
48 55 # Lists visible projects
49 56 def list
50 57 projects = Project.find :all,
51 58 :conditions => Project.visible_by(User.current),
52 59 :include => :parent
53 60 @project_tree = projects.group_by {|p| p.parent || p}
54 61 @project_tree.each_key {|p| @project_tree[p] -= [p]}
55 62 end
56 63
57 64 # Add a new project
58 65 def add
59 66 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
60 67 @trackers = Tracker.all
61 68 @root_projects = Project.find(:all,
62 69 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
63 70 :order => 'name')
64 71 @project = Project.new(params[:project])
65 72 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
66 73 if request.get?
67 74 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
68 75 @project.trackers = Tracker.all
69 76 else
70 77 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
71 78 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
72 79 @project.custom_values = @custom_values
73 80 if @project.save
74 81 @project.enabled_module_names = params[:enabled_modules]
75 82 flash[:notice] = l(:notice_successful_create)
76 83 redirect_to :controller => 'admin', :action => 'projects'
77 84 end
78 85 end
79 86 end
80 87
81 88 # Show @project
82 89 def show
83 90 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
84 91 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
85 92 @subprojects = @project.active_children
86 93 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
87 94 @trackers = @project.trackers
88 95 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
89 96 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
90 97 @total_hours = @project.time_entries.sum(:hours)
91 98 @key = User.current.rss_key
92 99 end
93 100
94 101 def settings
95 102 @root_projects = Project.find(:all,
96 103 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
97 104 :order => 'name')
98 105 @custom_fields = IssueCustomField.find(:all)
99 106 @issue_category ||= IssueCategory.new
100 107 @member ||= @project.members.new
101 108 @trackers = Tracker.all
102 109 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
103 110 @repository ||= @project.repository
104 111 @wiki ||= @project.wiki
105 112 end
106 113
107 114 # Edit @project
108 115 def edit
109 116 if request.post?
110 117 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
111 118 if params[:custom_fields]
112 119 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
113 120 @project.custom_values = @custom_values
114 121 end
115 122 @project.attributes = params[:project]
116 123 if @project.save
117 124 flash[:notice] = l(:notice_successful_update)
118 125 redirect_to :action => 'settings', :id => @project
119 126 else
120 127 settings
121 128 render :action => 'settings'
122 129 end
123 130 end
124 131 end
125 132
126 133 def modules
127 134 @project.enabled_module_names = params[:enabled_modules]
128 135 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
129 136 end
130 137
131 138 def archive
132 139 @project.archive if request.post? && @project.active?
133 140 redirect_to :controller => 'admin', :action => 'projects'
134 141 end
135 142
136 143 def unarchive
137 144 @project.unarchive if request.post? && !@project.active?
138 145 redirect_to :controller => 'admin', :action => 'projects'
139 146 end
140 147
141 148 # Delete @project
142 149 def destroy
143 150 @project_to_destroy = @project
144 151 if request.post? and params[:confirm]
145 152 @project_to_destroy.destroy
146 153 redirect_to :controller => 'admin', :action => 'projects'
147 154 end
148 155 # hide project in layout
149 156 @project = nil
150 157 end
151 158
152 159 # Add a new issue category to @project
153 160 def add_issue_category
154 161 @category = @project.issue_categories.build(params[:category])
155 162 if request.post? and @category.save
156 163 respond_to do |format|
157 164 format.html do
158 165 flash[:notice] = l(:notice_successful_create)
159 166 redirect_to :action => 'settings', :tab => 'categories', :id => @project
160 167 end
161 168 format.js do
162 169 # IE doesn't support the replace_html rjs method for select box options
163 170 render(:update) {|page| page.replace "issue_category_id",
164 171 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
165 172 }
166 173 end
167 174 end
168 175 end
169 176 end
170 177
171 178 # Add a new version to @project
172 179 def add_version
173 180 @version = @project.versions.build(params[:version])
174 181 if request.post? and @version.save
175 182 flash[:notice] = l(:notice_successful_create)
176 183 redirect_to :action => 'settings', :tab => 'versions', :id => @project
177 184 end
178 185 end
179 186
180 187 # Add a new issue to @project
181 188 # The new issue will be created from an existing one if copy_from parameter is given
182 189 def add_issue
183 190 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
184 191 @issue.project = @project
185 192 @issue.author = User.current
186 193 @issue.tracker ||= @project.trackers.find(params[:tracker_id])
187 194
188 195 default_status = IssueStatus.default
189 196 unless default_status
190 197 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
191 198 render :nothing => true, :layout => true
192 199 return
193 200 end
194 201 @issue.status = default_status
195 202 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker))
196 203
197 204 if request.get?
198 205 @issue.start_date ||= Date.today
199 206 @custom_values = @issue.custom_values.empty? ?
200 207 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
201 208 @issue.custom_values
202 209 else
203 210 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
204 211 # Check that the user is allowed to apply the requested status
205 212 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
206 213 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
207 214 @issue.custom_values = @custom_values
208 215 if @issue.save
209 216 attach_files(@issue, params[:attachments])
210 217 flash[:notice] = l(:notice_successful_create)
211 218 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
212 219 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
213 220 return
214 221 end
215 222 end
216 223 @priorities = Enumeration::get_values('IPRI')
217 224 end
218 225
219 226 # Bulk edit issues
220 227 def bulk_edit_issues
221 228 if request.post?
222 229 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
223 230 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
224 231 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
225 232 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
226 233 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
227 234 issues = @project.issues.find_all_by_id(params[:issue_ids])
228 235 unsaved_issue_ids = []
229 236 issues.each do |issue|
230 237 journal = issue.init_journal(User.current, params[:notes])
231 238 issue.priority = priority if priority
232 239 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
233 240 issue.category = category if category
234 241 issue.fixed_version = fixed_version if fixed_version
235 242 issue.start_date = params[:start_date] unless params[:start_date].blank?
236 243 issue.due_date = params[:due_date] unless params[:due_date].blank?
237 244 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
238 245 # Don't save any change to the issue if the user is not authorized to apply the requested status
239 246 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
240 247 # Send notification for each issue (if changed)
241 248 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
242 249 else
243 250 # Keep unsaved issue ids to display them in flash error
244 251 unsaved_issue_ids << issue.id
245 252 end
246 253 end
247 254 if unsaved_issue_ids.empty?
248 255 flash[:notice] = l(:notice_successful_update) unless issues.empty?
249 256 else
250 257 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
251 258 end
252 259 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
253 260 return
254 261 end
255 262 # Find potential statuses the user could be allowed to switch issues to
256 263 @available_statuses = Workflow.find(:all, :include => :new_status,
257 264 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
258 265 render :update do |page|
259 266 page.hide 'query_form'
260 267 page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
261 268 end
262 269 end
263 270
264 271 def move_issues
265 272 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
266 273 redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
267 274
268 275 @projects = []
269 276 # find projects to which the user is allowed to move the issue
270 277 if User.current.admin?
271 278 # admin is allowed to move issues to any active (visible) project
272 279 @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
273 280 else
274 281 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
275 282 end
276 283 @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
277 284 @target_project ||= @project
278 285 @trackers = @target_project.trackers
279 286 if request.post?
280 287 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
281 288 unsaved_issue_ids = []
282 289 @issues.each do |issue|
283 290 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
284 291 end
285 292 if unsaved_issue_ids.empty?
286 293 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
287 294 else
288 295 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
289 296 end
290 297 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
291 298 return
292 299 end
293 300 render :layout => false if request.xhr?
294 301 end
295 302
296 303 # Add a news to @project
297 304 def add_news
298 305 @news = News.new(:project => @project, :author => User.current)
299 306 if request.post?
300 307 @news.attributes = params[:news]
301 308 if @news.save
302 309 flash[:notice] = l(:notice_successful_create)
303 310 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
304 311 redirect_to :controller => 'news', :action => 'index', :project_id => @project
305 312 end
306 313 end
307 314 end
308 315
309 316 def add_file
310 317 if request.post?
311 318 @version = @project.versions.find_by_id(params[:version_id])
312 319 attachments = attach_files(@version, params[:attachments])
313 320 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
314 321 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
315 322 end
316 323 @versions = @project.versions.sort
317 324 end
318 325
319 326 def list_files
320 327 @versions = @project.versions.sort
321 328 end
322 329
323 330 # Show changelog for @project
324 331 def changelog
325 332 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
326 333 retrieve_selected_tracker_ids(@trackers)
327 334 @versions = @project.versions.sort
328 335 end
329 336
330 337 def roadmap
331 338 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
332 339 retrieve_selected_tracker_ids(@trackers)
333 340 @versions = @project.versions.sort
334 341 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
335 342 end
336 343
337 344 def activity
338 345 if params[:year] and params[:year].to_i > 1900
339 346 @year = params[:year].to_i
340 347 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
341 348 @month = params[:month].to_i
342 349 end
343 350 end
344 351 @year ||= Date.today.year
345 352 @month ||= Date.today.month
346 353
347 354 case params[:format]
348 355 when 'atom'
349 356 # 30 last days
350 357 @date_from = Date.today - 30
351 358 @date_to = Date.today + 1
352 359 else
353 360 # current month
354 361 @date_from = Date.civil(@year, @month, 1)
355 362 @date_to = @date_from >> 1
356 363 end
357 364
358 365 @event_types = %w(issues news files documents changesets wiki_pages messages)
359 366 @event_types.delete('wiki_pages') unless @project.wiki
360 367 @event_types.delete('changesets') unless @project.repository
361 368 @event_types.delete('messages') unless @project.boards.any?
362 369 # only show what the user is allowed to view
363 370 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
364 371
365 372 @scope = @event_types.select {|t| params["show_#{t}"]}
366 373 # default events if none is specified in parameters
367 374 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
368 375
369 376 @events = []
370 377
371 378 if @scope.include?('issues')
372 379 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
373 380 @events += @project.issues_status_changes(@date_from, @date_to)
374 381 end
375 382
376 383 if @scope.include?('news')
377 384 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
378 385 end
379 386
380 387 if @scope.include?('files')
381 388 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
382 389 end
383 390
384 391 if @scope.include?('documents')
385 392 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
386 393 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
387 394 end
388 395
389 396 if @scope.include?('wiki_pages')
390 397 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
391 398 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
392 399 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
393 400 "#{WikiContent.versioned_table_name}.id"
394 401 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
395 402 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
396 403 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
397 404 @project.id, @date_from, @date_to]
398 405
399 406 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
400 407 end
401 408
402 409 if @scope.include?('changesets')
403 410 @events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
404 411 end
405 412
406 413 if @scope.include?('messages')
407 414 @events += Message.find(:all,
408 415 :include => [:board, :author],
409 416 :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
410 417 end
411 418
412 419 @events_by_day = @events.group_by(&:event_date)
413 420
414 421 respond_to do |format|
415 422 format.html { render :layout => false if request.xhr? }
416 423 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
417 424 end
418 425 end
419 426
420 427 def calendar
421 428 @trackers = @project.rolled_up_trackers
422 429 retrieve_selected_tracker_ids(@trackers)
423 430
424 431 if params[:year] and params[:year].to_i > 1900
425 432 @year = params[:year].to_i
426 433 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
427 434 @month = params[:month].to_i
428 435 end
429 436 end
430 437 @year ||= Date.today.year
431 438 @month ||= Date.today.month
432 439 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
433 440
434 441 events = []
435 442 @project.issues_with_subprojects(params[:with_subprojects]) do
436 443 events += Issue.find(:all,
437 444 :include => [:tracker, :status, :assigned_to, :priority, :project],
438 445 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
439 446 ) unless @selected_tracker_ids.empty?
440 447 end
441 448 events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
442 449 @calendar.events = events
443 450
444 451 render :layout => false if request.xhr?
445 452 end
446 453
447 454 def gantt
448 455 @trackers = @project.rolled_up_trackers
449 456 retrieve_selected_tracker_ids(@trackers)
450 457
451 458 if params[:year] and params[:year].to_i >0
452 459 @year_from = params[:year].to_i
453 460 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
454 461 @month_from = params[:month].to_i
455 462 else
456 463 @month_from = 1
457 464 end
458 465 else
459 466 @month_from ||= Date.today.month
460 467 @year_from ||= Date.today.year
461 468 end
462 469
463 470 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
464 471 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
465 472 months = (params[:months] || User.current.pref[:gantt_months]).to_i
466 473 @months = (months > 0 && months < 25) ? months : 6
467 474
468 475 # Save gantt paramters as user preference (zoom and months count)
469 476 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
470 477 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
471 478 User.current.preference.save
472 479 end
473 480
474 481 @date_from = Date.civil(@year_from, @month_from, 1)
475 482 @date_to = (@date_from >> @months) - 1
476 483
477 484 @events = []
478 485 @project.issues_with_subprojects(params[:with_subprojects]) do
479 486 @events += Issue.find(:all,
480 487 :order => "start_date, due_date",
481 488 :include => [:tracker, :status, :assigned_to, :priority, :project],
482 489 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
483 490 ) unless @selected_tracker_ids.empty?
484 491 end
485 492 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
486 493 @events.sort! {|x,y| x.start_date <=> y.start_date }
487 494
488 495 if params[:format]=='pdf'
489 496 @options_for_rfpdf ||= {}
490 497 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
491 498 render :template => "projects/gantt.rfpdf", :layout => false
492 499 elsif params[:format]=='png' && respond_to?('gantt_image')
493 500 image = gantt_image(@events, @date_from, @months, @zoom)
494 501 image.format = 'PNG'
495 502 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
496 503 else
497 504 render :template => "projects/gantt.rhtml"
498 505 end
499 506 end
500 507
501 508 private
502 509 # Find project of id params[:id]
503 510 # if not found, redirect to project list
504 511 # Used as a before_filter
505 512 def find_project
506 513 @project = Project.find(params[:id])
507 514 rescue ActiveRecord::RecordNotFound
508 515 render_404
509 516 end
510 517
511 518 def retrieve_selected_tracker_ids(selectable_trackers)
512 519 if ids = params[:tracker_ids]
513 520 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
514 521 else
515 522 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
516 523 end
517 524 end
518 525 end
@@ -1,81 +1,82
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 layout 'base'
20 menu_item :issues
20 21 before_filter :find_project, :authorize
21 22
22 23 def index
23 24 @queries = @project.queries.find(:all,
24 25 :order => "name ASC",
25 26 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
26 27 end
27 28
28 29 def new
29 30 @query = Query.new(params[:query])
30 31 @query.project = @project
31 32 @query.user = User.current
32 33 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
33 34 @query.column_names = nil if params[:default_columns]
34 35
35 36 params[:fields].each do |field|
36 37 @query.add_filter(field, params[:operators][field], params[:values][field])
37 38 end if params[:fields]
38 39
39 40 if request.post? && params[:confirm] && @query.save
40 41 flash[:notice] = l(:notice_successful_create)
41 42 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
42 43 return
43 44 end
44 45 render :layout => false if request.xhr?
45 46 end
46 47
47 48 def edit
48 49 if request.post?
49 50 @query.filters = {}
50 51 params[:fields].each do |field|
51 52 @query.add_filter(field, params[:operators][field], params[:values][field])
52 53 end if params[:fields]
53 54 @query.attributes = params[:query]
54 55 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
55 56 @query.column_names = nil if params[:default_columns]
56 57
57 58 if @query.save
58 59 flash[:notice] = l(:notice_successful_update)
59 60 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
60 61 end
61 62 end
62 63 end
63 64
64 65 def destroy
65 66 @query.destroy if request.post?
66 67 redirect_to :controller => 'queries', :project_id => @project
67 68 end
68 69
69 70 private
70 71 def find_project
71 72 if params[:id]
72 73 @query = Query.find(params[:id])
73 74 @project = @query.project
74 75 render_403 unless @query.editable_by?(User.current)
75 76 else
76 77 @project = Project.find(params[:project_id])
77 78 end
78 79 rescue ActiveRecord::RecordNotFound
79 80 render_404
80 81 end
81 82 end
@@ -1,236 +1,237
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ReportsController < ApplicationController
19 19 layout 'base'
20 menu_item :issues
20 21 before_filter :find_project, :authorize
21 22
22 23 def issue_report
23 24 @statuses = IssueStatus.find(:all, :order => 'position')
24 25
25 26 case params[:detail]
26 27 when "tracker"
27 28 @field = "tracker_id"
28 29 @rows = @project.trackers
29 30 @data = issues_by_tracker
30 31 @report_title = l(:field_tracker)
31 32 render :template => "reports/issue_report_details"
32 33 when "version"
33 34 @field = "fixed_version_id"
34 35 @rows = @project.versions.sort
35 36 @data = issues_by_version
36 37 @report_title = l(:field_version)
37 38 render :template => "reports/issue_report_details"
38 39 when "priority"
39 40 @field = "priority_id"
40 41 @rows = Enumeration::get_values('IPRI')
41 42 @data = issues_by_priority
42 43 @report_title = l(:field_priority)
43 44 render :template => "reports/issue_report_details"
44 45 when "category"
45 46 @field = "category_id"
46 47 @rows = @project.issue_categories
47 48 @data = issues_by_category
48 49 @report_title = l(:field_category)
49 50 render :template => "reports/issue_report_details"
50 51 when "assigned_to"
51 52 @field = "assigned_to_id"
52 53 @rows = @project.members.collect { |m| m.user }
53 54 @data = issues_by_assigned_to
54 55 @report_title = l(:field_assigned_to)
55 56 render :template => "reports/issue_report_details"
56 57 when "author"
57 58 @field = "author_id"
58 59 @rows = @project.members.collect { |m| m.user }
59 60 @data = issues_by_author
60 61 @report_title = l(:field_author)
61 62 render :template => "reports/issue_report_details"
62 63 when "subproject"
63 64 @field = "project_id"
64 65 @rows = @project.active_children
65 66 @data = issues_by_subproject
66 67 @report_title = l(:field_subproject)
67 68 render :template => "reports/issue_report_details"
68 69 else
69 70 @trackers = @project.trackers
70 71 @versions = @project.versions.sort
71 72 @priorities = Enumeration::get_values('IPRI')
72 73 @categories = @project.issue_categories
73 74 @assignees = @project.members.collect { |m| m.user }
74 75 @authors = @project.members.collect { |m| m.user }
75 76 @subprojects = @project.active_children
76 77 issues_by_tracker
77 78 issues_by_version
78 79 issues_by_priority
79 80 issues_by_category
80 81 issues_by_assigned_to
81 82 issues_by_author
82 83 issues_by_subproject
83 84
84 85 render :template => "reports/issue_report"
85 86 end
86 87 end
87 88
88 89 def delays
89 90 @trackers = Tracker.find(:all)
90 91 if request.get?
91 92 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
92 93 else
93 94 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
94 95 end
95 96 @selected_tracker_ids ||= []
96 97 @raw =
97 98 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
98 99 FROM issue_histories a, issue_histories b, issues i
99 100 WHERE a.status_id =5
100 101 AND a.issue_id = b.issue_id
101 102 AND a.issue_id = i.id
102 103 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
103 104 AND b.id = (
104 105 SELECT min( c.id )
105 106 FROM issue_histories c
106 107 WHERE b.issue_id = c.issue_id )
107 108 GROUP BY delay") unless @selected_tracker_ids.empty?
108 109 @raw ||=[]
109 110
110 111 @x_from = 0
111 112 @x_to = 0
112 113 @y_from = 0
113 114 @y_to = 0
114 115 @sum_total = 0
115 116 @sum_delay = 0
116 117 @raw.each do |r|
117 118 @x_to = [r['delay'].to_i, @x_to].max
118 119 @y_to = [r['total'].to_i, @y_to].max
119 120 @sum_total = @sum_total + r['total'].to_i
120 121 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
121 122 end
122 123 end
123 124
124 125 private
125 126 # Find project of id params[:id]
126 127 def find_project
127 128 @project = Project.find(params[:id])
128 129 rescue ActiveRecord::RecordNotFound
129 130 render_404
130 131 end
131 132
132 133 def issues_by_tracker
133 134 @issues_by_tracker ||=
134 135 ActiveRecord::Base.connection.select_all("select s.id as status_id,
135 136 s.is_closed as closed,
136 137 t.id as tracker_id,
137 138 count(i.id) as total
138 139 from
139 140 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
140 141 where
141 142 i.status_id=s.id
142 143 and i.tracker_id=t.id
143 144 and i.project_id=#{@project.id}
144 145 group by s.id, s.is_closed, t.id")
145 146 end
146 147
147 148 def issues_by_version
148 149 @issues_by_version ||=
149 150 ActiveRecord::Base.connection.select_all("select s.id as status_id,
150 151 s.is_closed as closed,
151 152 v.id as fixed_version_id,
152 153 count(i.id) as total
153 154 from
154 155 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
155 156 where
156 157 i.status_id=s.id
157 158 and i.fixed_version_id=v.id
158 159 and i.project_id=#{@project.id}
159 160 group by s.id, s.is_closed, v.id")
160 161 end
161 162
162 163 def issues_by_priority
163 164 @issues_by_priority ||=
164 165 ActiveRecord::Base.connection.select_all("select s.id as status_id,
165 166 s.is_closed as closed,
166 167 p.id as priority_id,
167 168 count(i.id) as total
168 169 from
169 170 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
170 171 where
171 172 i.status_id=s.id
172 173 and i.priority_id=p.id
173 174 and i.project_id=#{@project.id}
174 175 group by s.id, s.is_closed, p.id")
175 176 end
176 177
177 178 def issues_by_category
178 179 @issues_by_category ||=
179 180 ActiveRecord::Base.connection.select_all("select s.id as status_id,
180 181 s.is_closed as closed,
181 182 c.id as category_id,
182 183 count(i.id) as total
183 184 from
184 185 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
185 186 where
186 187 i.status_id=s.id
187 188 and i.category_id=c.id
188 189 and i.project_id=#{@project.id}
189 190 group by s.id, s.is_closed, c.id")
190 191 end
191 192
192 193 def issues_by_assigned_to
193 194 @issues_by_assigned_to ||=
194 195 ActiveRecord::Base.connection.select_all("select s.id as status_id,
195 196 s.is_closed as closed,
196 197 a.id as assigned_to_id,
197 198 count(i.id) as total
198 199 from
199 200 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
200 201 where
201 202 i.status_id=s.id
202 203 and i.assigned_to_id=a.id
203 204 and i.project_id=#{@project.id}
204 205 group by s.id, s.is_closed, a.id")
205 206 end
206 207
207 208 def issues_by_author
208 209 @issues_by_author ||=
209 210 ActiveRecord::Base.connection.select_all("select s.id as status_id,
210 211 s.is_closed as closed,
211 212 a.id as author_id,
212 213 count(i.id) as total
213 214 from
214 215 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
215 216 where
216 217 i.status_id=s.id
217 218 and i.author_id=a.id
218 219 and i.project_id=#{@project.id}
219 220 group by s.id, s.is_closed, a.id")
220 221 end
221 222
222 223 def issues_by_subproject
223 224 @issues_by_subproject ||=
224 225 ActiveRecord::Base.connection.select_all("select s.id as status_id,
225 226 s.is_closed as closed,
226 227 i.project_id as project_id,
227 228 count(i.id) as total
228 229 from
229 230 #{Issue.table_name} i, #{IssueStatus.table_name} s
230 231 where
231 232 i.status_id=s.id
232 233 and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
233 234 group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
234 235 @issues_by_subproject ||= []
235 236 end
236 237 end
@@ -1,280 +1,281
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception
23 23 end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 layout 'base'
27 menu_item :repository
27 28 before_filter :find_repository, :except => :edit
28 29 before_filter :find_project, :only => :edit
29 30 before_filter :authorize
30 31 accept_key_auth :revisions
31 32
32 33 def edit
33 34 @repository = @project.repository
34 35 if !@repository
35 36 @repository = Repository.factory(params[:repository_scm])
36 37 @repository.project = @project
37 38 end
38 39 if request.post?
39 40 @repository.attributes = params[:repository]
40 41 @repository.save
41 42 end
42 43 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
43 44 end
44 45
45 46 def destroy
46 47 @repository.destroy
47 48 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
48 49 end
49 50
50 51 def show
51 52 # check if new revisions have been committed in the repository
52 53 @repository.fetch_changesets if Setting.autofetch_changesets?
53 54 # get entries for the browse frame
54 55 @entries = @repository.entries('')
55 56 # latest changesets
56 57 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
57 58 show_error and return unless @entries || @changesets.any?
58 59 end
59 60
60 61 def browse
61 62 @entries = @repository.entries(@path, @rev)
62 63 if request.xhr?
63 64 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
64 65 else
65 66 show_error unless @entries
66 67 end
67 68 end
68 69
69 70 def changes
70 71 @entry = @repository.scm.entry(@path, @rev)
71 72 show_error and return unless @entry
72 73 @changesets = @repository.changesets_for_path(@path)
73 74 end
74 75
75 76 def revisions
76 77 @changeset_count = @repository.changesets.count
77 78 @changeset_pages = Paginator.new self, @changeset_count,
78 79 per_page_option,
79 80 params['page']
80 81 @changesets = @repository.changesets.find(:all,
81 82 :limit => @changeset_pages.items_per_page,
82 83 :offset => @changeset_pages.current.offset)
83 84
84 85 respond_to do |format|
85 86 format.html { render :layout => false if request.xhr? }
86 87 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
87 88 end
88 89 end
89 90
90 91 def entry
91 92 @content = @repository.scm.cat(@path, @rev)
92 93 show_error and return unless @content
93 94 if 'raw' == params[:format]
94 95 send_data @content, :filename => @path.split('/').last
95 96 else
96 97 # Prevent empty lines when displaying a file with Windows style eol
97 98 @content.gsub!("\r\n", "\n")
98 99 end
99 100 end
100 101
101 102 def annotate
102 103 @annotate = @repository.scm.annotate(@path, @rev)
103 104 show_error and return if @annotate.nil? || @annotate.empty?
104 105 end
105 106
106 107 def revision
107 108 @changeset = @repository.changesets.find_by_revision(@rev)
108 109 raise ChangesetNotFound unless @changeset
109 110 @changes_count = @changeset.changes.size
110 111 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
111 112 @changes = @changeset.changes.find(:all,
112 113 :limit => @changes_pages.items_per_page,
113 114 :offset => @changes_pages.current.offset)
114 115
115 116 respond_to do |format|
116 117 format.html
117 118 format.js {render :layout => false}
118 119 end
119 120 rescue ChangesetNotFound
120 121 show_error
121 122 end
122 123
123 124 def diff
124 125 @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
125 126 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
126 127 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
127 128
128 129 # Save diff type as user preference
129 130 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
130 131 User.current.pref[:diff_type] = @diff_type
131 132 User.current.preference.save
132 133 end
133 134
134 135 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
135 136 unless read_fragment(@cache_key)
136 137 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
137 138 show_error and return unless @diff
138 139 end
139 140 end
140 141
141 142 def stats
142 143 end
143 144
144 145 def graph
145 146 data = nil
146 147 case params[:graph]
147 148 when "commits_per_month"
148 149 data = graph_commits_per_month(@repository)
149 150 when "commits_per_author"
150 151 data = graph_commits_per_author(@repository)
151 152 end
152 153 if data
153 154 headers["Content-Type"] = "image/svg+xml"
154 155 send_data(data, :type => "image/svg+xml", :disposition => "inline")
155 156 else
156 157 render_404
157 158 end
158 159 end
159 160
160 161 private
161 162 def find_project
162 163 @project = Project.find(params[:id])
163 164 rescue ActiveRecord::RecordNotFound
164 165 render_404
165 166 end
166 167
167 168 def find_repository
168 169 @project = Project.find(params[:id])
169 170 @repository = @project.repository
170 171 render_404 and return false unless @repository
171 172 @path = params[:path].join('/') unless params[:path].nil?
172 173 @path ||= ''
173 174 @rev = params[:rev].to_i if params[:rev]
174 175 rescue ActiveRecord::RecordNotFound
175 176 render_404
176 177 end
177 178
178 179 def show_error
179 180 flash.now[:error] = l(:notice_scm_error)
180 181 render :nothing => true, :layout => true
181 182 end
182 183
183 184 def graph_commits_per_month(repository)
184 185 @date_to = Date.today
185 186 @date_from = @date_to << 11
186 187 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
187 188 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
188 189 commits_by_month = [0] * 12
189 190 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
190 191
191 192 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
192 193 changes_by_month = [0] * 12
193 194 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
194 195
195 196 fields = []
196 197 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
197 198 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
198 199
199 200 graph = SVG::Graph::Bar.new(
200 201 :height => 300,
201 202 :width => 500,
202 203 :fields => fields.reverse,
203 204 :stack => :side,
204 205 :scale_integers => true,
205 206 :step_x_labels => 2,
206 207 :show_data_values => false,
207 208 :graph_title => l(:label_commits_per_month),
208 209 :show_graph_title => true
209 210 )
210 211
211 212 graph.add_data(
212 213 :data => commits_by_month[0..11].reverse,
213 214 :title => l(:label_revision_plural)
214 215 )
215 216
216 217 graph.add_data(
217 218 :data => changes_by_month[0..11].reverse,
218 219 :title => l(:label_change_plural)
219 220 )
220 221
221 222 graph.burn
222 223 end
223 224
224 225 def graph_commits_per_author(repository)
225 226 commits_by_author = repository.changesets.count(:all, :group => :committer)
226 227 commits_by_author.sort! {|x, y| x.last <=> y.last}
227 228
228 229 changes_by_author = repository.changes.count(:all, :group => :committer)
229 230 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
230 231
231 232 fields = commits_by_author.collect {|r| r.first}
232 233 commits_data = commits_by_author.collect {|r| r.last}
233 234 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
234 235
235 236 fields = fields + [""]*(10 - fields.length) if fields.length<10
236 237 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
237 238 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
238 239
239 240 graph = SVG::Graph::BarHorizontal.new(
240 241 :height => 300,
241 242 :width => 500,
242 243 :fields => fields,
243 244 :stack => :side,
244 245 :scale_integers => true,
245 246 :show_data_values => false,
246 247 :rotate_y_labels => false,
247 248 :graph_title => l(:label_commits_per_author),
248 249 :show_graph_title => true
249 250 )
250 251
251 252 graph.add_data(
252 253 :data => commits_data,
253 254 :title => l(:label_revision_plural)
254 255 )
255 256
256 257 graph.add_data(
257 258 :data => changes_data,
258 259 :title => l(:label_change_plural)
259 260 )
260 261
261 262 graph.burn
262 263 end
263 264
264 265 end
265 266
266 267 class Date
267 268 def months_ago(date = Date.today)
268 269 (date.year - self.year)*12 + (date.month - self.month)
269 270 end
270 271
271 272 def weeks_ago(date = Date.today)
272 273 (date.year - self.year)*52 + (date.cweek - self.cweek)
273 274 end
274 275 end
275 276
276 277 class String
277 278 def with_leading_slash
278 279 starts_with?('/') ? self : "/#{self}"
279 280 end
280 281 end
@@ -1,172 +1,173
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :issues
20 21 before_filter :find_project, :authorize
21 22
22 23 helper :sort
23 24 include SortHelper
24 25 helper :issues
25 26
26 27 def report
27 28 @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
28 29 :values => @project.versions,
29 30 :label => :label_version},
30 31 'category' => {:sql => "#{Issue.table_name}.category_id",
31 32 :values => @project.issue_categories,
32 33 :label => :field_category},
33 34 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
34 35 :values => @project.users,
35 36 :label => :label_member},
36 37 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
37 38 :values => Tracker.find(:all),
38 39 :label => :label_tracker},
39 40 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
40 41 :values => Enumeration::get_values('ACTI'),
41 42 :label => :label_activity}
42 43 }
43 44
44 45 @criterias = params[:criterias] || []
45 46 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
46 47 @criterias.uniq!
47 48
48 49 @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
49 50
50 51 if params[:date_from]
51 52 begin; @date_from = params[:date_from].to_date; rescue; end
52 53 end
53 54 if params[:date_to]
54 55 begin; @date_to = params[:date_to].to_date; rescue; end
55 56 end
56 57 @date_from ||= Date.civil(Date.today.year, 1, 1)
57 58 @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1
58 59
59 60 unless @criterias.empty?
60 61 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
61 62 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
62 63
63 64 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
64 65 sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
65 66 sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
66 67 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
67 68 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
68 69
69 70 @hours = ActiveRecord::Base.connection.select_all(sql)
70 71
71 72 @hours.each do |row|
72 73 case @columns
73 74 when 'year'
74 75 row['year'] = row['tyear']
75 76 when 'month'
76 77 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
77 78 when 'week'
78 79 row['week'] = "#{row['tyear']}-#{row['tweek']}"
79 80 end
80 81 end
81 82 end
82 83
83 84 @periods = []
84 85 date_from = @date_from
85 86 # 100 columns max
86 87 while date_from < @date_to && @periods.length < 100
87 88 case @columns
88 89 when 'year'
89 90 @periods << "#{date_from.year}"
90 91 date_from = date_from >> 12
91 92 when 'month'
92 93 @periods << "#{date_from.year}-#{date_from.month}"
93 94 date_from = date_from >> 1
94 95 when 'week'
95 96 @periods << "#{date_from.year}-#{date_from.cweek}"
96 97 date_from = date_from + 7
97 98 end
98 99 end
99 100
100 101 render :layout => false if request.xhr?
101 102 end
102 103
103 104 def details
104 105 sort_init 'spent_on', 'desc'
105 106 sort_update
106 107
107 108 @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
108 109
109 110 @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
110 111 @owner_id = User.current.id
111 112
112 113 send_csv and return if 'csv' == params[:export]
113 114 render :action => 'details', :layout => false if request.xhr?
114 115 end
115 116
116 117 def edit
117 118 render_404 and return if @time_entry && @time_entry.user != User.current
118 119 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
119 120 @time_entry.attributes = params[:time_entry]
120 121 if request.post? and @time_entry.save
121 122 flash[:notice] = l(:notice_successful_update)
122 123 redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
123 124 return
124 125 end
125 126 @activities = Enumeration::get_values('ACTI')
126 127 end
127 128
128 129 private
129 130 def find_project
130 131 if params[:id]
131 132 @time_entry = TimeEntry.find(params[:id])
132 133 @project = @time_entry.project
133 134 elsif params[:issue_id]
134 135 @issue = Issue.find(params[:issue_id])
135 136 @project = @issue.project
136 137 elsif params[:project_id]
137 138 @project = Project.find(params[:project_id])
138 139 else
139 140 render_404
140 141 return false
141 142 end
142 143 end
143 144
144 145 def send_csv
145 146 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
146 147 export = StringIO.new
147 148 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
148 149 # csv header fields
149 150 headers = [l(:field_spent_on),
150 151 l(:field_user),
151 152 l(:field_activity),
152 153 l(:field_issue),
153 154 l(:field_hours),
154 155 l(:field_comments)
155 156 ]
156 157 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
157 158 # csv lines
158 159 @entries.each do |entry|
159 160 fields = [l_date(entry.spent_on),
160 161 entry.user.name,
161 162 entry.activity.name,
162 163 (entry.issue ? entry.issue.id : nil),
163 164 entry.hours,
164 165 entry.comments
165 166 ]
166 167 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
167 168 end
168 169 end
169 170 export.rewind
170 171 send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
171 172 end
172 173 end
@@ -1,71 +1,72
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class VersionsController < ApplicationController
19 19 layout 'base'
20 menu_item :roadmap
20 21 before_filter :find_project, :authorize
21 22
22 23 cache_sweeper :version_sweeper, :only => [ :edit, :destroy ]
23 24
24 25 def show
25 26 end
26 27
27 28 def edit
28 29 if request.post? and @version.update_attributes(params[:version])
29 30 flash[:notice] = l(:notice_successful_update)
30 31 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
31 32 end
32 33 end
33 34
34 35 def destroy
35 36 @version.destroy
36 37 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
37 38 rescue
38 39 flash[:error] = "Unable to delete version"
39 40 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
40 41 end
41 42
42 43 def download
43 44 @attachment = @version.attachments.find(params[:attachment_id])
44 45 @attachment.increment_download
45 46 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
46 47 :type => @attachment.content_type
47 48 rescue
48 49 render_404
49 50 end
50 51
51 52 def destroy_file
52 53 @version.attachments.find(params[:attachment_id]).destroy
53 54 flash[:notice] = l(:notice_successful_delete)
54 55 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
55 56 end
56 57
57 58 def status_by
58 59 respond_to do |format|
59 60 format.html { render :action => 'show' }
60 61 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
61 62 end
62 63 end
63 64
64 65 private
65 66 def find_project
66 67 @version = Version.find(params[:id])
67 68 @project = @version.project
68 69 rescue ActiveRecord::RecordNotFound
69 70 render_404
70 71 end
71 72 end
@@ -1,44 +1,45
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WikisController < ApplicationController
19 19 layout 'base'
20 menu_item :settings
20 21 before_filter :find_project, :authorize
21 22
22 23 # Create or update a project's wiki
23 24 def edit
24 25 @wiki = @project.wiki || Wiki.new(:project => @project)
25 26 @wiki.attributes = params[:wiki]
26 27 @wiki.save if request.post?
27 28 render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
28 29 end
29 30
30 31 # Delete a project's wiki
31 32 def destroy
32 33 if request.post? && params[:confirm] && @project.wiki
33 34 @project.wiki.destroy
34 35 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
35 36 end
36 37 end
37 38
38 39 private
39 40 def find_project
40 41 @project = Project.find(params[:id])
41 42 rescue ActiveRecord::RecordNotFound
42 43 render_404
43 44 end
44 45 end
@@ -1,81 +1,75
1 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 3 <head>
4 4 <title><%=h html_title %></title>
5 5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 9 <%= javascript_include_tag :defaults %>
10 10 <%= stylesheet_link_tag 'jstoolbar' %>
11 11 <!--[if IE]>
12 12 <style type="text/css">
13 13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 15 </style>
16 16 <![endif]-->
17 17
18 18 <!-- page specific tags --><%= yield :header_tags %>
19 19 </head>
20 20 <body>
21 21 <div id="wrapper">
22 22 <div id="top-menu">
23 23 <div id="account">
24 24 <% if User.current.logged? %>
25 25 <%=l(:label_logged_as)%> <%= User.current.login %> -
26 26 <%= link_to l(:label_my_account), { :controller => 'my', :action => 'account' }, :class => 'myaccount' %>
27 27 <%= link_to_signout %>
28 28 <% else %>
29 29 <%= link_to_signin %>
30 30 <%= link_to(l(:label_register), { :controller => 'account',:action => 'register' }, :class => 'register') if Setting.self_registration? %>
31 31 <% end %>
32 32 </div>
33 33 <%= link_to l(:label_home), home_url, :class => 'home' %>
34 34 <%= link_to l(:label_my_page), { :controller => 'my', :action => 'page'}, :class => 'mypage' if User.current.logged? %>
35 35 <%= link_to l(:label_project_plural), { :controller => 'projects' }, :class => 'projects' %>
36 36 <%= link_to l(:label_administration), { :controller => 'admin' }, :class => 'admin' if User.current.admin? %>
37 37 <%= link_to l(:label_help), Redmine::Info.help_url, :class => 'help' %>
38 38 </div>
39 39
40 40 <div id="header">
41 41 <div id="quick-search">
42 42 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
43 43 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
44 44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
45 45 <% end %>
46 46 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
47 47 </div>
48 48
49 49 <h1><%= h(@project ? @project.name : Setting.app_title) %></h1>
50 50
51 51 <div id="main-menu">
52 <ul>
53 <% Redmine::MenuManager.allowed_items(:project_menu, User.current, @project).each do |item| %>
54 <% unless item.condition && !item.condition.call(@project) %>
55 <li><%= link_to l(item.name), {item.param => @project}.merge(item.url) %></li>
56 <% end %>
57 <% end if @project && !@project.new_record? %>
58 </ul>
52 <%= render_main_menu(@project) %>
59 53 </div>
60 54 </div>
61 55
62 56 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 57 <div id="sidebar">
64 58 <%= yield :sidebar %>
65 59 </div>
66 60
67 61 <div id="content">
68 62 <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
69 63 <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
70 64 <%= yield %>
71 65 </div>
72 66 </div>
73 67
74 68 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
75 69
76 70 <div id="footer">
77 71 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> <%= Redmine::VERSION %> &copy; 2006-2007 Jean-Philippe Lang
78 72 </div>
79 73 </div>
80 74 </body>
81 75 </html>
@@ -1,105 +1,108
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/mime_type'
4 4 require 'redmine/themes'
5 5 require 'redmine/plugin'
6 6
7 7 begin
8 8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
9 9 rescue LoadError
10 10 # RMagick is not available
11 11 end
12 12
13 13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar )
14 14
15 15 # Permissions
16 16 Redmine::AccessControl.map do |map|
17 17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
18 18 map.permission :search_project, {:search => :index}, :public => true
19 19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
20 20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
21 21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
22 22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
23 23
24 24 map.project_module :issue_tracking do |map|
25 25 # Issue categories
26 26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
27 27 # Issues
28 28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
29 29 :issues => [:index, :changes, :show, :context_menu],
30 30 :versions => [:show, :status_by],
31 31 :queries => :index,
32 32 :reports => :issue_report}, :public => true
33 33 map.permission :add_issues, {:projects => :add_issue}
34 34 map.permission :edit_issues, {:projects => :bulk_edit_issues,
35 35 :issues => [:edit, :update, :destroy_attachment]}
36 36 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
37 37 map.permission :add_issue_notes, {:issues => :update}
38 38 map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin
39 39 map.permission :delete_issues, {:issues => :destroy}, :require => :member
40 40 # Queries
41 41 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
42 42 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
43 43 # Gantt & calendar
44 44 map.permission :view_gantt, :projects => :gantt
45 45 map.permission :view_calendar, :projects => :calendar
46 46 end
47 47
48 48 map.project_module :time_tracking do |map|
49 49 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
50 50 map.permission :view_time_entries, :timelog => [:details, :report]
51 51 end
52 52
53 53 map.project_module :news do |map|
54 54 map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member
55 55 map.permission :view_news, {:news => [:index, :show]}, :public => true
56 56 map.permission :comment_news, {:news => :add_comment}
57 57 end
58 58
59 59 map.project_module :documents do |map|
60 60 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
61 61 map.permission :view_documents, :documents => [:index, :show, :download]
62 62 end
63 63
64 64 map.project_module :files do |map|
65 65 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
66 66 map.permission :view_files, :projects => :list_files, :versions => :download
67 67 end
68 68
69 69 map.project_module :wiki do |map|
70 70 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
71 71 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
72 72 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
73 73 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
74 74 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
75 75 end
76 76
77 77 map.project_module :repository do |map|
78 78 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
79 79 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
80 80 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
81 81 end
82 82
83 83 map.project_module :boards do |map|
84 84 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
85 85 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
86 86 map.permission :add_messages, {:messages => [:new, :reply]}
87 87 map.permission :edit_messages, {:messages => :edit}, :require => :member
88 88 map.permission :delete_messages, {:messages => :destroy}, :require => :member
89 89 end
90 90 end
91 91
92 92 # Project menu configuration
93 93 Redmine::MenuManager.map :project_menu do |menu|
94 menu.push :label_overview, :controller => 'projects', :action => 'show'
95 menu.push :label_activity, :controller => 'projects', :action => 'activity'
96 menu.push :label_roadmap, :controller => 'projects', :action => 'roadmap'
97 menu.push :label_issue_plural, { :controller => 'issues', :action => 'index' }, :param => :project_id
98 menu.push :label_news_plural, { :controller => 'news', :action => 'index' }, :param => :project_id
99 menu.push :label_document_plural, { :controller => 'documents', :action => 'index' }, :param => :project_id
100 menu.push :label_wiki, { :controller => 'wiki', :action => 'index', :page => nil }, :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
101 menu.push :label_board_plural, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, :if => Proc.new { |p| p.boards.any? }
102 menu.push :label_attachment_plural, :controller => 'projects', :action => 'list_files'
103 menu.push :label_repository, { :controller => 'repositories', :action => 'show' }, :if => Proc.new { |p| p.repository && !p.repository.new_record? }
104 menu.push :label_settings, :controller => 'projects', :action => 'settings'
94 menu.push :overview, { :controller => 'projects', :action => 'show' }, :caption => :label_overview
95 menu.push :activity, { :controller => 'projects', :action => 'activity' }, :caption => :label_activity
96 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, :caption => :label_roadmap
97 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
98 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
99 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
100 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
101 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }, :caption => :label_wiki
102 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
103 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
104 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
105 menu.push :repository, { :controller => 'repositories', :action => 'show' },
106 :if => Proc.new { |p| p.repository && !p.repository.new_record? }, :caption => :label_repository
107 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :caption => :label_settings
105 108 end
@@ -1,61 +1,119
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module MenuManager
20 module MenuController
21 def self.included(base)
22 base.extend(ClassMethods)
23 end
24
25 module ClassMethods
26 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
27 mattr_accessor :menu_items
28
29 # Set the menu item name for a controller or specific actions
30 # Examples:
31 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
32 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
33 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
34 #
35 # The default menu item name for a controller is controller_name by default
36 # Eg. the default menu item name for ProjectsController is :projects
37 def menu_item(id, options = {})
38 if actions = options[:only]
39 actions = [] << actions unless actions.is_a?(Array)
40 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
41 else
42 menu_items[controller_name.to_sym][:default] = id
43 end
44 end
45 end
46
47 def menu_items
48 self.class.menu_items
49 end
50
51 # Returns the menu item name according to the current action
52 def current_menu_item
53 menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
54 menu_items[controller_name.to_sym][:default]
55 end
56 end
57
58 module MenuHelper
59 # Returns the current menu item name
60 def current_menu_item
61 @controller.current_menu_item
62 end
63
64 # Renders the application main menu as a ul element
65 def render_main_menu(project)
66 links = []
67 Redmine::MenuManager.allowed_items(:project_menu, User.current, project).each do |item|
68 unless item.condition && !item.condition.call(project)
69 links << content_tag('li',
70 link_to(l(item.caption), {item.param => project}.merge(item.url),
71 :class => (current_menu_item == item.name ? 'selected' : nil)))
72 end
73 end if project && !project.new_record?
74 links.empty? ? nil : content_tag('ul', links.join("\n"))
75 end
76 end
20 77
21 78 class << self
22 79 def map(menu_name)
23 80 mapper = Mapper.new
24 81 yield mapper
25 82 @items ||= {}
26 83 @items[menu_name.to_sym] ||= []
27 84 @items[menu_name.to_sym] += mapper.items
28 85 end
29 86
30 87 def items(menu_name)
31 88 @items[menu_name.to_sym] || []
32 89 end
33 90
34 91 def allowed_items(menu_name, user, project)
35 92 items(menu_name).select {|item| user && user.allowed_to?(item.url, project)}
36 93 end
37 94 end
38 95
39 96 class Mapper
40 97 def push(name, url, options={})
41 98 @items ||= []
42 99 @items << MenuItem.new(name, url, options)
43 100 end
44 101
45 102 def items
46 103 @items
47 104 end
48 105 end
49 106
50 107 class MenuItem
51 attr_reader :name, :url, :param, :condition
108 attr_reader :name, :url, :param, :condition, :caption
52 109
53 110 def initialize(name, url, options)
54 111 @name = name
55 112 @url = url
56 113 @condition = options[:if]
57 114 @param = options[:param] || :id
115 @caption = options[:caption] || name.to_s.humanize
58 116 end
59 117 end
60 118 end
61 119 end
@@ -1,520 +1,520
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
13 13 #top-menu a {color: #fff; padding-right: 4px;}
14 14 #account {float:right;}
15 15
16 16 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
17 17 #header a {color:#f8f8f8;}
18 18 #quick-search {float:right;}
19 19
20 20 #main-menu {position: absolute; bottom: 0px; left:6px;}
21 21 #main-menu ul {margin: 0; padding: 0;}
22 22 #main-menu li {
23 23 float:left;
24 24 list-style-type:none;
25 25 margin: 0px 10px 0px 0px;
26 26 padding: 0px 0px 0px 0px;
27 27 white-space:nowrap;
28 28 }
29 29 #main-menu li a {
30 30 display: block;
31 31 color: #fff;
32 32 text-decoration: none;
33 33 margin: 0;
34 34 padding: 4px 4px 4px 4px;
35 35 background: #2C4056;
36 36 }
37 #main-menu li a:hover {background:#759FCF;}
37 #main-menu li a:hover, #main-menu li a.selected {background:#759FCF;}
38 38
39 39 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
40 40
41 41 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
42 42 * html #sidebar{ width: 17%; }
43 43 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
44 44 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
45 45 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
46 46
47 47 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
48 48 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
49 49 html>body #content {
50 50 height: auto;
51 51 min-height: 600px;
52 52 }
53 53
54 54 #main.nosidebar #sidebar{ display: none; }
55 55 #main.nosidebar #content{ width: auto; border-right: 0; }
56 56
57 57 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
58 58
59 59 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
60 60 #login-form table td {padding: 6px;}
61 61 #login-form label {font-weight: bold;}
62 62
63 63 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
64 64
65 65 /***** Links *****/
66 66 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
67 67 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
68 68 a img{ border: 0; }
69 69
70 70 /***** Tables *****/
71 71 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
72 72 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
73 73 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
74 74 table.list td.id { width: 2%; text-align: center;}
75 75 table.list td.checkbox { width: 15px; padding: 0px;}
76 76
77 77 tr.issue { text-align: center; white-space: nowrap; }
78 78 tr.issue td.subject, tr.issue td.category { white-space: normal; }
79 79 tr.issue td.subject { text-align: left; }
80 80 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
81 81
82 82 tr.entry { border: 1px solid #f8f8f8; }
83 83 tr.entry td { white-space: nowrap; }
84 84 tr.entry td.filename { width: 30%; }
85 85 tr.entry td.size { text-align: right; font-size: 90%; }
86 86 tr.entry td.revision, tr.entry td.author { text-align: center; }
87 87 tr.entry td.age { text-align: right; }
88 88
89 89 tr.changeset td.author { text-align: center; width: 15%; }
90 90 tr.changeset td.committed_on { text-align: center; width: 15%; }
91 91
92 92 tr.message { height: 2.6em; }
93 93 tr.message td.last_message { font-size: 80%; }
94 94 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
95 95 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
96 96
97 97 table.list tbody tr:hover { background-color:#ffffdd; }
98 98 table td {padding:2px;}
99 99 table p {margin:0;}
100 100 .odd {background-color:#f6f7f8;}
101 101 .even {background-color: #fff;}
102 102
103 103 .highlight { background-color: #FCFD8D;}
104 104 .highlight.token-1 { background-color: #faa;}
105 105 .highlight.token-2 { background-color: #afa;}
106 106 .highlight.token-3 { background-color: #aaf;}
107 107
108 108 .box{
109 109 padding:6px;
110 110 margin-bottom: 10px;
111 111 background-color:#f6f6f6;
112 112 color:#505050;
113 113 line-height:1.5em;
114 114 border: 1px solid #e4e4e4;
115 115 }
116 116
117 117 div.square {
118 118 border: 1px solid #999;
119 119 float: left;
120 120 margin: .3em .4em 0 .4em;
121 121 overflow: hidden;
122 122 width: .6em; height: .6em;
123 123 }
124 124
125 125 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
126 126 .contextual input {font-size:0.9em;}
127 127
128 128 .splitcontentleft{float:left; width:49%;}
129 129 .splitcontentright{float:right; width:49%;}
130 130 form {display: inline;}
131 131 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
132 132 fieldset {border: 1px solid #e4e4e4; margin:0;}
133 133 legend {color: #484848;}
134 134 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
135 135 textarea.wiki-edit { width: 99%; }
136 136 li p {margin-top: 0;}
137 137 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
138 138 .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;}
139 139 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
140 140
141 141 .pagination {font-size: 90%}
142 142 p.pagination {margin-top:8px;}
143 143
144 144 /***** Tabular forms ******/
145 145 .tabular p{
146 146 margin: 0;
147 147 padding: 5px 0 8px 0;
148 148 padding-left: 180px; /*width of left column containing the label elements*/
149 149 height: 1%;
150 150 clear:left;
151 151 }
152 152
153 153 .tabular label{
154 154 font-weight: bold;
155 155 float: left;
156 156 text-align: right;
157 157 margin-left: -180px; /*width of left column*/
158 158 width: 175px; /*width of labels. Should be smaller than left column to create some right
159 159 margin*/
160 160 }
161 161
162 162 .tabular label.floating{
163 163 font-weight: normal;
164 164 margin-left: 0px;
165 165 text-align: left;
166 166 width: 200px;
167 167 }
168 168
169 169 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
170 170
171 171 .tabular.settings p{ padding-left: 300px; }
172 172 .tabular.settings label{ margin-left: -300px; width: 295px; }
173 173
174 174 .required {color: #bb0000;}
175 175 .summary {font-style: italic;}
176 176
177 177 div.attachments p { margin:4px 0 2px 0; }
178 178
179 179 /***** Flash & error messages ****/
180 180 #errorExplanation, div.flash, .nodata {
181 181 padding: 4px 4px 4px 30px;
182 182 margin-bottom: 12px;
183 183 font-size: 1.1em;
184 184 border: 2px solid;
185 185 }
186 186
187 187 div.flash {margin-top: 8px;}
188 188
189 189 div.flash.error, #errorExplanation {
190 190 background: url(../images/false.png) 8px 5px no-repeat;
191 191 background-color: #ffe3e3;
192 192 border-color: #dd0000;
193 193 color: #550000;
194 194 }
195 195
196 196 div.flash.notice {
197 197 background: url(../images/true.png) 8px 5px no-repeat;
198 198 background-color: #dfffdf;
199 199 border-color: #9fcf9f;
200 200 color: #005f00;
201 201 }
202 202
203 203 .nodata {
204 204 text-align: center;
205 205 background-color: #FFEBC1;
206 206 border-color: #FDBF3B;
207 207 color: #A6750C;
208 208 }
209 209
210 210 #errorExplanation ul { font-size: 0.9em;}
211 211
212 212 /***** Ajax indicator ******/
213 213 #ajax-indicator {
214 214 position: absolute; /* fixed not supported by IE */
215 215 background-color:#eee;
216 216 border: 1px solid #bbb;
217 217 top:35%;
218 218 left:40%;
219 219 width:20%;
220 220 font-weight:bold;
221 221 text-align:center;
222 222 padding:0.6em;
223 223 z-index:100;
224 224 filter:alpha(opacity=50);
225 225 -moz-opacity:0.5;
226 226 opacity: 0.5;
227 227 -khtml-opacity: 0.5;
228 228 }
229 229
230 230 html>body #ajax-indicator { position: fixed; }
231 231
232 232 #ajax-indicator span {
233 233 background-position: 0% 40%;
234 234 background-repeat: no-repeat;
235 235 background-image: url(../images/loading.gif);
236 236 padding-left: 26px;
237 237 vertical-align: bottom;
238 238 }
239 239
240 240 /***** Calendar *****/
241 241 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
242 242 table.cal thead th {width: 14%;}
243 243 table.cal tbody tr {height: 100px;}
244 244 table.cal th { background-color:#EEEEEE; padding: 4px; }
245 245 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
246 246 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
247 247 table.cal td.odd p.day-num {color: #bbb;}
248 248 table.cal td.today {background:#ffffdd;}
249 249 table.cal td.today p.day-num {font-weight: bold;}
250 250
251 251 /***** Tooltips ******/
252 252 .tooltip{position:relative;z-index:24;}
253 253 .tooltip:hover{z-index:25;color:#000;}
254 254 .tooltip span.tip{display: none; text-align:left;}
255 255
256 256 div.tooltip:hover span.tip{
257 257 display:block;
258 258 position:absolute;
259 259 top:12px; left:24px; width:270px;
260 260 border:1px solid #555;
261 261 background-color:#fff;
262 262 padding: 4px;
263 263 font-size: 0.8em;
264 264 color:#505050;
265 265 }
266 266
267 267 /***** Progress bar *****/
268 268 table.progress {
269 269 border: 1px solid #D7D7D7;
270 270 border-collapse: collapse;
271 271 border-spacing: 0pt;
272 272 empty-cells: show;
273 273 text-align: center;
274 274 float:left;
275 275 margin: 1px 6px 1px 0px;
276 276 }
277 277
278 278 table.progress td { height: 0.9em; }
279 279 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
280 280 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
281 281 table.progress td.open { background: #FFF none repeat scroll 0%; }
282 282 p.pourcent {font-size: 80%;}
283 283 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
284 284
285 285 div#status_by { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; }
286 286
287 287 /***** Tabs *****/
288 288 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
289 289 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
290 290 #content .tabs>ul { bottom:-1px; } /* others */
291 291 #content .tabs ul li {
292 292 float:left;
293 293 list-style-type:none;
294 294 white-space:nowrap;
295 295 margin-right:8px;
296 296 background:#fff;
297 297 }
298 298 #content .tabs ul li a{
299 299 display:block;
300 300 font-size: 0.9em;
301 301 text-decoration:none;
302 302 line-height:1.3em;
303 303 padding:4px 6px 4px 6px;
304 304 border: 1px solid #ccc;
305 305 border-bottom: 1px solid #bbbbbb;
306 306 background-color: #eeeeee;
307 307 color:#777;
308 308 font-weight:bold;
309 309 }
310 310
311 311 #content .tabs ul li a:hover {
312 312 background-color: #ffffdd;
313 313 text-decoration:none;
314 314 }
315 315
316 316 #content .tabs ul li a.selected {
317 317 background-color: #fff;
318 318 border: 1px solid #bbbbbb;
319 319 border-bottom: 1px solid #fff;
320 320 }
321 321
322 322 #content .tabs ul li a.selected:hover {
323 323 background-color: #fff;
324 324 }
325 325
326 326 /***** Diff *****/
327 327 .diff_out { background: #fcc; }
328 328 .diff_in { background: #cfc; }
329 329
330 330 /***** Wiki *****/
331 331 div.wiki table {
332 332 border: 1px solid #505050;
333 333 border-collapse: collapse;
334 334 }
335 335
336 336 div.wiki table, div.wiki td, div.wiki th {
337 337 border: 1px solid #bbb;
338 338 padding: 4px;
339 339 }
340 340
341 341 div.wiki .external {
342 342 background-position: 0% 60%;
343 343 background-repeat: no-repeat;
344 344 padding-left: 12px;
345 345 background-image: url(../images/external.png);
346 346 }
347 347
348 348 div.wiki a.new {
349 349 color: #b73535;
350 350 }
351 351
352 352 div.wiki pre {
353 353 margin: 1em 1em 1em 1.6em;
354 354 padding: 2px;
355 355 background-color: #fafafa;
356 356 border: 1px solid #dadada;
357 357 width:95%;
358 358 overflow-x: auto;
359 359 }
360 360
361 361 div.wiki div.toc {
362 362 background-color: #ffffdd;
363 363 border: 1px solid #e4e4e4;
364 364 padding: 4px;
365 365 line-height: 1.2em;
366 366 margin-bottom: 12px;
367 367 margin-right: 12px;
368 368 display: table
369 369 }
370 370 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
371 371
372 372 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
373 373 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
374 374
375 375 div.wiki div.toc a {
376 376 display: block;
377 377 font-size: 0.9em;
378 378 font-weight: normal;
379 379 text-decoration: none;
380 380 color: #606060;
381 381 }
382 382 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
383 383
384 384 div.wiki div.toc a.heading2 { margin-left: 6px; }
385 385 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
386 386
387 387 /***** My page layout *****/
388 388 .block-receiver {
389 389 border:1px dashed #c0c0c0;
390 390 margin-bottom: 20px;
391 391 padding: 15px 0 15px 0;
392 392 }
393 393
394 394 .mypage-box {
395 395 margin:0 0 20px 0;
396 396 color:#505050;
397 397 line-height:1.5em;
398 398 }
399 399
400 400 .handle {
401 401 cursor: move;
402 402 }
403 403
404 404 a.close-icon {
405 405 display:block;
406 406 margin-top:3px;
407 407 overflow:hidden;
408 408 width:12px;
409 409 height:12px;
410 410 background-repeat: no-repeat;
411 411 cursor:pointer;
412 412 background-image:url('../images/close.png');
413 413 }
414 414
415 415 a.close-icon:hover {
416 416 background-image:url('../images/close_hl.png');
417 417 }
418 418
419 419 /***** Gantt chart *****/
420 420 .gantt_hdr {
421 421 position:absolute;
422 422 top:0;
423 423 height:16px;
424 424 border-top: 1px solid #c0c0c0;
425 425 border-bottom: 1px solid #c0c0c0;
426 426 border-right: 1px solid #c0c0c0;
427 427 text-align: center;
428 428 overflow: hidden;
429 429 }
430 430
431 431 .task {
432 432 position: absolute;
433 433 height:8px;
434 434 font-size:0.8em;
435 435 color:#888;
436 436 padding:0;
437 437 margin:0;
438 438 line-height:0.8em;
439 439 }
440 440
441 441 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
442 442 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
443 443 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
444 444 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
445 445
446 446 /***** Icons *****/
447 447 .icon {
448 448 background-position: 0% 40%;
449 449 background-repeat: no-repeat;
450 450 padding-left: 20px;
451 451 padding-top: 2px;
452 452 padding-bottom: 3px;
453 453 }
454 454
455 455 .icon22 {
456 456 background-position: 0% 40%;
457 457 background-repeat: no-repeat;
458 458 padding-left: 26px;
459 459 line-height: 22px;
460 460 vertical-align: middle;
461 461 }
462 462
463 463 .icon-add { background-image: url(../images/add.png); }
464 464 .icon-edit { background-image: url(../images/edit.png); }
465 465 .icon-copy { background-image: url(../images/copy.png); }
466 466 .icon-del { background-image: url(../images/delete.png); }
467 467 .icon-move { background-image: url(../images/move.png); }
468 468 .icon-save { background-image: url(../images/save.png); }
469 469 .icon-cancel { background-image: url(../images/cancel.png); }
470 470 .icon-pdf { background-image: url(../images/pdf.png); }
471 471 .icon-csv { background-image: url(../images/csv.png); }
472 472 .icon-html { background-image: url(../images/html.png); }
473 473 .icon-image { background-image: url(../images/image.png); }
474 474 .icon-txt { background-image: url(../images/txt.png); }
475 475 .icon-file { background-image: url(../images/file.png); }
476 476 .icon-folder { background-image: url(../images/folder.png); }
477 477 .open .icon-folder { background-image: url(../images/folder_open.png); }
478 478 .icon-package { background-image: url(../images/package.png); }
479 479 .icon-home { background-image: url(../images/home.png); }
480 480 .icon-user { background-image: url(../images/user.png); }
481 481 .icon-mypage { background-image: url(../images/user_page.png); }
482 482 .icon-admin { background-image: url(../images/admin.png); }
483 483 .icon-projects { background-image: url(../images/projects.png); }
484 484 .icon-logout { background-image: url(../images/logout.png); }
485 485 .icon-help { background-image: url(../images/help.png); }
486 486 .icon-attachment { background-image: url(../images/attachment.png); }
487 487 .icon-index { background-image: url(../images/index.png); }
488 488 .icon-history { background-image: url(../images/history.png); }
489 489 .icon-feed { background-image: url(../images/feed.png); }
490 490 .icon-time { background-image: url(../images/time.png); }
491 491 .icon-stats { background-image: url(../images/stats.png); }
492 492 .icon-warning { background-image: url(../images/warning.png); }
493 493 .icon-fav { background-image: url(../images/fav.png); }
494 494 .icon-fav-off { background-image: url(../images/fav_off.png); }
495 495 .icon-reload { background-image: url(../images/reload.png); }
496 496 .icon-lock { background-image: url(../images/locked.png); }
497 497 .icon-unlock { background-image: url(../images/unlock.png); }
498 498 .icon-note { background-image: url(../images/note.png); }
499 499 .icon-checked { background-image: url(../images/true.png); }
500 500
501 501 .icon22-projects { background-image: url(../images/22x22/projects.png); }
502 502 .icon22-users { background-image: url(../images/22x22/users.png); }
503 503 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
504 504 .icon22-role { background-image: url(../images/22x22/role.png); }
505 505 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
506 506 .icon22-options { background-image: url(../images/22x22/options.png); }
507 507 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
508 508 .icon22-authent { background-image: url(../images/22x22/authent.png); }
509 509 .icon22-info { background-image: url(../images/22x22/info.png); }
510 510 .icon22-comment { background-image: url(../images/22x22/comment.png); }
511 511 .icon22-package { background-image: url(../images/22x22/package.png); }
512 512 .icon22-settings { background-image: url(../images/22x22/settings.png); }
513 513 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
514 514
515 515 /***** Media print specific styles *****/
516 516 @media print {
517 517 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
518 518 #main { background: #fff; }
519 519 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
520 520 }
General Comments 0
You need to be logged in to leave comments. Login now