To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / lib / redmine @ 1568:bc47b68a9487
| 1 | 909:cbb26bc654de | Chris | # Redmine - project management software |
|---|---|---|---|
| 2 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 3 | 0:513646585e45 | Chris | # |
| 4 | # This program is free software; you can redistribute it and/or |
||
| 5 | # modify it under the terms of the GNU General Public License |
||
| 6 | # as published by the Free Software Foundation; either version 2 |
||
| 7 | # of the License, or (at your option) any later version. |
||
| 8 | 909:cbb26bc654de | Chris | # |
| 9 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 12 | # GNU General Public License for more details. |
||
| 13 | 909:cbb26bc654de | Chris | # |
| 14 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program; if not, write to the Free Software |
||
| 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 17 | |||
| 18 | module Redmine |
||
| 19 | module AccessControl |
||
| 20 | 909:cbb26bc654de | Chris | |
| 21 | 0:513646585e45 | Chris | class << self |
| 22 | def map |
||
| 23 | mapper = Mapper.new |
||
| 24 | yield mapper |
||
| 25 | @permissions ||= [] |
||
| 26 | @permissions += mapper.mapped_permissions |
||
| 27 | end |
||
| 28 | 909:cbb26bc654de | Chris | |
| 29 | 0:513646585e45 | Chris | def permissions |
| 30 | @permissions |
||
| 31 | end |
||
| 32 | 909:cbb26bc654de | Chris | |
| 33 | 0:513646585e45 | Chris | # Returns the permission of given name or nil if it wasn't found |
| 34 | # Argument should be a symbol |
||
| 35 | def permission(name) |
||
| 36 | permissions.detect {|p| p.name == name}
|
||
| 37 | end |
||
| 38 | 909:cbb26bc654de | Chris | |
| 39 | 0:513646585e45 | Chris | # Returns the actions that are allowed by the permission of given name |
| 40 | def allowed_actions(permission_name) |
||
| 41 | perm = permission(permission_name) |
||
| 42 | perm ? perm.actions : [] |
||
| 43 | end |
||
| 44 | 909:cbb26bc654de | Chris | |
| 45 | 0:513646585e45 | Chris | def public_permissions |
| 46 | @public_permissions ||= @permissions.select {|p| p.public?}
|
||
| 47 | end |
||
| 48 | 909:cbb26bc654de | Chris | |
| 49 | 0:513646585e45 | Chris | def members_only_permissions |
| 50 | @members_only_permissions ||= @permissions.select {|p| p.require_member?}
|
||
| 51 | end |
||
| 52 | 909:cbb26bc654de | Chris | |
| 53 | 0:513646585e45 | Chris | def loggedin_only_permissions |
| 54 | @loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
|
||
| 55 | end |
||
| 56 | 909:cbb26bc654de | Chris | |
| 57 | 1115:433d4f72a19b | Chris | def read_action?(action) |
| 58 | if action.is_a?(Symbol) |
||
| 59 | perm = permission(action) |
||
| 60 | !perm.nil? && perm.read? |
||
| 61 | else |
||
| 62 | s = "#{action[:controller]}/#{action[:action]}"
|
||
| 63 | 1517:dffacf8a6908 | Chris | permissions.detect {|p| p.actions.include?(s) && p.read?}.present?
|
| 64 | 1115:433d4f72a19b | Chris | end |
| 65 | end |
||
| 66 | |||
| 67 | 0:513646585e45 | Chris | def available_project_modules |
| 68 | @available_project_modules ||= @permissions.collect(&:project_module).uniq.compact |
||
| 69 | end |
||
| 70 | 909:cbb26bc654de | Chris | |
| 71 | 0:513646585e45 | Chris | def modules_permissions(modules) |
| 72 | @permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
|
||
| 73 | end |
||
| 74 | end |
||
| 75 | 909:cbb26bc654de | Chris | |
| 76 | 0:513646585e45 | Chris | class Mapper |
| 77 | def initialize |
||
| 78 | @project_module = nil |
||
| 79 | end |
||
| 80 | 909:cbb26bc654de | Chris | |
| 81 | 0:513646585e45 | Chris | def permission(name, hash, options={})
|
| 82 | @permissions ||= [] |
||
| 83 | options.merge!(:project_module => @project_module) |
||
| 84 | @permissions << Permission.new(name, hash, options) |
||
| 85 | end |
||
| 86 | 909:cbb26bc654de | Chris | |
| 87 | 0:513646585e45 | Chris | def project_module(name, options={})
|
| 88 | @project_module = name |
||
| 89 | yield self |
||
| 90 | @project_module = nil |
||
| 91 | end |
||
| 92 | 909:cbb26bc654de | Chris | |
| 93 | 0:513646585e45 | Chris | def mapped_permissions |
| 94 | @permissions |
||
| 95 | end |
||
| 96 | end |
||
| 97 | 909:cbb26bc654de | Chris | |
| 98 | 0:513646585e45 | Chris | class Permission |
| 99 | attr_reader :name, :actions, :project_module |
||
| 100 | 909:cbb26bc654de | Chris | |
| 101 | 0:513646585e45 | Chris | def initialize(name, hash, options) |
| 102 | @name = name |
||
| 103 | @actions = [] |
||
| 104 | @public = options[:public] || false |
||
| 105 | @require = options[:require] |
||
| 106 | 1115:433d4f72a19b | Chris | @read = options[:read] || false |
| 107 | 0:513646585e45 | Chris | @project_module = options[:project_module] |
| 108 | hash.each do |controller, actions| |
||
| 109 | if actions.is_a? Array |
||
| 110 | @actions << actions.collect {|action| "#{controller}/#{action}"}
|
||
| 111 | else |
||
| 112 | @actions << "#{controller}/#{actions}"
|
||
| 113 | end |
||
| 114 | end |
||
| 115 | @actions.flatten! |
||
| 116 | end |
||
| 117 | 909:cbb26bc654de | Chris | |
| 118 | 0:513646585e45 | Chris | def public? |
| 119 | @public |
||
| 120 | end |
||
| 121 | 909:cbb26bc654de | Chris | |
| 122 | 0:513646585e45 | Chris | def require_member? |
| 123 | @require && @require == :member |
||
| 124 | end |
||
| 125 | 909:cbb26bc654de | Chris | |
| 126 | 0:513646585e45 | Chris | def require_loggedin? |
| 127 | @require && (@require == :member || @require == :loggedin) |
||
| 128 | end |
||
| 129 | 1115:433d4f72a19b | Chris | |
| 130 | def read? |
||
| 131 | @read |
||
| 132 | end |
||
| 133 | 909:cbb26bc654de | Chris | end |
| 134 | 0:513646585e45 | Chris | end |
| 135 | end |
||
| 136 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 137 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 138 | 0:513646585e45 | Chris | # |
| 139 | # This program is free software; you can redistribute it and/or |
||
| 140 | # modify it under the terms of the GNU General Public License |
||
| 141 | # as published by the Free Software Foundation; either version 2 |
||
| 142 | # of the License, or (at your option) any later version. |
||
| 143 | 909:cbb26bc654de | Chris | # |
| 144 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 145 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 146 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 147 | # GNU General Public License for more details. |
||
| 148 | 909:cbb26bc654de | Chris | # |
| 149 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 150 | # along with this program; if not, write to the Free Software |
||
| 151 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 152 | |||
| 153 | module Redmine |
||
| 154 | module AccessKeys |
||
| 155 | ACCESSKEYS = {:edit => 'e',
|
||
| 156 | :preview => 'r', |
||
| 157 | :quick_search => 'f', |
||
| 158 | :search => '4', |
||
| 159 | :new_issue => '7' |
||
| 160 | }.freeze unless const_defined?(:ACCESSKEYS) |
||
| 161 | 909:cbb26bc654de | Chris | |
| 162 | 0:513646585e45 | Chris | def self.key_for(action) |
| 163 | ACCESSKEYS[action] |
||
| 164 | end |
||
| 165 | end |
||
| 166 | end |
||
| 167 | # Redmine - project management software |
||
| 168 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 169 | 0:513646585e45 | Chris | # |
| 170 | # This program is free software; you can redistribute it and/or |
||
| 171 | # modify it under the terms of the GNU General Public License |
||
| 172 | # as published by the Free Software Foundation; either version 2 |
||
| 173 | # of the License, or (at your option) any later version. |
||
| 174 | 909:cbb26bc654de | Chris | # |
| 175 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 176 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 177 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 178 | # GNU General Public License for more details. |
||
| 179 | 909:cbb26bc654de | Chris | # |
| 180 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 181 | # along with this program; if not, write to the Free Software |
||
| 182 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 183 | |||
| 184 | module Redmine |
||
| 185 | module Activity |
||
| 186 | 909:cbb26bc654de | Chris | |
| 187 | 0:513646585e45 | Chris | mattr_accessor :available_event_types, :default_event_types, :providers |
| 188 | 909:cbb26bc654de | Chris | |
| 189 | 0:513646585e45 | Chris | @@available_event_types = [] |
| 190 | @@default_event_types = [] |
||
| 191 | @@providers = Hash.new {|h,k| h[k]=[] }
|
||
| 192 | |||
| 193 | class << self |
||
| 194 | def map(&block) |
||
| 195 | yield self |
||
| 196 | end |
||
| 197 | 909:cbb26bc654de | Chris | |
| 198 | 0:513646585e45 | Chris | # Registers an activity provider |
| 199 | def register(event_type, options={})
|
||
| 200 | options.assert_valid_keys(:class_name, :default) |
||
| 201 | 909:cbb26bc654de | Chris | |
| 202 | 0:513646585e45 | Chris | event_type = event_type.to_s |
| 203 | providers = options[:class_name] || event_type.classify |
||
| 204 | providers = ([] << providers) unless providers.is_a?(Array) |
||
| 205 | 909:cbb26bc654de | Chris | |
| 206 | 0:513646585e45 | Chris | @@available_event_types << event_type unless @@available_event_types.include?(event_type) |
| 207 | @@default_event_types << event_type unless options[:default] == false |
||
| 208 | @@providers[event_type] += providers |
||
| 209 | end |
||
| 210 | end |
||
| 211 | end |
||
| 212 | end |
||
| 213 | # Redmine - project management software |
||
| 214 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 215 | 0:513646585e45 | Chris | # |
| 216 | # This program is free software; you can redistribute it and/or |
||
| 217 | # modify it under the terms of the GNU General Public License |
||
| 218 | # as published by the Free Software Foundation; either version 2 |
||
| 219 | # of the License, or (at your option) any later version. |
||
| 220 | 909:cbb26bc654de | Chris | # |
| 221 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 222 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 223 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 224 | # GNU General Public License for more details. |
||
| 225 | 909:cbb26bc654de | Chris | # |
| 226 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 227 | # along with this program; if not, write to the Free Software |
||
| 228 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 229 | |||
| 230 | module Redmine |
||
| 231 | module Activity |
||
| 232 | # Class used to retrieve activity events |
||
| 233 | class Fetcher |
||
| 234 | attr_reader :user, :project, :scope |
||
| 235 | 909:cbb26bc654de | Chris | |
| 236 | 0:513646585e45 | Chris | # Needs to be unloaded in development mode |
| 237 | @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
|
||
| 238 | 909:cbb26bc654de | Chris | |
| 239 | 0:513646585e45 | Chris | def initialize(user, options={})
|
| 240 | options.assert_valid_keys(:project, :with_subprojects, :author) |
||
| 241 | @user = user |
||
| 242 | @project = options[:project] |
||
| 243 | @options = options |
||
| 244 | 909:cbb26bc654de | Chris | |
| 245 | 0:513646585e45 | Chris | @scope = event_types |
| 246 | end |
||
| 247 | 909:cbb26bc654de | Chris | |
| 248 | 0:513646585e45 | Chris | # Returns an array of available event types |
| 249 | def event_types |
||
| 250 | return @event_types unless @event_types.nil? |
||
| 251 | 909:cbb26bc654de | Chris | |
| 252 | 0:513646585e45 | Chris | @event_types = Redmine::Activity.available_event_types |
| 253 | @event_types = @event_types.select {|o| @project.self_and_descendants.detect {|p| @user.allowed_to?("view_#{o}".to_sym, p)}} if @project
|
||
| 254 | @event_types |
||
| 255 | end |
||
| 256 | 909:cbb26bc654de | Chris | |
| 257 | 0:513646585e45 | Chris | # Yields to filter the activity scope |
| 258 | def scope_select(&block) |
||
| 259 | @scope = @scope.select {|t| yield t }
|
||
| 260 | end |
||
| 261 | 909:cbb26bc654de | Chris | |
| 262 | 0:513646585e45 | Chris | # Sets the scope |
| 263 | # Argument can be :all, :default or an array of event types |
||
| 264 | def scope=(s) |
||
| 265 | case s |
||
| 266 | when :all |
||
| 267 | @scope = event_types |
||
| 268 | when :default |
||
| 269 | default_scope! |
||
| 270 | else |
||
| 271 | @scope = s & event_types |
||
| 272 | end |
||
| 273 | end |
||
| 274 | 909:cbb26bc654de | Chris | |
| 275 | 0:513646585e45 | Chris | # Resets the scope to the default scope |
| 276 | def default_scope! |
||
| 277 | @scope = Redmine::Activity.default_event_types |
||
| 278 | end |
||
| 279 | 909:cbb26bc654de | Chris | |
| 280 | 0:513646585e45 | Chris | # Returns an array of events for the given date range |
| 281 | # sorted in reverse chronological order |
||
| 282 | def events(from = nil, to = nil, options={})
|
||
| 283 | e = [] |
||
| 284 | @options[:limit] = options[:limit] |
||
| 285 | 909:cbb26bc654de | Chris | |
| 286 | 0:513646585e45 | Chris | @scope.each do |event_type| |
| 287 | constantized_providers(event_type).each do |provider| |
||
| 288 | e += provider.find_events(event_type, @user, from, to, @options) |
||
| 289 | end |
||
| 290 | end |
||
| 291 | 909:cbb26bc654de | Chris | |
| 292 | 0:513646585e45 | Chris | e.sort! {|a,b| b.event_datetime <=> a.event_datetime}
|
| 293 | 909:cbb26bc654de | Chris | |
| 294 | 0:513646585e45 | Chris | if options[:limit] |
| 295 | e = e.slice(0, options[:limit]) |
||
| 296 | end |
||
| 297 | e |
||
| 298 | end |
||
| 299 | 909:cbb26bc654de | Chris | |
| 300 | 0:513646585e45 | Chris | private |
| 301 | 909:cbb26bc654de | Chris | |
| 302 | 0:513646585e45 | Chris | def constantized_providers(event_type) |
| 303 | @@constantized_providers[event_type] |
||
| 304 | end |
||
| 305 | end |
||
| 306 | end |
||
| 307 | end |
||
| 308 | 245:051f544170fe | Chris | # Redmine - project management software |
| 309 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 310 | 245:051f544170fe | Chris | # |
| 311 | # This program is free software; you can redistribute it and/or |
||
| 312 | # modify it under the terms of the GNU General Public License |
||
| 313 | # as published by the Free Software Foundation; either version 2 |
||
| 314 | # of the License, or (at your option) any later version. |
||
| 315 | # |
||
| 316 | # This program is distributed in the hope that it will be useful, |
||
| 317 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 318 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 319 | # GNU General Public License for more details. |
||
| 320 | # |
||
| 321 | # You should have received a copy of the GNU General Public License |
||
| 322 | # along with this program; if not, write to the Free Software |
||
| 323 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 324 | |||
| 325 | module Redmine |
||
| 326 | module Ciphering |
||
| 327 | 909:cbb26bc654de | Chris | def self.included(base) |
| 328 | 245:051f544170fe | Chris | base.extend ClassMethods |
| 329 | end |
||
| 330 | 909:cbb26bc654de | Chris | |
| 331 | 245:051f544170fe | Chris | class << self |
| 332 | def encrypt_text(text) |
||
| 333 | 909:cbb26bc654de | Chris | if cipher_key.blank? || text.blank? |
| 334 | 245:051f544170fe | Chris | text |
| 335 | else |
||
| 336 | c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
||
| 337 | iv = c.random_iv |
||
| 338 | c.encrypt |
||
| 339 | c.key = cipher_key |
||
| 340 | c.iv = iv |
||
| 341 | e = c.update(text.to_s) |
||
| 342 | e << c.final |
||
| 343 | "aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--')
|
||
| 344 | end |
||
| 345 | end |
||
| 346 | 909:cbb26bc654de | Chris | |
| 347 | 245:051f544170fe | Chris | def decrypt_text(text) |
| 348 | if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/) |
||
| 349 | 909:cbb26bc654de | Chris | if cipher_key.blank? |
| 350 | logger.error "Attempt to decrypt a ciphered text with no cipher key configured in config/configuration.yml" if logger |
||
| 351 | return text |
||
| 352 | end |
||
| 353 | 245:051f544170fe | Chris | text = match[1] |
| 354 | c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
||
| 355 | e, iv = text.split("--").map {|s| Base64.decode64(s)}
|
||
| 356 | c.decrypt |
||
| 357 | c.key = cipher_key |
||
| 358 | c.iv = iv |
||
| 359 | d = c.update(e) |
||
| 360 | d << c.final |
||
| 361 | else |
||
| 362 | text |
||
| 363 | end |
||
| 364 | end |
||
| 365 | 909:cbb26bc654de | Chris | |
| 366 | 245:051f544170fe | Chris | def cipher_key |
| 367 | key = Redmine::Configuration['database_cipher_key'].to_s |
||
| 368 | key.blank? ? nil : Digest::SHA256.hexdigest(key) |
||
| 369 | end |
||
| 370 | 909:cbb26bc654de | Chris | |
| 371 | def logger |
||
| 372 | Rails.logger |
||
| 373 | end |
||
| 374 | 245:051f544170fe | Chris | end |
| 375 | 909:cbb26bc654de | Chris | |
| 376 | 245:051f544170fe | Chris | module ClassMethods |
| 377 | def encrypt_all(attribute) |
||
| 378 | transaction do |
||
| 379 | all.each do |object| |
||
| 380 | clear = object.send(attribute) |
||
| 381 | object.send "#{attribute}=", clear
|
||
| 382 | 1115:433d4f72a19b | Chris | raise(ActiveRecord::Rollback) unless object.save(:validation => false) |
| 383 | 245:051f544170fe | Chris | end |
| 384 | end ? true : false |
||
| 385 | end |
||
| 386 | 909:cbb26bc654de | Chris | |
| 387 | 245:051f544170fe | Chris | def decrypt_all(attribute) |
| 388 | transaction do |
||
| 389 | all.each do |object| |
||
| 390 | clear = object.send(attribute) |
||
| 391 | 1115:433d4f72a19b | Chris | object.send :write_attribute, attribute, clear |
| 392 | raise(ActiveRecord::Rollback) unless object.save(:validation => false) |
||
| 393 | 245:051f544170fe | Chris | end |
| 394 | end |
||
| 395 | end ? true : false |
||
| 396 | end |
||
| 397 | 909:cbb26bc654de | Chris | |
| 398 | 245:051f544170fe | Chris | private |
| 399 | 909:cbb26bc654de | Chris | |
| 400 | 245:051f544170fe | Chris | # Returns the value of the given ciphered attribute |
| 401 | def read_ciphered_attribute(attribute) |
||
| 402 | Redmine::Ciphering.decrypt_text(read_attribute(attribute)) |
||
| 403 | end |
||
| 404 | 909:cbb26bc654de | Chris | |
| 405 | 245:051f544170fe | Chris | # Sets the value of the given ciphered attribute |
| 406 | def write_ciphered_attribute(attribute, value) |
||
| 407 | write_attribute(attribute, Redmine::Ciphering.encrypt_text(value)) |
||
| 408 | end |
||
| 409 | end |
||
| 410 | end |
||
| 411 | 1464:261b3d9a4903 | Chris | if RUBY_VERSION < '1.9' |
| 412 | require 'iconv' |
||
| 413 | end |
||
| 414 | 441:cbce1fd3b1b7 | Chris | |
| 415 | module Redmine |
||
| 416 | module CodesetUtil |
||
| 417 | |||
| 418 | def self.replace_invalid_utf8(str) |
||
| 419 | return str if str.nil? |
||
| 420 | if str.respond_to?(:force_encoding) |
||
| 421 | str.force_encoding('UTF-8')
|
||
| 422 | if ! str.valid_encoding? |
||
| 423 | str = str.encode("US-ASCII", :invalid => :replace,
|
||
| 424 | :undef => :replace, :replace => '?').encode("UTF-8")
|
||
| 425 | end |
||
| 426 | 909:cbb26bc654de | Chris | elsif RUBY_PLATFORM == 'java' |
| 427 | begin |
||
| 428 | ic = Iconv.new('UTF-8', 'UTF-8')
|
||
| 429 | str = ic.iconv(str) |
||
| 430 | rescue |
||
| 431 | str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
|
||
| 432 | end |
||
| 433 | 441:cbce1fd3b1b7 | Chris | else |
| 434 | ic = Iconv.new('UTF-8', 'UTF-8')
|
||
| 435 | txtar = "" |
||
| 436 | begin |
||
| 437 | txtar += ic.iconv(str) |
||
| 438 | rescue Iconv::IllegalSequence |
||
| 439 | txtar += $!.success |
||
| 440 | str = '?' + $!.failed[1,$!.failed.length] |
||
| 441 | retry |
||
| 442 | rescue |
||
| 443 | txtar += $!.success |
||
| 444 | end |
||
| 445 | str = txtar |
||
| 446 | end |
||
| 447 | str |
||
| 448 | end |
||
| 449 | 909:cbb26bc654de | Chris | |
| 450 | def self.to_utf8(str, encoding) |
||
| 451 | return str if str.nil? |
||
| 452 | str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
|
||
| 453 | if str.empty? |
||
| 454 | str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
|
||
| 455 | return str |
||
| 456 | end |
||
| 457 | enc = encoding.blank? ? "UTF-8" : encoding |
||
| 458 | if str.respond_to?(:force_encoding) |
||
| 459 | if enc.upcase != "UTF-8" |
||
| 460 | str.force_encoding(enc) |
||
| 461 | str = str.encode("UTF-8", :invalid => :replace,
|
||
| 462 | :undef => :replace, :replace => '?') |
||
| 463 | else |
||
| 464 | str.force_encoding("UTF-8")
|
||
| 465 | if ! str.valid_encoding? |
||
| 466 | str = str.encode("US-ASCII", :invalid => :replace,
|
||
| 467 | :undef => :replace, :replace => '?').encode("UTF-8")
|
||
| 468 | end |
||
| 469 | end |
||
| 470 | elsif RUBY_PLATFORM == 'java' |
||
| 471 | begin |
||
| 472 | ic = Iconv.new('UTF-8', enc)
|
||
| 473 | str = ic.iconv(str) |
||
| 474 | rescue |
||
| 475 | str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
|
||
| 476 | end |
||
| 477 | else |
||
| 478 | ic = Iconv.new('UTF-8', enc)
|
||
| 479 | txtar = "" |
||
| 480 | begin |
||
| 481 | txtar += ic.iconv(str) |
||
| 482 | rescue Iconv::IllegalSequence |
||
| 483 | txtar += $!.success |
||
| 484 | str = '?' + $!.failed[1,$!.failed.length] |
||
| 485 | retry |
||
| 486 | rescue |
||
| 487 | txtar += $!.success |
||
| 488 | end |
||
| 489 | str = txtar |
||
| 490 | end |
||
| 491 | str |
||
| 492 | end |
||
| 493 | |||
| 494 | def self.to_utf8_by_setting(str) |
||
| 495 | return str if str.nil? |
||
| 496 | str = self.to_utf8_by_setting_internal(str) |
||
| 497 | if str.respond_to?(:force_encoding) |
||
| 498 | str.force_encoding('UTF-8')
|
||
| 499 | end |
||
| 500 | str |
||
| 501 | end |
||
| 502 | |||
| 503 | def self.to_utf8_by_setting_internal(str) |
||
| 504 | return str if str.nil? |
||
| 505 | if str.respond_to?(:force_encoding) |
||
| 506 | str.force_encoding('ASCII-8BIT')
|
||
| 507 | end |
||
| 508 | return str if str.empty? |
||
| 509 | return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii |
||
| 510 | if str.respond_to?(:force_encoding) |
||
| 511 | str.force_encoding('UTF-8')
|
||
| 512 | end |
||
| 513 | encodings = Setting.repositories_encodings.split(',').collect(&:strip)
|
||
| 514 | encodings.each do |encoding| |
||
| 515 | 1464:261b3d9a4903 | Chris | if str.respond_to?(:force_encoding) |
| 516 | begin |
||
| 517 | str.force_encoding(encoding) |
||
| 518 | utf8 = str.encode('UTF-8')
|
||
| 519 | return utf8 if utf8.valid_encoding? |
||
| 520 | rescue |
||
| 521 | # do nothing here and try the next encoding |
||
| 522 | end |
||
| 523 | else |
||
| 524 | begin |
||
| 525 | return Iconv.conv('UTF-8', encoding, str)
|
||
| 526 | rescue Iconv::Failure |
||
| 527 | # do nothing here and try the next encoding |
||
| 528 | end |
||
| 529 | 909:cbb26bc654de | Chris | end |
| 530 | end |
||
| 531 | str = self.replace_invalid_utf8(str) |
||
| 532 | if str.respond_to?(:force_encoding) |
||
| 533 | str.force_encoding('UTF-8')
|
||
| 534 | end |
||
| 535 | str |
||
| 536 | end |
||
| 537 | |||
| 538 | def self.from_utf8(str, encoding) |
||
| 539 | str ||= '' |
||
| 540 | if str.respond_to?(:force_encoding) |
||
| 541 | str.force_encoding('UTF-8')
|
||
| 542 | if encoding.upcase != 'UTF-8' |
||
| 543 | str = str.encode(encoding, :invalid => :replace, |
||
| 544 | :undef => :replace, :replace => '?') |
||
| 545 | else |
||
| 546 | str = self.replace_invalid_utf8(str) |
||
| 547 | end |
||
| 548 | elsif RUBY_PLATFORM == 'java' |
||
| 549 | begin |
||
| 550 | ic = Iconv.new(encoding, 'UTF-8') |
||
| 551 | str = ic.iconv(str) |
||
| 552 | rescue |
||
| 553 | str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
|
||
| 554 | end |
||
| 555 | else |
||
| 556 | ic = Iconv.new(encoding, 'UTF-8') |
||
| 557 | txtar = "" |
||
| 558 | begin |
||
| 559 | txtar += ic.iconv(str) |
||
| 560 | rescue Iconv::IllegalSequence |
||
| 561 | txtar += $!.success |
||
| 562 | str = '?' + $!.failed[1, $!.failed.length] |
||
| 563 | retry |
||
| 564 | rescue |
||
| 565 | txtar += $!.success |
||
| 566 | end |
||
| 567 | str = txtar |
||
| 568 | end |
||
| 569 | end |
||
| 570 | 441:cbce1fd3b1b7 | Chris | end |
| 571 | end |
||
| 572 | 210:0579821a129a | Chris | # Redmine - project management software |
| 573 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 574 | 210:0579821a129a | Chris | # |
| 575 | # This program is free software; you can redistribute it and/or |
||
| 576 | # modify it under the terms of the GNU General Public License |
||
| 577 | # as published by the Free Software Foundation; either version 2 |
||
| 578 | # of the License, or (at your option) any later version. |
||
| 579 | 909:cbb26bc654de | Chris | # |
| 580 | 210:0579821a129a | Chris | # This program is distributed in the hope that it will be useful, |
| 581 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 582 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 583 | # GNU General Public License for more details. |
||
| 584 | 909:cbb26bc654de | Chris | # |
| 585 | 210:0579821a129a | Chris | # You should have received a copy of the GNU General Public License |
| 586 | # along with this program; if not, write to the Free Software |
||
| 587 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 588 | |||
| 589 | module Redmine |
||
| 590 | module Configuration |
||
| 591 | 909:cbb26bc654de | Chris | |
| 592 | 210:0579821a129a | Chris | # Configuration default values |
| 593 | @defaults = {
|
||
| 594 | 1464:261b3d9a4903 | Chris | 'email_delivery' => nil, |
| 595 | 'max_concurrent_ajax_uploads' => 2 |
||
| 596 | 210:0579821a129a | Chris | } |
| 597 | 909:cbb26bc654de | Chris | |
| 598 | 210:0579821a129a | Chris | @config = nil |
| 599 | 909:cbb26bc654de | Chris | |
| 600 | 210:0579821a129a | Chris | class << self |
| 601 | # Loads the Redmine configuration file |
||
| 602 | # Valid options: |
||
| 603 | # * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml) |
||
| 604 | 909:cbb26bc654de | Chris | # * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env) |
| 605 | 210:0579821a129a | Chris | def load(options={})
|
| 606 | filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml') |
||
| 607 | env = options[:env] || Rails.env |
||
| 608 | 909:cbb26bc654de | Chris | |
| 609 | 210:0579821a129a | Chris | @config = @defaults.dup |
| 610 | 909:cbb26bc654de | Chris | |
| 611 | 210:0579821a129a | Chris | load_deprecated_email_configuration(env) |
| 612 | if File.file?(filename) |
||
| 613 | @config.merge!(load_from_yaml(filename, env)) |
||
| 614 | end |
||
| 615 | 909:cbb26bc654de | Chris | |
| 616 | 210:0579821a129a | Chris | # Compatibility mode for those who copy email.yml over configuration.yml |
| 617 | %w(delivery_method smtp_settings sendmail_settings).each do |key| |
||
| 618 | if value = @config.delete(key) |
||
| 619 | @config['email_delivery'] ||= {}
|
||
| 620 | @config['email_delivery'][key] = value |
||
| 621 | end |
||
| 622 | end |
||
| 623 | 909:cbb26bc654de | Chris | |
| 624 | 210:0579821a129a | Chris | if @config['email_delivery'] |
| 625 | ActionMailer::Base.perform_deliveries = true |
||
| 626 | @config['email_delivery'].each do |k, v| |
||
| 627 | v.symbolize_keys! if v.respond_to?(:symbolize_keys!) |
||
| 628 | ActionMailer::Base.send("#{k}=", v)
|
||
| 629 | end |
||
| 630 | end |
||
| 631 | 909:cbb26bc654de | Chris | |
| 632 | 210:0579821a129a | Chris | @config |
| 633 | end |
||
| 634 | 909:cbb26bc654de | Chris | |
| 635 | 210:0579821a129a | Chris | # Returns a configuration setting |
| 636 | def [](name) |
||
| 637 | load unless @config |
||
| 638 | @config[name] |
||
| 639 | end |
||
| 640 | 909:cbb26bc654de | Chris | |
| 641 | 245:051f544170fe | Chris | # Yields a block with the specified hash configuration settings |
| 642 | def with(settings) |
||
| 643 | settings.stringify_keys! |
||
| 644 | load unless @config |
||
| 645 | was = settings.keys.inject({}) {|h,v| h[v] = @config[v]; h}
|
||
| 646 | @config.merge! settings |
||
| 647 | yield if block_given? |
||
| 648 | @config.merge! was |
||
| 649 | end |
||
| 650 | 909:cbb26bc654de | Chris | |
| 651 | 210:0579821a129a | Chris | private |
| 652 | 909:cbb26bc654de | Chris | |
| 653 | 210:0579821a129a | Chris | def load_from_yaml(filename, env) |
| 654 | 909:cbb26bc654de | Chris | yaml = nil |
| 655 | begin |
||
| 656 | yaml = YAML::load_file(filename) |
||
| 657 | rescue ArgumentError |
||
| 658 | $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid YAML file and could not be loaded."
|
||
| 659 | exit 1 |
||
| 660 | end |
||
| 661 | 210:0579821a129a | Chris | conf = {}
|
| 662 | if yaml.is_a?(Hash) |
||
| 663 | if yaml['default'] |
||
| 664 | conf.merge!(yaml['default']) |
||
| 665 | end |
||
| 666 | if yaml[env] |
||
| 667 | conf.merge!(yaml[env]) |
||
| 668 | end |
||
| 669 | else |
||
| 670 | 909:cbb26bc654de | Chris | $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid Redmine configuration file."
|
| 671 | 210:0579821a129a | Chris | exit 1 |
| 672 | end |
||
| 673 | conf |
||
| 674 | end |
||
| 675 | 909:cbb26bc654de | Chris | |
| 676 | 210:0579821a129a | Chris | def load_deprecated_email_configuration(env) |
| 677 | deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml') |
||
| 678 | if File.file?(deprecated_email_conf) |
||
| 679 | warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting." |
||
| 680 | @config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)})
|
||
| 681 | end |
||
| 682 | end |
||
| 683 | end |
||
| 684 | end |
||
| 685 | end |
||
| 686 | 0:513646585e45 | Chris | Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
|
| 687 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 688 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 689 | 1115:433d4f72a19b | Chris | # |
| 690 | # This program is free software; you can redistribute it and/or |
||
| 691 | # modify it under the terms of the GNU General Public License |
||
| 692 | # as published by the Free Software Foundation; either version 2 |
||
| 693 | # of the License, or (at your option) any later version. |
||
| 694 | # |
||
| 695 | # This program is distributed in the hope that it will be useful, |
||
| 696 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 697 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 698 | # GNU General Public License for more details. |
||
| 699 | # |
||
| 700 | # You should have received a copy of the GNU General Public License |
||
| 701 | # along with this program; if not, write to the Free Software |
||
| 702 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 703 | |||
| 704 | module ActiveRecord |
||
| 705 | module FinderMethods |
||
| 706 | def find_ids(*args) |
||
| 707 | find_ids_with_associations |
||
| 708 | end |
||
| 709 | |||
| 710 | private |
||
| 711 | |||
| 712 | def find_ids_with_associations |
||
| 713 | join_dependency = construct_join_dependency_for_association_find |
||
| 714 | relation = construct_relation_for_association_find_ids(join_dependency) |
||
| 715 | rows = connection.select_all(relation, 'SQL', relation.bind_values) |
||
| 716 | rows.map {|row| row["id"].to_i}
|
||
| 717 | rescue ThrowResult |
||
| 718 | [] |
||
| 719 | end |
||
| 720 | |||
| 721 | def construct_relation_for_association_find_ids(join_dependency) |
||
| 722 | relation = except(:includes, :eager_load, :preload, :select).select("#{table_name}.id")
|
||
| 723 | apply_join_dependency(relation, join_dependency) |
||
| 724 | end |
||
| 725 | end |
||
| 726 | end |
||
| 727 | 1464:261b3d9a4903 | Chris | |
| 728 | class DateValidator < ActiveModel::EachValidator |
||
| 729 | def validate_each(record, attribute, value) |
||
| 730 | before_type_cast = record.attributes_before_type_cast[attribute.to_s] |
||
| 731 | if before_type_cast.is_a?(String) && before_type_cast.present? |
||
| 732 | # TODO: #*_date_before_type_cast returns a Mysql::Time with ruby1.8+mysql gem |
||
| 733 | unless before_type_cast =~ /\A\d{4}-\d{2}-\d{2}( 00:00:00)?\z/ && value
|
||
| 734 | record.errors.add attribute, :not_a_date |
||
| 735 | end |
||
| 736 | end |
||
| 737 | end |
||
| 738 | end |
||
| 739 | 1115:433d4f72a19b | Chris | require File.dirname(__FILE__) + '/date/calculations' |
| 740 | |||
| 741 | class Date #:nodoc: |
||
| 742 | include Redmine::CoreExtensions::Date::Calculations |
||
| 743 | end |
||
| 744 | # Redmine - project management software |
||
| 745 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 746 | 1115:433d4f72a19b | Chris | # |
| 747 | # This program is free software; you can redistribute it and/or |
||
| 748 | # modify it under the terms of the GNU General Public License |
||
| 749 | # as published by the Free Software Foundation; either version 2 |
||
| 750 | # of the License, or (at your option) any later version. |
||
| 751 | # |
||
| 752 | # This program is distributed in the hope that it will be useful, |
||
| 753 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 754 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 755 | # GNU General Public License for more details. |
||
| 756 | # |
||
| 757 | # You should have received a copy of the GNU General Public License |
||
| 758 | # along with this program; if not, write to the Free Software |
||
| 759 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 760 | |||
| 761 | module Redmine #:nodoc: |
||
| 762 | module CoreExtensions #:nodoc: |
||
| 763 | module Date #:nodoc: |
||
| 764 | # Custom date calculations |
||
| 765 | module Calculations |
||
| 766 | # Returns difference with specified date in months |
||
| 767 | def months_ago(date = self.class.today) |
||
| 768 | (date.year - self.year)*12 + (date.month - self.month) |
||
| 769 | end |
||
| 770 | |||
| 771 | # Returns difference with specified date in weeks |
||
| 772 | def weeks_ago(date = self.class.today) |
||
| 773 | (date.year - self.year)*52 + (date.cweek - self.cweek) |
||
| 774 | end |
||
| 775 | end |
||
| 776 | end |
||
| 777 | end |
||
| 778 | end |
||
| 779 | 0:513646585e45 | Chris | require File.dirname(__FILE__) + '/string/conversions' |
| 780 | require File.dirname(__FILE__) + '/string/inflections' |
||
| 781 | |||
| 782 | class String #:nodoc: |
||
| 783 | include Redmine::CoreExtensions::String::Conversions |
||
| 784 | include Redmine::CoreExtensions::String::Inflections |
||
| 785 | 1115:433d4f72a19b | Chris | |
| 786 | def is_binary_data? |
||
| 787 | ( self.count( "^ -~", "^\r\n" ).fdiv(self.size) > 0.3 || self.index( "\x00" ) ) unless empty? |
||
| 788 | end |
||
| 789 | 0:513646585e45 | Chris | end |
| 790 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 791 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 792 | 0:513646585e45 | Chris | # |
| 793 | # This program is free software; you can redistribute it and/or |
||
| 794 | # modify it under the terms of the GNU General Public License |
||
| 795 | # as published by the Free Software Foundation; either version 2 |
||
| 796 | # of the License, or (at your option) any later version. |
||
| 797 | 909:cbb26bc654de | Chris | # |
| 798 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 799 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 800 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 801 | # GNU General Public License for more details. |
||
| 802 | 909:cbb26bc654de | Chris | # |
| 803 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 804 | # along with this program; if not, write to the Free Software |
||
| 805 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 806 | |||
| 807 | module Redmine #:nodoc: |
||
| 808 | module CoreExtensions #:nodoc: |
||
| 809 | module String #:nodoc: |
||
| 810 | # Custom string conversions |
||
| 811 | module Conversions |
||
| 812 | # Parses hours format and returns a float |
||
| 813 | def to_hours |
||
| 814 | s = self.dup |
||
| 815 | s.strip! |
||
| 816 | if s =~ %r{^(\d+([.,]\d+)?)h?$}
|
||
| 817 | s = $1 |
||
| 818 | else |
||
| 819 | # 2:30 => 2.5 |
||
| 820 | s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
|
||
| 821 | # 2h30, 2h, 30m => 2.5, 2, 0.5 |
||
| 822 | 1115:433d4f72a19b | Chris | s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}i) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
|
| 823 | 0:513646585e45 | Chris | end |
| 824 | # 2,5 => 2.5 |
||
| 825 | s.gsub!(',', '.')
|
||
| 826 | begin; Kernel.Float(s); rescue; nil; end |
||
| 827 | end |
||
| 828 | 909:cbb26bc654de | Chris | |
| 829 | 0:513646585e45 | Chris | # Object#to_a removed in ruby1.9 |
| 830 | if RUBY_VERSION > '1.9' |
||
| 831 | def to_a |
||
| 832 | [self.dup] |
||
| 833 | end |
||
| 834 | end |
||
| 835 | end |
||
| 836 | end |
||
| 837 | end |
||
| 838 | end |
||
| 839 | # Redmine - project management software |
||
| 840 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 841 | 0:513646585e45 | Chris | # |
| 842 | # This program is free software; you can redistribute it and/or |
||
| 843 | # modify it under the terms of the GNU General Public License |
||
| 844 | # as published by the Free Software Foundation; either version 2 |
||
| 845 | # of the License, or (at your option) any later version. |
||
| 846 | 909:cbb26bc654de | Chris | # |
| 847 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 848 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 849 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 850 | # GNU General Public License for more details. |
||
| 851 | 909:cbb26bc654de | Chris | # |
| 852 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 853 | # along with this program; if not, write to the Free Software |
||
| 854 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 855 | |||
| 856 | module Redmine #:nodoc: |
||
| 857 | module CoreExtensions #:nodoc: |
||
| 858 | module String #:nodoc: |
||
| 859 | # Custom string inflections |
||
| 860 | module Inflections |
||
| 861 | def with_leading_slash |
||
| 862 | starts_with?('/') ? self : "/#{ self }"
|
||
| 863 | end |
||
| 864 | end |
||
| 865 | end |
||
| 866 | end |
||
| 867 | end |
||
| 868 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 869 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 870 | 0:513646585e45 | Chris | # |
| 871 | # This program is free software; you can redistribute it and/or |
||
| 872 | # modify it under the terms of the GNU General Public License |
||
| 873 | # as published by the Free Software Foundation; either version 2 |
||
| 874 | # of the License, or (at your option) any later version. |
||
| 875 | 909:cbb26bc654de | Chris | # |
| 876 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 877 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 878 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 879 | # GNU General Public License for more details. |
||
| 880 | 909:cbb26bc654de | Chris | # |
| 881 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 882 | # along with this program; if not, write to the Free Software |
||
| 883 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 884 | |||
| 885 | module Redmine |
||
| 886 | module DefaultData |
||
| 887 | class DataAlreadyLoaded < Exception; end |
||
| 888 | |||
| 889 | module Loader |
||
| 890 | include Redmine::I18n |
||
| 891 | 909:cbb26bc654de | Chris | |
| 892 | 0:513646585e45 | Chris | class << self |
| 893 | # Returns true if no data is already loaded in the database |
||
| 894 | # otherwise false |
||
| 895 | def no_data? |
||
| 896 | 1464:261b3d9a4903 | Chris | !Role.where(:builtin => 0).exists? && |
| 897 | !Tracker.exists? && |
||
| 898 | !IssueStatus.exists? && |
||
| 899 | !Enumeration.exists? |
||
| 900 | 0:513646585e45 | Chris | end |
| 901 | 909:cbb26bc654de | Chris | |
| 902 | 0:513646585e45 | Chris | # Loads the default data |
| 903 | # Raises a RecordNotSaved exception if something goes wrong |
||
| 904 | def load(lang=nil) |
||
| 905 | raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
|
||
| 906 | set_language_if_valid(lang) |
||
| 907 | 909:cbb26bc654de | Chris | |
| 908 | 0:513646585e45 | Chris | Role.transaction do |
| 909 | # Roles |
||
| 910 | 909:cbb26bc654de | Chris | manager = Role.create! :name => l(:default_role_manager), |
| 911 | 441:cbce1fd3b1b7 | Chris | :issues_visibility => 'all', |
| 912 | 0:513646585e45 | Chris | :position => 1 |
| 913 | manager.permissions = manager.setable_permissions.collect {|p| p.name}
|
||
| 914 | manager.save! |
||
| 915 | 909:cbb26bc654de | Chris | |
| 916 | developer = Role.create! :name => l(:default_role_developer), |
||
| 917 | :position => 2, |
||
| 918 | :permissions => [:manage_versions, |
||
| 919 | 0:513646585e45 | Chris | :manage_categories, |
| 920 | :view_issues, |
||
| 921 | :add_issues, |
||
| 922 | :edit_issues, |
||
| 923 | 1115:433d4f72a19b | Chris | :view_private_notes, |
| 924 | :set_notes_private, |
||
| 925 | 0:513646585e45 | Chris | :manage_issue_relations, |
| 926 | :manage_subtasks, |
||
| 927 | :add_issue_notes, |
||
| 928 | :save_queries, |
||
| 929 | :view_gantt, |
||
| 930 | :view_calendar, |
||
| 931 | :log_time, |
||
| 932 | :view_time_entries, |
||
| 933 | :comment_news, |
||
| 934 | :view_documents, |
||
| 935 | :view_wiki_pages, |
||
| 936 | :view_wiki_edits, |
||
| 937 | :edit_wiki_pages, |
||
| 938 | :delete_wiki_pages, |
||
| 939 | :add_messages, |
||
| 940 | :edit_own_messages, |
||
| 941 | :view_files, |
||
| 942 | :manage_files, |
||
| 943 | :browse_repository, |
||
| 944 | :view_changesets, |
||
| 945 | 1115:433d4f72a19b | Chris | :commit_access, |
| 946 | :manage_related_issues] |
||
| 947 | 909:cbb26bc654de | Chris | |
| 948 | 0:513646585e45 | Chris | reporter = Role.create! :name => l(:default_role_reporter), |
| 949 | :position => 3, |
||
| 950 | :permissions => [:view_issues, |
||
| 951 | :add_issues, |
||
| 952 | :add_issue_notes, |
||
| 953 | :save_queries, |
||
| 954 | :view_gantt, |
||
| 955 | :view_calendar, |
||
| 956 | :log_time, |
||
| 957 | :view_time_entries, |
||
| 958 | :comment_news, |
||
| 959 | :view_documents, |
||
| 960 | :view_wiki_pages, |
||
| 961 | :view_wiki_edits, |
||
| 962 | :add_messages, |
||
| 963 | :edit_own_messages, |
||
| 964 | :view_files, |
||
| 965 | :browse_repository, |
||
| 966 | :view_changesets] |
||
| 967 | 909:cbb26bc654de | Chris | |
| 968 | 0:513646585e45 | Chris | Role.non_member.update_attribute :permissions, [:view_issues, |
| 969 | :add_issues, |
||
| 970 | :add_issue_notes, |
||
| 971 | :save_queries, |
||
| 972 | :view_gantt, |
||
| 973 | :view_calendar, |
||
| 974 | :view_time_entries, |
||
| 975 | :comment_news, |
||
| 976 | :view_documents, |
||
| 977 | :view_wiki_pages, |
||
| 978 | :view_wiki_edits, |
||
| 979 | :add_messages, |
||
| 980 | :view_files, |
||
| 981 | :browse_repository, |
||
| 982 | :view_changesets] |
||
| 983 | 909:cbb26bc654de | Chris | |
| 984 | 0:513646585e45 | Chris | Role.anonymous.update_attribute :permissions, [:view_issues, |
| 985 | :view_gantt, |
||
| 986 | :view_calendar, |
||
| 987 | :view_time_entries, |
||
| 988 | :view_documents, |
||
| 989 | :view_wiki_pages, |
||
| 990 | :view_wiki_edits, |
||
| 991 | :view_files, |
||
| 992 | :browse_repository, |
||
| 993 | :view_changesets] |
||
| 994 | 909:cbb26bc654de | Chris | |
| 995 | 0:513646585e45 | Chris | # Trackers |
| 996 | Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1) |
||
| 997 | Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2) |
||
| 998 | Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3) |
||
| 999 | 909:cbb26bc654de | Chris | |
| 1000 | 0:513646585e45 | Chris | # Issue statuses |
| 1001 | new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1) |
||
| 1002 | in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2) |
||
| 1003 | resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3) |
||
| 1004 | feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4) |
||
| 1005 | closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5) |
||
| 1006 | rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6) |
||
| 1007 | 909:cbb26bc654de | Chris | |
| 1008 | 0:513646585e45 | Chris | # Workflow |
| 1009 | 1464:261b3d9a4903 | Chris | Tracker.all.each { |t|
|
| 1010 | IssueStatus.all.each { |os|
|
||
| 1011 | IssueStatus.all.each { |ns|
|
||
| 1012 | 1115:433d4f72a19b | Chris | WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns |
| 1013 | 909:cbb26bc654de | Chris | } |
| 1014 | } |
||
| 1015 | 0:513646585e45 | Chris | } |
| 1016 | 909:cbb26bc654de | Chris | |
| 1017 | 1464:261b3d9a4903 | Chris | Tracker.all.each { |t|
|
| 1018 | 0:513646585e45 | Chris | [new, in_progress, resolved, feedback].each { |os|
|
| 1019 | [in_progress, resolved, feedback, closed].each { |ns|
|
||
| 1020 | 1115:433d4f72a19b | Chris | WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns |
| 1021 | 909:cbb26bc654de | Chris | } |
| 1022 | } |
||
| 1023 | 0:513646585e45 | Chris | } |
| 1024 | 909:cbb26bc654de | Chris | |
| 1025 | 1464:261b3d9a4903 | Chris | Tracker.all.each { |t|
|
| 1026 | 0:513646585e45 | Chris | [new, in_progress, resolved, feedback].each { |os|
|
| 1027 | [closed].each { |ns|
|
||
| 1028 | 1115:433d4f72a19b | Chris | WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns |
| 1029 | 909:cbb26bc654de | Chris | } |
| 1030 | 0:513646585e45 | Chris | } |
| 1031 | 1115:433d4f72a19b | Chris | WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id) |
| 1032 | 0:513646585e45 | Chris | } |
| 1033 | 909:cbb26bc654de | Chris | |
| 1034 | 0:513646585e45 | Chris | # Enumerations |
| 1035 | IssuePriority.create!(:name => l(:default_priority_low), :position => 1) |
||
| 1036 | IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true) |
||
| 1037 | IssuePriority.create!(:name => l(:default_priority_high), :position => 3) |
||
| 1038 | IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4) |
||
| 1039 | IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5) |
||
| 1040 | 909:cbb26bc654de | Chris | |
| 1041 | 1115:433d4f72a19b | Chris | DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1) |
| 1042 | DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2) |
||
| 1043 | |||
| 1044 | 0:513646585e45 | Chris | TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1) |
| 1045 | TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2) |
||
| 1046 | end |
||
| 1047 | true |
||
| 1048 | end |
||
| 1049 | end |
||
| 1050 | end |
||
| 1051 | end |
||
| 1052 | end |
||
| 1053 | # encoding: utf-8 |
||
| 1054 | # |
||
| 1055 | # Redmine - project management software |
||
| 1056 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 1057 | 0:513646585e45 | Chris | # |
| 1058 | # This program is free software; you can redistribute it and/or |
||
| 1059 | # modify it under the terms of the GNU General Public License |
||
| 1060 | # as published by the Free Software Foundation; either version 2 |
||
| 1061 | # of the License, or (at your option) any later version. |
||
| 1062 | 441:cbce1fd3b1b7 | Chris | # |
| 1063 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 1064 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 1065 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 1066 | # GNU General Public License for more details. |
||
| 1067 | 441:cbce1fd3b1b7 | Chris | # |
| 1068 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 1069 | # along with this program; if not, write to the Free Software |
||
| 1070 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 1071 | |||
| 1072 | 1115:433d4f72a19b | Chris | require 'tcpdf' |
| 1073 | 441:cbce1fd3b1b7 | Chris | require 'fpdf/chinese' |
| 1074 | require 'fpdf/japanese' |
||
| 1075 | require 'fpdf/korean' |
||
| 1076 | 0:513646585e45 | Chris | |
| 1077 | 1464:261b3d9a4903 | Chris | if RUBY_VERSION < '1.9' |
| 1078 | require 'iconv' |
||
| 1079 | end |
||
| 1080 | |||
| 1081 | 0:513646585e45 | Chris | module Redmine |
| 1082 | module Export |
||
| 1083 | module PDF |
||
| 1084 | include ActionView::Helpers::TextHelper |
||
| 1085 | include ActionView::Helpers::NumberHelper |
||
| 1086 | 909:cbb26bc654de | Chris | include IssuesHelper |
| 1087 | 441:cbce1fd3b1b7 | Chris | |
| 1088 | class ITCPDF < TCPDF |
||
| 1089 | 0:513646585e45 | Chris | include Redmine::I18n |
| 1090 | attr_accessor :footer_date |
||
| 1091 | 441:cbce1fd3b1b7 | Chris | |
| 1092 | 1115:433d4f72a19b | Chris | def initialize(lang, orientation='P') |
| 1093 | 909:cbb26bc654de | Chris | @@k_path_cache = Rails.root.join('tmp', 'pdf')
|
| 1094 | FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache) |
||
| 1095 | 0:513646585e45 | Chris | set_language_if_valid lang |
| 1096 | 441:cbce1fd3b1b7 | Chris | pdf_encoding = l(:general_pdf_encoding).upcase |
| 1097 | 1115:433d4f72a19b | Chris | super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding) |
| 1098 | 507:0c939c159af4 | Chris | case current_language.to_s.downcase |
| 1099 | when 'vi' |
||
| 1100 | @font_for_content = 'DejaVuSans' |
||
| 1101 | @font_for_footer = 'DejaVuSans' |
||
| 1102 | 0:513646585e45 | Chris | else |
| 1103 | 507:0c939c159af4 | Chris | case pdf_encoding |
| 1104 | when 'UTF-8' |
||
| 1105 | @font_for_content = 'FreeSans' |
||
| 1106 | @font_for_footer = 'FreeSans' |
||
| 1107 | when 'CP949' |
||
| 1108 | extend(PDF_Korean) |
||
| 1109 | AddUHCFont() |
||
| 1110 | @font_for_content = 'UHC' |
||
| 1111 | @font_for_footer = 'UHC' |
||
| 1112 | when 'CP932', 'SJIS', 'SHIFT_JIS' |
||
| 1113 | extend(PDF_Japanese) |
||
| 1114 | AddSJISFont() |
||
| 1115 | @font_for_content = 'SJIS' |
||
| 1116 | @font_for_footer = 'SJIS' |
||
| 1117 | when 'GB18030' |
||
| 1118 | extend(PDF_Chinese) |
||
| 1119 | AddGBFont() |
||
| 1120 | @font_for_content = 'GB' |
||
| 1121 | @font_for_footer = 'GB' |
||
| 1122 | when 'BIG5' |
||
| 1123 | extend(PDF_Chinese) |
||
| 1124 | AddBig5Font() |
||
| 1125 | @font_for_content = 'Big5' |
||
| 1126 | @font_for_footer = 'Big5' |
||
| 1127 | else |
||
| 1128 | @font_for_content = 'Arial' |
||
| 1129 | @font_for_footer = 'Helvetica' |
||
| 1130 | end |
||
| 1131 | 0:513646585e45 | Chris | end |
| 1132 | SetCreator(Redmine::Info.app_name) |
||
| 1133 | SetFont(@font_for_content) |
||
| 1134 | 1115:433d4f72a19b | Chris | @outlines = [] |
| 1135 | @outlineRoot = nil |
||
| 1136 | 0:513646585e45 | Chris | end |
| 1137 | 441:cbce1fd3b1b7 | Chris | |
| 1138 | 0:513646585e45 | Chris | def SetFontStyle(style, size) |
| 1139 | SetFont(@font_for_content, style, size) |
||
| 1140 | end |
||
| 1141 | 441:cbce1fd3b1b7 | Chris | |
| 1142 | 0:513646585e45 | Chris | def SetTitle(txt) |
| 1143 | txt = begin |
||
| 1144 | 1464:261b3d9a4903 | Chris | utf16txt = to_utf16(txt) |
| 1145 | 0:513646585e45 | Chris | hextxt = "<FEFF" # FEFF is BOM |
| 1146 | hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
|
||
| 1147 | hextxt << ">" |
||
| 1148 | rescue |
||
| 1149 | txt |
||
| 1150 | end || '' |
||
| 1151 | super(txt) |
||
| 1152 | end |
||
| 1153 | 441:cbce1fd3b1b7 | Chris | |
| 1154 | 0:513646585e45 | Chris | def textstring(s) |
| 1155 | # Format a text string |
||
| 1156 | if s =~ /^</ # This means the string is hex-dumped. |
||
| 1157 | return s |
||
| 1158 | else |
||
| 1159 | return '('+escape(s)+')'
|
||
| 1160 | end |
||
| 1161 | end |
||
| 1162 | 441:cbce1fd3b1b7 | Chris | |
| 1163 | def fix_text_encoding(txt) |
||
| 1164 | 909:cbb26bc654de | Chris | RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding)) |
| 1165 | 0:513646585e45 | Chris | end |
| 1166 | 441:cbce1fd3b1b7 | Chris | |
| 1167 | 1294:3e4c3460b6ca | Chris | def formatted_text(text) |
| 1168 | html = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) |
||
| 1169 | # Strip {{toc}} tags
|
||
| 1170 | html.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
|
||
| 1171 | html |
||
| 1172 | end |
||
| 1173 | |||
| 1174 | 1464:261b3d9a4903 | Chris | # Encodes an UTF-8 string to UTF-16BE |
| 1175 | def to_utf16(str) |
||
| 1176 | if str.respond_to?(:encode) |
||
| 1177 | str.encode('UTF-16BE')
|
||
| 1178 | else |
||
| 1179 | Iconv.conv('UTF-16BE', 'UTF-8', str)
|
||
| 1180 | end |
||
| 1181 | end |
||
| 1182 | |||
| 1183 | 507:0c939c159af4 | Chris | def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='') |
| 1184 | Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link) |
||
| 1185 | 441:cbce1fd3b1b7 | Chris | end |
| 1186 | |||
| 1187 | 507:0c939c159af4 | Chris | def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1) |
| 1188 | MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln) |
||
| 1189 | 441:cbce1fd3b1b7 | Chris | end |
| 1190 | |||
| 1191 | 909:cbb26bc654de | Chris | def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) |
| 1192 | @attachments = attachments |
||
| 1193 | writeHTMLCell(w, h, x, y, |
||
| 1194 | 1294:3e4c3460b6ca | Chris | fix_text_encoding(formatted_text(txt)), |
| 1195 | 909:cbb26bc654de | Chris | border, ln, fill) |
| 1196 | end |
||
| 1197 | |||
| 1198 | def getImageFilename(attrname) |
||
| 1199 | # attrname: general_pdf_encoding string file/uri name |
||
| 1200 | atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding)) |
||
| 1201 | if atta |
||
| 1202 | return atta.diskfile |
||
| 1203 | else |
||
| 1204 | return nil |
||
| 1205 | end |
||
| 1206 | end |
||
| 1207 | |||
| 1208 | 0:513646585e45 | Chris | def Footer |
| 1209 | SetFont(@font_for_footer, 'I', 8) |
||
| 1210 | SetY(-15) |
||
| 1211 | SetX(15) |
||
| 1212 | 441:cbce1fd3b1b7 | Chris | RDMCell(0, 5, @footer_date, 0, 0, 'L') |
| 1213 | 0:513646585e45 | Chris | SetY(-15) |
| 1214 | SetX(-30) |
||
| 1215 | 441:cbce1fd3b1b7 | Chris | RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
|
| 1216 | 0:513646585e45 | Chris | end |
| 1217 | 1115:433d4f72a19b | Chris | |
| 1218 | def Bookmark(txt, level=0, y=0) |
||
| 1219 | if (y == -1) |
||
| 1220 | y = GetY() |
||
| 1221 | end |
||
| 1222 | @outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k}
|
||
| 1223 | end |
||
| 1224 | |||
| 1225 | def bookmark_title(txt) |
||
| 1226 | txt = begin |
||
| 1227 | 1464:261b3d9a4903 | Chris | utf16txt = to_utf16(txt) |
| 1228 | 1115:433d4f72a19b | Chris | hextxt = "<FEFF" # FEFF is BOM |
| 1229 | hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
|
||
| 1230 | hextxt << ">" |
||
| 1231 | rescue |
||
| 1232 | txt |
||
| 1233 | end || '' |
||
| 1234 | end |
||
| 1235 | |||
| 1236 | def putbookmarks |
||
| 1237 | nb=@outlines.size |
||
| 1238 | return if (nb==0) |
||
| 1239 | lru=[] |
||
| 1240 | level=0 |
||
| 1241 | @outlines.each_with_index do |o, i| |
||
| 1242 | if(o[:l]>0) |
||
| 1243 | parent=lru[o[:l]-1] |
||
| 1244 | #Set parent and last pointers |
||
| 1245 | @outlines[i][:parent]=parent |
||
| 1246 | @outlines[parent][:last]=i |
||
| 1247 | if (o[:l]>level) |
||
| 1248 | #Level increasing: set first pointer |
||
| 1249 | @outlines[parent][:first]=i |
||
| 1250 | end |
||
| 1251 | else |
||
| 1252 | @outlines[i][:parent]=nb |
||
| 1253 | end |
||
| 1254 | if (o[:l]<=level && i>0) |
||
| 1255 | #Set prev and next pointers |
||
| 1256 | prev=lru[o[:l]] |
||
| 1257 | @outlines[prev][:next]=i |
||
| 1258 | @outlines[i][:prev]=prev |
||
| 1259 | end |
||
| 1260 | lru[o[:l]]=i |
||
| 1261 | level=o[:l] |
||
| 1262 | end |
||
| 1263 | #Outline items |
||
| 1264 | n=self.n+1 |
||
| 1265 | @outlines.each_with_index do |o, i| |
||
| 1266 | newobj() |
||
| 1267 | out('<</Title '+bookmark_title(o[:t]))
|
||
| 1268 | out("/Parent #{n+o[:parent]} 0 R")
|
||
| 1269 | if (o[:prev]) |
||
| 1270 | out("/Prev #{n+o[:prev]} 0 R")
|
||
| 1271 | end |
||
| 1272 | if (o[:next]) |
||
| 1273 | out("/Next #{n+o[:next]} 0 R")
|
||
| 1274 | end |
||
| 1275 | if (o[:first]) |
||
| 1276 | out("/First #{n+o[:first]} 0 R")
|
||
| 1277 | end |
||
| 1278 | if (o[:last]) |
||
| 1279 | out("/Last #{n+o[:last]} 0 R")
|
||
| 1280 | end |
||
| 1281 | out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]])
|
||
| 1282 | out('/Count 0>>')
|
||
| 1283 | out('endobj')
|
||
| 1284 | end |
||
| 1285 | #Outline root |
||
| 1286 | newobj() |
||
| 1287 | @outlineRoot=self.n |
||
| 1288 | out("<</Type /Outlines /First #{n} 0 R");
|
||
| 1289 | out("/Last #{n+lru[0]} 0 R>>");
|
||
| 1290 | out('endobj');
|
||
| 1291 | end |
||
| 1292 | |||
| 1293 | def putresources() |
||
| 1294 | super |
||
| 1295 | putbookmarks() |
||
| 1296 | end |
||
| 1297 | |||
| 1298 | def putcatalog() |
||
| 1299 | super |
||
| 1300 | if(@outlines.size > 0) |
||
| 1301 | out("/Outlines #{@outlineRoot} 0 R");
|
||
| 1302 | out('/PageMode /UseOutlines');
|
||
| 1303 | end |
||
| 1304 | end |
||
| 1305 | end |
||
| 1306 | |||
| 1307 | # fetch row values |
||
| 1308 | def fetch_row_values(issue, query, level) |
||
| 1309 | query.inline_columns.collect do |column| |
||
| 1310 | s = if column.is_a?(QueryCustomFieldColumn) |
||
| 1311 | 1464:261b3d9a4903 | Chris | cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
|
| 1312 | 1517:dffacf8a6908 | Chris | show_value(cv, false) |
| 1313 | 1115:433d4f72a19b | Chris | else |
| 1314 | value = issue.send(column.name) |
||
| 1315 | if column.name == :subject |
||
| 1316 | value = " " * level + value |
||
| 1317 | end |
||
| 1318 | if value.is_a?(Date) |
||
| 1319 | format_date(value) |
||
| 1320 | elsif value.is_a?(Time) |
||
| 1321 | format_time(value) |
||
| 1322 | else |
||
| 1323 | value |
||
| 1324 | end |
||
| 1325 | end |
||
| 1326 | s.to_s |
||
| 1327 | end |
||
| 1328 | end |
||
| 1329 | |||
| 1330 | # calculate columns width |
||
| 1331 | def calc_col_width(issues, query, table_width, pdf) |
||
| 1332 | # calculate statistics |
||
| 1333 | # by captions |
||
| 1334 | pdf.SetFontStyle('B',8)
|
||
| 1335 | col_padding = pdf.GetStringWidth('OO')
|
||
| 1336 | col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding}
|
||
| 1337 | col_width_max = Array.new(col_width_min) |
||
| 1338 | col_width_avg = Array.new(col_width_min) |
||
| 1339 | word_width_max = query.inline_columns.map {|c|
|
||
| 1340 | n = 10 |
||
| 1341 | c.caption.split.each {|w|
|
||
| 1342 | x = pdf.GetStringWidth(w) + col_padding |
||
| 1343 | n = x if n < x |
||
| 1344 | } |
||
| 1345 | n |
||
| 1346 | } |
||
| 1347 | |||
| 1348 | # by properties of issues |
||
| 1349 | pdf.SetFontStyle('',8)
|
||
| 1350 | col_padding = pdf.GetStringWidth('OO')
|
||
| 1351 | k = 1 |
||
| 1352 | issue_list(issues) {|issue, level|
|
||
| 1353 | k += 1 |
||
| 1354 | values = fetch_row_values(issue, query, level) |
||
| 1355 | values.each_with_index {|v,i|
|
||
| 1356 | n = pdf.GetStringWidth(v) + col_padding |
||
| 1357 | col_width_max[i] = n if col_width_max[i] < n |
||
| 1358 | col_width_min[i] = n if col_width_min[i] > n |
||
| 1359 | col_width_avg[i] += n |
||
| 1360 | v.split.each {|w|
|
||
| 1361 | x = pdf.GetStringWidth(w) + col_padding |
||
| 1362 | word_width_max[i] = x if word_width_max[i] < x |
||
| 1363 | } |
||
| 1364 | } |
||
| 1365 | } |
||
| 1366 | col_width_avg.map! {|x| x / k}
|
||
| 1367 | |||
| 1368 | # calculate columns width |
||
| 1369 | 1464:261b3d9a4903 | Chris | ratio = table_width / col_width_avg.inject(0, :+) |
| 1370 | 1115:433d4f72a19b | Chris | col_width = col_width_avg.map {|w| w * ratio}
|
| 1371 | |||
| 1372 | # correct max word width if too many columns |
||
| 1373 | 1464:261b3d9a4903 | Chris | ratio = table_width / word_width_max.inject(0, :+) |
| 1374 | 1115:433d4f72a19b | Chris | word_width_max.map! {|v| v * ratio} if ratio < 1
|
| 1375 | |||
| 1376 | # correct and lock width of some columns |
||
| 1377 | done = 1 |
||
| 1378 | col_fix = [] |
||
| 1379 | col_width.each_with_index do |w,i| |
||
| 1380 | if w > col_width_max[i] |
||
| 1381 | col_width[i] = col_width_max[i] |
||
| 1382 | col_fix[i] = 1 |
||
| 1383 | done = 0 |
||
| 1384 | elsif w < word_width_max[i] |
||
| 1385 | col_width[i] = word_width_max[i] |
||
| 1386 | col_fix[i] = 1 |
||
| 1387 | done = 0 |
||
| 1388 | else |
||
| 1389 | col_fix[i] = 0 |
||
| 1390 | end |
||
| 1391 | end |
||
| 1392 | |||
| 1393 | # iterate while need to correct and lock coluns width |
||
| 1394 | while done == 0 |
||
| 1395 | # calculate free & locked columns width |
||
| 1396 | done = 1 |
||
| 1397 | fix_col_width = 0 |
||
| 1398 | free_col_width = 0 |
||
| 1399 | col_width.each_with_index do |w,i| |
||
| 1400 | if col_fix[i] == 1 |
||
| 1401 | fix_col_width += w |
||
| 1402 | else |
||
| 1403 | free_col_width += w |
||
| 1404 | end |
||
| 1405 | end |
||
| 1406 | |||
| 1407 | # calculate column normalizing ratio |
||
| 1408 | if free_col_width == 0 |
||
| 1409 | 1464:261b3d9a4903 | Chris | ratio = table_width / col_width.inject(0, :+) |
| 1410 | 1115:433d4f72a19b | Chris | else |
| 1411 | ratio = (table_width - fix_col_width) / free_col_width |
||
| 1412 | end |
||
| 1413 | |||
| 1414 | # correct columns width |
||
| 1415 | col_width.each_with_index do |w,i| |
||
| 1416 | if col_fix[i] == 0 |
||
| 1417 | col_width[i] = w * ratio |
||
| 1418 | |||
| 1419 | # check if column width less then max word width |
||
| 1420 | if col_width[i] < word_width_max[i] |
||
| 1421 | col_width[i] = word_width_max[i] |
||
| 1422 | col_fix[i] = 1 |
||
| 1423 | done = 0 |
||
| 1424 | elsif col_width[i] > col_width_max[i] |
||
| 1425 | col_width[i] = col_width_max[i] |
||
| 1426 | col_fix[i] = 1 |
||
| 1427 | done = 0 |
||
| 1428 | end |
||
| 1429 | end |
||
| 1430 | end |
||
| 1431 | end |
||
| 1432 | col_width |
||
| 1433 | end |
||
| 1434 | |||
| 1435 | 1464:261b3d9a4903 | Chris | def render_table_header(pdf, query, col_width, row_height, table_width) |
| 1436 | 1115:433d4f72a19b | Chris | # headers |
| 1437 | pdf.SetFontStyle('B',8)
|
||
| 1438 | pdf.SetFillColor(230, 230, 230) |
||
| 1439 | |||
| 1440 | # render it background to find the max height used |
||
| 1441 | base_x = pdf.GetX |
||
| 1442 | base_y = pdf.GetY |
||
| 1443 | max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) |
||
| 1444 | 1464:261b3d9a4903 | Chris | pdf.Rect(base_x, base_y, table_width, max_height, 'FD'); |
| 1445 | 1115:433d4f72a19b | Chris | pdf.SetXY(base_x, base_y); |
| 1446 | |||
| 1447 | # write the cells on page |
||
| 1448 | issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) |
||
| 1449 | 1464:261b3d9a4903 | Chris | issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width) |
| 1450 | 1115:433d4f72a19b | Chris | pdf.SetY(base_y + max_height); |
| 1451 | |||
| 1452 | # rows |
||
| 1453 | pdf.SetFontStyle('',8)
|
||
| 1454 | pdf.SetFillColor(255, 255, 255) |
||
| 1455 | 0:513646585e45 | Chris | end |
| 1456 | 441:cbce1fd3b1b7 | Chris | |
| 1457 | 0:513646585e45 | Chris | # Returns a PDF string of a list of issues |
| 1458 | def issues_to_pdf(issues, project, query) |
||
| 1459 | 1115:433d4f72a19b | Chris | pdf = ITCPDF.new(current_language, "L") |
| 1460 | 0:513646585e45 | Chris | title = query.new_record? ? l(:label_issue_plural) : query.name |
| 1461 | title = "#{project} - #{title}" if project
|
||
| 1462 | pdf.SetTitle(title) |
||
| 1463 | 441:cbce1fd3b1b7 | Chris | pdf.alias_nb_pages |
| 1464 | 0:513646585e45 | Chris | pdf.footer_date = format_date(Date.today) |
| 1465 | 441:cbce1fd3b1b7 | Chris | pdf.SetAutoPageBreak(false) |
| 1466 | 0:513646585e45 | Chris | pdf.AddPage("L")
|
| 1467 | 441:cbce1fd3b1b7 | Chris | |
| 1468 | # Landscape A4 = 210 x 297 mm |
||
| 1469 | page_height = 210 |
||
| 1470 | page_width = 297 |
||
| 1471 | 1464:261b3d9a4903 | Chris | left_margin = 10 |
| 1472 | 441:cbce1fd3b1b7 | Chris | right_margin = 10 |
| 1473 | bottom_margin = 20 |
||
| 1474 | 1115:433d4f72a19b | Chris | row_height = 4 |
| 1475 | 441:cbce1fd3b1b7 | Chris | |
| 1476 | # column widths |
||
| 1477 | 1464:261b3d9a4903 | Chris | table_width = page_width - right_margin - left_margin |
| 1478 | 0:513646585e45 | Chris | col_width = [] |
| 1479 | 1115:433d4f72a19b | Chris | unless query.inline_columns.empty? |
| 1480 | 1464:261b3d9a4903 | Chris | col_width = calc_col_width(issues, query, table_width, pdf) |
| 1481 | table_width = col_width.inject(0, :+) |
||
| 1482 | 1115:433d4f72a19b | Chris | end |
| 1483 | |||
| 1484 | 1464:261b3d9a4903 | Chris | # use full width if the description is displayed |
| 1485 | 1115:433d4f72a19b | Chris | if table_width > 0 && query.has_column?(:description) |
| 1486 | 1464:261b3d9a4903 | Chris | col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
|
| 1487 | table_width = col_width.inject(0, :+) |
||
| 1488 | 0:513646585e45 | Chris | end |
| 1489 | 441:cbce1fd3b1b7 | Chris | |
| 1490 | 0:513646585e45 | Chris | # title |
| 1491 | 441:cbce1fd3b1b7 | Chris | pdf.SetFontStyle('B',11)
|
| 1492 | pdf.RDMCell(190,10, title) |
||
| 1493 | 0:513646585e45 | Chris | pdf.Ln |
| 1494 | 1464:261b3d9a4903 | Chris | render_table_header(pdf, query, col_width, row_height, table_width) |
| 1495 | 0:513646585e45 | Chris | previous_group = false |
| 1496 | 909:cbb26bc654de | Chris | issue_list(issues) do |issue, level| |
| 1497 | 441:cbce1fd3b1b7 | Chris | if query.grouped? && |
| 1498 | (group = query.group_by_column.value(issue)) != previous_group |
||
| 1499 | 1115:433d4f72a19b | Chris | pdf.SetFontStyle('B',10)
|
| 1500 | group_label = group.blank? ? 'None' : group.to_s.dup |
||
| 1501 | group_label << " (#{query.issue_count_by_group[group]})"
|
||
| 1502 | pdf.Bookmark group_label, 0, -1 |
||
| 1503 | 1464:261b3d9a4903 | Chris | pdf.RDMCell(table_width, row_height * 2, group_label, 1, 1, 'L') |
| 1504 | 0:513646585e45 | Chris | pdf.SetFontStyle('',8)
|
| 1505 | previous_group = group |
||
| 1506 | end |
||
| 1507 | 1115:433d4f72a19b | Chris | |
| 1508 | # fetch row values |
||
| 1509 | col_values = fetch_row_values(issue, query, level) |
||
| 1510 | 441:cbce1fd3b1b7 | Chris | |
| 1511 | # render it off-page to find the max height used |
||
| 1512 | base_x = pdf.GetX |
||
| 1513 | base_y = pdf.GetY |
||
| 1514 | pdf.SetY(2 * page_height) |
||
| 1515 | max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) |
||
| 1516 | pdf.SetXY(base_x, base_y) |
||
| 1517 | |||
| 1518 | # make new page if it doesn't fit on the current one |
||
| 1519 | space_left = page_height - base_y - bottom_margin |
||
| 1520 | if max_height > space_left |
||
| 1521 | pdf.AddPage("L")
|
||
| 1522 | 1464:261b3d9a4903 | Chris | render_table_header(pdf, query, col_width, row_height, table_width) |
| 1523 | 441:cbce1fd3b1b7 | Chris | base_x = pdf.GetX |
| 1524 | base_y = pdf.GetY |
||
| 1525 | end |
||
| 1526 | |||
| 1527 | # write the cells on page |
||
| 1528 | issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) |
||
| 1529 | 1464:261b3d9a4903 | Chris | issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width) |
| 1530 | 441:cbce1fd3b1b7 | Chris | pdf.SetY(base_y + max_height); |
| 1531 | 1115:433d4f72a19b | Chris | |
| 1532 | if query.has_column?(:description) && issue.description? |
||
| 1533 | pdf.SetX(10) |
||
| 1534 | pdf.SetAutoPageBreak(true, 20) |
||
| 1535 | pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT") |
||
| 1536 | pdf.SetAutoPageBreak(false) |
||
| 1537 | end |
||
| 1538 | 0:513646585e45 | Chris | end |
| 1539 | 441:cbce1fd3b1b7 | Chris | |
| 1540 | 0:513646585e45 | Chris | if issues.size == Setting.issues_export_limit.to_i |
| 1541 | pdf.SetFontStyle('B',10)
|
||
| 1542 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(0, row_height, '...') |
| 1543 | 0:513646585e45 | Chris | end |
| 1544 | pdf.Output |
||
| 1545 | end |
||
| 1546 | 22:40f7cfd4df19 | chris | |
| 1547 | 441:cbce1fd3b1b7 | Chris | # Renders MultiCells and returns the maximum height used |
| 1548 | 1464:261b3d9a4903 | Chris | def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false) |
| 1549 | 441:cbce1fd3b1b7 | Chris | base_y = pdf.GetY |
| 1550 | max_height = row_height |
||
| 1551 | col_values.each_with_index do |column, i| |
||
| 1552 | col_x = pdf.GetX |
||
| 1553 | if head == true |
||
| 1554 | pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1) |
||
| 1555 | else |
||
| 1556 | pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1) |
||
| 1557 | end |
||
| 1558 | max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height |
||
| 1559 | pdf.SetXY(col_x + col_widths[i], base_y); |
||
| 1560 | end |
||
| 1561 | return max_height |
||
| 1562 | end |
||
| 1563 | |||
| 1564 | # Draw lines to close the row (MultiCell border drawing in not uniform) |
||
| 1565 | 1464:261b3d9a4903 | Chris | # |
| 1566 | # parameter "col_id_width" is not used. it is kept for compatibility. |
||
| 1567 | 441:cbce1fd3b1b7 | Chris | def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y, |
| 1568 | 1464:261b3d9a4903 | Chris | col_id_width, col_widths) |
| 1569 | col_x = top_x |
||
| 1570 | 441:cbce1fd3b1b7 | Chris | pdf.Line(col_x, top_y, col_x, lower_y) # id right border |
| 1571 | col_widths.each do |width| |
||
| 1572 | col_x += width |
||
| 1573 | pdf.Line(col_x, top_y, col_x, lower_y) # columns right border |
||
| 1574 | end |
||
| 1575 | pdf.Line(top_x, top_y, top_x, lower_y) # left border |
||
| 1576 | pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border |
||
| 1577 | end |
||
| 1578 | |||
| 1579 | 0:513646585e45 | Chris | # Returns a PDF string of a single issue |
| 1580 | 1115:433d4f72a19b | Chris | def issue_to_pdf(issue, assoc={})
|
| 1581 | 441:cbce1fd3b1b7 | Chris | pdf = ITCPDF.new(current_language) |
| 1582 | 1115:433d4f72a19b | Chris | pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}")
|
| 1583 | 441:cbce1fd3b1b7 | Chris | pdf.alias_nb_pages |
| 1584 | 0:513646585e45 | Chris | pdf.footer_date = format_date(Date.today) |
| 1585 | pdf.AddPage |
||
| 1586 | 441:cbce1fd3b1b7 | Chris | pdf.SetFontStyle('B',11)
|
| 1587 | 1115:433d4f72a19b | Chris | buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
|
| 1588 | 909:cbb26bc654de | Chris | pdf.RDMMultiCell(190, 5, buf) |
| 1589 | pdf.SetFontStyle('',8)
|
||
| 1590 | base_x = pdf.GetX |
||
| 1591 | i = 1 |
||
| 1592 | 1115:433d4f72a19b | Chris | issue.ancestors.visible.each do |ancestor| |
| 1593 | 909:cbb26bc654de | Chris | pdf.SetX(base_x + i) |
| 1594 | buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
|
||
| 1595 | pdf.RDMMultiCell(190 - i, 5, buf) |
||
| 1596 | i += 1 if i < 35 |
||
| 1597 | end |
||
| 1598 | 1115:433d4f72a19b | Chris | pdf.SetFontStyle('B',11)
|
| 1599 | pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) |
||
| 1600 | pdf.SetFontStyle('',8)
|
||
| 1601 | pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
|
||
| 1602 | 0:513646585e45 | Chris | pdf.Ln |
| 1603 | 441:cbce1fd3b1b7 | Chris | |
| 1604 | 1115:433d4f72a19b | Chris | left = [] |
| 1605 | left << [l(:field_status), issue.status] |
||
| 1606 | left << [l(:field_priority), issue.priority] |
||
| 1607 | left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
|
||
| 1608 | left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
|
||
| 1609 | left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
|
||
| 1610 | 441:cbce1fd3b1b7 | Chris | |
| 1611 | 1115:433d4f72a19b | Chris | right = [] |
| 1612 | right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
|
||
| 1613 | right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
|
||
| 1614 | right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
|
||
| 1615 | right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
|
||
| 1616 | right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) |
||
| 1617 | 441:cbce1fd3b1b7 | Chris | |
| 1618 | 1115:433d4f72a19b | Chris | rows = left.size > right.size ? left.size : right.size |
| 1619 | while left.size < rows |
||
| 1620 | left << nil |
||
| 1621 | end |
||
| 1622 | while right.size < rows |
||
| 1623 | right << nil |
||
| 1624 | 0:513646585e45 | Chris | end |
| 1625 | 441:cbce1fd3b1b7 | Chris | |
| 1626 | 1464:261b3d9a4903 | Chris | half = (issue.visible_custom_field_values.size / 2.0).ceil |
| 1627 | issue.visible_custom_field_values.each_with_index do |custom_value, i| |
||
| 1628 | 1517:dffacf8a6908 | Chris | (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] |
| 1629 | 1115:433d4f72a19b | Chris | end |
| 1630 | 507:0c939c159af4 | Chris | |
| 1631 | 1115:433d4f72a19b | Chris | rows = left.size > right.size ? left.size : right.size |
| 1632 | rows.times do |i| |
||
| 1633 | item = left[i] |
||
| 1634 | pdf.SetFontStyle('B',9)
|
||
| 1635 | pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
|
||
| 1636 | pdf.SetFontStyle('',9)
|
||
| 1637 | pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") |
||
| 1638 | |||
| 1639 | item = right[i] |
||
| 1640 | pdf.SetFontStyle('B',9)
|
||
| 1641 | pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
|
||
| 1642 | pdf.SetFontStyle('',9)
|
||
| 1643 | pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") |
||
| 1644 | pdf.Ln |
||
| 1645 | end |
||
| 1646 | 441:cbce1fd3b1b7 | Chris | |
| 1647 | 0:513646585e45 | Chris | pdf.SetFontStyle('B',9)
|
| 1648 | 507:0c939c159af4 | Chris | pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) |
| 1649 | 0:513646585e45 | Chris | pdf.SetFontStyle('',9)
|
| 1650 | 909:cbb26bc654de | Chris | |
| 1651 | # Set resize image scale |
||
| 1652 | pdf.SetImageScale(1.6) |
||
| 1653 | pdf.RDMwriteHTMLCell(35+155, 5, 0, 0, |
||
| 1654 | issue.description.to_s, issue.attachments, "LRB") |
||
| 1655 | |||
| 1656 | unless issue.leaf? |
||
| 1657 | # for CJK |
||
| 1658 | truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 ) |
||
| 1659 | pdf.SetFontStyle('B',9)
|
||
| 1660 | pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR") |
||
| 1661 | pdf.Ln |
||
| 1662 | 1115:433d4f72a19b | Chris | issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| |
| 1663 | 1517:dffacf8a6908 | Chris | buf = "#{child.tracker} # #{child.id}: #{child.subject}".
|
| 1664 | truncate(truncate_length) |
||
| 1665 | 909:cbb26bc654de | Chris | level = 10 if level >= 10 |
| 1666 | pdf.SetFontStyle('',8)
|
||
| 1667 | pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L") |
||
| 1668 | pdf.SetFontStyle('B',8)
|
||
| 1669 | pdf.RDMCell(20,5, child.status.to_s, "R") |
||
| 1670 | pdf.Ln |
||
| 1671 | end |
||
| 1672 | end |
||
| 1673 | |||
| 1674 | relations = issue.relations.select { |r| r.other_issue(issue).visible? }
|
||
| 1675 | unless relations.empty? |
||
| 1676 | # for CJK |
||
| 1677 | truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 ) |
||
| 1678 | pdf.SetFontStyle('B',9)
|
||
| 1679 | pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR") |
||
| 1680 | pdf.Ln |
||
| 1681 | relations.each do |relation| |
||
| 1682 | buf = "" |
||
| 1683 | buf += "#{l(relation.label_for(issue))} "
|
||
| 1684 | if relation.delay && relation.delay != 0 |
||
| 1685 | buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
|
||
| 1686 | end |
||
| 1687 | if Setting.cross_project_issue_relations? |
||
| 1688 | buf += "#{relation.other_issue(issue).project} - "
|
||
| 1689 | end |
||
| 1690 | buf += "#{relation.other_issue(issue).tracker}" +
|
||
| 1691 | " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
|
||
| 1692 | 1517:dffacf8a6908 | Chris | buf = buf.truncate(truncate_length) |
| 1693 | 909:cbb26bc654de | Chris | pdf.SetFontStyle('', 8)
|
| 1694 | pdf.RDMCell(35+155-60, 5, buf, "L") |
||
| 1695 | pdf.SetFontStyle('B',8)
|
||
| 1696 | pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "") |
||
| 1697 | pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "") |
||
| 1698 | pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R") |
||
| 1699 | pdf.Ln |
||
| 1700 | end |
||
| 1701 | end |
||
| 1702 | pdf.RDMCell(190,5, "", "T") |
||
| 1703 | 0:513646585e45 | Chris | pdf.Ln |
| 1704 | 441:cbce1fd3b1b7 | Chris | |
| 1705 | if issue.changesets.any? && |
||
| 1706 | User.current.allowed_to?(:view_changesets, issue.project) |
||
| 1707 | 0:513646585e45 | Chris | pdf.SetFontStyle('B',9)
|
| 1708 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(190,5, l(:label_associated_revisions), "B") |
| 1709 | 0:513646585e45 | Chris | pdf.Ln |
| 1710 | for changeset in issue.changesets |
||
| 1711 | pdf.SetFontStyle('B',8)
|
||
| 1712 | 507:0c939c159af4 | Chris | csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
|
| 1713 | csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s |
||
| 1714 | pdf.RDMCell(190, 5, csstr) |
||
| 1715 | 0:513646585e45 | Chris | pdf.Ln |
| 1716 | unless changeset.comments.blank? |
||
| 1717 | pdf.SetFontStyle('',8)
|
||
| 1718 | 909:cbb26bc654de | Chris | pdf.RDMwriteHTMLCell(190,5,0,0, |
| 1719 | changeset.comments.to_s, issue.attachments, "") |
||
| 1720 | 441:cbce1fd3b1b7 | Chris | end |
| 1721 | 0:513646585e45 | Chris | pdf.Ln |
| 1722 | end |
||
| 1723 | end |
||
| 1724 | 441:cbce1fd3b1b7 | Chris | |
| 1725 | 1115:433d4f72a19b | Chris | if assoc[:journals].present? |
| 1726 | pdf.SetFontStyle('B',9)
|
||
| 1727 | pdf.RDMCell(190,5, l(:label_history), "B") |
||
| 1728 | 0:513646585e45 | Chris | pdf.Ln |
| 1729 | 1115:433d4f72a19b | Chris | assoc[:journals].each do |journal| |
| 1730 | pdf.SetFontStyle('B',8)
|
||
| 1731 | title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
|
||
| 1732 | title << " (#{l(:field_private_notes)})" if journal.private_notes?
|
||
| 1733 | pdf.RDMCell(190,5, title) |
||
| 1734 | pdf.Ln |
||
| 1735 | pdf.SetFontStyle('I',8)
|
||
| 1736 | 1464:261b3d9a4903 | Chris | details_to_strings(journal.visible_details, true).each do |string| |
| 1737 | 1115:433d4f72a19b | Chris | pdf.RDMMultiCell(190,5, "- " + string) |
| 1738 | end |
||
| 1739 | if journal.notes? |
||
| 1740 | pdf.Ln unless journal.details.empty? |
||
| 1741 | pdf.SetFontStyle('',8)
|
||
| 1742 | pdf.RDMwriteHTMLCell(190,5,0,0, |
||
| 1743 | journal.notes.to_s, issue.attachments, "") |
||
| 1744 | end |
||
| 1745 | pdf.Ln |
||
| 1746 | 0:513646585e45 | Chris | end |
| 1747 | end |
||
| 1748 | 441:cbce1fd3b1b7 | Chris | |
| 1749 | 0:513646585e45 | Chris | if issue.attachments.any? |
| 1750 | pdf.SetFontStyle('B',9)
|
||
| 1751 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(190,5, l(:label_attachment_plural), "B") |
| 1752 | 0:513646585e45 | Chris | pdf.Ln |
| 1753 | for attachment in issue.attachments |
||
| 1754 | pdf.SetFontStyle('',8)
|
||
| 1755 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(80,5, attachment.filename) |
| 1756 | pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") |
||
| 1757 | pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") |
||
| 1758 | pdf.RDMCell(65,5, attachment.author.name,0,0,"R") |
||
| 1759 | 0:513646585e45 | Chris | pdf.Ln |
| 1760 | end |
||
| 1761 | end |
||
| 1762 | pdf.Output |
||
| 1763 | end |
||
| 1764 | 22:40f7cfd4df19 | chris | |
| 1765 | 1115:433d4f72a19b | Chris | # Returns a PDF string of a set of wiki pages |
| 1766 | def wiki_pages_to_pdf(pages, project) |
||
| 1767 | pdf = ITCPDF.new(current_language) |
||
| 1768 | pdf.SetTitle(project.name) |
||
| 1769 | pdf.alias_nb_pages |
||
| 1770 | pdf.footer_date = format_date(Date.today) |
||
| 1771 | pdf.AddPage |
||
| 1772 | pdf.SetFontStyle('B',11)
|
||
| 1773 | pdf.RDMMultiCell(190,5, project.name) |
||
| 1774 | pdf.Ln |
||
| 1775 | # Set resize image scale |
||
| 1776 | pdf.SetImageScale(1.6) |
||
| 1777 | pdf.SetFontStyle('',9)
|
||
| 1778 | write_page_hierarchy(pdf, pages.group_by(&:parent_id)) |
||
| 1779 | pdf.Output |
||
| 1780 | end |
||
| 1781 | |||
| 1782 | 909:cbb26bc654de | Chris | # Returns a PDF string of a single wiki page |
| 1783 | 1115:433d4f72a19b | Chris | def wiki_page_to_pdf(page, project) |
| 1784 | 909:cbb26bc654de | Chris | pdf = ITCPDF.new(current_language) |
| 1785 | pdf.SetTitle("#{project} - #{page.title}")
|
||
| 1786 | pdf.alias_nb_pages |
||
| 1787 | pdf.footer_date = format_date(Date.today) |
||
| 1788 | pdf.AddPage |
||
| 1789 | pdf.SetFontStyle('B',11)
|
||
| 1790 | pdf.RDMMultiCell(190,5, |
||
| 1791 | "#{project} - #{page.title} - # #{page.content.version}")
|
||
| 1792 | pdf.Ln |
||
| 1793 | # Set resize image scale |
||
| 1794 | pdf.SetImageScale(1.6) |
||
| 1795 | pdf.SetFontStyle('',9)
|
||
| 1796 | 1115:433d4f72a19b | Chris | write_wiki_page(pdf, page) |
| 1797 | pdf.Output |
||
| 1798 | end |
||
| 1799 | |||
| 1800 | def write_page_hierarchy(pdf, pages, node=nil, level=0) |
||
| 1801 | if pages[node] |
||
| 1802 | pages[node].each do |page| |
||
| 1803 | if @new_page |
||
| 1804 | pdf.AddPage |
||
| 1805 | else |
||
| 1806 | @new_page = true |
||
| 1807 | end |
||
| 1808 | pdf.Bookmark page.title, level |
||
| 1809 | write_wiki_page(pdf, page) |
||
| 1810 | write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id] |
||
| 1811 | end |
||
| 1812 | end |
||
| 1813 | end |
||
| 1814 | |||
| 1815 | def write_wiki_page(pdf, page) |
||
| 1816 | 909:cbb26bc654de | Chris | pdf.RDMwriteHTMLCell(190,5,0,0, |
| 1817 | 1115:433d4f72a19b | Chris | page.content.text.to_s, page.attachments, 0) |
| 1818 | 909:cbb26bc654de | Chris | if page.attachments.any? |
| 1819 | pdf.Ln |
||
| 1820 | pdf.SetFontStyle('B',9)
|
||
| 1821 | pdf.RDMCell(190,5, l(:label_attachment_plural), "B") |
||
| 1822 | pdf.Ln |
||
| 1823 | for attachment in page.attachments |
||
| 1824 | pdf.SetFontStyle('',8)
|
||
| 1825 | pdf.RDMCell(80,5, attachment.filename) |
||
| 1826 | pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") |
||
| 1827 | pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") |
||
| 1828 | pdf.RDMCell(65,5, attachment.author.name,0,0,"R") |
||
| 1829 | pdf.Ln |
||
| 1830 | end |
||
| 1831 | end |
||
| 1832 | end |
||
| 1833 | |||
| 1834 | 441:cbce1fd3b1b7 | Chris | class RDMPdfEncoding |
| 1835 | 909:cbb26bc654de | Chris | def self.rdm_from_utf8(txt, encoding) |
| 1836 | 441:cbce1fd3b1b7 | Chris | txt ||= '' |
| 1837 | 909:cbb26bc654de | Chris | txt = Redmine::CodesetUtil.from_utf8(txt, encoding) |
| 1838 | 441:cbce1fd3b1b7 | Chris | if txt.respond_to?(:force_encoding) |
| 1839 | txt.force_encoding('ASCII-8BIT')
|
||
| 1840 | end |
||
| 1841 | txt |
||
| 1842 | end |
||
| 1843 | 909:cbb26bc654de | Chris | |
| 1844 | def self.attach(attachments, filename, encoding) |
||
| 1845 | filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding) |
||
| 1846 | atta = nil |
||
| 1847 | if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i |
||
| 1848 | atta = Attachment.latest_attach(attachments, filename_utf8) |
||
| 1849 | end |
||
| 1850 | if atta && atta.readable? && atta.visible? |
||
| 1851 | return atta |
||
| 1852 | else |
||
| 1853 | return nil |
||
| 1854 | end |
||
| 1855 | end |
||
| 1856 | 441:cbce1fd3b1b7 | Chris | end |
| 1857 | 0:513646585e45 | Chris | end |
| 1858 | end |
||
| 1859 | end |
||
| 1860 | 1517:dffacf8a6908 | Chris | # Redmine - project management software |
| 1861 | # Copyright (C) 2006-2014 Jean-Philippe Lang |
||
| 1862 | # |
||
| 1863 | # This program is free software; you can redistribute it and/or |
||
| 1864 | # modify it under the terms of the GNU General Public License |
||
| 1865 | # as published by the Free Software Foundation; either version 2 |
||
| 1866 | # of the License, or (at your option) any later version. |
||
| 1867 | # |
||
| 1868 | # This program is distributed in the hope that it will be useful, |
||
| 1869 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 1870 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 1871 | # GNU General Public License for more details. |
||
| 1872 | # |
||
| 1873 | # You should have received a copy of the GNU General Public License |
||
| 1874 | # along with this program; if not, write to the Free Software |
||
| 1875 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 1876 | |||
| 1877 | module Redmine |
||
| 1878 | module FieldFormat |
||
| 1879 | def self.add(name, klass) |
||
| 1880 | all[name.to_s] = klass.instance |
||
| 1881 | end |
||
| 1882 | |||
| 1883 | def self.delete(name) |
||
| 1884 | all.delete(name.to_s) |
||
| 1885 | end |
||
| 1886 | |||
| 1887 | def self.all |
||
| 1888 | @formats ||= Hash.new(Base.instance) |
||
| 1889 | end |
||
| 1890 | |||
| 1891 | def self.available_formats |
||
| 1892 | all.keys |
||
| 1893 | end |
||
| 1894 | |||
| 1895 | def self.find(name) |
||
| 1896 | all[name.to_s] |
||
| 1897 | end |
||
| 1898 | |||
| 1899 | # Return an array of custom field formats which can be used in select_tag |
||
| 1900 | def self.as_select(class_name=nil) |
||
| 1901 | formats = all.values.select do |format| |
||
| 1902 | format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name) |
||
| 1903 | end |
||
| 1904 | formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
|
||
| 1905 | end |
||
| 1906 | |||
| 1907 | class Base |
||
| 1908 | include Singleton |
||
| 1909 | include Redmine::I18n |
||
| 1910 | include ERB::Util |
||
| 1911 | |||
| 1912 | class_attribute :format_name |
||
| 1913 | self.format_name = nil |
||
| 1914 | |||
| 1915 | # Set this to true if the format supports multiple values |
||
| 1916 | class_attribute :multiple_supported |
||
| 1917 | self.multiple_supported = false |
||
| 1918 | |||
| 1919 | # Set this to true if the format supports textual search on custom values |
||
| 1920 | class_attribute :searchable_supported |
||
| 1921 | self.searchable_supported = false |
||
| 1922 | |||
| 1923 | # Restricts the classes that the custom field can be added to |
||
| 1924 | # Set to nil for no restrictions |
||
| 1925 | class_attribute :customized_class_names |
||
| 1926 | self.customized_class_names = nil |
||
| 1927 | |||
| 1928 | # Name of the partial for editing the custom field |
||
| 1929 | class_attribute :form_partial |
||
| 1930 | self.form_partial = nil |
||
| 1931 | |||
| 1932 | def self.add(name) |
||
| 1933 | self.format_name = name |
||
| 1934 | Redmine::FieldFormat.add(name, self) |
||
| 1935 | end |
||
| 1936 | private_class_method :add |
||
| 1937 | |||
| 1938 | def self.field_attributes(*args) |
||
| 1939 | CustomField.store_accessor :format_store, *args |
||
| 1940 | end |
||
| 1941 | |||
| 1942 | field_attributes :url_pattern |
||
| 1943 | |||
| 1944 | def name |
||
| 1945 | self.class.format_name |
||
| 1946 | end |
||
| 1947 | |||
| 1948 | def label |
||
| 1949 | "label_#{name}"
|
||
| 1950 | end |
||
| 1951 | |||
| 1952 | def cast_custom_value(custom_value) |
||
| 1953 | cast_value(custom_value.custom_field, custom_value.value, custom_value.customized) |
||
| 1954 | end |
||
| 1955 | |||
| 1956 | def cast_value(custom_field, value, customized=nil) |
||
| 1957 | if value.blank? |
||
| 1958 | nil |
||
| 1959 | elsif value.is_a?(Array) |
||
| 1960 | casted = value.map do |v| |
||
| 1961 | cast_single_value(custom_field, v, customized) |
||
| 1962 | end |
||
| 1963 | casted.compact.sort |
||
| 1964 | else |
||
| 1965 | cast_single_value(custom_field, value, customized) |
||
| 1966 | end |
||
| 1967 | end |
||
| 1968 | |||
| 1969 | def cast_single_value(custom_field, value, customized=nil) |
||
| 1970 | value.to_s |
||
| 1971 | end |
||
| 1972 | |||
| 1973 | def target_class |
||
| 1974 | nil |
||
| 1975 | end |
||
| 1976 | |||
| 1977 | def possible_custom_value_options(custom_value) |
||
| 1978 | possible_values_options(custom_value.custom_field, custom_value.customized) |
||
| 1979 | end |
||
| 1980 | |||
| 1981 | def possible_values_options(custom_field, object=nil) |
||
| 1982 | [] |
||
| 1983 | end |
||
| 1984 | |||
| 1985 | # Returns the validation errors for custom_field |
||
| 1986 | # Should return an empty array if custom_field is valid |
||
| 1987 | def validate_custom_field(custom_field) |
||
| 1988 | [] |
||
| 1989 | end |
||
| 1990 | |||
| 1991 | # Returns the validation error messages for custom_value |
||
| 1992 | # Should return an empty array if custom_value is valid |
||
| 1993 | def validate_custom_value(custom_value) |
||
| 1994 | values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
|
||
| 1995 | errors = values.map do |value| |
||
| 1996 | validate_single_value(custom_value.custom_field, value, custom_value.customized) |
||
| 1997 | end |
||
| 1998 | errors.flatten.uniq |
||
| 1999 | end |
||
| 2000 | |||
| 2001 | def validate_single_value(custom_field, value, customized=nil) |
||
| 2002 | [] |
||
| 2003 | end |
||
| 2004 | |||
| 2005 | def formatted_custom_value(view, custom_value, html=false) |
||
| 2006 | formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html) |
||
| 2007 | end |
||
| 2008 | |||
| 2009 | def formatted_value(view, custom_field, value, customized=nil, html=false) |
||
| 2010 | casted = cast_value(custom_field, value, customized) |
||
| 2011 | if html && custom_field.url_pattern.present? |
||
| 2012 | texts_and_urls = Array.wrap(casted).map do |single_value| |
||
| 2013 | text = view.format_object(single_value, false).to_s |
||
| 2014 | url = url_from_pattern(custom_field, single_value, customized) |
||
| 2015 | [text, url] |
||
| 2016 | end |
||
| 2017 | links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
|
||
| 2018 | links.join(', ').html_safe
|
||
| 2019 | else |
||
| 2020 | casted |
||
| 2021 | end |
||
| 2022 | end |
||
| 2023 | |||
| 2024 | # Returns an URL generated with the custom field URL pattern |
||
| 2025 | # and variables substitution: |
||
| 2026 | # %value% => the custom field value |
||
| 2027 | # %id% => id of the customized object |
||
| 2028 | # %project_id% => id of the project of the customized object if defined |
||
| 2029 | # %project_identifier% => identifier of the project of the customized object if defined |
||
| 2030 | # %m1%, %m2%... => capture groups matches of the custom field regexp if defined |
||
| 2031 | def url_from_pattern(custom_field, value, customized) |
||
| 2032 | url = custom_field.url_pattern.to_s.dup |
||
| 2033 | url.gsub!('%value%') {value.to_s}
|
||
| 2034 | url.gsub!('%id%') {customized.id.to_s}
|
||
| 2035 | url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
|
||
| 2036 | url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
|
||
| 2037 | if custom_field.regexp.present? |
||
| 2038 | url.gsub!(%r{%m(\d+)%}) do
|
||
| 2039 | m = $1.to_i |
||
| 2040 | if matches ||= value.to_s.match(Regexp.new(custom_field.regexp)) |
||
| 2041 | matches[m].to_s |
||
| 2042 | end |
||
| 2043 | end |
||
| 2044 | end |
||
| 2045 | url |
||
| 2046 | end |
||
| 2047 | protected :url_from_pattern |
||
| 2048 | |||
| 2049 | def edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2050 | view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id)) |
||
| 2051 | end |
||
| 2052 | |||
| 2053 | def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
|
||
| 2054 | view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) + |
||
| 2055 | bulk_clear_tag(view, tag_id, tag_name, custom_field, value) |
||
| 2056 | end |
||
| 2057 | |||
| 2058 | def bulk_clear_tag(view, tag_id, tag_name, custom_field, value) |
||
| 2059 | if custom_field.is_required? |
||
| 2060 | ''.html_safe |
||
| 2061 | else |
||
| 2062 | view.content_tag('label',
|
||
| 2063 | view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
|
||
| 2064 | :class => 'inline' |
||
| 2065 | ) |
||
| 2066 | end |
||
| 2067 | end |
||
| 2068 | protected :bulk_clear_tag |
||
| 2069 | |||
| 2070 | def query_filter_options(custom_field, query) |
||
| 2071 | {:type => :string}
|
||
| 2072 | end |
||
| 2073 | |||
| 2074 | def before_custom_field_save(custom_field) |
||
| 2075 | end |
||
| 2076 | |||
| 2077 | # Returns a ORDER BY clause that can used to sort customized |
||
| 2078 | # objects by their value of the custom field. |
||
| 2079 | # Returns nil if the custom field can not be used for sorting. |
||
| 2080 | def order_statement(custom_field) |
||
| 2081 | # COALESCE is here to make sure that blank and NULL values are sorted equally |
||
| 2082 | "COALESCE(#{join_alias custom_field}.value, '')"
|
||
| 2083 | end |
||
| 2084 | |||
| 2085 | # Returns a GROUP BY clause that can used to group by custom value |
||
| 2086 | # Returns nil if the custom field can not be used for grouping. |
||
| 2087 | def group_statement(custom_field) |
||
| 2088 | nil |
||
| 2089 | end |
||
| 2090 | |||
| 2091 | # Returns a JOIN clause that is added to the query when sorting by custom values |
||
| 2092 | def join_for_order_statement(custom_field) |
||
| 2093 | alias_name = join_alias(custom_field) |
||
| 2094 | |||
| 2095 | "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
|
||
| 2096 | " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
|
||
| 2097 | " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
|
||
| 2098 | " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
|
||
| 2099 | " AND (#{custom_field.visibility_by_project_condition})" +
|
||
| 2100 | " AND #{alias_name}.value <> ''" +
|
||
| 2101 | " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
|
||
| 2102 | " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
|
||
| 2103 | " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
|
||
| 2104 | " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
|
||
| 2105 | end |
||
| 2106 | |||
| 2107 | def join_alias(custom_field) |
||
| 2108 | "cf_#{custom_field.id}"
|
||
| 2109 | end |
||
| 2110 | protected :join_alias |
||
| 2111 | end |
||
| 2112 | |||
| 2113 | class Unbounded < Base |
||
| 2114 | def validate_single_value(custom_field, value, customized=nil) |
||
| 2115 | errs = super |
||
| 2116 | value = value.to_s |
||
| 2117 | unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) |
||
| 2118 | errs << ::I18n.t('activerecord.errors.messages.invalid')
|
||
| 2119 | end |
||
| 2120 | if custom_field.min_length && value.length < custom_field.min_length |
||
| 2121 | errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
|
||
| 2122 | end |
||
| 2123 | if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length |
||
| 2124 | errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
|
||
| 2125 | end |
||
| 2126 | errs |
||
| 2127 | end |
||
| 2128 | end |
||
| 2129 | |||
| 2130 | class StringFormat < Unbounded |
||
| 2131 | add 'string' |
||
| 2132 | self.searchable_supported = true |
||
| 2133 | self.form_partial = 'custom_fields/formats/string' |
||
| 2134 | field_attributes :text_formatting |
||
| 2135 | |||
| 2136 | def formatted_value(view, custom_field, value, customized=nil, html=false) |
||
| 2137 | if html |
||
| 2138 | if custom_field.url_pattern.present? |
||
| 2139 | super |
||
| 2140 | elsif custom_field.text_formatting == 'full' |
||
| 2141 | view.textilizable(value, :object => customized) |
||
| 2142 | else |
||
| 2143 | value.to_s |
||
| 2144 | end |
||
| 2145 | else |
||
| 2146 | value.to_s |
||
| 2147 | end |
||
| 2148 | end |
||
| 2149 | end |
||
| 2150 | |||
| 2151 | class TextFormat < Unbounded |
||
| 2152 | add 'text' |
||
| 2153 | self.searchable_supported = true |
||
| 2154 | self.form_partial = 'custom_fields/formats/text' |
||
| 2155 | |||
| 2156 | def formatted_value(view, custom_field, value, customized=nil, html=false) |
||
| 2157 | if html |
||
| 2158 | if custom_field.text_formatting == 'full' |
||
| 2159 | view.textilizable(value, :object => customized) |
||
| 2160 | else |
||
| 2161 | view.simple_format(html_escape(value)) |
||
| 2162 | end |
||
| 2163 | else |
||
| 2164 | value.to_s |
||
| 2165 | end |
||
| 2166 | end |
||
| 2167 | |||
| 2168 | def edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2169 | view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3)) |
||
| 2170 | end |
||
| 2171 | |||
| 2172 | def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
|
||
| 2173 | view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) + |
||
| 2174 | '<br />'.html_safe + |
||
| 2175 | bulk_clear_tag(view, tag_id, tag_name, custom_field, value) |
||
| 2176 | end |
||
| 2177 | |||
| 2178 | def query_filter_options(custom_field, query) |
||
| 2179 | {:type => :text}
|
||
| 2180 | end |
||
| 2181 | end |
||
| 2182 | |||
| 2183 | class LinkFormat < StringFormat |
||
| 2184 | add 'link' |
||
| 2185 | self.searchable_supported = false |
||
| 2186 | self.form_partial = 'custom_fields/formats/link' |
||
| 2187 | |||
| 2188 | def formatted_value(view, custom_field, value, customized=nil, html=false) |
||
| 2189 | if html |
||
| 2190 | if custom_field.url_pattern.present? |
||
| 2191 | url = url_from_pattern(custom_field, value, customized) |
||
| 2192 | else |
||
| 2193 | url = value.to_s |
||
| 2194 | unless url =~ %r{\A[a-z]+://}i
|
||
| 2195 | # no protocol found, use http by default |
||
| 2196 | url = "http://" + url |
||
| 2197 | end |
||
| 2198 | end |
||
| 2199 | view.link_to value.to_s, url |
||
| 2200 | else |
||
| 2201 | value.to_s |
||
| 2202 | end |
||
| 2203 | end |
||
| 2204 | end |
||
| 2205 | |||
| 2206 | class Numeric < Unbounded |
||
| 2207 | self.form_partial = 'custom_fields/formats/numeric' |
||
| 2208 | |||
| 2209 | def order_statement(custom_field) |
||
| 2210 | # Make the database cast values into numeric |
||
| 2211 | # Postgresql will raise an error if a value can not be casted! |
||
| 2212 | # CustomValue validations should ensure that it doesn't occur |
||
| 2213 | "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
|
||
| 2214 | end |
||
| 2215 | end |
||
| 2216 | |||
| 2217 | class IntFormat < Numeric |
||
| 2218 | add 'int' |
||
| 2219 | |||
| 2220 | def label |
||
| 2221 | "label_integer" |
||
| 2222 | end |
||
| 2223 | |||
| 2224 | def cast_single_value(custom_field, value, customized=nil) |
||
| 2225 | value.to_i |
||
| 2226 | end |
||
| 2227 | |||
| 2228 | def validate_single_value(custom_field, value, customized=nil) |
||
| 2229 | errs = super |
||
| 2230 | errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
|
||
| 2231 | errs |
||
| 2232 | end |
||
| 2233 | |||
| 2234 | def query_filter_options(custom_field, query) |
||
| 2235 | {:type => :integer}
|
||
| 2236 | end |
||
| 2237 | |||
| 2238 | def group_statement(custom_field) |
||
| 2239 | order_statement(custom_field) |
||
| 2240 | end |
||
| 2241 | end |
||
| 2242 | |||
| 2243 | class FloatFormat < Numeric |
||
| 2244 | add 'float' |
||
| 2245 | |||
| 2246 | def cast_single_value(custom_field, value, customized=nil) |
||
| 2247 | value.to_f |
||
| 2248 | end |
||
| 2249 | |||
| 2250 | def validate_single_value(custom_field, value, customized=nil) |
||
| 2251 | errs = super |
||
| 2252 | errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
|
||
| 2253 | errs |
||
| 2254 | end |
||
| 2255 | |||
| 2256 | def query_filter_options(custom_field, query) |
||
| 2257 | {:type => :float}
|
||
| 2258 | end |
||
| 2259 | end |
||
| 2260 | |||
| 2261 | class DateFormat < Unbounded |
||
| 2262 | add 'date' |
||
| 2263 | self.form_partial = 'custom_fields/formats/date' |
||
| 2264 | |||
| 2265 | def cast_single_value(custom_field, value, customized=nil) |
||
| 2266 | value.to_date rescue nil |
||
| 2267 | end |
||
| 2268 | |||
| 2269 | def validate_single_value(custom_field, value, customized=nil) |
||
| 2270 | if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
|
||
| 2271 | [] |
||
| 2272 | else |
||
| 2273 | [::I18n.t('activerecord.errors.messages.not_a_date')]
|
||
| 2274 | end |
||
| 2275 | end |
||
| 2276 | |||
| 2277 | def edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2278 | view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) + |
||
| 2279 | view.calendar_for(tag_id) |
||
| 2280 | end |
||
| 2281 | |||
| 2282 | def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
|
||
| 2283 | view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) + |
||
| 2284 | view.calendar_for(tag_id) + |
||
| 2285 | bulk_clear_tag(view, tag_id, tag_name, custom_field, value) |
||
| 2286 | end |
||
| 2287 | |||
| 2288 | def query_filter_options(custom_field, query) |
||
| 2289 | {:type => :date}
|
||
| 2290 | end |
||
| 2291 | |||
| 2292 | def group_statement(custom_field) |
||
| 2293 | order_statement(custom_field) |
||
| 2294 | end |
||
| 2295 | end |
||
| 2296 | |||
| 2297 | class List < Base |
||
| 2298 | self.multiple_supported = true |
||
| 2299 | field_attributes :edit_tag_style |
||
| 2300 | |||
| 2301 | def edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2302 | if custom_value.custom_field.edit_tag_style == 'check_box' |
||
| 2303 | check_box_edit_tag(view, tag_id, tag_name, custom_value, options) |
||
| 2304 | else |
||
| 2305 | select_edit_tag(view, tag_id, tag_name, custom_value, options) |
||
| 2306 | end |
||
| 2307 | end |
||
| 2308 | |||
| 2309 | def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
|
||
| 2310 | opts = [] |
||
| 2311 | opts << [l(:label_no_change_option), ''] unless custom_field.multiple? |
||
| 2312 | opts << [l(:label_none), '__none__'] unless custom_field.is_required? |
||
| 2313 | opts += possible_values_options(custom_field, objects) |
||
| 2314 | view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?)) |
||
| 2315 | end |
||
| 2316 | |||
| 2317 | def query_filter_options(custom_field, query) |
||
| 2318 | {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
|
||
| 2319 | end |
||
| 2320 | |||
| 2321 | protected |
||
| 2322 | |||
| 2323 | # Renders the edit tag as a select tag |
||
| 2324 | def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2325 | blank_option = ''.html_safe |
||
| 2326 | unless custom_value.custom_field.multiple? |
||
| 2327 | if custom_value.custom_field.is_required? |
||
| 2328 | unless custom_value.custom_field.default_value.present? |
||
| 2329 | blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
|
||
| 2330 | end |
||
| 2331 | else |
||
| 2332 | blank_option = view.content_tag('option', ' '.html_safe, :value => '')
|
||
| 2333 | end |
||
| 2334 | end |
||
| 2335 | options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value) |
||
| 2336 | s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?)) |
||
| 2337 | if custom_value.custom_field.multiple? |
||
| 2338 | s << view.hidden_field_tag(tag_name, '') |
||
| 2339 | end |
||
| 2340 | s |
||
| 2341 | end |
||
| 2342 | |||
| 2343 | # Renders the edit tag as check box or radio tags |
||
| 2344 | def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2345 | opts = [] |
||
| 2346 | unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required? |
||
| 2347 | opts << ["(#{l(:label_none)})", '']
|
||
| 2348 | end |
||
| 2349 | opts += possible_custom_value_options(custom_value) |
||
| 2350 | s = ''.html_safe |
||
| 2351 | tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag |
||
| 2352 | opts.each do |label, value| |
||
| 2353 | value ||= label |
||
| 2354 | checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value |
||
| 2355 | tag = view.send(tag_method, tag_name, value, checked, :id => tag_id) |
||
| 2356 | # set the id on the first tag only |
||
| 2357 | tag_id = nil |
||
| 2358 | s << view.content_tag('label', tag + ' ' + label)
|
||
| 2359 | end |
||
| 2360 | if custom_value.custom_field.multiple? |
||
| 2361 | s << view.hidden_field_tag(tag_name, '') |
||
| 2362 | end |
||
| 2363 | css = "#{options[:class]} check_box_group"
|
||
| 2364 | view.content_tag('span', s, options.merge(:class => css))
|
||
| 2365 | end |
||
| 2366 | end |
||
| 2367 | |||
| 2368 | class ListFormat < List |
||
| 2369 | add 'list' |
||
| 2370 | self.searchable_supported = true |
||
| 2371 | self.form_partial = 'custom_fields/formats/list' |
||
| 2372 | |||
| 2373 | def possible_custom_value_options(custom_value) |
||
| 2374 | options = possible_values_options(custom_value.custom_field) |
||
| 2375 | missing = [custom_value.value].flatten.reject(&:blank?) - options |
||
| 2376 | if missing.any? |
||
| 2377 | options += missing |
||
| 2378 | end |
||
| 2379 | options |
||
| 2380 | end |
||
| 2381 | |||
| 2382 | def possible_values_options(custom_field, object=nil) |
||
| 2383 | custom_field.possible_values |
||
| 2384 | end |
||
| 2385 | |||
| 2386 | def validate_custom_field(custom_field) |
||
| 2387 | errors = [] |
||
| 2388 | errors << [:possible_values, :blank] if custom_field.possible_values.blank? |
||
| 2389 | errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array |
||
| 2390 | errors |
||
| 2391 | end |
||
| 2392 | |||
| 2393 | def validate_custom_value(custom_value) |
||
| 2394 | values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
|
||
| 2395 | invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values |
||
| 2396 | if invalid_values.any? |
||
| 2397 | [::I18n.t('activerecord.errors.messages.inclusion')]
|
||
| 2398 | else |
||
| 2399 | [] |
||
| 2400 | end |
||
| 2401 | end |
||
| 2402 | |||
| 2403 | def group_statement(custom_field) |
||
| 2404 | order_statement(custom_field) |
||
| 2405 | end |
||
| 2406 | end |
||
| 2407 | |||
| 2408 | class BoolFormat < List |
||
| 2409 | add 'bool' |
||
| 2410 | self.multiple_supported = false |
||
| 2411 | self.form_partial = 'custom_fields/formats/bool' |
||
| 2412 | |||
| 2413 | def label |
||
| 2414 | "label_boolean" |
||
| 2415 | end |
||
| 2416 | |||
| 2417 | def cast_single_value(custom_field, value, customized=nil) |
||
| 2418 | value == '1' ? true : false |
||
| 2419 | end |
||
| 2420 | |||
| 2421 | def possible_values_options(custom_field, object=nil) |
||
| 2422 | [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']] |
||
| 2423 | end |
||
| 2424 | |||
| 2425 | def group_statement(custom_field) |
||
| 2426 | order_statement(custom_field) |
||
| 2427 | end |
||
| 2428 | |||
| 2429 | def edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2430 | case custom_value.custom_field.edit_tag_style |
||
| 2431 | when 'check_box' |
||
| 2432 | single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options) |
||
| 2433 | when 'radio' |
||
| 2434 | check_box_edit_tag(view, tag_id, tag_name, custom_value, options) |
||
| 2435 | else |
||
| 2436 | select_edit_tag(view, tag_id, tag_name, custom_value, options) |
||
| 2437 | end |
||
| 2438 | end |
||
| 2439 | |||
| 2440 | # Renders the edit tag as a simple check box |
||
| 2441 | def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
|
||
| 2442 | s = ''.html_safe |
||
| 2443 | s << view.hidden_field_tag(tag_name, '0', :id => nil) |
||
| 2444 | s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id) |
||
| 2445 | view.content_tag('span', s, options)
|
||
| 2446 | end |
||
| 2447 | end |
||
| 2448 | |||
| 2449 | class RecordList < List |
||
| 2450 | self.customized_class_names = %w(Issue TimeEntry Version Project) |
||
| 2451 | |||
| 2452 | def cast_single_value(custom_field, value, customized=nil) |
||
| 2453 | target_class.find_by_id(value.to_i) if value.present? |
||
| 2454 | end |
||
| 2455 | |||
| 2456 | def target_class |
||
| 2457 | @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil |
||
| 2458 | end |
||
| 2459 | |||
| 2460 | def possible_custom_value_options(custom_value) |
||
| 2461 | options = possible_values_options(custom_value.custom_field, custom_value.customized) |
||
| 2462 | missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last) |
||
| 2463 | if missing.any? |
||
| 2464 | options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
|
||
| 2465 | #TODO: use #sort_by! when ruby1.8 support is dropped |
||
| 2466 | options = options.sort_by(&:first) |
||
| 2467 | end |
||
| 2468 | options |
||
| 2469 | end |
||
| 2470 | |||
| 2471 | def order_statement(custom_field) |
||
| 2472 | if target_class.respond_to?(:fields_for_order_statement) |
||
| 2473 | target_class.fields_for_order_statement(value_join_alias(custom_field)) |
||
| 2474 | end |
||
| 2475 | end |
||
| 2476 | |||
| 2477 | def group_statement(custom_field) |
||
| 2478 | "COALESCE(#{join_alias custom_field}.value, '')"
|
||
| 2479 | end |
||
| 2480 | |||
| 2481 | def join_for_order_statement(custom_field) |
||
| 2482 | alias_name = join_alias(custom_field) |
||
| 2483 | |||
| 2484 | "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
|
||
| 2485 | " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
|
||
| 2486 | " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
|
||
| 2487 | " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
|
||
| 2488 | " AND (#{custom_field.visibility_by_project_condition})" +
|
||
| 2489 | " AND #{alias_name}.value <> ''" +
|
||
| 2490 | " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
|
||
| 2491 | " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
|
||
| 2492 | " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
|
||
| 2493 | " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
|
||
| 2494 | " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
|
||
| 2495 | " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
|
||
| 2496 | end |
||
| 2497 | |||
| 2498 | def value_join_alias(custom_field) |
||
| 2499 | join_alias(custom_field) + "_" + custom_field.field_format |
||
| 2500 | end |
||
| 2501 | protected :value_join_alias |
||
| 2502 | end |
||
| 2503 | |||
| 2504 | class UserFormat < RecordList |
||
| 2505 | add 'user' |
||
| 2506 | self.form_partial = 'custom_fields/formats/user' |
||
| 2507 | field_attributes :user_role |
||
| 2508 | |||
| 2509 | def possible_values_options(custom_field, object=nil) |
||
| 2510 | if object.is_a?(Array) |
||
| 2511 | projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
|
||
| 2512 | projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
|
||
| 2513 | elsif object.respond_to?(:project) && object.project |
||
| 2514 | scope = object.project.users |
||
| 2515 | if custom_field.user_role.is_a?(Array) |
||
| 2516 | role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i) |
||
| 2517 | if role_ids.any? |
||
| 2518 | scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
|
||
| 2519 | end |
||
| 2520 | end |
||
| 2521 | scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
|
||
| 2522 | else |
||
| 2523 | [] |
||
| 2524 | end |
||
| 2525 | end |
||
| 2526 | |||
| 2527 | def before_custom_field_save(custom_field) |
||
| 2528 | super |
||
| 2529 | if custom_field.user_role.is_a?(Array) |
||
| 2530 | custom_field.user_role.map!(&:to_s).reject!(&:blank?) |
||
| 2531 | end |
||
| 2532 | end |
||
| 2533 | end |
||
| 2534 | |||
| 2535 | class VersionFormat < RecordList |
||
| 2536 | add 'version' |
||
| 2537 | self.form_partial = 'custom_fields/formats/version' |
||
| 2538 | field_attributes :version_status |
||
| 2539 | |||
| 2540 | def possible_values_options(custom_field, object=nil) |
||
| 2541 | if object.is_a?(Array) |
||
| 2542 | projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
|
||
| 2543 | projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
|
||
| 2544 | elsif object.respond_to?(:project) && object.project |
||
| 2545 | scope = object.project.shared_versions |
||
| 2546 | if custom_field.version_status.is_a?(Array) |
||
| 2547 | statuses = custom_field.version_status.map(&:to_s).reject(&:blank?) |
||
| 2548 | if statuses.any? |
||
| 2549 | scope = scope.where(:status => statuses.map(&:to_s)) |
||
| 2550 | end |
||
| 2551 | end |
||
| 2552 | scope.sort.collect {|u| [u.to_s, u.id.to_s]}
|
||
| 2553 | else |
||
| 2554 | [] |
||
| 2555 | end |
||
| 2556 | end |
||
| 2557 | |||
| 2558 | def before_custom_field_save(custom_field) |
||
| 2559 | super |
||
| 2560 | if custom_field.version_status.is_a?(Array) |
||
| 2561 | custom_field.version_status.map!(&:to_s).reject!(&:blank?) |
||
| 2562 | end |
||
| 2563 | end |
||
| 2564 | end |
||
| 2565 | end |
||
| 2566 | end |
||
| 2567 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 2568 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 2569 | 0:513646585e45 | Chris | # |
| 2570 | # This program is free software; you can redistribute it and/or |
||
| 2571 | # modify it under the terms of the GNU General Public License |
||
| 2572 | # as published by the Free Software Foundation; either version 2 |
||
| 2573 | # of the License, or (at your option) any later version. |
||
| 2574 | 909:cbb26bc654de | Chris | # |
| 2575 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 2576 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 2577 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 2578 | # GNU General Public License for more details. |
||
| 2579 | 909:cbb26bc654de | Chris | # |
| 2580 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 2581 | # along with this program; if not, write to the Free Software |
||
| 2582 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 2583 | |||
| 2584 | module Redmine |
||
| 2585 | module Helpers |
||
| 2586 | 909:cbb26bc654de | Chris | |
| 2587 | 0:513646585e45 | Chris | # Simple class to compute the start and end dates of a calendar |
| 2588 | class Calendar |
||
| 2589 | include Redmine::I18n |
||
| 2590 | attr_reader :startdt, :enddt |
||
| 2591 | 909:cbb26bc654de | Chris | |
| 2592 | 0:513646585e45 | Chris | def initialize(date, lang = current_language, period = :month) |
| 2593 | @date = date |
||
| 2594 | @events = [] |
||
| 2595 | @ending_events_by_days = {}
|
||
| 2596 | @starting_events_by_days = {}
|
||
| 2597 | 909:cbb26bc654de | Chris | set_language_if_valid lang |
| 2598 | 0:513646585e45 | Chris | case period |
| 2599 | when :month |
||
| 2600 | @startdt = Date.civil(date.year, date.month, 1) |
||
| 2601 | @enddt = (@startdt >> 1)-1 |
||
| 2602 | # starts from the first day of the week |
||
| 2603 | @startdt = @startdt - (@startdt.cwday - first_wday)%7 |
||
| 2604 | # ends on the last day of the week |
||
| 2605 | @enddt = @enddt + (last_wday - @enddt.cwday)%7 |
||
| 2606 | when :week |
||
| 2607 | @startdt = date - (date.cwday - first_wday)%7 |
||
| 2608 | @enddt = date + (last_wday - date.cwday)%7 |
||
| 2609 | else |
||
| 2610 | raise 'Invalid period' |
||
| 2611 | end |
||
| 2612 | end |
||
| 2613 | 909:cbb26bc654de | Chris | |
| 2614 | 0:513646585e45 | Chris | # Sets calendar events |
| 2615 | def events=(events) |
||
| 2616 | @events = events |
||
| 2617 | @ending_events_by_days = @events.group_by {|event| event.due_date}
|
||
| 2618 | @starting_events_by_days = @events.group_by {|event| event.start_date}
|
||
| 2619 | end |
||
| 2620 | 909:cbb26bc654de | Chris | |
| 2621 | 0:513646585e45 | Chris | # Returns events for the given day |
| 2622 | def events_on(day) |
||
| 2623 | ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq |
||
| 2624 | end |
||
| 2625 | 909:cbb26bc654de | Chris | |
| 2626 | 0:513646585e45 | Chris | # Calendar current month |
| 2627 | def month |
||
| 2628 | @date.month |
||
| 2629 | end |
||
| 2630 | 909:cbb26bc654de | Chris | |
| 2631 | 0:513646585e45 | Chris | # Return the first day of week |
| 2632 | # 1 = Monday ... 7 = Sunday |
||
| 2633 | def first_wday |
||
| 2634 | case Setting.start_of_week.to_i |
||
| 2635 | when 1 |
||
| 2636 | @first_dow ||= (1 - 1)%7 + 1 |
||
| 2637 | 441:cbce1fd3b1b7 | Chris | when 6 |
| 2638 | @first_dow ||= (6 - 1)%7 + 1 |
||
| 2639 | 0:513646585e45 | Chris | when 7 |
| 2640 | @first_dow ||= (7 - 1)%7 + 1 |
||
| 2641 | else |
||
| 2642 | @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1 |
||
| 2643 | end |
||
| 2644 | end |
||
| 2645 | 909:cbb26bc654de | Chris | |
| 2646 | 0:513646585e45 | Chris | def last_wday |
| 2647 | @last_dow ||= (first_wday + 5)%7 + 1 |
||
| 2648 | end |
||
| 2649 | 909:cbb26bc654de | Chris | end |
| 2650 | 0:513646585e45 | Chris | end |
| 2651 | end |
||
| 2652 | 245:051f544170fe | Chris | # Redmine - project management software |
| 2653 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 2654 | 245:051f544170fe | Chris | # |
| 2655 | # This program is free software; you can redistribute it and/or |
||
| 2656 | # modify it under the terms of the GNU General Public License |
||
| 2657 | # as published by the Free Software Foundation; either version 2 |
||
| 2658 | # of the License, or (at your option) any later version. |
||
| 2659 | 909:cbb26bc654de | Chris | # |
| 2660 | 245:051f544170fe | Chris | # This program is distributed in the hope that it will be useful, |
| 2661 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 2662 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 2663 | # GNU General Public License for more details. |
||
| 2664 | 909:cbb26bc654de | Chris | # |
| 2665 | 245:051f544170fe | Chris | # You should have received a copy of the GNU General Public License |
| 2666 | # along with this program; if not, write to the Free Software |
||
| 2667 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 2668 | |||
| 2669 | 1464:261b3d9a4903 | Chris | require 'diff' |
| 2670 | |||
| 2671 | 245:051f544170fe | Chris | module Redmine |
| 2672 | module Helpers |
||
| 2673 | class Diff |
||
| 2674 | include ERB::Util |
||
| 2675 | include ActionView::Helpers::TagHelper |
||
| 2676 | include ActionView::Helpers::TextHelper |
||
| 2677 | attr_reader :diff, :words |
||
| 2678 | 909:cbb26bc654de | Chris | |
| 2679 | 245:051f544170fe | Chris | def initialize(content_to, content_from) |
| 2680 | @words = content_to.to_s.split(/(\s+)/) |
||
| 2681 | @words = @words.select {|word| word != ' '}
|
||
| 2682 | words_from = content_from.to_s.split(/(\s+)/) |
||
| 2683 | 909:cbb26bc654de | Chris | words_from = words_from.select {|word| word != ' '}
|
| 2684 | 245:051f544170fe | Chris | @diff = words_from.diff @words |
| 2685 | end |
||
| 2686 | 909:cbb26bc654de | Chris | |
| 2687 | 245:051f544170fe | Chris | def to_html |
| 2688 | words = self.words.collect{|word| h(word)}
|
||
| 2689 | words_add = 0 |
||
| 2690 | words_del = 0 |
||
| 2691 | dels = 0 |
||
| 2692 | del_off = 0 |
||
| 2693 | diff.diffs.each do |diff| |
||
| 2694 | add_at = nil |
||
| 2695 | add_to = nil |
||
| 2696 | del_at = nil |
||
| 2697 | 909:cbb26bc654de | Chris | deleted = "" |
| 2698 | 245:051f544170fe | Chris | diff.each do |change| |
| 2699 | pos = change[1] |
||
| 2700 | if change[0] == "+" |
||
| 2701 | add_at = pos + dels unless add_at |
||
| 2702 | add_to = pos + dels |
||
| 2703 | words_add += 1 |
||
| 2704 | else |
||
| 2705 | del_at = pos unless del_at |
||
| 2706 | 1115:433d4f72a19b | Chris | deleted << ' ' unless deleted.empty? |
| 2707 | deleted << h(change[2]) |
||
| 2708 | 245:051f544170fe | Chris | words_del += 1 |
| 2709 | end |
||
| 2710 | end |
||
| 2711 | if add_at |
||
| 2712 | 1115:433d4f72a19b | Chris | words[add_at] = '<span class="diff_in">'.html_safe + words[add_at] |
| 2713 | words[add_to] = words[add_to] + '</span>'.html_safe |
||
| 2714 | 245:051f544170fe | Chris | end |
| 2715 | if del_at |
||
| 2716 | 1115:433d4f72a19b | Chris | words.insert del_at - del_off + dels + words_add, '<span class="diff_out">'.html_safe + deleted + '</span>'.html_safe |
| 2717 | 245:051f544170fe | Chris | dels += 1 |
| 2718 | del_off += words_del |
||
| 2719 | words_del = 0 |
||
| 2720 | end |
||
| 2721 | end |
||
| 2722 | 909:cbb26bc654de | Chris | words.join(' ').html_safe
|
| 2723 | 245:051f544170fe | Chris | end |
| 2724 | end |
||
| 2725 | end |
||
| 2726 | end |
||
| 2727 | 0:513646585e45 | Chris | # Redmine - project management software |
| 2728 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 2729 | 0:513646585e45 | Chris | # |
| 2730 | # This program is free software; you can redistribute it and/or |
||
| 2731 | # modify it under the terms of the GNU General Public License |
||
| 2732 | # as published by the Free Software Foundation; either version 2 |
||
| 2733 | # of the License, or (at your option) any later version. |
||
| 2734 | 441:cbce1fd3b1b7 | Chris | # |
| 2735 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 2736 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 2737 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 2738 | # GNU General Public License for more details. |
||
| 2739 | 441:cbce1fd3b1b7 | Chris | # |
| 2740 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 2741 | # along with this program; if not, write to the Free Software |
||
| 2742 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 2743 | |||
| 2744 | module Redmine |
||
| 2745 | module Helpers |
||
| 2746 | # Simple class to handle gantt chart data |
||
| 2747 | class Gantt |
||
| 2748 | 22:40f7cfd4df19 | chris | include ERB::Util |
| 2749 | include Redmine::I18n |
||
| 2750 | 1115:433d4f72a19b | Chris | include Redmine::Utils::DateCalculation |
| 2751 | 22:40f7cfd4df19 | chris | |
| 2752 | 1464:261b3d9a4903 | Chris | # Relation types that are rendered |
| 2753 | DRAW_TYPES = {
|
||
| 2754 | IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
|
||
| 2755 | IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
|
||
| 2756 | }.freeze |
||
| 2757 | |||
| 2758 | 22:40f7cfd4df19 | chris | # :nodoc: |
| 2759 | # Some utility methods for the PDF export |
||
| 2760 | class PDF |
||
| 2761 | MaxCharactorsForSubject = 45 |
||
| 2762 | TotalWidth = 280 |
||
| 2763 | LeftPaneWidth = 100 |
||
| 2764 | |||
| 2765 | def self.right_pane_width |
||
| 2766 | TotalWidth - LeftPaneWidth |
||
| 2767 | end |
||
| 2768 | end |
||
| 2769 | |||
| 2770 | 119:8661b858af72 | Chris | attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows |
| 2771 | 22:40f7cfd4df19 | chris | attr_accessor :query |
| 2772 | attr_accessor :project |
||
| 2773 | attr_accessor :view |
||
| 2774 | 441:cbce1fd3b1b7 | Chris | |
| 2775 | 0:513646585e45 | Chris | def initialize(options={})
|
| 2776 | options = options.dup |
||
| 2777 | if options[:year] && options[:year].to_i >0 |
||
| 2778 | @year_from = options[:year].to_i |
||
| 2779 | if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 |
||
| 2780 | @month_from = options[:month].to_i |
||
| 2781 | else |
||
| 2782 | @month_from = 1 |
||
| 2783 | end |
||
| 2784 | else |
||
| 2785 | @month_from ||= Date.today.month |
||
| 2786 | @year_from ||= Date.today.year |
||
| 2787 | end |
||
| 2788 | zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i |
||
| 2789 | 441:cbce1fd3b1b7 | Chris | @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 |
| 2790 | 0:513646585e45 | Chris | months = (options[:months] || User.current.pref[:gantt_months]).to_i |
| 2791 | @months = (months > 0 && months < 25) ? months : 6 |
||
| 2792 | # Save gantt parameters as user preference (zoom and months count) |
||
| 2793 | 1115:433d4f72a19b | Chris | if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || |
| 2794 | @months != User.current.pref[:gantt_months])) |
||
| 2795 | 0:513646585e45 | Chris | User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months |
| 2796 | User.current.preference.save |
||
| 2797 | end |
||
| 2798 | @date_from = Date.civil(@year_from, @month_from, 1) |
||
| 2799 | @date_to = (@date_from >> @months) - 1 |
||
| 2800 | 119:8661b858af72 | Chris | @subjects = '' |
| 2801 | @lines = '' |
||
| 2802 | @number_of_rows = nil |
||
| 2803 | @issue_ancestors = [] |
||
| 2804 | @truncated = false |
||
| 2805 | if options.has_key?(:max_rows) |
||
| 2806 | @max_rows = options[:max_rows] |
||
| 2807 | else |
||
| 2808 | @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i |
||
| 2809 | end |
||
| 2810 | 0:513646585e45 | Chris | end |
| 2811 | 22:40f7cfd4df19 | chris | |
| 2812 | def common_params |
||
| 2813 | { :controller => 'gantts', :action => 'show', :project_id => @project }
|
||
| 2814 | 0:513646585e45 | Chris | end |
| 2815 | 441:cbce1fd3b1b7 | Chris | |
| 2816 | 0:513646585e45 | Chris | def params |
| 2817 | 1115:433d4f72a19b | Chris | common_params.merge({:zoom => zoom, :year => year_from,
|
| 2818 | :month => month_from, :months => months}) |
||
| 2819 | 0:513646585e45 | Chris | end |
| 2820 | 441:cbce1fd3b1b7 | Chris | |
| 2821 | 0:513646585e45 | Chris | def params_previous |
| 2822 | 1115:433d4f72a19b | Chris | common_params.merge({:year => (date_from << months).year,
|
| 2823 | :month => (date_from << months).month, |
||
| 2824 | :zoom => zoom, :months => months}) |
||
| 2825 | 0:513646585e45 | Chris | end |
| 2826 | 441:cbce1fd3b1b7 | Chris | |
| 2827 | 0:513646585e45 | Chris | def params_next |
| 2828 | 1115:433d4f72a19b | Chris | common_params.merge({:year => (date_from >> months).year,
|
| 2829 | :month => (date_from >> months).month, |
||
| 2830 | :zoom => zoom, :months => months}) |
||
| 2831 | 0:513646585e45 | Chris | end |
| 2832 | 22:40f7cfd4df19 | chris | |
| 2833 | # Returns the number of rows that will be rendered on the Gantt chart |
||
| 2834 | def number_of_rows |
||
| 2835 | 119:8661b858af72 | Chris | return @number_of_rows if @number_of_rows |
| 2836 | 441:cbce1fd3b1b7 | Chris | rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
|
| 2837 | 119:8661b858af72 | Chris | rows > @max_rows ? @max_rows : rows |
| 2838 | 22:40f7cfd4df19 | chris | end |
| 2839 | |||
| 2840 | # Returns the number of rows that will be used to list a project on |
||
| 2841 | # the Gantt chart. This will recurse for each subproject. |
||
| 2842 | def number_of_rows_on_project(project) |
||
| 2843 | 441:cbce1fd3b1b7 | Chris | return 0 unless projects.include?(project) |
| 2844 | 22:40f7cfd4df19 | chris | count = 1 |
| 2845 | 441:cbce1fd3b1b7 | Chris | count += project_issues(project).size |
| 2846 | count += project_versions(project).size |
||
| 2847 | 22:40f7cfd4df19 | chris | count |
| 2848 | end |
||
| 2849 | |||
| 2850 | # Renders the subjects of the Gantt chart, the left side. |
||
| 2851 | def subjects(options={})
|
||
| 2852 | 119:8661b858af72 | Chris | render(options.merge(:only => :subjects)) unless @subjects_rendered |
| 2853 | @subjects |
||
| 2854 | 22:40f7cfd4df19 | chris | end |
| 2855 | |||
| 2856 | # Renders the lines of the Gantt chart, the right side |
||
| 2857 | def lines(options={})
|
||
| 2858 | 119:8661b858af72 | Chris | render(options.merge(:only => :lines)) unless @lines_rendered |
| 2859 | @lines |
||
| 2860 | end |
||
| 2861 | 441:cbce1fd3b1b7 | Chris | |
| 2862 | # Returns issues that will be rendered |
||
| 2863 | def issues |
||
| 2864 | @issues ||= @query.issues( |
||
| 2865 | :include => [:assigned_to, :tracker, :priority, :category, :fixed_version], |
||
| 2866 | :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
|
||
| 2867 | :limit => @max_rows |
||
| 2868 | ) |
||
| 2869 | end |
||
| 2870 | |||
| 2871 | 1464:261b3d9a4903 | Chris | # Returns a hash of the relations between the issues that are present on the gantt |
| 2872 | # and that should be displayed, grouped by issue ids. |
||
| 2873 | def relations |
||
| 2874 | return @relations if @relations |
||
| 2875 | if issues.any? |
||
| 2876 | issue_ids = issues.map(&:id) |
||
| 2877 | @relations = IssueRelation. |
||
| 2878 | where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). |
||
| 2879 | group_by(&:issue_from_id) |
||
| 2880 | else |
||
| 2881 | @relations = {}
|
||
| 2882 | end |
||
| 2883 | end |
||
| 2884 | |||
| 2885 | 441:cbce1fd3b1b7 | Chris | # Return all the project nodes that will be displayed |
| 2886 | def projects |
||
| 2887 | return @projects if @projects |
||
| 2888 | ids = issues.collect(&:project).uniq.collect(&:id) |
||
| 2889 | if ids.any? |
||
| 2890 | # All issues projects and their visible ancestors |
||
| 2891 | 1464:261b3d9a4903 | Chris | @projects = Project.visible. |
| 2892 | joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt").
|
||
| 2893 | where("child.id IN (?)", ids).
|
||
| 2894 | order("#{Project.table_name}.lft ASC").
|
||
| 2895 | uniq. |
||
| 2896 | all |
||
| 2897 | 441:cbce1fd3b1b7 | Chris | else |
| 2898 | @projects = [] |
||
| 2899 | end |
||
| 2900 | end |
||
| 2901 | |||
| 2902 | # Returns the issues that belong to +project+ |
||
| 2903 | def project_issues(project) |
||
| 2904 | @issues_by_project ||= issues.group_by(&:project) |
||
| 2905 | @issues_by_project[project] || [] |
||
| 2906 | end |
||
| 2907 | |||
| 2908 | # Returns the distinct versions of the issues that belong to +project+ |
||
| 2909 | def project_versions(project) |
||
| 2910 | project_issues(project).collect(&:fixed_version).compact.uniq |
||
| 2911 | end |
||
| 2912 | |||
| 2913 | # Returns the issues that belong to +project+ and are assigned to +version+ |
||
| 2914 | def version_issues(project, version) |
||
| 2915 | project_issues(project).select {|issue| issue.fixed_version == version}
|
||
| 2916 | end |
||
| 2917 | |||
| 2918 | 119:8661b858af72 | Chris | def render(options={})
|
| 2919 | 1115:433d4f72a19b | Chris | options = {:top => 0, :top_increment => 20,
|
| 2920 | :indent_increment => 20, :render => :subject, |
||
| 2921 | :format => :html}.merge(options) |
||
| 2922 | 441:cbce1fd3b1b7 | Chris | indent = options[:indent] || 4 |
| 2923 | 119:8661b858af72 | Chris | @subjects = '' unless options[:only] == :lines |
| 2924 | @lines = '' unless options[:only] == :subjects |
||
| 2925 | @number_of_rows = 0 |
||
| 2926 | 441:cbce1fd3b1b7 | Chris | Project.project_tree(projects) do |project, level| |
| 2927 | options[:indent] = indent + level * options[:indent_increment] |
||
| 2928 | render_project(project, options) |
||
| 2929 | break if abort? |
||
| 2930 | 22:40f7cfd4df19 | chris | end |
| 2931 | 119:8661b858af72 | Chris | @subjects_rendered = true unless options[:only] == :lines |
| 2932 | @lines_rendered = true unless options[:only] == :subjects |
||
| 2933 | render_end(options) |
||
| 2934 | 22:40f7cfd4df19 | chris | end |
| 2935 | |||
| 2936 | def render_project(project, options={})
|
||
| 2937 | 119:8661b858af72 | Chris | subject_for_project(project, options) unless options[:only] == :lines |
| 2938 | line_for_project(project, options) unless options[:only] == :subjects |
||
| 2939 | 22:40f7cfd4df19 | chris | options[:top] += options[:top_increment] |
| 2940 | options[:indent] += options[:indent_increment] |
||
| 2941 | 119:8661b858af72 | Chris | @number_of_rows += 1 |
| 2942 | return if abort? |
||
| 2943 | 441:cbce1fd3b1b7 | Chris | issues = project_issues(project).select {|i| i.fixed_version.nil?}
|
| 2944 | 1464:261b3d9a4903 | Chris | self.class.sort_issues!(issues) |
| 2945 | 22:40f7cfd4df19 | chris | if issues |
| 2946 | 119:8661b858af72 | Chris | render_issues(issues, options) |
| 2947 | return if abort? |
||
| 2948 | 22:40f7cfd4df19 | chris | end |
| 2949 | 441:cbce1fd3b1b7 | Chris | versions = project_versions(project) |
| 2950 | 1464:261b3d9a4903 | Chris | self.class.sort_versions!(versions) |
| 2951 | 441:cbce1fd3b1b7 | Chris | versions.each do |version| |
| 2952 | render_version(project, version, options) |
||
| 2953 | 22:40f7cfd4df19 | chris | end |
| 2954 | # Remove indent to hit the next sibling |
||
| 2955 | options[:indent] -= options[:indent_increment] |
||
| 2956 | end |
||
| 2957 | |||
| 2958 | def render_issues(issues, options={})
|
||
| 2959 | 119:8661b858af72 | Chris | @issue_ancestors = [] |
| 2960 | 22:40f7cfd4df19 | chris | issues.each do |i| |
| 2961 | 119:8661b858af72 | Chris | subject_for_issue(i, options) unless options[:only] == :lines |
| 2962 | line_for_issue(i, options) unless options[:only] == :subjects |
||
| 2963 | 22:40f7cfd4df19 | chris | options[:top] += options[:top_increment] |
| 2964 | 119:8661b858af72 | Chris | @number_of_rows += 1 |
| 2965 | break if abort? |
||
| 2966 | 22:40f7cfd4df19 | chris | end |
| 2967 | 119:8661b858af72 | Chris | options[:indent] -= (options[:indent_increment] * @issue_ancestors.size) |
| 2968 | 22:40f7cfd4df19 | chris | end |
| 2969 | |||
| 2970 | 441:cbce1fd3b1b7 | Chris | def render_version(project, version, options={})
|
| 2971 | 22:40f7cfd4df19 | chris | # Version header |
| 2972 | 119:8661b858af72 | Chris | subject_for_version(version, options) unless options[:only] == :lines |
| 2973 | line_for_version(version, options) unless options[:only] == :subjects |
||
| 2974 | 22:40f7cfd4df19 | chris | options[:top] += options[:top_increment] |
| 2975 | 119:8661b858af72 | Chris | @number_of_rows += 1 |
| 2976 | return if abort? |
||
| 2977 | 441:cbce1fd3b1b7 | Chris | issues = version_issues(project, version) |
| 2978 | 22:40f7cfd4df19 | chris | if issues |
| 2979 | 1464:261b3d9a4903 | Chris | self.class.sort_issues!(issues) |
| 2980 | 22:40f7cfd4df19 | chris | # Indent issues |
| 2981 | options[:indent] += options[:indent_increment] |
||
| 2982 | 119:8661b858af72 | Chris | render_issues(issues, options) |
| 2983 | 22:40f7cfd4df19 | chris | options[:indent] -= options[:indent_increment] |
| 2984 | end |
||
| 2985 | 119:8661b858af72 | Chris | end |
| 2986 | 441:cbce1fd3b1b7 | Chris | |
| 2987 | 119:8661b858af72 | Chris | def render_end(options={})
|
| 2988 | case options[:format] |
||
| 2989 | 441:cbce1fd3b1b7 | Chris | when :pdf |
| 2990 | 119:8661b858af72 | Chris | options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) |
| 2991 | end |
||
| 2992 | 22:40f7cfd4df19 | chris | end |
| 2993 | |||
| 2994 | def subject_for_project(project, options) |
||
| 2995 | case options[:format] |
||
| 2996 | when :html |
||
| 2997 | 1115:433d4f72a19b | Chris | html_class = "" |
| 2998 | html_class << 'icon icon-projects ' |
||
| 2999 | html_class << (project.overdue? ? 'project-overdue' : '') |
||
| 3000 | s = view.link_to_project(project).html_safe |
||
| 3001 | subject = view.content_tag(:span, s, |
||
| 3002 | :class => html_class).html_safe |
||
| 3003 | 119:8661b858af72 | Chris | html_subject(options, subject, :css => "project-name") |
| 3004 | 22:40f7cfd4df19 | chris | when :image |
| 3005 | 119:8661b858af72 | Chris | image_subject(options, project.name) |
| 3006 | 22:40f7cfd4df19 | chris | when :pdf |
| 3007 | 119:8661b858af72 | Chris | pdf_new_page?(options) |
| 3008 | pdf_subject(options, project.name) |
||
| 3009 | 22:40f7cfd4df19 | chris | end |
| 3010 | end |
||
| 3011 | |||
| 3012 | def line_for_project(project, options) |
||
| 3013 | 37:94944d00e43c | chris | # Skip versions that don't have a start_date or due date |
| 3014 | if project.is_a?(Project) && project.start_date && project.due_date |
||
| 3015 | 22:40f7cfd4df19 | chris | options[:zoom] ||= 1 |
| 3016 | options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] |
||
| 3017 | 119:8661b858af72 | Chris | coords = coordinates(project.start_date, project.due_date, nil, options[:zoom]) |
| 3018 | label = h(project) |
||
| 3019 | 22:40f7cfd4df19 | chris | case options[:format] |
| 3020 | when :html |
||
| 3021 | 119:8661b858af72 | Chris | html_task(options, coords, :css => "project task", :label => label, :markers => true) |
| 3022 | 22:40f7cfd4df19 | chris | when :image |
| 3023 | 119:8661b858af72 | Chris | image_task(options, coords, :label => label, :markers => true, :height => 3) |
| 3024 | 22:40f7cfd4df19 | chris | when :pdf |
| 3025 | 119:8661b858af72 | Chris | pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) |
| 3026 | 22:40f7cfd4df19 | chris | end |
| 3027 | else |
||
| 3028 | '' |
||
| 3029 | end |
||
| 3030 | end |
||
| 3031 | |||
| 3032 | def subject_for_version(version, options) |
||
| 3033 | case options[:format] |
||
| 3034 | when :html |
||
| 3035 | 1115:433d4f72a19b | Chris | html_class = "" |
| 3036 | html_class << 'icon icon-package ' |
||
| 3037 | html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " |
||
| 3038 | html_class << (version.overdue? ? 'version-overdue' : '') |
||
| 3039 | 1464:261b3d9a4903 | Chris | html_class << ' version-closed' unless version.open? |
| 3040 | 1517:dffacf8a6908 | Chris | if version.start_date && version.due_date && version.completed_percent |
| 3041 | 1464:261b3d9a4903 | Chris | progress_date = calc_progress_date(version.start_date, |
| 3042 | 1517:dffacf8a6908 | Chris | version.due_date, version.completed_percent) |
| 3043 | 1464:261b3d9a4903 | Chris | html_class << ' behind-start-date' if progress_date < self.date_from |
| 3044 | html_class << ' over-end-date' if progress_date > self.date_to |
||
| 3045 | end |
||
| 3046 | 1115:433d4f72a19b | Chris | s = view.link_to_version(version).html_safe |
| 3047 | subject = view.content_tag(:span, s, |
||
| 3048 | :class => html_class).html_safe |
||
| 3049 | 1464:261b3d9a4903 | Chris | html_subject(options, subject, :css => "version-name", |
| 3050 | :id => "version-#{version.id}")
|
||
| 3051 | 22:40f7cfd4df19 | chris | when :image |
| 3052 | 119:8661b858af72 | Chris | image_subject(options, version.to_s_with_project) |
| 3053 | 22:40f7cfd4df19 | chris | when :pdf |
| 3054 | 119:8661b858af72 | Chris | pdf_new_page?(options) |
| 3055 | pdf_subject(options, version.to_s_with_project) |
||
| 3056 | 22:40f7cfd4df19 | chris | end |
| 3057 | end |
||
| 3058 | |||
| 3059 | def line_for_version(version, options) |
||
| 3060 | # Skip versions that don't have a start_date |
||
| 3061 | 1464:261b3d9a4903 | Chris | if version.is_a?(Version) && version.due_date && version.start_date |
| 3062 | 22:40f7cfd4df19 | chris | options[:zoom] ||= 1 |
| 3063 | options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] |
||
| 3064 | 1115:433d4f72a19b | Chris | coords = coordinates(version.start_date, |
| 3065 | 1464:261b3d9a4903 | Chris | version.due_date, version.completed_percent, |
| 3066 | 1115:433d4f72a19b | Chris | options[:zoom]) |
| 3067 | 1464:261b3d9a4903 | Chris | label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
|
| 3068 | 119:8661b858af72 | Chris | label = h("#{version.project} -") + label unless @project && @project == version.project
|
| 3069 | 22:40f7cfd4df19 | chris | case options[:format] |
| 3070 | when :html |
||
| 3071 | 1464:261b3d9a4903 | Chris | html_task(options, coords, :css => "version task", |
| 3072 | :label => label, :markers => true, :version => version) |
||
| 3073 | 22:40f7cfd4df19 | chris | when :image |
| 3074 | 119:8661b858af72 | Chris | image_task(options, coords, :label => label, :markers => true, :height => 3) |
| 3075 | 22:40f7cfd4df19 | chris | when :pdf |
| 3076 | 119:8661b858af72 | Chris | pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) |
| 3077 | 22:40f7cfd4df19 | chris | end |
| 3078 | else |
||
| 3079 | '' |
||
| 3080 | end |
||
| 3081 | end |
||
| 3082 | |||
| 3083 | def subject_for_issue(issue, options) |
||
| 3084 | 119:8661b858af72 | Chris | while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last) |
| 3085 | @issue_ancestors.pop |
||
| 3086 | options[:indent] -= options[:indent_increment] |
||
| 3087 | end |
||
| 3088 | output = case options[:format] |
||
| 3089 | 22:40f7cfd4df19 | chris | when :html |
| 3090 | 119:8661b858af72 | Chris | css_classes = '' |
| 3091 | css_classes << ' issue-overdue' if issue.overdue? |
||
| 3092 | css_classes << ' issue-behind-schedule' if issue.behind_schedule? |
||
| 3093 | css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to |
||
| 3094 | 1464:261b3d9a4903 | Chris | css_classes << ' issue-closed' if issue.closed? |
| 3095 | if issue.start_date && issue.due_before && issue.done_ratio |
||
| 3096 | progress_date = calc_progress_date(issue.start_date, |
||
| 3097 | issue.due_before, issue.done_ratio) |
||
| 3098 | css_classes << ' behind-start-date' if progress_date < self.date_from |
||
| 3099 | css_classes << ' over-end-date' if progress_date > self.date_to |
||
| 3100 | end |
||
| 3101 | 1115:433d4f72a19b | Chris | s = "".html_safe |
| 3102 | 119:8661b858af72 | Chris | if issue.assigned_to.present? |
| 3103 | assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name |
||
| 3104 | 1115:433d4f72a19b | Chris | s << view.avatar(issue.assigned_to, |
| 3105 | :class => 'gravatar icon-gravatar', |
||
| 3106 | :size => 10, |
||
| 3107 | :title => assigned_string).to_s.html_safe |
||
| 3108 | 119:8661b858af72 | Chris | end |
| 3109 | 1115:433d4f72a19b | Chris | s << view.link_to_issue(issue).html_safe |
| 3110 | subject = view.content_tag(:span, s, :class => css_classes).html_safe |
||
| 3111 | html_subject(options, subject, :css => "issue-subject", |
||
| 3112 | 1464:261b3d9a4903 | Chris | :title => issue.subject, :id => "issue-#{issue.id}") + "\n"
|
| 3113 | 119:8661b858af72 | Chris | when :image |
| 3114 | image_subject(options, issue.subject) |
||
| 3115 | when :pdf |
||
| 3116 | pdf_new_page?(options) |
||
| 3117 | pdf_subject(options, issue.subject) |
||
| 3118 | end |
||
| 3119 | unless issue.leaf? |
||
| 3120 | @issue_ancestors << issue |
||
| 3121 | options[:indent] += options[:indent_increment] |
||
| 3122 | end |
||
| 3123 | output |
||
| 3124 | 22:40f7cfd4df19 | chris | end |
| 3125 | |||
| 3126 | def line_for_issue(issue, options) |
||
| 3127 | # Skip issues that don't have a due_before (due_date or version's due_date) |
||
| 3128 | if issue.is_a?(Issue) && issue.due_before |
||
| 3129 | 119:8661b858af72 | Chris | coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom]) |
| 3130 | 1115:433d4f72a19b | Chris | label = "#{issue.status.name} #{issue.done_ratio}%"
|
| 3131 | 22:40f7cfd4df19 | chris | case options[:format] |
| 3132 | when :html |
||
| 3133 | 1115:433d4f72a19b | Chris | html_task(options, coords, |
| 3134 | :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), |
||
| 3135 | :label => label, :issue => issue, |
||
| 3136 | :markers => !issue.leaf?) |
||
| 3137 | 22:40f7cfd4df19 | chris | when :image |
| 3138 | 119:8661b858af72 | Chris | image_task(options, coords, :label => label) |
| 3139 | 22:40f7cfd4df19 | chris | when :pdf |
| 3140 | 119:8661b858af72 | Chris | pdf_task(options, coords, :label => label) |
| 3141 | end |
||
| 3142 | 22:40f7cfd4df19 | chris | else |
| 3143 | '' |
||
| 3144 | end |
||
| 3145 | end |
||
| 3146 | |||
| 3147 | 0:513646585e45 | Chris | # Generates a gantt image |
| 3148 | # Only defined if RMagick is avalaible |
||
| 3149 | 22:40f7cfd4df19 | chris | def to_image(format='PNG') |
| 3150 | 1115:433d4f72a19b | Chris | date_to = (@date_from >> @months) - 1 |
| 3151 | 0:513646585e45 | Chris | show_weeks = @zoom > 1 |
| 3152 | show_days = @zoom > 2 |
||
| 3153 | 1:cca12e1c1fd4 | Chris | subject_width = 400 |
| 3154 | 441:cbce1fd3b1b7 | Chris | header_height = 18 |
| 3155 | 0:513646585e45 | Chris | # width of one day in pixels |
| 3156 | 1115:433d4f72a19b | Chris | zoom = @zoom * 2 |
| 3157 | g_width = (@date_to - @date_from + 1) * zoom |
||
| 3158 | 22:40f7cfd4df19 | chris | g_height = 20 * number_of_rows + 30 |
| 3159 | 1115:433d4f72a19b | Chris | headers_height = (show_weeks ? 2 * header_height : header_height) |
| 3160 | 441:cbce1fd3b1b7 | Chris | height = g_height + headers_height |
| 3161 | 0:513646585e45 | Chris | imgl = Magick::ImageList.new |
| 3162 | 1115:433d4f72a19b | Chris | imgl.new_image(subject_width + g_width + 1, height) |
| 3163 | 0:513646585e45 | Chris | gc = Magick::Draw.new |
| 3164 | 1115:433d4f72a19b | Chris | gc.font = Redmine::Configuration['rmagick_font_path'] || "" |
| 3165 | 0:513646585e45 | Chris | # Subjects |
| 3166 | 119:8661b858af72 | Chris | gc.stroke('transparent')
|
| 3167 | 441:cbce1fd3b1b7 | Chris | subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) |
| 3168 | 0:513646585e45 | Chris | # Months headers |
| 3169 | month_f = @date_from |
||
| 3170 | left = subject_width |
||
| 3171 | 441:cbce1fd3b1b7 | Chris | @months.times do |
| 3172 | 0:513646585e45 | Chris | width = ((month_f >> 1) - month_f) * zoom |
| 3173 | gc.fill('white')
|
||
| 3174 | gc.stroke('grey')
|
||
| 3175 | gc.stroke_width(1) |
||
| 3176 | gc.rectangle(left, 0, left + width, height) |
||
| 3177 | gc.fill('black')
|
||
| 3178 | gc.stroke('transparent')
|
||
| 3179 | gc.stroke_width(1) |
||
| 3180 | gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
|
||
| 3181 | left = left + width |
||
| 3182 | month_f = month_f >> 1 |
||
| 3183 | end |
||
| 3184 | # Weeks headers |
||
| 3185 | if show_weeks |
||
| 3186 | 1115:433d4f72a19b | Chris | left = subject_width |
| 3187 | height = header_height |
||
| 3188 | if @date_from.cwday == 1 |
||
| 3189 | # date_from is monday |
||
| 3190 | week_f = date_from |
||
| 3191 | else |
||
| 3192 | # find next monday after date_from |
||
| 3193 | week_f = @date_from + (7 - @date_from.cwday + 1) |
||
| 3194 | width = (7 - @date_from.cwday + 1) * zoom |
||
| 3195 | gc.fill('white')
|
||
| 3196 | gc.stroke('grey')
|
||
| 3197 | gc.stroke_width(1) |
||
| 3198 | gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1) |
||
| 3199 | left = left + width |
||
| 3200 | end |
||
| 3201 | while week_f <= date_to |
||
| 3202 | width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom |
||
| 3203 | gc.fill('white')
|
||
| 3204 | gc.stroke('grey')
|
||
| 3205 | gc.stroke_width(1) |
||
| 3206 | gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1) |
||
| 3207 | gc.fill('black')
|
||
| 3208 | gc.stroke('transparent')
|
||
| 3209 | gc.stroke_width(1) |
||
| 3210 | gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s) |
||
| 3211 | left = left + width |
||
| 3212 | week_f = week_f + 7 |
||
| 3213 | end |
||
| 3214 | 0:513646585e45 | Chris | end |
| 3215 | # Days details (week-end in grey) |
||
| 3216 | if show_days |
||
| 3217 | 1115:433d4f72a19b | Chris | left = subject_width |
| 3218 | height = g_height + header_height - 1 |
||
| 3219 | wday = @date_from.cwday |
||
| 3220 | (date_to - @date_from + 1).to_i.times do |
||
| 3221 | width = zoom |
||
| 3222 | gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') |
||
| 3223 | gc.stroke('#ddd')
|
||
| 3224 | gc.stroke_width(1) |
||
| 3225 | gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1) |
||
| 3226 | left = left + width |
||
| 3227 | wday = wday + 1 |
||
| 3228 | wday = 1 if wday > 7 |
||
| 3229 | end |
||
| 3230 | 0:513646585e45 | Chris | end |
| 3231 | # border |
||
| 3232 | gc.fill('transparent')
|
||
| 3233 | gc.stroke('grey')
|
||
| 3234 | gc.stroke_width(1) |
||
| 3235 | 1115:433d4f72a19b | Chris | gc.rectangle(0, 0, subject_width + g_width, headers_height) |
| 3236 | 0:513646585e45 | Chris | gc.stroke('black')
|
| 3237 | 1115:433d4f72a19b | Chris | gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1) |
| 3238 | 0:513646585e45 | Chris | # content |
| 3239 | 441:cbce1fd3b1b7 | Chris | top = headers_height + 20 |
| 3240 | 119:8661b858af72 | Chris | gc.stroke('transparent')
|
| 3241 | 1115:433d4f72a19b | Chris | lines(:image => gc, :top => top, :zoom => zoom, |
| 3242 | :subject_width => subject_width, :format => :image) |
||
| 3243 | 0:513646585e45 | Chris | # today red line |
| 3244 | if Date.today >= @date_from and Date.today <= date_to |
||
| 3245 | gc.stroke('red')
|
||
| 3246 | 1115:433d4f72a19b | Chris | x = (Date.today - @date_from + 1) * zoom + subject_width |
| 3247 | gc.line(x, headers_height, x, headers_height + g_height - 1) |
||
| 3248 | 441:cbce1fd3b1b7 | Chris | end |
| 3249 | 0:513646585e45 | Chris | gc.draw(imgl) |
| 3250 | imgl.format = format |
||
| 3251 | imgl.to_blob |
||
| 3252 | end if Object.const_defined?(:Magick) |
||
| 3253 | 22:40f7cfd4df19 | chris | |
| 3254 | def to_pdf |
||
| 3255 | 441:cbce1fd3b1b7 | Chris | pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) |
| 3256 | 22:40f7cfd4df19 | chris | pdf.SetTitle("#{l(:label_gantt)} #{project}")
|
| 3257 | 441:cbce1fd3b1b7 | Chris | pdf.alias_nb_pages |
| 3258 | 22:40f7cfd4df19 | chris | pdf.footer_date = format_date(Date.today) |
| 3259 | pdf.AddPage("L")
|
||
| 3260 | 1115:433d4f72a19b | Chris | pdf.SetFontStyle('B', 12)
|
| 3261 | 22:40f7cfd4df19 | chris | pdf.SetX(15) |
| 3262 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s) |
| 3263 | 22:40f7cfd4df19 | chris | pdf.Ln |
| 3264 | 1115:433d4f72a19b | Chris | pdf.SetFontStyle('B', 9)
|
| 3265 | 22:40f7cfd4df19 | chris | subject_width = PDF::LeftPaneWidth |
| 3266 | 441:cbce1fd3b1b7 | Chris | header_height = 5 |
| 3267 | headers_height = header_height |
||
| 3268 | 22:40f7cfd4df19 | chris | show_weeks = false |
| 3269 | show_days = false |
||
| 3270 | if self.months < 7 |
||
| 3271 | show_weeks = true |
||
| 3272 | 1115:433d4f72a19b | Chris | headers_height = 2 * header_height |
| 3273 | 22:40f7cfd4df19 | chris | if self.months < 3 |
| 3274 | show_days = true |
||
| 3275 | 1115:433d4f72a19b | Chris | headers_height = 3 * header_height |
| 3276 | 22:40f7cfd4df19 | chris | end |
| 3277 | end |
||
| 3278 | g_width = PDF.right_pane_width |
||
| 3279 | zoom = (g_width) / (self.date_to - self.date_from + 1) |
||
| 3280 | g_height = 120 |
||
| 3281 | 441:cbce1fd3b1b7 | Chris | t_height = g_height + headers_height |
| 3282 | 22:40f7cfd4df19 | chris | y_start = pdf.GetY |
| 3283 | # Months headers |
||
| 3284 | month_f = self.date_from |
||
| 3285 | left = subject_width |
||
| 3286 | 441:cbce1fd3b1b7 | Chris | height = header_height |
| 3287 | self.months.times do |
||
| 3288 | width = ((month_f >> 1) - month_f) * zoom |
||
| 3289 | 22:40f7cfd4df19 | chris | pdf.SetY(y_start) |
| 3290 | pdf.SetX(left) |
||
| 3291 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
|
| 3292 | 22:40f7cfd4df19 | chris | left = left + width |
| 3293 | month_f = month_f >> 1 |
||
| 3294 | 441:cbce1fd3b1b7 | Chris | end |
| 3295 | 22:40f7cfd4df19 | chris | # Weeks headers |
| 3296 | if show_weeks |
||
| 3297 | left = subject_width |
||
| 3298 | 441:cbce1fd3b1b7 | Chris | height = header_height |
| 3299 | 22:40f7cfd4df19 | chris | if self.date_from.cwday == 1 |
| 3300 | # self.date_from is monday |
||
| 3301 | week_f = self.date_from |
||
| 3302 | else |
||
| 3303 | # find next monday after self.date_from |
||
| 3304 | week_f = self.date_from + (7 - self.date_from.cwday + 1) |
||
| 3305 | width = (7 - self.date_from.cwday + 1) * zoom-1 |
||
| 3306 | 441:cbce1fd3b1b7 | Chris | pdf.SetY(y_start + header_height) |
| 3307 | 22:40f7cfd4df19 | chris | pdf.SetX(left) |
| 3308 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(width + 1, height, "", "LTR") |
| 3309 | 1115:433d4f72a19b | Chris | left = left + width + 1 |
| 3310 | 22:40f7cfd4df19 | chris | end |
| 3311 | while week_f <= self.date_to |
||
| 3312 | width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom |
||
| 3313 | 441:cbce1fd3b1b7 | Chris | pdf.SetY(y_start + header_height) |
| 3314 | 22:40f7cfd4df19 | chris | pdf.SetX(left) |
| 3315 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") |
| 3316 | 22:40f7cfd4df19 | chris | left = left + width |
| 3317 | 1115:433d4f72a19b | Chris | week_f = week_f + 7 |
| 3318 | 22:40f7cfd4df19 | chris | end |
| 3319 | end |
||
| 3320 | # Days headers |
||
| 3321 | if show_days |
||
| 3322 | left = subject_width |
||
| 3323 | 441:cbce1fd3b1b7 | Chris | height = header_height |
| 3324 | 22:40f7cfd4df19 | chris | wday = self.date_from.cwday |
| 3325 | 1115:433d4f72a19b | Chris | pdf.SetFontStyle('B', 7)
|
| 3326 | 441:cbce1fd3b1b7 | Chris | (self.date_to - self.date_from + 1).to_i.times do |
| 3327 | 22:40f7cfd4df19 | chris | width = zoom |
| 3328 | 441:cbce1fd3b1b7 | Chris | pdf.SetY(y_start + 2 * header_height) |
| 3329 | 22:40f7cfd4df19 | chris | pdf.SetX(left) |
| 3330 | 441:cbce1fd3b1b7 | Chris | pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C") |
| 3331 | 22:40f7cfd4df19 | chris | left = left + width |
| 3332 | wday = wday + 1 |
||
| 3333 | wday = 1 if wday > 7 |
||
| 3334 | end |
||
| 3335 | end |
||
| 3336 | pdf.SetY(y_start) |
||
| 3337 | pdf.SetX(15) |
||
| 3338 | 1115:433d4f72a19b | Chris | pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1) |
| 3339 | 22:40f7cfd4df19 | chris | # Tasks |
| 3340 | 441:cbce1fd3b1b7 | Chris | top = headers_height + y_start |
| 3341 | 119:8661b858af72 | Chris | options = {
|
| 3342 | :top => top, |
||
| 3343 | :zoom => zoom, |
||
| 3344 | :subject_width => subject_width, |
||
| 3345 | :g_width => g_width, |
||
| 3346 | :indent => 0, |
||
| 3347 | :indent_increment => 5, |
||
| 3348 | :top_increment => 5, |
||
| 3349 | :format => :pdf, |
||
| 3350 | :pdf => pdf |
||
| 3351 | } |
||
| 3352 | render(options) |
||
| 3353 | 22:40f7cfd4df19 | chris | pdf.Output |
| 3354 | end |
||
| 3355 | 441:cbce1fd3b1b7 | Chris | |
| 3356 | 0:513646585e45 | Chris | private |
| 3357 | 441:cbce1fd3b1b7 | Chris | |
| 3358 | 119:8661b858af72 | Chris | def coordinates(start_date, end_date, progress, zoom=nil) |
| 3359 | zoom ||= @zoom |
||
| 3360 | coords = {}
|
||
| 3361 | if start_date && end_date && start_date < self.date_to && end_date > self.date_from |
||
| 3362 | if start_date > self.date_from |
||
| 3363 | coords[:start] = start_date - self.date_from |
||
| 3364 | coords[:bar_start] = start_date - self.date_from |
||
| 3365 | else |
||
| 3366 | coords[:bar_start] = 0 |
||
| 3367 | end |
||
| 3368 | if end_date < self.date_to |
||
| 3369 | coords[:end] = end_date - self.date_from |
||
| 3370 | coords[:bar_end] = end_date - self.date_from + 1 |
||
| 3371 | else |
||
| 3372 | coords[:bar_end] = self.date_to - self.date_from + 1 |
||
| 3373 | end |
||
| 3374 | if progress |
||
| 3375 | 1464:261b3d9a4903 | Chris | progress_date = calc_progress_date(start_date, end_date, progress) |
| 3376 | 119:8661b858af72 | Chris | if progress_date > self.date_from && progress_date > start_date |
| 3377 | if progress_date < self.date_to |
||
| 3378 | 441:cbce1fd3b1b7 | Chris | coords[:bar_progress_end] = progress_date - self.date_from |
| 3379 | 119:8661b858af72 | Chris | else |
| 3380 | coords[:bar_progress_end] = self.date_to - self.date_from + 1 |
||
| 3381 | end |
||
| 3382 | end |
||
| 3383 | if progress_date < Date.today |
||
| 3384 | late_date = [Date.today, end_date].min |
||
| 3385 | if late_date > self.date_from && late_date > start_date |
||
| 3386 | if late_date < self.date_to |
||
| 3387 | coords[:bar_late_end] = late_date - self.date_from + 1 |
||
| 3388 | else |
||
| 3389 | coords[:bar_late_end] = self.date_to - self.date_from + 1 |
||
| 3390 | end |
||
| 3391 | end |
||
| 3392 | end |
||
| 3393 | end |
||
| 3394 | end |
||
| 3395 | # Transforms dates into pixels witdh |
||
| 3396 | coords.keys.each do |key| |
||
| 3397 | coords[key] = (coords[key] * zoom).floor |
||
| 3398 | end |
||
| 3399 | coords |
||
| 3400 | end |
||
| 3401 | 22:40f7cfd4df19 | chris | |
| 3402 | 1464:261b3d9a4903 | Chris | def calc_progress_date(start_date, end_date, progress) |
| 3403 | start_date + (end_date - start_date + 1) * (progress / 100.0) |
||
| 3404 | 119:8661b858af72 | Chris | end |
| 3405 | 441:cbce1fd3b1b7 | Chris | |
| 3406 | 1464:261b3d9a4903 | Chris | def self.sort_issues!(issues) |
| 3407 | issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
|
||
| 3408 | end |
||
| 3409 | |||
| 3410 | def self.sort_issue_logic(issue) |
||
| 3411 | julian_date = Date.new() |
||
| 3412 | ancesters_start_date = [] |
||
| 3413 | current_issue = issue |
||
| 3414 | begin |
||
| 3415 | ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id]) |
||
| 3416 | current_issue = current_issue.parent |
||
| 3417 | end while (current_issue) |
||
| 3418 | ancesters_start_date |
||
| 3419 | end |
||
| 3420 | |||
| 3421 | def self.sort_versions!(versions) |
||
| 3422 | versions.sort! |
||
| 3423 | 119:8661b858af72 | Chris | end |
| 3424 | 441:cbce1fd3b1b7 | Chris | |
| 3425 | 119:8661b858af72 | Chris | def current_limit |
| 3426 | if @max_rows |
||
| 3427 | @max_rows - @number_of_rows |
||
| 3428 | else |
||
| 3429 | nil |
||
| 3430 | end |
||
| 3431 | end |
||
| 3432 | 441:cbce1fd3b1b7 | Chris | |
| 3433 | 119:8661b858af72 | Chris | def abort? |
| 3434 | if @max_rows && @number_of_rows >= @max_rows |
||
| 3435 | @truncated = true |
||
| 3436 | end |
||
| 3437 | end |
||
| 3438 | 441:cbce1fd3b1b7 | Chris | |
| 3439 | 119:8661b858af72 | Chris | def pdf_new_page?(options) |
| 3440 | if options[:top] > 180 |
||
| 3441 | options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) |
||
| 3442 | options[:pdf].AddPage("L")
|
||
| 3443 | options[:top] = 15 |
||
| 3444 | options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1) |
||
| 3445 | end |
||
| 3446 | end |
||
| 3447 | 441:cbce1fd3b1b7 | Chris | |
| 3448 | 119:8661b858af72 | Chris | def html_subject(params, subject, options={})
|
| 3449 | 245:051f544170fe | Chris | style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
| 3450 | style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
||
| 3451 | 1464:261b3d9a4903 | Chris | output = view.content_tag(:div, subject, |
| 3452 | 1115:433d4f72a19b | Chris | :class => options[:css], :style => style, |
| 3453 | 1464:261b3d9a4903 | Chris | :title => options[:title], |
| 3454 | :id => options[:id]) |
||
| 3455 | 119:8661b858af72 | Chris | @subjects << output |
| 3456 | output |
||
| 3457 | end |
||
| 3458 | 441:cbce1fd3b1b7 | Chris | |
| 3459 | 119:8661b858af72 | Chris | def pdf_subject(params, subject, options={})
|
| 3460 | params[:pdf].SetY(params[:top]) |
||
| 3461 | params[:pdf].SetX(15) |
||
| 3462 | char_limit = PDF::MaxCharactorsForSubject - params[:indent] |
||
| 3463 | 1115:433d4f72a19b | Chris | params[:pdf].RDMCell(params[:subject_width] - 15, 5, |
| 3464 | (" " * params[:indent]) +
|
||
| 3465 | subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
|
||
| 3466 | "LR") |
||
| 3467 | 119:8661b858af72 | Chris | params[:pdf].SetY(params[:top]) |
| 3468 | params[:pdf].SetX(params[:subject_width]) |
||
| 3469 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(params[:g_width], 5, "", "LR") |
| 3470 | 119:8661b858af72 | Chris | end |
| 3471 | 441:cbce1fd3b1b7 | Chris | |
| 3472 | 119:8661b858af72 | Chris | def image_subject(params, subject, options={})
|
| 3473 | params[:image].fill('black')
|
||
| 3474 | params[:image].stroke('transparent')
|
||
| 3475 | params[:image].stroke_width(1) |
||
| 3476 | params[:image].text(params[:indent], params[:top] + 2, subject) |
||
| 3477 | end |
||
| 3478 | 441:cbce1fd3b1b7 | Chris | |
| 3479 | 1464:261b3d9a4903 | Chris | def issue_relations(issue) |
| 3480 | rels = {}
|
||
| 3481 | if relations[issue.id] |
||
| 3482 | relations[issue.id].each do |relation| |
||
| 3483 | (rels[relation.relation_type] ||= []) << relation.issue_to_id |
||
| 3484 | end |
||
| 3485 | end |
||
| 3486 | rels |
||
| 3487 | end |
||
| 3488 | |||
| 3489 | 119:8661b858af72 | Chris | def html_task(params, coords, options={})
|
| 3490 | output = '' |
||
| 3491 | # Renders the task bar, with progress and late |
||
| 3492 | if coords[:bar_start] && coords[:bar_end] |
||
| 3493 | 1115:433d4f72a19b | Chris | width = coords[:bar_end] - coords[:bar_start] - 2 |
| 3494 | style = "" |
||
| 3495 | style << "top:#{params[:top]}px;"
|
||
| 3496 | style << "left:#{coords[:bar_start]}px;"
|
||
| 3497 | style << "width:#{width}px;"
|
||
| 3498 | 1464:261b3d9a4903 | Chris | html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
|
| 3499 | html_id = "task-todo-version-#{options[:version].id}" if options[:version]
|
||
| 3500 | content_opt = {:style => style,
|
||
| 3501 | :class => "#{options[:css]} task_todo",
|
||
| 3502 | :id => html_id} |
||
| 3503 | if options[:issue] |
||
| 3504 | rels = issue_relations(options[:issue]) |
||
| 3505 | if rels.present? |
||
| 3506 | content_opt[:data] = {"rels" => rels.to_json}
|
||
| 3507 | end |
||
| 3508 | end |
||
| 3509 | output << view.content_tag(:div, ' '.html_safe, content_opt) |
||
| 3510 | 119:8661b858af72 | Chris | if coords[:bar_late_end] |
| 3511 | 1115:433d4f72a19b | Chris | width = coords[:bar_late_end] - coords[:bar_start] - 2 |
| 3512 | style = "" |
||
| 3513 | style << "top:#{params[:top]}px;"
|
||
| 3514 | style << "left:#{coords[:bar_start]}px;"
|
||
| 3515 | style << "width:#{width}px;"
|
||
| 3516 | output << view.content_tag(:div, ' '.html_safe, |
||
| 3517 | :style => style, |
||
| 3518 | :class => "#{options[:css]} task_late")
|
||
| 3519 | 0:513646585e45 | Chris | end |
| 3520 | 119:8661b858af72 | Chris | if coords[:bar_progress_end] |
| 3521 | 1115:433d4f72a19b | Chris | width = coords[:bar_progress_end] - coords[:bar_start] - 2 |
| 3522 | style = "" |
||
| 3523 | style << "top:#{params[:top]}px;"
|
||
| 3524 | style << "left:#{coords[:bar_start]}px;"
|
||
| 3525 | style << "width:#{width}px;"
|
||
| 3526 | 1464:261b3d9a4903 | Chris | html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
|
| 3527 | html_id = "task-done-version-#{options[:version].id}" if options[:version]
|
||
| 3528 | 1115:433d4f72a19b | Chris | output << view.content_tag(:div, ' '.html_safe, |
| 3529 | :style => style, |
||
| 3530 | 1464:261b3d9a4903 | Chris | :class => "#{options[:css]} task_done",
|
| 3531 | :id => html_id) |
||
| 3532 | 119:8661b858af72 | Chris | end |
| 3533 | end |
||
| 3534 | # Renders the markers |
||
| 3535 | if options[:markers] |
||
| 3536 | if coords[:start] |
||
| 3537 | 1115:433d4f72a19b | Chris | style = "" |
| 3538 | style << "top:#{params[:top]}px;"
|
||
| 3539 | style << "left:#{coords[:start]}px;"
|
||
| 3540 | style << "width:15px;" |
||
| 3541 | output << view.content_tag(:div, ' '.html_safe, |
||
| 3542 | :style => style, |
||
| 3543 | :class => "#{options[:css]} marker starting")
|
||
| 3544 | 119:8661b858af72 | Chris | end |
| 3545 | if coords[:end] |
||
| 3546 | 1115:433d4f72a19b | Chris | style = "" |
| 3547 | style << "top:#{params[:top]}px;"
|
||
| 3548 | style << "left:#{coords[:end] + params[:zoom]}px;"
|
||
| 3549 | style << "width:15px;" |
||
| 3550 | output << view.content_tag(:div, ' '.html_safe, |
||
| 3551 | :style => style, |
||
| 3552 | :class => "#{options[:css]} marker ending")
|
||
| 3553 | 119:8661b858af72 | Chris | end |
| 3554 | end |
||
| 3555 | # Renders the label on the right |
||
| 3556 | if options[:label] |
||
| 3557 | 1115:433d4f72a19b | Chris | style = "" |
| 3558 | style << "top:#{params[:top]}px;"
|
||
| 3559 | style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
||
| 3560 | style << "width:15px;" |
||
| 3561 | output << view.content_tag(:div, options[:label], |
||
| 3562 | :style => style, |
||
| 3563 | :class => "#{options[:css]} label")
|
||
| 3564 | 119:8661b858af72 | Chris | end |
| 3565 | # Renders the tooltip |
||
| 3566 | if options[:issue] && coords[:bar_start] && coords[:bar_end] |
||
| 3567 | 1115:433d4f72a19b | Chris | s = view.content_tag(:span, |
| 3568 | view.render_issue_tooltip(options[:issue]).html_safe, |
||
| 3569 | :class => "tip") |
||
| 3570 | style = "" |
||
| 3571 | style << "position: absolute;" |
||
| 3572 | style << "top:#{params[:top]}px;"
|
||
| 3573 | style << "left:#{coords[:bar_start]}px;"
|
||
| 3574 | style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
||
| 3575 | style << "height:12px;" |
||
| 3576 | output << view.content_tag(:div, s.html_safe, |
||
| 3577 | :style => style, |
||
| 3578 | :class => "tooltip") |
||
| 3579 | 119:8661b858af72 | Chris | end |
| 3580 | @lines << output |
||
| 3581 | output |
||
| 3582 | end |
||
| 3583 | 441:cbce1fd3b1b7 | Chris | |
| 3584 | 119:8661b858af72 | Chris | def pdf_task(params, coords, options={})
|
| 3585 | height = options[:height] || 2 |
||
| 3586 | # Renders the task bar, with progress and late |
||
| 3587 | if coords[:bar_start] && coords[:bar_end] |
||
| 3588 | 1115:433d4f72a19b | Chris | params[:pdf].SetY(params[:top] + 1.5) |
| 3589 | 119:8661b858af72 | Chris | params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) |
| 3590 | 1115:433d4f72a19b | Chris | params[:pdf].SetFillColor(200, 200, 200) |
| 3591 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1) |
| 3592 | 119:8661b858af72 | Chris | if coords[:bar_late_end] |
| 3593 | 1115:433d4f72a19b | Chris | params[:pdf].SetY(params[:top] + 1.5) |
| 3594 | 119:8661b858af72 | Chris | params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) |
| 3595 | 1115:433d4f72a19b | Chris | params[:pdf].SetFillColor(255, 100, 100) |
| 3596 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1) |
| 3597 | 119:8661b858af72 | Chris | end |
| 3598 | if coords[:bar_progress_end] |
||
| 3599 | 1115:433d4f72a19b | Chris | params[:pdf].SetY(params[:top] + 1.5) |
| 3600 | 119:8661b858af72 | Chris | params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) |
| 3601 | 1115:433d4f72a19b | Chris | params[:pdf].SetFillColor(90, 200, 90) |
| 3602 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1) |
| 3603 | 119:8661b858af72 | Chris | end |
| 3604 | end |
||
| 3605 | # Renders the markers |
||
| 3606 | if options[:markers] |
||
| 3607 | if coords[:start] |
||
| 3608 | params[:pdf].SetY(params[:top] + 1) |
||
| 3609 | params[:pdf].SetX(params[:subject_width] + coords[:start] - 1) |
||
| 3610 | 1115:433d4f72a19b | Chris | params[:pdf].SetFillColor(50, 50, 200) |
| 3611 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) |
| 3612 | 119:8661b858af72 | Chris | end |
| 3613 | if coords[:end] |
||
| 3614 | params[:pdf].SetY(params[:top] + 1) |
||
| 3615 | params[:pdf].SetX(params[:subject_width] + coords[:end] - 1) |
||
| 3616 | 1115:433d4f72a19b | Chris | params[:pdf].SetFillColor(50, 50, 200) |
| 3617 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) |
| 3618 | 119:8661b858af72 | Chris | end |
| 3619 | end |
||
| 3620 | # Renders the label on the right |
||
| 3621 | if options[:label] |
||
| 3622 | params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5) |
||
| 3623 | 441:cbce1fd3b1b7 | Chris | params[:pdf].RDMCell(30, 2, options[:label]) |
| 3624 | 0:513646585e45 | Chris | end |
| 3625 | end |
||
| 3626 | 22:40f7cfd4df19 | chris | |
| 3627 | 119:8661b858af72 | Chris | def image_task(params, coords, options={})
|
| 3628 | height = options[:height] || 6 |
||
| 3629 | # Renders the task bar, with progress and late |
||
| 3630 | if coords[:bar_start] && coords[:bar_end] |
||
| 3631 | params[:image].fill('#aaa')
|
||
| 3632 | 1115:433d4f72a19b | Chris | params[:image].rectangle(params[:subject_width] + coords[:bar_start], |
| 3633 | params[:top], |
||
| 3634 | params[:subject_width] + coords[:bar_end], |
||
| 3635 | params[:top] - height) |
||
| 3636 | 119:8661b858af72 | Chris | if coords[:bar_late_end] |
| 3637 | params[:image].fill('#f66')
|
||
| 3638 | 1115:433d4f72a19b | Chris | params[:image].rectangle(params[:subject_width] + coords[:bar_start], |
| 3639 | params[:top], |
||
| 3640 | params[:subject_width] + coords[:bar_late_end], |
||
| 3641 | params[:top] - height) |
||
| 3642 | 119:8661b858af72 | Chris | end |
| 3643 | if coords[:bar_progress_end] |
||
| 3644 | params[:image].fill('#00c600')
|
||
| 3645 | 1115:433d4f72a19b | Chris | params[:image].rectangle(params[:subject_width] + coords[:bar_start], |
| 3646 | params[:top], |
||
| 3647 | params[:subject_width] + coords[:bar_progress_end], |
||
| 3648 | params[:top] - height) |
||
| 3649 | 119:8661b858af72 | Chris | end |
| 3650 | end |
||
| 3651 | # Renders the markers |
||
| 3652 | if options[:markers] |
||
| 3653 | if coords[:start] |
||
| 3654 | x = params[:subject_width] + coords[:start] |
||
| 3655 | y = params[:top] - height / 2 |
||
| 3656 | params[:image].fill('blue')
|
||
| 3657 | 1115:433d4f72a19b | Chris | params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) |
| 3658 | 119:8661b858af72 | Chris | end |
| 3659 | if coords[:end] |
||
| 3660 | x = params[:subject_width] + coords[:end] + params[:zoom] |
||
| 3661 | y = params[:top] - height / 2 |
||
| 3662 | params[:image].fill('blue')
|
||
| 3663 | 1115:433d4f72a19b | Chris | params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) |
| 3664 | 119:8661b858af72 | Chris | end |
| 3665 | end |
||
| 3666 | # Renders the label on the right |
||
| 3667 | if options[:label] |
||
| 3668 | params[:image].fill('black')
|
||
| 3669 | 1115:433d4f72a19b | Chris | params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5, |
| 3670 | params[:top] + 1, |
||
| 3671 | options[:label]) |
||
| 3672 | 119:8661b858af72 | Chris | end |
| 3673 | end |
||
| 3674 | 0:513646585e45 | Chris | end |
| 3675 | end |
||
| 3676 | end |
||
| 3677 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 3678 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 3679 | 1115:433d4f72a19b | Chris | # |
| 3680 | # This program is free software; you can redistribute it and/or |
||
| 3681 | # modify it under the terms of the GNU General Public License |
||
| 3682 | # as published by the Free Software Foundation; either version 2 |
||
| 3683 | # of the License, or (at your option) any later version. |
||
| 3684 | # |
||
| 3685 | # This program is distributed in the hope that it will be useful, |
||
| 3686 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 3687 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 3688 | # GNU General Public License for more details. |
||
| 3689 | # |
||
| 3690 | # You should have received a copy of the GNU General Public License |
||
| 3691 | # along with this program; if not, write to the Free Software |
||
| 3692 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 3693 | |||
| 3694 | module Redmine |
||
| 3695 | module Helpers |
||
| 3696 | class TimeReport |
||
| 3697 | 1464:261b3d9a4903 | Chris | attr_reader :criteria, :columns, :hours, :total_hours, :periods |
| 3698 | 1115:433d4f72a19b | Chris | |
| 3699 | 1464:261b3d9a4903 | Chris | def initialize(project, issue, criteria, columns, time_entry_scope) |
| 3700 | 1115:433d4f72a19b | Chris | @project = project |
| 3701 | @issue = issue |
||
| 3702 | |||
| 3703 | @criteria = criteria || [] |
||
| 3704 | @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
|
||
| 3705 | @criteria.uniq! |
||
| 3706 | @criteria = @criteria[0,3] |
||
| 3707 | |||
| 3708 | @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month' |
||
| 3709 | 1464:261b3d9a4903 | Chris | @scope = time_entry_scope |
| 3710 | 1115:433d4f72a19b | Chris | |
| 3711 | run |
||
| 3712 | end |
||
| 3713 | |||
| 3714 | def available_criteria |
||
| 3715 | @available_criteria || load_available_criteria |
||
| 3716 | end |
||
| 3717 | |||
| 3718 | private |
||
| 3719 | |||
| 3720 | def run |
||
| 3721 | unless @criteria.empty? |
||
| 3722 | time_columns = %w(tyear tmonth tweek spent_on) |
||
| 3723 | @hours = [] |
||
| 3724 | 1517:dffacf8a6908 | Chris | @scope.includes(:issue, :activity). |
| 3725 | group(@criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).
|
||
| 3726 | joins(@criteria.collect{|criteria| @available_criteria[criteria][:joins]}.compact).
|
||
| 3727 | sum(:hours).each do |hash, hours| |
||
| 3728 | 1115:433d4f72a19b | Chris | h = {'hours' => hours}
|
| 3729 | (@criteria + time_columns).each_with_index do |name, i| |
||
| 3730 | h[name] = hash[i] |
||
| 3731 | end |
||
| 3732 | @hours << h |
||
| 3733 | end |
||
| 3734 | |||
| 3735 | @hours.each do |row| |
||
| 3736 | case @columns |
||
| 3737 | when 'year' |
||
| 3738 | row['year'] = row['tyear'] |
||
| 3739 | when 'month' |
||
| 3740 | row['month'] = "#{row['tyear']}-#{row['tmonth']}"
|
||
| 3741 | when 'week' |
||
| 3742 | 1464:261b3d9a4903 | Chris | row['week'] = "#{row['spent_on'].cwyear}-#{row['tweek']}"
|
| 3743 | 1115:433d4f72a19b | Chris | when 'day' |
| 3744 | row['day'] = "#{row['spent_on']}"
|
||
| 3745 | end |
||
| 3746 | end |
||
| 3747 | |||
| 3748 | 1464:261b3d9a4903 | Chris | min = @hours.collect {|row| row['spent_on']}.min
|
| 3749 | @from = min ? min.to_date : Date.today |
||
| 3750 | 1115:433d4f72a19b | Chris | |
| 3751 | 1464:261b3d9a4903 | Chris | max = @hours.collect {|row| row['spent_on']}.max
|
| 3752 | @to = max ? max.to_date : Date.today |
||
| 3753 | 1115:433d4f72a19b | Chris | |
| 3754 | @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
|
||
| 3755 | |||
| 3756 | @periods = [] |
||
| 3757 | # Date#at_beginning_of_ not supported in Rails 1.2.x |
||
| 3758 | date_from = @from.to_time |
||
| 3759 | # 100 columns max |
||
| 3760 | while date_from <= @to.to_time && @periods.length < 100 |
||
| 3761 | case @columns |
||
| 3762 | when 'year' |
||
| 3763 | @periods << "#{date_from.year}"
|
||
| 3764 | date_from = (date_from + 1.year).at_beginning_of_year |
||
| 3765 | when 'month' |
||
| 3766 | @periods << "#{date_from.year}-#{date_from.month}"
|
||
| 3767 | date_from = (date_from + 1.month).at_beginning_of_month |
||
| 3768 | when 'week' |
||
| 3769 | 1464:261b3d9a4903 | Chris | @periods << "#{date_from.to_date.cwyear}-#{date_from.to_date.cweek}"
|
| 3770 | 1115:433d4f72a19b | Chris | date_from = (date_from + 7.day).at_beginning_of_week |
| 3771 | when 'day' |
||
| 3772 | @periods << "#{date_from.to_date}"
|
||
| 3773 | date_from = date_from + 1.day |
||
| 3774 | end |
||
| 3775 | end |
||
| 3776 | end |
||
| 3777 | end |
||
| 3778 | |||
| 3779 | def load_available_criteria |
||
| 3780 | @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
|
||
| 3781 | :klass => Project, |
||
| 3782 | :label => :label_project}, |
||
| 3783 | 'status' => {:sql => "#{Issue.table_name}.status_id",
|
||
| 3784 | :klass => IssueStatus, |
||
| 3785 | :label => :field_status}, |
||
| 3786 | 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
|
||
| 3787 | :klass => Version, |
||
| 3788 | :label => :label_version}, |
||
| 3789 | 'category' => {:sql => "#{Issue.table_name}.category_id",
|
||
| 3790 | :klass => IssueCategory, |
||
| 3791 | :label => :field_category}, |
||
| 3792 | 1464:261b3d9a4903 | Chris | 'user' => {:sql => "#{TimeEntry.table_name}.user_id",
|
| 3793 | 1115:433d4f72a19b | Chris | :klass => User, |
| 3794 | 1464:261b3d9a4903 | Chris | :label => :label_user}, |
| 3795 | 1115:433d4f72a19b | Chris | 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
|
| 3796 | :klass => Tracker, |
||
| 3797 | :label => :label_tracker}, |
||
| 3798 | 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
|
||
| 3799 | :klass => TimeEntryActivity, |
||
| 3800 | :label => :label_activity}, |
||
| 3801 | 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
|
||
| 3802 | :klass => Issue, |
||
| 3803 | :label => :label_issue} |
||
| 3804 | } |
||
| 3805 | |||
| 3806 | 1464:261b3d9a4903 | Chris | # Add time entry custom fields |
| 3807 | custom_fields = TimeEntryCustomField.all |
||
| 3808 | # Add project custom fields |
||
| 3809 | custom_fields += ProjectCustomField.all |
||
| 3810 | # Add issue custom fields |
||
| 3811 | custom_fields += (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) |
||
| 3812 | # Add time entry activity custom fields |
||
| 3813 | custom_fields += TimeEntryActivityCustomField.all |
||
| 3814 | |||
| 3815 | 1115:433d4f72a19b | Chris | # Add list and boolean custom fields as available criteria |
| 3816 | 1517:dffacf8a6908 | Chris | custom_fields.select {|cf| %w(list bool).include?(cf.field_format) && !cf.multiple?}.each do |cf|
|
| 3817 | @available_criteria["cf_#{cf.id}"] = {:sql => cf.group_statement,
|
||
| 3818 | 1464:261b3d9a4903 | Chris | :joins => cf.join_for_order_statement, |
| 3819 | 1115:433d4f72a19b | Chris | :format => cf.field_format, |
| 3820 | 1517:dffacf8a6908 | Chris | :custom_field => cf, |
| 3821 | 1115:433d4f72a19b | Chris | :label => cf.name} |
| 3822 | end |
||
| 3823 | |||
| 3824 | @available_criteria |
||
| 3825 | end |
||
| 3826 | end |
||
| 3827 | end |
||
| 3828 | end |
||
| 3829 | 0:513646585e45 | Chris | # Redmine - project management software |
| 3830 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 3831 | 0:513646585e45 | Chris | # |
| 3832 | # This program is free software; you can redistribute it and/or |
||
| 3833 | # modify it under the terms of the GNU General Public License |
||
| 3834 | # as published by the Free Software Foundation; either version 2 |
||
| 3835 | # of the License, or (at your option) any later version. |
||
| 3836 | 909:cbb26bc654de | Chris | # |
| 3837 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 3838 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 3839 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 3840 | # GNU General Public License for more details. |
||
| 3841 | 909:cbb26bc654de | Chris | # |
| 3842 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 3843 | # along with this program; if not, write to the Free Software |
||
| 3844 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 3845 | |||
| 3846 | module Redmine |
||
| 3847 | module Hook |
||
| 3848 | @@listener_classes = [] |
||
| 3849 | @@listeners = nil |
||
| 3850 | @@hook_listeners = {}
|
||
| 3851 | 909:cbb26bc654de | Chris | |
| 3852 | 0:513646585e45 | Chris | class << self |
| 3853 | # Adds a listener class. |
||
| 3854 | # Automatically called when a class inherits from Redmine::Hook::Listener. |
||
| 3855 | def add_listener(klass) |
||
| 3856 | raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton) |
||
| 3857 | @@listener_classes << klass |
||
| 3858 | clear_listeners_instances |
||
| 3859 | end |
||
| 3860 | 909:cbb26bc654de | Chris | |
| 3861 | 0:513646585e45 | Chris | # Returns all the listerners instances. |
| 3862 | def listeners |
||
| 3863 | @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
|
||
| 3864 | end |
||
| 3865 | 909:cbb26bc654de | Chris | |
| 3866 | 0:513646585e45 | Chris | # Returns the listeners instances for the given hook. |
| 3867 | def hook_listeners(hook) |
||
| 3868 | @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
|
||
| 3869 | end |
||
| 3870 | 909:cbb26bc654de | Chris | |
| 3871 | 0:513646585e45 | Chris | # Clears all the listeners. |
| 3872 | def clear_listeners |
||
| 3873 | @@listener_classes = [] |
||
| 3874 | clear_listeners_instances |
||
| 3875 | end |
||
| 3876 | 909:cbb26bc654de | Chris | |
| 3877 | 0:513646585e45 | Chris | # Clears all the listeners instances. |
| 3878 | def clear_listeners_instances |
||
| 3879 | @@listeners = nil |
||
| 3880 | @@hook_listeners = {}
|
||
| 3881 | end |
||
| 3882 | 909:cbb26bc654de | Chris | |
| 3883 | 0:513646585e45 | Chris | # Calls a hook. |
| 3884 | # Returns the listeners response. |
||
| 3885 | def call_hook(hook, context={})
|
||
| 3886 | 37:94944d00e43c | chris | [].tap do |response| |
| 3887 | 0:513646585e45 | Chris | hls = hook_listeners(hook) |
| 3888 | if hls.any? |
||
| 3889 | hls.each {|listener| response << listener.send(hook, context)}
|
||
| 3890 | end |
||
| 3891 | end |
||
| 3892 | end |
||
| 3893 | end |
||
| 3894 | |||
| 3895 | # Base class for hook listeners. |
||
| 3896 | class Listener |
||
| 3897 | include Singleton |
||
| 3898 | include Redmine::I18n |
||
| 3899 | |||
| 3900 | # Registers the listener |
||
| 3901 | def self.inherited(child) |
||
| 3902 | Redmine::Hook.add_listener(child) |
||
| 3903 | super |
||
| 3904 | end |
||
| 3905 | |||
| 3906 | end |
||
| 3907 | |||
| 3908 | # Listener class used for views hooks. |
||
| 3909 | # Listeners that inherit this class will include various helpers by default. |
||
| 3910 | class ViewListener < Listener |
||
| 3911 | include ERB::Util |
||
| 3912 | include ActionView::Helpers::TagHelper |
||
| 3913 | include ActionView::Helpers::FormHelper |
||
| 3914 | include ActionView::Helpers::FormTagHelper |
||
| 3915 | include ActionView::Helpers::FormOptionsHelper |
||
| 3916 | include ActionView::Helpers::JavaScriptHelper |
||
| 3917 | include ActionView::Helpers::NumberHelper |
||
| 3918 | include ActionView::Helpers::UrlHelper |
||
| 3919 | include ActionView::Helpers::AssetTagHelper |
||
| 3920 | include ActionView::Helpers::TextHelper |
||
| 3921 | 1115:433d4f72a19b | Chris | include Rails.application.routes.url_helpers |
| 3922 | 0:513646585e45 | Chris | include ApplicationHelper |
| 3923 | |||
| 3924 | # Default to creating links using only the path. Subclasses can |
||
| 3925 | # change this default as needed |
||
| 3926 | def self.default_url_options |
||
| 3927 | {:only_path => true }
|
||
| 3928 | end |
||
| 3929 | 909:cbb26bc654de | Chris | |
| 3930 | 0:513646585e45 | Chris | # Helper method to directly render a partial using the context: |
| 3931 | 909:cbb26bc654de | Chris | # |
| 3932 | 0:513646585e45 | Chris | # class MyHook < Redmine::Hook::ViewListener |
| 3933 | 909:cbb26bc654de | Chris | # render_on :view_issues_show_details_bottom, :partial => "show_more_data" |
| 3934 | 0:513646585e45 | Chris | # end |
| 3935 | # |
||
| 3936 | def self.render_on(hook, options={})
|
||
| 3937 | define_method hook do |context| |
||
| 3938 | 1115:433d4f72a19b | Chris | if context[:hook_caller].respond_to?(:render) |
| 3939 | context[:hook_caller].send(:render, {:locals => context}.merge(options))
|
||
| 3940 | elsif context[:controller].is_a?(ActionController::Base) |
||
| 3941 | context[:controller].send(:render_to_string, {:locals => context}.merge(options))
|
||
| 3942 | else |
||
| 3943 | raise "Cannot render #{self.name} hook from #{context[:hook_caller].class.name}"
|
||
| 3944 | end |
||
| 3945 | 0:513646585e45 | Chris | end |
| 3946 | end |
||
| 3947 | 1115:433d4f72a19b | Chris | |
| 3948 | def controller |
||
| 3949 | nil |
||
| 3950 | end |
||
| 3951 | |||
| 3952 | def config |
||
| 3953 | ActionController::Base.config |
||
| 3954 | end |
||
| 3955 | 0:513646585e45 | Chris | end |
| 3956 | |||
| 3957 | 909:cbb26bc654de | Chris | # Helper module included in ApplicationHelper and ActionController so that |
| 3958 | 0:513646585e45 | Chris | # hooks can be called in views like this: |
| 3959 | 909:cbb26bc654de | Chris | # |
| 3960 | 0:513646585e45 | Chris | # <%= call_hook(:some_hook) %> |
| 3961 | 909:cbb26bc654de | Chris | # <%= call_hook(:another_hook, :foo => 'bar') %> |
| 3962 | # |
||
| 3963 | 0:513646585e45 | Chris | # Or in controllers like: |
| 3964 | # call_hook(:some_hook) |
||
| 3965 | 909:cbb26bc654de | Chris | # call_hook(:another_hook, :foo => 'bar') |
| 3966 | # |
||
| 3967 | # Hooks added to views will be concatenated into a string. Hooks added to |
||
| 3968 | 0:513646585e45 | Chris | # controllers will return an array of results. |
| 3969 | # |
||
| 3970 | # Several objects are automatically added to the call context: |
||
| 3971 | 909:cbb26bc654de | Chris | # |
| 3972 | 0:513646585e45 | Chris | # * project => current project |
| 3973 | # * request => Request instance |
||
| 3974 | # * controller => current Controller instance |
||
| 3975 | 1115:433d4f72a19b | Chris | # * hook_caller => object that called the hook |
| 3976 | 909:cbb26bc654de | Chris | # |
| 3977 | 0:513646585e45 | Chris | module Helper |
| 3978 | def call_hook(hook, context={})
|
||
| 3979 | if is_a?(ActionController::Base) |
||
| 3980 | 1115:433d4f72a19b | Chris | default_context = {:controller => self, :project => @project, :request => request, :hook_caller => self}
|
| 3981 | 0:513646585e45 | Chris | Redmine::Hook.call_hook(hook, default_context.merge(context)) |
| 3982 | else |
||
| 3983 | 1115:433d4f72a19b | Chris | default_context = { :project => @project, :hook_caller => self }
|
| 3984 | default_context[:controller] = controller if respond_to?(:controller) |
||
| 3985 | default_context[:request] = request if respond_to?(:request) |
||
| 3986 | Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
|
||
| 3987 | 909:cbb26bc654de | Chris | end |
| 3988 | 0:513646585e45 | Chris | end |
| 3989 | end |
||
| 3990 | end |
||
| 3991 | end |
||
| 3992 | |||
| 3993 | ApplicationHelper.send(:include, Redmine::Hook::Helper) |
||
| 3994 | ActionController::Base.send(:include, Redmine::Hook::Helper) |
||
| 3995 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 3996 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 3997 | 1115:433d4f72a19b | Chris | # |
| 3998 | # This program is free software; you can redistribute it and/or |
||
| 3999 | # modify it under the terms of the GNU General Public License |
||
| 4000 | # as published by the Free Software Foundation; either version 2 |
||
| 4001 | # of the License, or (at your option) any later version. |
||
| 4002 | # |
||
| 4003 | # This program is distributed in the hope that it will be useful, |
||
| 4004 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 4005 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 4006 | # GNU General Public License for more details. |
||
| 4007 | # |
||
| 4008 | # You should have received a copy of the GNU General Public License |
||
| 4009 | # along with this program; if not, write to the Free Software |
||
| 4010 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 4011 | |||
| 4012 | 0:513646585e45 | Chris | module Redmine |
| 4013 | module I18n |
||
| 4014 | def self.included(base) |
||
| 4015 | base.extend Redmine::I18n |
||
| 4016 | end |
||
| 4017 | 909:cbb26bc654de | Chris | |
| 4018 | 0:513646585e45 | Chris | def l(*args) |
| 4019 | case args.size |
||
| 4020 | when 1 |
||
| 4021 | ::I18n.t(*args) |
||
| 4022 | when 2 |
||
| 4023 | if args.last.is_a?(Hash) |
||
| 4024 | ::I18n.t(*args) |
||
| 4025 | elsif args.last.is_a?(String) |
||
| 4026 | ::I18n.t(args.first, :value => args.last) |
||
| 4027 | else |
||
| 4028 | ::I18n.t(args.first, :count => args.last) |
||
| 4029 | end |
||
| 4030 | else |
||
| 4031 | raise "Translation string with multiple values: #{args.first}"
|
||
| 4032 | end |
||
| 4033 | end |
||
| 4034 | |||
| 4035 | def l_or_humanize(s, options={})
|
||
| 4036 | k = "#{options[:prefix]}#{s}".to_sym
|
||
| 4037 | ::I18n.t(k, :default => s.to_s.humanize) |
||
| 4038 | end |
||
| 4039 | 909:cbb26bc654de | Chris | |
| 4040 | 0:513646585e45 | Chris | def l_hours(hours) |
| 4041 | hours = hours.to_f |
||
| 4042 | l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f))
|
||
| 4043 | end |
||
| 4044 | 909:cbb26bc654de | Chris | |
| 4045 | 0:513646585e45 | Chris | def ll(lang, str, value=nil) |
| 4046 | ::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" })
|
||
| 4047 | end |
||
| 4048 | |||
| 4049 | def format_date(date) |
||
| 4050 | return nil unless date |
||
| 4051 | 1115:433d4f72a19b | Chris | options = {}
|
| 4052 | options[:format] = Setting.date_format unless Setting.date_format.blank? |
||
| 4053 | options[:locale] = User.current.language unless User.current.language.blank? |
||
| 4054 | ::I18n.l(date.to_date, options) |
||
| 4055 | 0:513646585e45 | Chris | end |
| 4056 | 909:cbb26bc654de | Chris | |
| 4057 | 0:513646585e45 | Chris | def format_time(time, include_date = true) |
| 4058 | return nil unless time |
||
| 4059 | 1115:433d4f72a19b | Chris | options = {}
|
| 4060 | options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format) |
||
| 4061 | options[:locale] = User.current.language unless User.current.language.blank? |
||
| 4062 | 0:513646585e45 | Chris | time = time.to_time if time.is_a?(String) |
| 4063 | zone = User.current.time_zone |
||
| 4064 | local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time) |
||
| 4065 | 1115:433d4f72a19b | Chris | (include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options)
|
| 4066 | 0:513646585e45 | Chris | end |
| 4067 | |||
| 4068 | def day_name(day) |
||
| 4069 | ::I18n.t('date.day_names')[day % 7]
|
||
| 4070 | end |
||
| 4071 | 909:cbb26bc654de | Chris | |
| 4072 | 1115:433d4f72a19b | Chris | def day_letter(day) |
| 4073 | ::I18n.t('date.abbr_day_names')[day % 7].first
|
||
| 4074 | end |
||
| 4075 | |||
| 4076 | 0:513646585e45 | Chris | def month_name(month) |
| 4077 | ::I18n.t('date.month_names')[month]
|
||
| 4078 | end |
||
| 4079 | 909:cbb26bc654de | Chris | |
| 4080 | 0:513646585e45 | Chris | def valid_languages |
| 4081 | 1115:433d4f72a19b | Chris | ::I18n.available_locales |
| 4082 | end |
||
| 4083 | |||
| 4084 | # Returns an array of languages names and code sorted by names, example: |
||
| 4085 | # [["Deutsch", "de"], ["English", "en"] ...] |
||
| 4086 | # |
||
| 4087 | # The result is cached to prevent from loading all translations files. |
||
| 4088 | def languages_options |
||
| 4089 | ActionController::Base.cache_store.fetch "i18n/languages_options" do |
||
| 4090 | valid_languages.map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.sort {|x,y| x.first <=> y.first }
|
||
| 4091 | end |
||
| 4092 | 0:513646585e45 | Chris | end |
| 4093 | 909:cbb26bc654de | Chris | |
| 4094 | 0:513646585e45 | Chris | def find_language(lang) |
| 4095 | @@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k }
|
||
| 4096 | @@languages_lookup[lang.to_s.downcase] |
||
| 4097 | end |
||
| 4098 | 909:cbb26bc654de | Chris | |
| 4099 | 0:513646585e45 | Chris | def set_language_if_valid(lang) |
| 4100 | if l = find_language(lang) |
||
| 4101 | ::I18n.locale = l |
||
| 4102 | end |
||
| 4103 | end |
||
| 4104 | 909:cbb26bc654de | Chris | |
| 4105 | 0:513646585e45 | Chris | def current_language |
| 4106 | ::I18n.locale |
||
| 4107 | end |
||
| 4108 | 1115:433d4f72a19b | Chris | |
| 4109 | # Custom backend based on I18n::Backend::Simple with the following changes: |
||
| 4110 | # * lazy loading of translation files |
||
| 4111 | # * available_locales are determined by looking at translation file names |
||
| 4112 | class Backend |
||
| 4113 | (class << self; self; end).class_eval { public :include }
|
||
| 4114 | |||
| 4115 | module Implementation |
||
| 4116 | include ::I18n::Backend::Base |
||
| 4117 | |||
| 4118 | # Stores translations for the given locale in memory. |
||
| 4119 | # This uses a deep merge for the translations hash, so existing |
||
| 4120 | # translations will be overwritten by new ones only at the deepest |
||
| 4121 | # level of the hash. |
||
| 4122 | def store_translations(locale, data, options = {})
|
||
| 4123 | locale = locale.to_sym |
||
| 4124 | translations[locale] ||= {}
|
||
| 4125 | data = data.deep_symbolize_keys |
||
| 4126 | translations[locale].deep_merge!(data) |
||
| 4127 | end |
||
| 4128 | |||
| 4129 | # Get available locales from the translations filenames |
||
| 4130 | def available_locales |
||
| 4131 | @available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym)
|
||
| 4132 | end |
||
| 4133 | |||
| 4134 | # Clean up translations |
||
| 4135 | def reload! |
||
| 4136 | @translations = nil |
||
| 4137 | @available_locales = nil |
||
| 4138 | super |
||
| 4139 | end |
||
| 4140 | |||
| 4141 | protected |
||
| 4142 | |||
| 4143 | def init_translations(locale) |
||
| 4144 | locale = locale.to_s |
||
| 4145 | paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
|
||
| 4146 | load_translations(paths) |
||
| 4147 | translations[locale] ||= {}
|
||
| 4148 | end |
||
| 4149 | |||
| 4150 | def translations |
||
| 4151 | @translations ||= {}
|
||
| 4152 | end |
||
| 4153 | |||
| 4154 | # Looks up a translation from the translations hash. Returns nil if |
||
| 4155 | # eiher key is nil, or locale, scope or key do not exist as a key in the |
||
| 4156 | # nested translations hash. Splits keys or scopes containing dots |
||
| 4157 | # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as |
||
| 4158 | # <tt>%w(currency format)</tt>. |
||
| 4159 | def lookup(locale, key, scope = [], options = {})
|
||
| 4160 | init_translations(locale) unless translations.key?(locale) |
||
| 4161 | keys = ::I18n.normalize_keys(locale, key, scope, options[:separator]) |
||
| 4162 | |||
| 4163 | keys.inject(translations) do |result, _key| |
||
| 4164 | _key = _key.to_sym |
||
| 4165 | return nil unless result.is_a?(Hash) && result.has_key?(_key) |
||
| 4166 | result = result[_key] |
||
| 4167 | result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol) |
||
| 4168 | result |
||
| 4169 | end |
||
| 4170 | end |
||
| 4171 | end |
||
| 4172 | |||
| 4173 | include Implementation |
||
| 4174 | # Adds fallback to default locale for untranslated strings |
||
| 4175 | include ::I18n::Backend::Fallbacks |
||
| 4176 | end |
||
| 4177 | 0:513646585e45 | Chris | end |
| 4178 | end |
||
| 4179 | # Redmine - project management software |
||
| 4180 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 4181 | 0:513646585e45 | Chris | # |
| 4182 | # This program is free software; you can redistribute it and/or |
||
| 4183 | # modify it under the terms of the GNU General Public License |
||
| 4184 | # as published by the Free Software Foundation; either version 2 |
||
| 4185 | # of the License, or (at your option) any later version. |
||
| 4186 | 909:cbb26bc654de | Chris | # |
| 4187 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 4188 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 4189 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 4190 | # GNU General Public License for more details. |
||
| 4191 | 909:cbb26bc654de | Chris | # |
| 4192 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 4193 | # along with this program; if not, write to the Free Software |
||
| 4194 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 4195 | |||
| 4196 | require 'net/imap' |
||
| 4197 | |||
| 4198 | module Redmine |
||
| 4199 | module IMAP |
||
| 4200 | class << self |
||
| 4201 | def check(imap_options={}, options={})
|
||
| 4202 | host = imap_options[:host] || '127.0.0.1' |
||
| 4203 | port = imap_options[:port] || '143' |
||
| 4204 | ssl = !imap_options[:ssl].nil? |
||
| 4205 | folder = imap_options[:folder] || 'INBOX' |
||
| 4206 | 909:cbb26bc654de | Chris | |
| 4207 | imap = Net::IMAP.new(host, port, ssl) |
||
| 4208 | 0:513646585e45 | Chris | imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil? |
| 4209 | imap.select(folder) |
||
| 4210 | 1464:261b3d9a4903 | Chris | imap.uid_search(['NOT', 'SEEN']).each do |uid| |
| 4211 | msg = imap.uid_fetch(uid,'RFC822')[0].attr['RFC822'] |
||
| 4212 | logger.debug "Receiving message #{uid}" if logger && logger.debug?
|
||
| 4213 | 0:513646585e45 | Chris | if MailHandler.receive(msg, options) |
| 4214 | 1464:261b3d9a4903 | Chris | logger.debug "Message #{uid} successfully received" if logger && logger.debug?
|
| 4215 | 0:513646585e45 | Chris | if imap_options[:move_on_success] |
| 4216 | 1464:261b3d9a4903 | Chris | imap.uid_copy(uid, imap_options[:move_on_success]) |
| 4217 | 0:513646585e45 | Chris | end |
| 4218 | 1464:261b3d9a4903 | Chris | imap.uid_store(uid, "+FLAGS", [:Seen, :Deleted]) |
| 4219 | 0:513646585e45 | Chris | else |
| 4220 | 1464:261b3d9a4903 | Chris | logger.debug "Message #{uid} can not be processed" if logger && logger.debug?
|
| 4221 | imap.uid_store(uid, "+FLAGS", [:Seen]) |
||
| 4222 | 0:513646585e45 | Chris | if imap_options[:move_on_failure] |
| 4223 | 1464:261b3d9a4903 | Chris | imap.uid_copy(uid, imap_options[:move_on_failure]) |
| 4224 | imap.uid_store(uid, "+FLAGS", [:Deleted]) |
||
| 4225 | 0:513646585e45 | Chris | end |
| 4226 | end |
||
| 4227 | end |
||
| 4228 | imap.expunge |
||
| 4229 | 1464:261b3d9a4903 | Chris | imap.logout |
| 4230 | imap.disconnect |
||
| 4231 | 0:513646585e45 | Chris | end |
| 4232 | 909:cbb26bc654de | Chris | |
| 4233 | 0:513646585e45 | Chris | private |
| 4234 | 909:cbb26bc654de | Chris | |
| 4235 | 0:513646585e45 | Chris | def logger |
| 4236 | 1115:433d4f72a19b | Chris | ::Rails.logger |
| 4237 | 0:513646585e45 | Chris | end |
| 4238 | end |
||
| 4239 | end |
||
| 4240 | end |
||
| 4241 | module Redmine |
||
| 4242 | module Info |
||
| 4243 | class << self |
||
| 4244 | def app_name; 'Redmine' end |
||
| 4245 | def url; 'http://www.redmine.org/' end |
||
| 4246 | 33:9f4ebcdd78a6 | Chris | def help_url; '/projects/soundsoftware-site/wiki/Help' end |
| 4247 | 0:513646585e45 | Chris | def versioned_name; "#{app_name} #{Redmine::VERSION}" end
|
| 4248 | |||
| 4249 | 1115:433d4f72a19b | Chris | def environment |
| 4250 | s = "Environment:\n" |
||
| 4251 | s << [ |
||
| 4252 | ["Redmine version", Redmine::VERSION], |
||
| 4253 | 1464:261b3d9a4903 | Chris | ["Ruby version", "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"],
|
| 4254 | 1115:433d4f72a19b | Chris | ["Rails version", Rails::VERSION::STRING], |
| 4255 | ["Environment", Rails.env], |
||
| 4256 | ["Database adapter", ActiveRecord::Base.connection.adapter_name] |
||
| 4257 | 1464:261b3d9a4903 | Chris | ].map {|info| " %-30s %s" % info}.join("\n") + "\n"
|
| 4258 | 1115:433d4f72a19b | Chris | |
| 4259 | 1464:261b3d9a4903 | Chris | s << "SCM:\n" |
| 4260 | Redmine::Scm::Base.all.each do |scm| |
||
| 4261 | scm_class = "Repository::#{scm}".constantize
|
||
| 4262 | if scm_class.scm_available |
||
| 4263 | s << " %-30s %s\n" % [scm, scm_class.scm_version_string] |
||
| 4264 | end |
||
| 4265 | end |
||
| 4266 | |||
| 4267 | s << "Redmine plugins:\n" |
||
| 4268 | 1115:433d4f72a19b | Chris | plugins = Redmine::Plugin.all |
| 4269 | if plugins.any? |
||
| 4270 | 1464:261b3d9a4903 | Chris | s << plugins.map {|plugin| " %-30s %s" % [plugin.id.to_s, plugin.version.to_s]}.join("\n")
|
| 4271 | 1115:433d4f72a19b | Chris | else |
| 4272 | s << " no plugin installed" |
||
| 4273 | end |
||
| 4274 | 0:513646585e45 | Chris | end |
| 4275 | end |
||
| 4276 | end |
||
| 4277 | end |
||
| 4278 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 4279 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 4280 | 0:513646585e45 | Chris | # |
| 4281 | # This program is free software; you can redistribute it and/or |
||
| 4282 | # modify it under the terms of the GNU General Public License |
||
| 4283 | # as published by the Free Software Foundation; either version 2 |
||
| 4284 | # of the License, or (at your option) any later version. |
||
| 4285 | 909:cbb26bc654de | Chris | # |
| 4286 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 4287 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 4288 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 4289 | # GNU General Public License for more details. |
||
| 4290 | 909:cbb26bc654de | Chris | # |
| 4291 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 4292 | # along with this program; if not, write to the Free Software |
||
| 4293 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 4294 | |||
| 4295 | module Redmine |
||
| 4296 | module MenuManager |
||
| 4297 | class MenuError < StandardError #:nodoc: |
||
| 4298 | end |
||
| 4299 | 909:cbb26bc654de | Chris | |
| 4300 | 0:513646585e45 | Chris | module MenuController |
| 4301 | def self.included(base) |
||
| 4302 | base.extend(ClassMethods) |
||
| 4303 | end |
||
| 4304 | |||
| 4305 | module ClassMethods |
||
| 4306 | @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
|
||
| 4307 | mattr_accessor :menu_items |
||
| 4308 | 909:cbb26bc654de | Chris | |
| 4309 | 0:513646585e45 | Chris | # Set the menu item name for a controller or specific actions |
| 4310 | # Examples: |
||
| 4311 | # * menu_item :tickets # => sets the menu name to :tickets for the whole controller |
||
| 4312 | # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only |
||
| 4313 | # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only |
||
| 4314 | 909:cbb26bc654de | Chris | # |
| 4315 | 0:513646585e45 | Chris | # The default menu item name for a controller is controller_name by default |
| 4316 | # Eg. the default menu item name for ProjectsController is :projects |
||
| 4317 | def menu_item(id, options = {})
|
||
| 4318 | if actions = options[:only] |
||
| 4319 | actions = [] << actions unless actions.is_a?(Array) |
||
| 4320 | actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
|
||
| 4321 | else |
||
| 4322 | menu_items[controller_name.to_sym][:default] = id |
||
| 4323 | end |
||
| 4324 | end |
||
| 4325 | end |
||
| 4326 | 909:cbb26bc654de | Chris | |
| 4327 | 0:513646585e45 | Chris | def menu_items |
| 4328 | self.class.menu_items |
||
| 4329 | end |
||
| 4330 | 909:cbb26bc654de | Chris | |
| 4331 | 0:513646585e45 | Chris | # Returns the menu item name according to the current action |
| 4332 | def current_menu_item |
||
| 4333 | @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] || |
||
| 4334 | menu_items[controller_name.to_sym][:default] |
||
| 4335 | end |
||
| 4336 | 909:cbb26bc654de | Chris | |
| 4337 | 0:513646585e45 | Chris | # Redirects user to the menu item of the given project |
| 4338 | # Returns false if user is not authorized |
||
| 4339 | def redirect_to_project_menu_item(project, name) |
||
| 4340 | item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
|
||
| 4341 | if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project)) |
||
| 4342 | redirect_to({item.param => project}.merge(item.url))
|
||
| 4343 | return true |
||
| 4344 | end |
||
| 4345 | false |
||
| 4346 | end |
||
| 4347 | end |
||
| 4348 | 909:cbb26bc654de | Chris | |
| 4349 | 0:513646585e45 | Chris | module MenuHelper |
| 4350 | # Returns the current menu item name |
||
| 4351 | def current_menu_item |
||
| 4352 | 909:cbb26bc654de | Chris | controller.current_menu_item |
| 4353 | 0:513646585e45 | Chris | end |
| 4354 | 909:cbb26bc654de | Chris | |
| 4355 | 0:513646585e45 | Chris | # Renders the application main menu |
| 4356 | def render_main_menu(project) |
||
| 4357 | render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project) |
||
| 4358 | end |
||
| 4359 | 909:cbb26bc654de | Chris | |
| 4360 | 0:513646585e45 | Chris | def display_main_menu?(project) |
| 4361 | menu_name = project && !project.new_record? ? :project_menu : :application_menu |
||
| 4362 | 1115:433d4f72a19b | Chris | Redmine::MenuManager.items(menu_name).children.present? |
| 4363 | 0:513646585e45 | Chris | end |
| 4364 | |||
| 4365 | def render_menu(menu, project=nil) |
||
| 4366 | links = [] |
||
| 4367 | menu_items_for(menu, project) do |node| |
||
| 4368 | links << render_menu_node(node, project) |
||
| 4369 | end |
||
| 4370 | 909:cbb26bc654de | Chris | links.empty? ? nil : content_tag('ul', links.join("\n").html_safe)
|
| 4371 | 0:513646585e45 | Chris | end |
| 4372 | |||
| 4373 | def render_menu_node(node, project=nil) |
||
| 4374 | 1115:433d4f72a19b | Chris | if node.children.present? || !node.child_menus.nil? |
| 4375 | 0:513646585e45 | Chris | return render_menu_node_with_children(node, project) |
| 4376 | else |
||
| 4377 | caption, url, selected = extract_node_details(node, project) |
||
| 4378 | return content_tag('li',
|
||
| 4379 | render_single_menu_node(node, caption, url, selected)) |
||
| 4380 | end |
||
| 4381 | end |
||
| 4382 | |||
| 4383 | def render_menu_node_with_children(node, project=nil) |
||
| 4384 | caption, url, selected = extract_node_details(node, project) |
||
| 4385 | |||
| 4386 | 37:94944d00e43c | chris | html = [].tap do |html| |
| 4387 | 0:513646585e45 | Chris | html << '<li>' |
| 4388 | # Parent |
||
| 4389 | html << render_single_menu_node(node, caption, url, selected) |
||
| 4390 | |||
| 4391 | # Standard children |
||
| 4392 | 1115:433d4f72a19b | Chris | standard_children_list = "".html_safe.tap do |child_html| |
| 4393 | 0:513646585e45 | Chris | node.children.each do |child| |
| 4394 | child_html << render_menu_node(child, project) |
||
| 4395 | end |
||
| 4396 | end |
||
| 4397 | |||
| 4398 | html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty? |
||
| 4399 | |||
| 4400 | # Unattached children |
||
| 4401 | unattached_children_list = render_unattached_children_menu(node, project) |
||
| 4402 | html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank? |
||
| 4403 | |||
| 4404 | html << '</li>' |
||
| 4405 | end |
||
| 4406 | 1115:433d4f72a19b | Chris | return html.join("\n").html_safe
|
| 4407 | 0:513646585e45 | Chris | end |
| 4408 | |||
| 4409 | # Returns a list of unattached children menu items |
||
| 4410 | def render_unattached_children_menu(node, project) |
||
| 4411 | return nil unless node.child_menus |
||
| 4412 | |||
| 4413 | 1115:433d4f72a19b | Chris | "".html_safe.tap do |child_html| |
| 4414 | 0:513646585e45 | Chris | unattached_children = node.child_menus.call(project) |
| 4415 | # Tree nodes support #each so we need to do object detection |
||
| 4416 | if unattached_children.is_a? Array |
||
| 4417 | unattached_children.each do |child| |
||
| 4418 | 909:cbb26bc654de | Chris | child_html << content_tag(:li, render_unattached_menu_item(child, project)) |
| 4419 | 0:513646585e45 | Chris | end |
| 4420 | else |
||
| 4421 | raise MenuError, ":child_menus must be an array of MenuItems" |
||
| 4422 | end |
||
| 4423 | end |
||
| 4424 | end |
||
| 4425 | |||
| 4426 | def render_single_menu_node(item, caption, url, selected) |
||
| 4427 | link_to(h(caption), url, item.html_options(:selected => selected)) |
||
| 4428 | end |
||
| 4429 | |||
| 4430 | def render_unattached_menu_item(menu_item, project) |
||
| 4431 | raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem |
||
| 4432 | |||
| 4433 | if User.current.allowed_to?(menu_item.url, project) |
||
| 4434 | link_to(h(menu_item.caption), |
||
| 4435 | menu_item.url, |
||
| 4436 | menu_item.html_options) |
||
| 4437 | end |
||
| 4438 | end |
||
| 4439 | 909:cbb26bc654de | Chris | |
| 4440 | 0:513646585e45 | Chris | def menu_items_for(menu, project=nil) |
| 4441 | items = [] |
||
| 4442 | Redmine::MenuManager.items(menu).root.children.each do |node| |
||
| 4443 | if allowed_node?(node, User.current, project) |
||
| 4444 | if block_given? |
||
| 4445 | yield node |
||
| 4446 | else |
||
| 4447 | items << node # TODO: not used? |
||
| 4448 | end |
||
| 4449 | end |
||
| 4450 | end |
||
| 4451 | return block_given? ? nil : items |
||
| 4452 | end |
||
| 4453 | |||
| 4454 | def extract_node_details(node, project=nil) |
||
| 4455 | item = node |
||
| 4456 | url = case item.url |
||
| 4457 | when Hash |
||
| 4458 | project.nil? ? item.url : {item.param => project}.merge(item.url)
|
||
| 4459 | when Symbol |
||
| 4460 | send(item.url) |
||
| 4461 | else |
||
| 4462 | item.url |
||
| 4463 | end |
||
| 4464 | caption = item.caption(project) |
||
| 4465 | return [caption, url, (current_menu_item == item.name)] |
||
| 4466 | end |
||
| 4467 | |||
| 4468 | # Checks if a user is allowed to access the menu item by: |
||
| 4469 | # |
||
| 4470 | 1464:261b3d9a4903 | Chris | # * Checking the url target (project only) |
| 4471 | 0:513646585e45 | Chris | # * Checking the conditions of the item |
| 4472 | def allowed_node?(node, user, project) |
||
| 4473 | 1464:261b3d9a4903 | Chris | if project && user && !user.allowed_to?(node.url, project) |
| 4474 | return false |
||
| 4475 | end |
||
| 4476 | 0:513646585e45 | Chris | if node.condition && !node.condition.call(project) |
| 4477 | # Condition that doesn't pass |
||
| 4478 | return false |
||
| 4479 | end |
||
| 4480 | 1464:261b3d9a4903 | Chris | return true |
| 4481 | 0:513646585e45 | Chris | end |
| 4482 | end |
||
| 4483 | 909:cbb26bc654de | Chris | |
| 4484 | 0:513646585e45 | Chris | class << self |
| 4485 | def map(menu_name) |
||
| 4486 | @items ||= {}
|
||
| 4487 | mapper = Mapper.new(menu_name.to_sym, @items) |
||
| 4488 | if block_given? |
||
| 4489 | yield mapper |
||
| 4490 | else |
||
| 4491 | mapper |
||
| 4492 | end |
||
| 4493 | end |
||
| 4494 | 909:cbb26bc654de | Chris | |
| 4495 | 0:513646585e45 | Chris | def items(menu_name) |
| 4496 | 1115:433d4f72a19b | Chris | @items[menu_name.to_sym] || MenuNode.new(:root, {})
|
| 4497 | 0:513646585e45 | Chris | end |
| 4498 | end |
||
| 4499 | 909:cbb26bc654de | Chris | |
| 4500 | 0:513646585e45 | Chris | class Mapper |
| 4501 | 1464:261b3d9a4903 | Chris | attr_reader :menu, :menu_items |
| 4502 | |||
| 4503 | 0:513646585e45 | Chris | def initialize(menu, items) |
| 4504 | 1115:433d4f72a19b | Chris | items[menu] ||= MenuNode.new(:root, {})
|
| 4505 | 0:513646585e45 | Chris | @menu = menu |
| 4506 | @menu_items = items[menu] |
||
| 4507 | end |
||
| 4508 | 909:cbb26bc654de | Chris | |
| 4509 | 0:513646585e45 | Chris | # Adds an item at the end of the menu. Available options: |
| 4510 | # * param: the parameter name that is used for the project id (default is :id) |
||
| 4511 | # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true |
||
| 4512 | # * caption that can be: |
||
| 4513 | # * a localized string Symbol |
||
| 4514 | # * a String |
||
| 4515 | # * a Proc that can take the project as argument |
||
| 4516 | # * before, after: specify where the menu item should be inserted (eg. :after => :activity) |
||
| 4517 | # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues) |
||
| 4518 | # * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item. |
||
| 4519 | # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
|
||
| 4520 | # * last: menu item will stay at the end (eg. :last => true) |
||
| 4521 | # * html_options: a hash of html options that are passed to link_to |
||
| 4522 | def push(name, url, options={})
|
||
| 4523 | options = options.dup |
||
| 4524 | |||
| 4525 | if options[:parent] |
||
| 4526 | subtree = self.find(options[:parent]) |
||
| 4527 | if subtree |
||
| 4528 | target_root = subtree |
||
| 4529 | else |
||
| 4530 | target_root = @menu_items.root |
||
| 4531 | end |
||
| 4532 | |||
| 4533 | else |
||
| 4534 | target_root = @menu_items.root |
||
| 4535 | end |
||
| 4536 | |||
| 4537 | # menu item position |
||
| 4538 | if first = options.delete(:first) |
||
| 4539 | target_root.prepend(MenuItem.new(name, url, options)) |
||
| 4540 | elsif before = options.delete(:before) |
||
| 4541 | |||
| 4542 | if exists?(before) |
||
| 4543 | target_root.add_at(MenuItem.new(name, url, options), position_of(before)) |
||
| 4544 | else |
||
| 4545 | target_root.add(MenuItem.new(name, url, options)) |
||
| 4546 | end |
||
| 4547 | |||
| 4548 | elsif after = options.delete(:after) |
||
| 4549 | |||
| 4550 | if exists?(after) |
||
| 4551 | target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1) |
||
| 4552 | else |
||
| 4553 | target_root.add(MenuItem.new(name, url, options)) |
||
| 4554 | end |
||
| 4555 | 909:cbb26bc654de | Chris | |
| 4556 | 0:513646585e45 | Chris | elsif options[:last] # don't delete, needs to be stored |
| 4557 | target_root.add_last(MenuItem.new(name, url, options)) |
||
| 4558 | else |
||
| 4559 | target_root.add(MenuItem.new(name, url, options)) |
||
| 4560 | end |
||
| 4561 | end |
||
| 4562 | 909:cbb26bc654de | Chris | |
| 4563 | 0:513646585e45 | Chris | # Removes a menu item |
| 4564 | def delete(name) |
||
| 4565 | if found = self.find(name) |
||
| 4566 | @menu_items.remove!(found) |
||
| 4567 | end |
||
| 4568 | end |
||
| 4569 | |||
| 4570 | # Checks if a menu item exists |
||
| 4571 | def exists?(name) |
||
| 4572 | @menu_items.any? {|node| node.name == name}
|
||
| 4573 | end |
||
| 4574 | |||
| 4575 | def find(name) |
||
| 4576 | @menu_items.find {|node| node.name == name}
|
||
| 4577 | end |
||
| 4578 | |||
| 4579 | def position_of(name) |
||
| 4580 | @menu_items.each do |node| |
||
| 4581 | if node.name == name |
||
| 4582 | return node.position |
||
| 4583 | end |
||
| 4584 | end |
||
| 4585 | end |
||
| 4586 | end |
||
| 4587 | 909:cbb26bc654de | Chris | |
| 4588 | 1115:433d4f72a19b | Chris | class MenuNode |
| 4589 | include Enumerable |
||
| 4590 | attr_accessor :parent |
||
| 4591 | attr_reader :last_items_count, :name |
||
| 4592 | |||
| 4593 | def initialize(name, content = nil) |
||
| 4594 | @name = name |
||
| 4595 | @children = [] |
||
| 4596 | @last_items_count = 0 |
||
| 4597 | end |
||
| 4598 | |||
| 4599 | def children |
||
| 4600 | if block_given? |
||
| 4601 | @children.each {|child| yield child}
|
||
| 4602 | else |
||
| 4603 | @children |
||
| 4604 | end |
||
| 4605 | end |
||
| 4606 | |||
| 4607 | # Returns the number of descendants + 1 |
||
| 4608 | def size |
||
| 4609 | @children.inject(1) {|sum, node| sum + node.size}
|
||
| 4610 | end |
||
| 4611 | |||
| 4612 | def each &block |
||
| 4613 | yield self |
||
| 4614 | children { |child| child.each(&block) }
|
||
| 4615 | end |
||
| 4616 | |||
| 4617 | # Adds a child at first position |
||
| 4618 | def prepend(child) |
||
| 4619 | add_at(child, 0) |
||
| 4620 | end |
||
| 4621 | |||
| 4622 | # Adds a child at given position |
||
| 4623 | def add_at(child, position) |
||
| 4624 | raise "Child already added" if find {|node| node.name == child.name}
|
||
| 4625 | |||
| 4626 | @children = @children.insert(position, child) |
||
| 4627 | child.parent = self |
||
| 4628 | child |
||
| 4629 | end |
||
| 4630 | |||
| 4631 | # Adds a child as last child |
||
| 4632 | def add_last(child) |
||
| 4633 | add_at(child, -1) |
||
| 4634 | @last_items_count += 1 |
||
| 4635 | child |
||
| 4636 | end |
||
| 4637 | |||
| 4638 | # Adds a child |
||
| 4639 | def add(child) |
||
| 4640 | position = @children.size - @last_items_count |
||
| 4641 | add_at(child, position) |
||
| 4642 | end |
||
| 4643 | alias :<< :add |
||
| 4644 | |||
| 4645 | # Removes a child |
||
| 4646 | def remove!(child) |
||
| 4647 | @children.delete(child) |
||
| 4648 | @last_items_count -= +1 if child && child.last |
||
| 4649 | child.parent = nil |
||
| 4650 | child |
||
| 4651 | end |
||
| 4652 | |||
| 4653 | # Returns the position for this node in it's parent |
||
| 4654 | def position |
||
| 4655 | self.parent.children.index(self) |
||
| 4656 | end |
||
| 4657 | |||
| 4658 | # Returns the root for this node |
||
| 4659 | def root |
||
| 4660 | root = self |
||
| 4661 | root = root.parent while root.parent |
||
| 4662 | root |
||
| 4663 | end |
||
| 4664 | end |
||
| 4665 | |||
| 4666 | class MenuItem < MenuNode |
||
| 4667 | 0:513646585e45 | Chris | include Redmine::I18n |
| 4668 | attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last |
||
| 4669 | 909:cbb26bc654de | Chris | |
| 4670 | 0:513646585e45 | Chris | def initialize(name, url, options) |
| 4671 | raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
|
||
| 4672 | raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
|
||
| 4673 | raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym |
||
| 4674 | raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
|
||
| 4675 | @name = name |
||
| 4676 | @url = url |
||
| 4677 | @condition = options[:if] |
||
| 4678 | @param = options[:param] || :id |
||
| 4679 | @caption = options[:caption] |
||
| 4680 | @html_options = options[:html] || {}
|
||
| 4681 | # Adds a unique class to each menu item based on its name |
||
| 4682 | @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
|
||
| 4683 | @parent = options[:parent] |
||
| 4684 | @child_menus = options[:children] |
||
| 4685 | @last = options[:last] || false |
||
| 4686 | super @name.to_sym |
||
| 4687 | end |
||
| 4688 | 909:cbb26bc654de | Chris | |
| 4689 | 0:513646585e45 | Chris | def caption(project=nil) |
| 4690 | if @caption.is_a?(Proc) |
||
| 4691 | c = @caption.call(project).to_s |
||
| 4692 | c = @name.to_s.humanize if c.blank? |
||
| 4693 | c |
||
| 4694 | else |
||
| 4695 | if @caption.nil? |
||
| 4696 | l_or_humanize(name, :prefix => 'label_') |
||
| 4697 | else |
||
| 4698 | @caption.is_a?(Symbol) ? l(@caption) : @caption |
||
| 4699 | end |
||
| 4700 | end |
||
| 4701 | end |
||
| 4702 | 909:cbb26bc654de | Chris | |
| 4703 | 0:513646585e45 | Chris | def html_options(options={})
|
| 4704 | if options[:selected] |
||
| 4705 | o = @html_options.dup |
||
| 4706 | o[:class] += ' selected' |
||
| 4707 | o |
||
| 4708 | else |
||
| 4709 | @html_options |
||
| 4710 | end |
||
| 4711 | end |
||
| 4712 | 909:cbb26bc654de | Chris | end |
| 4713 | 0:513646585e45 | Chris | end |
| 4714 | end |
||
| 4715 | # Redmine - project management software |
||
| 4716 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 4717 | 0:513646585e45 | Chris | # |
| 4718 | # This program is free software; you can redistribute it and/or |
||
| 4719 | # modify it under the terms of the GNU General Public License |
||
| 4720 | # as published by the Free Software Foundation; either version 2 |
||
| 4721 | # of the License, or (at your option) any later version. |
||
| 4722 | 909:cbb26bc654de | Chris | # |
| 4723 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 4724 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 4725 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 4726 | # GNU General Public License for more details. |
||
| 4727 | 909:cbb26bc654de | Chris | # |
| 4728 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 4729 | # along with this program; if not, write to the Free Software |
||
| 4730 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 4731 | |||
| 4732 | 1517:dffacf8a6908 | Chris | require 'mime/types' |
| 4733 | |||
| 4734 | 0:513646585e45 | Chris | module Redmine |
| 4735 | module MimeType |
||
| 4736 | |||
| 4737 | MIME_TYPES = {
|
||
| 4738 | 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade', |
||
| 4739 | 'text/css' => 'css', |
||
| 4740 | 'text/html' => 'html,htm,xhtml', |
||
| 4741 | 'text/jsp' => 'jsp', |
||
| 4742 | 'text/x-c' => 'c,cpp,cc,h,hh', |
||
| 4743 | 'text/x-csharp' => 'cs', |
||
| 4744 | 'text/x-java' => 'java', |
||
| 4745 | 'text/x-html-template' => 'rhtml', |
||
| 4746 | 'text/x-perl' => 'pl,pm', |
||
| 4747 | 'text/x-php' => 'php,php3,php4,php5', |
||
| 4748 | 'text/x-python' => 'py', |
||
| 4749 | 'text/x-ruby' => 'rb,rbw,ruby,rake,erb', |
||
| 4750 | 'text/x-csh' => 'csh', |
||
| 4751 | 'text/x-sh' => 'sh', |
||
| 4752 | 'text/xml' => 'xml,xsd,mxml', |
||
| 4753 | 'text/yaml' => 'yml,yaml', |
||
| 4754 | 'text/csv' => 'csv', |
||
| 4755 | 441:cbce1fd3b1b7 | Chris | 'text/x-po' => 'po', |
| 4756 | 0:513646585e45 | Chris | 'image/gif' => 'gif', |
| 4757 | 'image/jpeg' => 'jpg,jpeg,jpe', |
||
| 4758 | 'image/png' => 'png', |
||
| 4759 | 'image/tiff' => 'tiff,tif', |
||
| 4760 | 'image/x-ms-bmp' => 'bmp', |
||
| 4761 | 1115:433d4f72a19b | Chris | 'application/javascript' => 'js', |
| 4762 | 0:513646585e45 | Chris | 'application/pdf' => 'pdf', |
| 4763 | }.freeze |
||
| 4764 | 909:cbb26bc654de | Chris | |
| 4765 | 0:513646585e45 | Chris | EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
|
| 4766 | exts.split(',').each {|ext| map[ext.strip] = type}
|
||
| 4767 | map |
||
| 4768 | end |
||
| 4769 | 909:cbb26bc654de | Chris | |
| 4770 | 0:513646585e45 | Chris | # returns mime type for name or nil if unknown |
| 4771 | def self.of(name) |
||
| 4772 | 1517:dffacf8a6908 | Chris | return nil unless name.present? |
| 4773 | if m = name.to_s.match(/(^|\.)([^\.]+)$/) |
||
| 4774 | extension = m[2].downcase |
||
| 4775 | @known_types ||= Hash.new do |h, ext| |
||
| 4776 | type = EXTENSIONS[ext] |
||
| 4777 | type ||= MIME::Types.type_for(ext).first.to_s.presence |
||
| 4778 | h[ext] = type |
||
| 4779 | end |
||
| 4780 | @known_types[extension] |
||
| 4781 | end |
||
| 4782 | 0:513646585e45 | Chris | end |
| 4783 | 909:cbb26bc654de | Chris | |
| 4784 | 0:513646585e45 | Chris | # Returns the css class associated to |
| 4785 | # the mime type of name |
||
| 4786 | def self.css_class_of(name) |
||
| 4787 | mime = of(name) |
||
| 4788 | mime && mime.gsub('/', '-')
|
||
| 4789 | end |
||
| 4790 | 909:cbb26bc654de | Chris | |
| 4791 | 0:513646585e45 | Chris | def self.main_mimetype_of(name) |
| 4792 | mimetype = of(name) |
||
| 4793 | mimetype.split('/').first if mimetype
|
||
| 4794 | end |
||
| 4795 | 909:cbb26bc654de | Chris | |
| 4796 | 0:513646585e45 | Chris | # return true if mime-type for name is type/* |
| 4797 | # otherwise false |
||
| 4798 | def self.is_type?(type, name) |
||
| 4799 | main_mimetype = main_mimetype_of(name) |
||
| 4800 | type.to_s == main_mimetype |
||
| 4801 | 909:cbb26bc654de | Chris | end |
| 4802 | 0:513646585e45 | Chris | end |
| 4803 | end |
||
| 4804 | 37:94944d00e43c | chris | module Redmine |
| 4805 | class Notifiable < Struct.new(:name, :parent) |
||
| 4806 | |||
| 4807 | def to_s |
||
| 4808 | name |
||
| 4809 | end |
||
| 4810 | 909:cbb26bc654de | Chris | |
| 4811 | 37:94944d00e43c | chris | # TODO: Plugin API for adding a new notification? |
| 4812 | def self.all |
||
| 4813 | notifications = [] |
||
| 4814 | notifications << Notifiable.new('issue_added')
|
||
| 4815 | notifications << Notifiable.new('issue_updated')
|
||
| 4816 | notifications << Notifiable.new('issue_note_added', 'issue_updated')
|
||
| 4817 | notifications << Notifiable.new('issue_status_updated', 'issue_updated')
|
||
| 4818 | notifications << Notifiable.new('issue_priority_updated', 'issue_updated')
|
||
| 4819 | notifications << Notifiable.new('news_added')
|
||
| 4820 | 441:cbce1fd3b1b7 | Chris | notifications << Notifiable.new('news_comment_added')
|
| 4821 | 37:94944d00e43c | chris | notifications << Notifiable.new('document_added')
|
| 4822 | notifications << Notifiable.new('file_added')
|
||
| 4823 | notifications << Notifiable.new('message_posted')
|
||
| 4824 | notifications << Notifiable.new('wiki_content_added')
|
||
| 4825 | notifications << Notifiable.new('wiki_content_updated')
|
||
| 4826 | notifications |
||
| 4827 | end |
||
| 4828 | end |
||
| 4829 | end |
||
| 4830 | 1464:261b3d9a4903 | Chris | # encoding: utf-8 |
| 4831 | # |
||
| 4832 | # Redmine - project management software |
||
| 4833 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 4834 | 1464:261b3d9a4903 | Chris | # |
| 4835 | # This program is free software; you can redistribute it and/or |
||
| 4836 | # modify it under the terms of the GNU General Public License |
||
| 4837 | # as published by the Free Software Foundation; either version 2 |
||
| 4838 | # of the License, or (at your option) any later version. |
||
| 4839 | # |
||
| 4840 | # This program is distributed in the hope that it will be useful, |
||
| 4841 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 4842 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 4843 | # GNU General Public License for more details. |
||
| 4844 | # |
||
| 4845 | # You should have received a copy of the GNU General Public License |
||
| 4846 | # along with this program; if not, write to the Free Software |
||
| 4847 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 4848 | |||
| 4849 | module Redmine |
||
| 4850 | module Pagination |
||
| 4851 | class Paginator |
||
| 4852 | attr_reader :item_count, :per_page, :page, :page_param |
||
| 4853 | |||
| 4854 | def initialize(*args) |
||
| 4855 | if args.first.is_a?(ActionController::Base) |
||
| 4856 | args.shift |
||
| 4857 | ActiveSupport::Deprecation.warn "Paginator no longer takes a controller instance as the first argument. Remove it from #new arguments." |
||
| 4858 | end |
||
| 4859 | item_count, per_page, page, page_param = *args |
||
| 4860 | |||
| 4861 | @item_count = item_count |
||
| 4862 | @per_page = per_page |
||
| 4863 | page = (page || 1).to_i |
||
| 4864 | if page < 1 |
||
| 4865 | page = 1 |
||
| 4866 | end |
||
| 4867 | @page = page |
||
| 4868 | @page_param = page_param || :page |
||
| 4869 | end |
||
| 4870 | |||
| 4871 | def offset |
||
| 4872 | (page - 1) * per_page |
||
| 4873 | end |
||
| 4874 | |||
| 4875 | def first_page |
||
| 4876 | if item_count > 0 |
||
| 4877 | 1 |
||
| 4878 | end |
||
| 4879 | end |
||
| 4880 | |||
| 4881 | def previous_page |
||
| 4882 | if page > 1 |
||
| 4883 | page - 1 |
||
| 4884 | end |
||
| 4885 | end |
||
| 4886 | |||
| 4887 | def next_page |
||
| 4888 | if last_item < item_count |
||
| 4889 | page + 1 |
||
| 4890 | end |
||
| 4891 | end |
||
| 4892 | |||
| 4893 | def last_page |
||
| 4894 | if item_count > 0 |
||
| 4895 | (item_count - 1) / per_page + 1 |
||
| 4896 | end |
||
| 4897 | end |
||
| 4898 | |||
| 4899 | def first_item |
||
| 4900 | item_count == 0 ? 0 : (offset + 1) |
||
| 4901 | end |
||
| 4902 | |||
| 4903 | def last_item |
||
| 4904 | l = first_item + per_page - 1 |
||
| 4905 | l > item_count ? item_count : l |
||
| 4906 | end |
||
| 4907 | |||
| 4908 | def linked_pages |
||
| 4909 | pages = [] |
||
| 4910 | if item_count > 0 |
||
| 4911 | pages += [first_page, page, last_page] |
||
| 4912 | pages += ((page-2)..(page+2)).to_a.select {|p| p > first_page && p < last_page}
|
||
| 4913 | end |
||
| 4914 | pages = pages.compact.uniq.sort |
||
| 4915 | if pages.size > 1 |
||
| 4916 | pages |
||
| 4917 | else |
||
| 4918 | [] |
||
| 4919 | end |
||
| 4920 | end |
||
| 4921 | |||
| 4922 | def items_per_page |
||
| 4923 | ActiveSupport::Deprecation.warn "Paginator#items_per_page will be removed. Use #per_page instead." |
||
| 4924 | per_page |
||
| 4925 | end |
||
| 4926 | |||
| 4927 | def current |
||
| 4928 | ActiveSupport::Deprecation.warn "Paginator#current will be removed. Use .offset instead of .current.offset." |
||
| 4929 | self |
||
| 4930 | end |
||
| 4931 | end |
||
| 4932 | |||
| 4933 | # Paginates the given scope or model. Returns a Paginator instance and |
||
| 4934 | # the collection of objects for the current page. |
||
| 4935 | # |
||
| 4936 | # Options: |
||
| 4937 | # :parameter name of the page parameter |
||
| 4938 | # |
||
| 4939 | # Examples: |
||
| 4940 | # @user_pages, @users = paginate User.where(:status => 1) |
||
| 4941 | # |
||
| 4942 | def paginate(scope, options={})
|
||
| 4943 | options = options.dup |
||
| 4944 | finder_options = options.extract!( |
||
| 4945 | :conditions, |
||
| 4946 | :order, |
||
| 4947 | :joins, |
||
| 4948 | :include, |
||
| 4949 | :select |
||
| 4950 | ) |
||
| 4951 | if scope.is_a?(Symbol) || finder_options.values.compact.any? |
||
| 4952 | return deprecated_paginate(scope, finder_options, options) |
||
| 4953 | end |
||
| 4954 | |||
| 4955 | paginator = paginator(scope.count, options) |
||
| 4956 | collection = scope.limit(paginator.per_page).offset(paginator.offset).to_a |
||
| 4957 | |||
| 4958 | return paginator, collection |
||
| 4959 | end |
||
| 4960 | |||
| 4961 | def deprecated_paginate(arg, finder_options, options={})
|
||
| 4962 | ActiveSupport::Deprecation.warn "#paginate with a Symbol and/or find options is depreceted and will be removed. Use a scope instead." |
||
| 4963 | klass = arg.is_a?(Symbol) ? arg.to_s.classify.constantize : arg |
||
| 4964 | scope = klass.scoped(finder_options) |
||
| 4965 | paginate(scope, options) |
||
| 4966 | end |
||
| 4967 | |||
| 4968 | def paginator(item_count, options={})
|
||
| 4969 | options.assert_valid_keys :parameter, :per_page |
||
| 4970 | |||
| 4971 | page_param = options[:parameter] || :page |
||
| 4972 | page = (params[page_param] || 1).to_i |
||
| 4973 | per_page = options[:per_page] || per_page_option |
||
| 4974 | Paginator.new(item_count, per_page, page, page_param) |
||
| 4975 | end |
||
| 4976 | |||
| 4977 | module Helper |
||
| 4978 | include Redmine::I18n |
||
| 4979 | |||
| 4980 | # Renders the pagination links for the given paginator. |
||
| 4981 | # |
||
| 4982 | # Options: |
||
| 4983 | # :per_page_links if set to false, the "Per page" links are not rendered |
||
| 4984 | # |
||
| 4985 | def pagination_links_full(*args) |
||
| 4986 | pagination_links_each(*args) do |text, parameters, options| |
||
| 4987 | if block_given? |
||
| 4988 | yield text, parameters, options |
||
| 4989 | else |
||
| 4990 | link_to text, params.merge(parameters), options |
||
| 4991 | end |
||
| 4992 | end |
||
| 4993 | end |
||
| 4994 | |||
| 4995 | # Yields the given block with the text and parameters |
||
| 4996 | # for each pagination link and returns a string that represents the links |
||
| 4997 | def pagination_links_each(paginator, count=nil, options={}, &block)
|
||
| 4998 | options.assert_valid_keys :per_page_links |
||
| 4999 | |||
| 5000 | per_page_links = options.delete(:per_page_links) |
||
| 5001 | per_page_links = false if count.nil? |
||
| 5002 | page_param = paginator.page_param |
||
| 5003 | |||
| 5004 | html = '' |
||
| 5005 | if paginator.previous_page |
||
| 5006 | # \xc2\xab(utf-8) = « |
||
| 5007 | text = "\xc2\xab " + l(:label_previous) |
||
| 5008 | html << yield(text, {page_param => paginator.previous_page}, :class => 'previous') + ' '
|
||
| 5009 | end |
||
| 5010 | |||
| 5011 | previous = nil |
||
| 5012 | paginator.linked_pages.each do |page| |
||
| 5013 | if previous && previous != page - 1 |
||
| 5014 | html << content_tag('span', '...', :class => 'spacer') + ' '
|
||
| 5015 | end |
||
| 5016 | if page == paginator.page |
||
| 5017 | html << content_tag('span', page.to_s, :class => 'current page')
|
||
| 5018 | else |
||
| 5019 | html << yield(page.to_s, {page_param => page}, :class => 'page')
|
||
| 5020 | end |
||
| 5021 | html << ' ' |
||
| 5022 | previous = page |
||
| 5023 | end |
||
| 5024 | |||
| 5025 | if paginator.next_page |
||
| 5026 | # \xc2\xbb(utf-8) = » |
||
| 5027 | text = l(:label_next) + " \xc2\xbb" |
||
| 5028 | html << yield(text, {page_param => paginator.next_page}, :class => 'next') + ' '
|
||
| 5029 | end |
||
| 5030 | |||
| 5031 | html << content_tag('span', "(#{paginator.first_item}-#{paginator.last_item}/#{paginator.item_count})", :class => 'items') + ' '
|
||
| 5032 | |||
| 5033 | if per_page_links != false && links = per_page_links(paginator, &block) |
||
| 5034 | html << content_tag('span', links.to_s, :class => 'per-page')
|
||
| 5035 | end |
||
| 5036 | |||
| 5037 | html.html_safe |
||
| 5038 | end |
||
| 5039 | |||
| 5040 | # Renders the "Per page" links. |
||
| 5041 | def per_page_links(paginator, &block) |
||
| 5042 | values = per_page_options(paginator.per_page, paginator.item_count) |
||
| 5043 | if values.any? |
||
| 5044 | links = values.collect do |n| |
||
| 5045 | if n == paginator.per_page |
||
| 5046 | content_tag('span', n.to_s)
|
||
| 5047 | else |
||
| 5048 | yield(n, :per_page => n, paginator.page_param => nil) |
||
| 5049 | end |
||
| 5050 | end |
||
| 5051 | l(:label_display_per_page, links.join(', ')).html_safe
|
||
| 5052 | end |
||
| 5053 | end |
||
| 5054 | |||
| 5055 | def per_page_options(selected=nil, item_count=nil) |
||
| 5056 | options = Setting.per_page_options_array |
||
| 5057 | if item_count && options.any? |
||
| 5058 | if item_count > options.first |
||
| 5059 | max = options.detect {|value| value >= item_count} || item_count
|
||
| 5060 | else |
||
| 5061 | max = item_count |
||
| 5062 | end |
||
| 5063 | options = options.select {|value| value <= max || value == selected}
|
||
| 5064 | end |
||
| 5065 | if options.empty? || (options.size == 1 && options.first == selected) |
||
| 5066 | [] |
||
| 5067 | else |
||
| 5068 | options |
||
| 5069 | end |
||
| 5070 | end |
||
| 5071 | end |
||
| 5072 | end |
||
| 5073 | end |
||
| 5074 | 0:513646585e45 | Chris | # Redmine - project management software |
| 5075 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 5076 | 0:513646585e45 | Chris | # |
| 5077 | # This program is free software; you can redistribute it and/or |
||
| 5078 | # modify it under the terms of the GNU General Public License |
||
| 5079 | # as published by the Free Software Foundation; either version 2 |
||
| 5080 | # of the License, or (at your option) any later version. |
||
| 5081 | 909:cbb26bc654de | Chris | # |
| 5082 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 5083 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 5084 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 5085 | # GNU General Public License for more details. |
||
| 5086 | 909:cbb26bc654de | Chris | # |
| 5087 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 5088 | # along with this program; if not, write to the Free Software |
||
| 5089 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 5090 | |||
| 5091 | module Redmine |
||
| 5092 | module Platform |
||
| 5093 | class << self |
||
| 5094 | def mswin? |
||
| 5095 | 909:cbb26bc654de | Chris | (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || |
| 5096 | (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) |
||
| 5097 | 0:513646585e45 | Chris | end |
| 5098 | end |
||
| 5099 | end |
||
| 5100 | end |
||
| 5101 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 5102 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 5103 | 0:513646585e45 | Chris | # |
| 5104 | # This program is free software; you can redistribute it and/or |
||
| 5105 | # modify it under the terms of the GNU General Public License |
||
| 5106 | # as published by the Free Software Foundation; either version 2 |
||
| 5107 | # of the License, or (at your option) any later version. |
||
| 5108 | 909:cbb26bc654de | Chris | # |
| 5109 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 5110 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 5111 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 5112 | # GNU General Public License for more details. |
||
| 5113 | 909:cbb26bc654de | Chris | # |
| 5114 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 5115 | # along with this program; if not, write to the Free Software |
||
| 5116 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 5117 | |||
| 5118 | module Redmine #:nodoc: |
||
| 5119 | |||
| 5120 | class PluginNotFound < StandardError; end |
||
| 5121 | class PluginRequirementError < StandardError; end |
||
| 5122 | 909:cbb26bc654de | Chris | |
| 5123 | 0:513646585e45 | Chris | # Base class for Redmine plugins. |
| 5124 | # Plugins are registered using the <tt>register</tt> class method that acts as the public constructor. |
||
| 5125 | 909:cbb26bc654de | Chris | # |
| 5126 | 0:513646585e45 | Chris | # Redmine::Plugin.register :example do |
| 5127 | # name 'Example plugin' |
||
| 5128 | # author 'John Smith' |
||
| 5129 | # description 'This is an example plugin for Redmine' |
||
| 5130 | # version '0.0.1' |
||
| 5131 | # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
|
||
| 5132 | # end |
||
| 5133 | 909:cbb26bc654de | Chris | # |
| 5134 | 0:513646585e45 | Chris | # === Plugin attributes |
| 5135 | 909:cbb26bc654de | Chris | # |
| 5136 | 0:513646585e45 | Chris | # +settings+ is an optional attribute that let the plugin be configurable. |
| 5137 | # It must be a hash with the following keys: |
||
| 5138 | # * <tt>:default</tt>: default value for the plugin settings |
||
| 5139 | # * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory |
||
| 5140 | # Example: |
||
| 5141 | # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
|
||
| 5142 | # In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>. |
||
| 5143 | 909:cbb26bc654de | Chris | # |
| 5144 | 0:513646585e45 | Chris | # When rendered, the plugin settings value is available as the local variable +settings+ |
| 5145 | class Plugin |
||
| 5146 | 1115:433d4f72a19b | Chris | cattr_accessor :directory |
| 5147 | self.directory = File.join(Rails.root, 'plugins') |
||
| 5148 | |||
| 5149 | cattr_accessor :public_directory |
||
| 5150 | self.public_directory = File.join(Rails.root, 'public', 'plugin_assets') |
||
| 5151 | |||
| 5152 | 0:513646585e45 | Chris | @registered_plugins = {}
|
| 5153 | class << self |
||
| 5154 | attr_reader :registered_plugins |
||
| 5155 | private :new |
||
| 5156 | |||
| 5157 | def def_field(*names) |
||
| 5158 | 909:cbb26bc654de | Chris | class_eval do |
| 5159 | 0:513646585e45 | Chris | names.each do |name| |
| 5160 | 909:cbb26bc654de | Chris | define_method(name) do |*args| |
| 5161 | 0:513646585e45 | Chris | args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
|
| 5162 | end |
||
| 5163 | end |
||
| 5164 | end |
||
| 5165 | end |
||
| 5166 | end |
||
| 5167 | 1464:261b3d9a4903 | Chris | def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory |
| 5168 | 0:513646585e45 | Chris | attr_reader :id |
| 5169 | 909:cbb26bc654de | Chris | |
| 5170 | 0:513646585e45 | Chris | # Plugin constructor |
| 5171 | def self.register(id, &block) |
||
| 5172 | p = new(id) |
||
| 5173 | p.instance_eval(&block) |
||
| 5174 | 1464:261b3d9a4903 | Chris | |
| 5175 | 0:513646585e45 | Chris | # Set a default name if it was not provided during registration |
| 5176 | p.name(id.to_s.humanize) if p.name.nil? |
||
| 5177 | 1464:261b3d9a4903 | Chris | # Set a default directory if it was not provided during registration |
| 5178 | p.directory(File.join(self.directory, id.to_s)) if p.directory.nil? |
||
| 5179 | 1115:433d4f72a19b | Chris | |
| 5180 | 0:513646585e45 | Chris | # Adds plugin locales if any |
| 5181 | # YAML translation files should be found under <plugin>/config/locales/ |
||
| 5182 | 1517:dffacf8a6908 | Chris | Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml')) |
| 5183 | 1115:433d4f72a19b | Chris | |
| 5184 | # Prepends the app/views directory of the plugin to the view path |
||
| 5185 | view_path = File.join(p.directory, 'app', 'views') |
||
| 5186 | if File.directory?(view_path) |
||
| 5187 | ActionController::Base.prepend_view_path(view_path) |
||
| 5188 | ActionMailer::Base.prepend_view_path(view_path) |
||
| 5189 | end |
||
| 5190 | |||
| 5191 | # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
|
||
| 5192 | Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
|
||
| 5193 | ActiveSupport::Dependencies.autoload_paths += [dir] |
||
| 5194 | end |
||
| 5195 | |||
| 5196 | 0:513646585e45 | Chris | registered_plugins[id] = p |
| 5197 | end |
||
| 5198 | 909:cbb26bc654de | Chris | |
| 5199 | # Returns an array of all registered plugins |
||
| 5200 | 0:513646585e45 | Chris | def self.all |
| 5201 | registered_plugins.values.sort |
||
| 5202 | end |
||
| 5203 | 909:cbb26bc654de | Chris | |
| 5204 | 0:513646585e45 | Chris | # Finds a plugin by its id |
| 5205 | # Returns a PluginNotFound exception if the plugin doesn't exist |
||
| 5206 | def self.find(id) |
||
| 5207 | registered_plugins[id.to_sym] || raise(PluginNotFound) |
||
| 5208 | end |
||
| 5209 | 909:cbb26bc654de | Chris | |
| 5210 | 0:513646585e45 | Chris | # Clears the registered plugins hash |
| 5211 | # It doesn't unload installed plugins |
||
| 5212 | def self.clear |
||
| 5213 | @registered_plugins = {}
|
||
| 5214 | end |
||
| 5215 | 37:94944d00e43c | chris | |
| 5216 | # Checks if a plugin is installed |
||
| 5217 | # |
||
| 5218 | # @param [String] id name of the plugin |
||
| 5219 | def self.installed?(id) |
||
| 5220 | registered_plugins[id.to_sym].present? |
||
| 5221 | end |
||
| 5222 | 909:cbb26bc654de | Chris | |
| 5223 | 1115:433d4f72a19b | Chris | def self.load |
| 5224 | Dir.glob(File.join(self.directory, '*')).sort.each do |directory| |
||
| 5225 | if File.directory?(directory) |
||
| 5226 | lib = File.join(directory, "lib") |
||
| 5227 | if File.directory?(lib) |
||
| 5228 | $:.unshift lib |
||
| 5229 | ActiveSupport::Dependencies.autoload_paths += [lib] |
||
| 5230 | end |
||
| 5231 | initializer = File.join(directory, "init.rb") |
||
| 5232 | if File.file?(initializer) |
||
| 5233 | require initializer |
||
| 5234 | end |
||
| 5235 | end |
||
| 5236 | end |
||
| 5237 | end |
||
| 5238 | |||
| 5239 | 0:513646585e45 | Chris | def initialize(id) |
| 5240 | @id = id.to_sym |
||
| 5241 | end |
||
| 5242 | 909:cbb26bc654de | Chris | |
| 5243 | 1464:261b3d9a4903 | Chris | def public_directory |
| 5244 | File.join(self.class.public_directory, id.to_s) |
||
| 5245 | 1115:433d4f72a19b | Chris | end |
| 5246 | |||
| 5247 | 1464:261b3d9a4903 | Chris | def to_param |
| 5248 | id |
||
| 5249 | 1115:433d4f72a19b | Chris | end |
| 5250 | |||
| 5251 | def assets_directory |
||
| 5252 | File.join(directory, 'assets') |
||
| 5253 | end |
||
| 5254 | |||
| 5255 | 0:513646585e45 | Chris | def <=>(plugin) |
| 5256 | self.id.to_s <=> plugin.id.to_s |
||
| 5257 | end |
||
| 5258 | 909:cbb26bc654de | Chris | |
| 5259 | 0:513646585e45 | Chris | # Sets a requirement on Redmine version |
| 5260 | # Raises a PluginRequirementError exception if the requirement is not met |
||
| 5261 | # |
||
| 5262 | # Examples |
||
| 5263 | # # Requires Redmine 0.7.3 or higher |
||
| 5264 | # requires_redmine :version_or_higher => '0.7.3' |
||
| 5265 | # requires_redmine '0.7.3' |
||
| 5266 | # |
||
| 5267 | 1115:433d4f72a19b | Chris | # # Requires Redmine 0.7.x or higher |
| 5268 | # requires_redmine '0.7' |
||
| 5269 | # |
||
| 5270 | 0:513646585e45 | Chris | # # Requires a specific Redmine version |
| 5271 | # requires_redmine :version => '0.7.3' # 0.7.3 only |
||
| 5272 | 1115:433d4f72a19b | Chris | # requires_redmine :version => '0.7' # 0.7.x |
| 5273 | 0:513646585e45 | Chris | # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0 |
| 5274 | 1115:433d4f72a19b | Chris | # |
| 5275 | # # Requires a Redmine version within a range |
||
| 5276 | # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1 |
||
| 5277 | # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x |
||
| 5278 | 0:513646585e45 | Chris | def requires_redmine(arg) |
| 5279 | arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
|
||
| 5280 | arg.assert_valid_keys(:version, :version_or_higher) |
||
| 5281 | 909:cbb26bc654de | Chris | |
| 5282 | 0:513646585e45 | Chris | current = Redmine::VERSION.to_a |
| 5283 | 1115:433d4f72a19b | Chris | arg.each do |k, req| |
| 5284 | 0:513646585e45 | Chris | case k |
| 5285 | when :version_or_higher |
||
| 5286 | 1115:433d4f72a19b | Chris | raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
|
| 5287 | unless compare_versions(req, current) <= 0 |
||
| 5288 | raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
|
||
| 5289 | 0:513646585e45 | Chris | end |
| 5290 | when :version |
||
| 5291 | 1115:433d4f72a19b | Chris | req = [req] if req.is_a?(String) |
| 5292 | if req.is_a?(Array) |
||
| 5293 | unless req.detect {|ver| compare_versions(ver, current) == 0}
|
||
| 5294 | raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
|
||
| 5295 | end |
||
| 5296 | elsif req.is_a?(Range) |
||
| 5297 | unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 |
||
| 5298 | raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
|
||
| 5299 | end |
||
| 5300 | else |
||
| 5301 | raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
|
||
| 5302 | 0:513646585e45 | Chris | end |
| 5303 | end |
||
| 5304 | end |
||
| 5305 | true |
||
| 5306 | end |
||
| 5307 | |||
| 5308 | 1115:433d4f72a19b | Chris | def compare_versions(requirement, current) |
| 5309 | requirement = requirement.split('.').collect(&:to_i)
|
||
| 5310 | requirement <=> current.slice(0, requirement.size) |
||
| 5311 | end |
||
| 5312 | private :compare_versions |
||
| 5313 | |||
| 5314 | 0:513646585e45 | Chris | # Sets a requirement on a Redmine plugin version |
| 5315 | # Raises a PluginRequirementError exception if the requirement is not met |
||
| 5316 | # |
||
| 5317 | # Examples |
||
| 5318 | # # Requires a plugin named :foo version 0.7.3 or higher |
||
| 5319 | # requires_redmine_plugin :foo, :version_or_higher => '0.7.3' |
||
| 5320 | # requires_redmine_plugin :foo, '0.7.3' |
||
| 5321 | # |
||
| 5322 | # # Requires a specific version of a Redmine plugin |
||
| 5323 | # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only |
||
| 5324 | # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0 |
||
| 5325 | def requires_redmine_plugin(plugin_name, arg) |
||
| 5326 | arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
|
||
| 5327 | arg.assert_valid_keys(:version, :version_or_higher) |
||
| 5328 | |||
| 5329 | plugin = Plugin.find(plugin_name) |
||
| 5330 | current = plugin.version.split('.').collect(&:to_i)
|
||
| 5331 | |||
| 5332 | arg.each do |k, v| |
||
| 5333 | v = [] << v unless v.is_a?(Array) |
||
| 5334 | versions = v.collect {|s| s.split('.').collect(&:to_i)}
|
||
| 5335 | case k |
||
| 5336 | when :version_or_higher |
||
| 5337 | raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
|
||
| 5338 | unless (current <=> versions.first) >= 0 |
||
| 5339 | raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
|
||
| 5340 | end |
||
| 5341 | when :version |
||
| 5342 | unless versions.include?(current.slice(0,3)) |
||
| 5343 | raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
|
||
| 5344 | end |
||
| 5345 | end |
||
| 5346 | end |
||
| 5347 | true |
||
| 5348 | end |
||
| 5349 | |||
| 5350 | # Adds an item to the given +menu+. |
||
| 5351 | # The +id+ parameter (equals to the project id) is automatically added to the url. |
||
| 5352 | # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
|
||
| 5353 | 909:cbb26bc654de | Chris | # |
| 5354 | 0:513646585e45 | Chris | # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu |
| 5355 | 909:cbb26bc654de | Chris | # |
| 5356 | 0:513646585e45 | Chris | def menu(menu, item, url, options={})
|
| 5357 | Redmine::MenuManager.map(menu).push(item, url, options) |
||
| 5358 | end |
||
| 5359 | alias :add_menu_item :menu |
||
| 5360 | 909:cbb26bc654de | Chris | |
| 5361 | 0:513646585e45 | Chris | # Removes +item+ from the given +menu+. |
| 5362 | def delete_menu_item(menu, item) |
||
| 5363 | Redmine::MenuManager.map(menu).delete(item) |
||
| 5364 | end |
||
| 5365 | |||
| 5366 | # Defines a permission called +name+ for the given +actions+. |
||
| 5367 | 909:cbb26bc654de | Chris | # |
| 5368 | 0:513646585e45 | Chris | # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array): |
| 5369 | # permission :destroy_contacts, { :contacts => :destroy }
|
||
| 5370 | # permission :view_contacts, { :contacts => [:index, :show] }
|
||
| 5371 | 909:cbb26bc654de | Chris | # |
| 5372 | 1115:433d4f72a19b | Chris | # The +options+ argument is a hash that accept the following keys: |
| 5373 | # * :public => the permission is public if set to true (implicitly given to any user) |
||
| 5374 | # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member |
||
| 5375 | # * :read => set it to true so that the permission is still granted on closed projects |
||
| 5376 | 909:cbb26bc654de | Chris | # |
| 5377 | 0:513646585e45 | Chris | # Examples |
| 5378 | # # A permission that is implicitly given to any user |
||
| 5379 | # # This permission won't appear on the Roles & Permissions setup screen |
||
| 5380 | 1115:433d4f72a19b | Chris | # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
|
| 5381 | 909:cbb26bc654de | Chris | # |
| 5382 | 0:513646585e45 | Chris | # # A permission that can be given to any user |
| 5383 | # permission :say_hello, { :example => :say_hello }
|
||
| 5384 | 909:cbb26bc654de | Chris | # |
| 5385 | 0:513646585e45 | Chris | # # A permission that can be given to registered users only |
| 5386 | # permission :say_hello, { :example => :say_hello }, :require => :loggedin
|
||
| 5387 | 909:cbb26bc654de | Chris | # |
| 5388 | 0:513646585e45 | Chris | # # A permission that can be given to project members only |
| 5389 | # permission :say_hello, { :example => :say_hello }, :require => :member
|
||
| 5390 | def permission(name, actions, options = {})
|
||
| 5391 | if @project_module |
||
| 5392 | Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
|
||
| 5393 | else |
||
| 5394 | Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
|
||
| 5395 | end |
||
| 5396 | end |
||
| 5397 | 909:cbb26bc654de | Chris | |
| 5398 | 0:513646585e45 | Chris | # Defines a project module, that can be enabled/disabled for each project. |
| 5399 | # Permissions defined inside +block+ will be bind to the module. |
||
| 5400 | 909:cbb26bc654de | Chris | # |
| 5401 | 0:513646585e45 | Chris | # project_module :things do |
| 5402 | # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
|
||
| 5403 | # permission :destroy_contacts, { :contacts => :destroy }
|
||
| 5404 | # end |
||
| 5405 | def project_module(name, &block) |
||
| 5406 | @project_module = name |
||
| 5407 | self.instance_eval(&block) |
||
| 5408 | @project_module = nil |
||
| 5409 | end |
||
| 5410 | 909:cbb26bc654de | Chris | |
| 5411 | 0:513646585e45 | Chris | # Registers an activity provider. |
| 5412 | # |
||
| 5413 | # Options: |
||
| 5414 | # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default) |
||
| 5415 | # * <tt>:default</tt> - setting this option to false will make the events not displayed by default |
||
| 5416 | 909:cbb26bc654de | Chris | # |
| 5417 | 0:513646585e45 | Chris | # A model can provide several activity event types. |
| 5418 | 909:cbb26bc654de | Chris | # |
| 5419 | 0:513646585e45 | Chris | # Examples: |
| 5420 | # register :news |
||
| 5421 | # register :scrums, :class_name => 'Meeting' |
||
| 5422 | # register :issues, :class_name => ['Issue', 'Journal'] |
||
| 5423 | 909:cbb26bc654de | Chris | # |
| 5424 | 0:513646585e45 | Chris | # Retrieving events: |
| 5425 | # Associated model(s) must implement the find_events class method. |
||
| 5426 | # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method. |
||
| 5427 | 909:cbb26bc654de | Chris | # |
| 5428 | # The following call should return all the scrum events visible by current user that occured in the 5 last days: |
||
| 5429 | 0:513646585e45 | Chris | # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
|
| 5430 | # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
|
||
| 5431 | 909:cbb26bc654de | Chris | # |
| 5432 | 0:513646585e45 | Chris | # Note that :view_scrums permission is required to view these events in the activity view. |
| 5433 | def activity_provider(*args) |
||
| 5434 | Redmine::Activity.register(*args) |
||
| 5435 | end |
||
| 5436 | 909:cbb26bc654de | Chris | |
| 5437 | 0:513646585e45 | Chris | # Registers a wiki formatter. |
| 5438 | # |
||
| 5439 | # Parameters: |
||
| 5440 | # * +name+ - human-readable name |
||
| 5441 | # * +formatter+ - formatter class, which should have an instance method +to_html+ |
||
| 5442 | # * +helper+ - helper module, which will be included by wiki pages |
||
| 5443 | def wiki_format_provider(name, formatter, helper) |
||
| 5444 | Redmine::WikiFormatting.register(name, formatter, helper) |
||
| 5445 | end |
||
| 5446 | |||
| 5447 | # Returns +true+ if the plugin can be configured. |
||
| 5448 | def configurable? |
||
| 5449 | settings && settings.is_a?(Hash) && !settings[:partial].blank? |
||
| 5450 | end |
||
| 5451 | 1115:433d4f72a19b | Chris | |
| 5452 | def mirror_assets |
||
| 5453 | source = assets_directory |
||
| 5454 | destination = public_directory |
||
| 5455 | return unless File.directory?(source) |
||
| 5456 | |||
| 5457 | source_files = Dir[source + "/**/*"] |
||
| 5458 | source_dirs = source_files.select { |d| File.directory?(d) }
|
||
| 5459 | source_files -= source_dirs |
||
| 5460 | |||
| 5461 | unless source_files.empty? |
||
| 5462 | base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, '')) |
||
| 5463 | begin |
||
| 5464 | FileUtils.mkdir_p(base_target_dir) |
||
| 5465 | rescue Exception => e |
||
| 5466 | raise "Could not create directory #{base_target_dir}: " + e.message
|
||
| 5467 | end |
||
| 5468 | end |
||
| 5469 | |||
| 5470 | source_dirs.each do |dir| |
||
| 5471 | # strip down these paths so we have simple, relative paths we can |
||
| 5472 | # add to the destination |
||
| 5473 | target_dir = File.join(destination, dir.gsub(source, '')) |
||
| 5474 | begin |
||
| 5475 | FileUtils.mkdir_p(target_dir) |
||
| 5476 | rescue Exception => e |
||
| 5477 | raise "Could not create directory #{target_dir}: " + e.message
|
||
| 5478 | end |
||
| 5479 | end |
||
| 5480 | |||
| 5481 | source_files.each do |file| |
||
| 5482 | begin |
||
| 5483 | target = File.join(destination, file.gsub(source, '')) |
||
| 5484 | unless File.exist?(target) && FileUtils.identical?(file, target) |
||
| 5485 | FileUtils.cp(file, target) |
||
| 5486 | end |
||
| 5487 | rescue Exception => e |
||
| 5488 | raise "Could not copy #{file} to #{target}: " + e.message
|
||
| 5489 | end |
||
| 5490 | end |
||
| 5491 | end |
||
| 5492 | |||
| 5493 | # Mirrors assets from one or all plugins to public/plugin_assets |
||
| 5494 | def self.mirror_assets(name=nil) |
||
| 5495 | if name.present? |
||
| 5496 | find(name).mirror_assets |
||
| 5497 | else |
||
| 5498 | all.each do |plugin| |
||
| 5499 | plugin.mirror_assets |
||
| 5500 | end |
||
| 5501 | end |
||
| 5502 | end |
||
| 5503 | |||
| 5504 | # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>) |
||
| 5505 | def migration_directory |
||
| 5506 | File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate') |
||
| 5507 | end |
||
| 5508 | |||
| 5509 | # Returns the version number of the latest migration for this plugin. Returns |
||
| 5510 | # nil if this plugin has no migrations. |
||
| 5511 | def latest_migration |
||
| 5512 | migrations.last |
||
| 5513 | end |
||
| 5514 | |||
| 5515 | # Returns the version numbers of all migrations for this plugin. |
||
| 5516 | def migrations |
||
| 5517 | migrations = Dir[migration_directory+"/*.rb"] |
||
| 5518 | migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
|
||
| 5519 | end |
||
| 5520 | |||
| 5521 | # Migrate this plugin to the given version |
||
| 5522 | def migrate(version = nil) |
||
| 5523 | puts "Migrating #{id} (#{name})..."
|
||
| 5524 | Redmine::Plugin::Migrator.migrate_plugin(self, version) |
||
| 5525 | end |
||
| 5526 | |||
| 5527 | # Migrates all plugins or a single plugin to a given version |
||
| 5528 | # Exemples: |
||
| 5529 | # Plugin.migrate |
||
| 5530 | # Plugin.migrate('sample_plugin')
|
||
| 5531 | # Plugin.migrate('sample_plugin', 1)
|
||
| 5532 | # |
||
| 5533 | def self.migrate(name=nil, version=nil) |
||
| 5534 | if name.present? |
||
| 5535 | find(name).migrate(version) |
||
| 5536 | else |
||
| 5537 | all.each do |plugin| |
||
| 5538 | plugin.migrate |
||
| 5539 | end |
||
| 5540 | end |
||
| 5541 | end |
||
| 5542 | |||
| 5543 | class Migrator < ActiveRecord::Migrator |
||
| 5544 | # We need to be able to set the 'current' plugin being migrated. |
||
| 5545 | cattr_accessor :current_plugin |
||
| 5546 | 1464:261b3d9a4903 | Chris | |
| 5547 | 1115:433d4f72a19b | Chris | class << self |
| 5548 | # Runs the migrations from a plugin, up (or down) to the version given |
||
| 5549 | def migrate_plugin(plugin, version) |
||
| 5550 | self.current_plugin = plugin |
||
| 5551 | return if current_version(plugin) == version |
||
| 5552 | migrate(plugin.migration_directory, version) |
||
| 5553 | end |
||
| 5554 | 1464:261b3d9a4903 | Chris | |
| 5555 | 1115:433d4f72a19b | Chris | def current_version(plugin=current_plugin) |
| 5556 | # Delete migrations that don't match .. to_i will work because the number comes first |
||
| 5557 | ::ActiveRecord::Base.connection.select_values( |
||
| 5558 | "SELECT version FROM #{schema_migrations_table_name}"
|
||
| 5559 | ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
|
||
| 5560 | end |
||
| 5561 | end |
||
| 5562 | 1464:261b3d9a4903 | Chris | |
| 5563 | 1115:433d4f72a19b | Chris | def migrated |
| 5564 | sm_table = self.class.schema_migrations_table_name |
||
| 5565 | ::ActiveRecord::Base.connection.select_values( |
||
| 5566 | "SELECT version FROM #{sm_table}"
|
||
| 5567 | ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
|
||
| 5568 | end |
||
| 5569 | 1464:261b3d9a4903 | Chris | |
| 5570 | 1115:433d4f72a19b | Chris | def record_version_state_after_migrating(version) |
| 5571 | super(version.to_s + "-" + current_plugin.id.to_s) |
||
| 5572 | end |
||
| 5573 | end |
||
| 5574 | 0:513646585e45 | Chris | end |
| 5575 | end |
||
| 5576 | # Redmine - project management software |
||
| 5577 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 5578 | 0:513646585e45 | Chris | # |
| 5579 | # This program is free software; you can redistribute it and/or |
||
| 5580 | # modify it under the terms of the GNU General Public License |
||
| 5581 | # as published by the Free Software Foundation; either version 2 |
||
| 5582 | # of the License, or (at your option) any later version. |
||
| 5583 | 909:cbb26bc654de | Chris | # |
| 5584 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 5585 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 5586 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 5587 | # GNU General Public License for more details. |
||
| 5588 | 909:cbb26bc654de | Chris | # |
| 5589 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 5590 | # along with this program; if not, write to the Free Software |
||
| 5591 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 5592 | |||
| 5593 | require 'net/pop' |
||
| 5594 | |||
| 5595 | module Redmine |
||
| 5596 | module POP3 |
||
| 5597 | class << self |
||
| 5598 | def check(pop_options={}, options={})
|
||
| 5599 | host = pop_options[:host] || '127.0.0.1' |
||
| 5600 | port = pop_options[:port] || '110' |
||
| 5601 | apop = (pop_options[:apop].to_s == '1') |
||
| 5602 | delete_unprocessed = (pop_options[:delete_unprocessed].to_s == '1') |
||
| 5603 | |||
| 5604 | pop = Net::POP3.APOP(apop).new(host,port) |
||
| 5605 | 210:0579821a129a | Chris | logger.debug "Connecting to #{host}..." if logger && logger.debug?
|
| 5606 | 0:513646585e45 | Chris | pop.start(pop_options[:username], pop_options[:password]) do |pop_session| |
| 5607 | if pop_session.mails.empty? |
||
| 5608 | 210:0579821a129a | Chris | logger.debug "No email to process" if logger && logger.debug? |
| 5609 | 0:513646585e45 | Chris | else |
| 5610 | 210:0579821a129a | Chris | logger.debug "#{pop_session.mails.size} email(s) to process..." if logger && logger.debug?
|
| 5611 | 0:513646585e45 | Chris | pop_session.each_mail do |msg| |
| 5612 | message = msg.pop |
||
| 5613 | 1115:433d4f72a19b | Chris | message_id = (message =~ /^Message-I[dD]: (.*)/ ? $1 : '').strip |
| 5614 | 0:513646585e45 | Chris | if MailHandler.receive(message, options) |
| 5615 | msg.delete |
||
| 5616 | 210:0579821a129a | Chris | logger.debug "--> Message #{message_id} processed and deleted from the server" if logger && logger.debug?
|
| 5617 | 0:513646585e45 | Chris | else |
| 5618 | if delete_unprocessed |
||
| 5619 | msg.delete |
||
| 5620 | 210:0579821a129a | Chris | logger.debug "--> Message #{message_id} NOT processed and deleted from the server" if logger && logger.debug?
|
| 5621 | 0:513646585e45 | Chris | else |
| 5622 | 210:0579821a129a | Chris | logger.debug "--> Message #{message_id} NOT processed and left on the server" if logger && logger.debug?
|
| 5623 | 0:513646585e45 | Chris | end |
| 5624 | end |
||
| 5625 | end |
||
| 5626 | end |
||
| 5627 | end |
||
| 5628 | end |
||
| 5629 | 909:cbb26bc654de | Chris | |
| 5630 | 210:0579821a129a | Chris | private |
| 5631 | |||
| 5632 | def logger |
||
| 5633 | 1115:433d4f72a19b | Chris | ::Rails.logger |
| 5634 | 210:0579821a129a | Chris | end |
| 5635 | 0:513646585e45 | Chris | end |
| 5636 | end |
||
| 5637 | end |
||
| 5638 | 119:8661b858af72 | Chris | # Redmine - project management software |
| 5639 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 5640 | 119:8661b858af72 | Chris | # |
| 5641 | # This program is free software; you can redistribute it and/or |
||
| 5642 | # modify it under the terms of the GNU General Public License |
||
| 5643 | # as published by the Free Software Foundation; either version 2 |
||
| 5644 | # of the License, or (at your option) any later version. |
||
| 5645 | 909:cbb26bc654de | Chris | # |
| 5646 | 119:8661b858af72 | Chris | # This program is distributed in the hope that it will be useful, |
| 5647 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 5648 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 5649 | # GNU General Public License for more details. |
||
| 5650 | 909:cbb26bc654de | Chris | # |
| 5651 | 119:8661b858af72 | Chris | # You should have received a copy of the GNU General Public License |
| 5652 | # along with this program; if not, write to the Free Software |
||
| 5653 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 5654 | |||
| 5655 | module Redmine |
||
| 5656 | module SafeAttributes |
||
| 5657 | def self.included(base) |
||
| 5658 | base.extend(ClassMethods) |
||
| 5659 | end |
||
| 5660 | 909:cbb26bc654de | Chris | |
| 5661 | 119:8661b858af72 | Chris | module ClassMethods |
| 5662 | # Declares safe attributes |
||
| 5663 | # An optional Proc can be given for conditional inclusion |
||
| 5664 | # |
||
| 5665 | # Example: |
||
| 5666 | # safe_attributes 'title', 'pages' |
||
| 5667 | # safe_attributes 'isbn', :if => {|book, user| book.author == user}
|
||
| 5668 | def safe_attributes(*args) |
||
| 5669 | @safe_attributes ||= [] |
||
| 5670 | if args.empty? |
||
| 5671 | 1115:433d4f72a19b | Chris | if superclass.include?(Redmine::SafeAttributes) |
| 5672 | @safe_attributes + superclass.safe_attributes |
||
| 5673 | else |
||
| 5674 | @safe_attributes |
||
| 5675 | end |
||
| 5676 | 119:8661b858af72 | Chris | else |
| 5677 | options = args.last.is_a?(Hash) ? args.pop : {}
|
||
| 5678 | @safe_attributes << [args, options] |
||
| 5679 | end |
||
| 5680 | end |
||
| 5681 | end |
||
| 5682 | 909:cbb26bc654de | Chris | |
| 5683 | 119:8661b858af72 | Chris | # Returns an array that can be safely set by user or current user |
| 5684 | # |
||
| 5685 | # Example: |
||
| 5686 | # book.safe_attributes # => ['title', 'pages'] |
||
| 5687 | # book.safe_attributes(book.author) # => ['title', 'pages', 'isbn'] |
||
| 5688 | 1115:433d4f72a19b | Chris | def safe_attribute_names(user=nil) |
| 5689 | return @safe_attribute_names if @safe_attribute_names && user.nil? |
||
| 5690 | 119:8661b858af72 | Chris | names = [] |
| 5691 | self.class.safe_attributes.collect do |attrs, options| |
||
| 5692 | 1115:433d4f72a19b | Chris | if options[:if].nil? || options[:if].call(self, user || User.current) |
| 5693 | 119:8661b858af72 | Chris | names += attrs.collect(&:to_s) |
| 5694 | end |
||
| 5695 | end |
||
| 5696 | 1115:433d4f72a19b | Chris | names.uniq! |
| 5697 | @safe_attribute_names = names if user.nil? |
||
| 5698 | names |
||
| 5699 | end |
||
| 5700 | |||
| 5701 | # Returns true if attr can be set by user or the current user |
||
| 5702 | def safe_attribute?(attr, user=nil) |
||
| 5703 | safe_attribute_names(user).include?(attr.to_s) |
||
| 5704 | 119:8661b858af72 | Chris | end |
| 5705 | 909:cbb26bc654de | Chris | |
| 5706 | 119:8661b858af72 | Chris | # Returns a hash with unsafe attributes removed |
| 5707 | # from the given attrs hash |
||
| 5708 | 909:cbb26bc654de | Chris | # |
| 5709 | 119:8661b858af72 | Chris | # Example: |
| 5710 | # book.delete_unsafe_attributes({'title' => 'My book', 'foo' => 'bar'})
|
||
| 5711 | # # => {'title' => 'My book'}
|
||
| 5712 | def delete_unsafe_attributes(attrs, user=User.current) |
||
| 5713 | safe = safe_attribute_names(user) |
||
| 5714 | attrs.dup.delete_if {|k,v| !safe.include?(k)}
|
||
| 5715 | end |
||
| 5716 | 909:cbb26bc654de | Chris | |
| 5717 | 119:8661b858af72 | Chris | # Sets attributes from attrs that are safe |
| 5718 | # attrs is a Hash with string keys |
||
| 5719 | def safe_attributes=(attrs, user=User.current) |
||
| 5720 | return unless attrs.is_a?(Hash) |
||
| 5721 | self.attributes = delete_unsafe_attributes(attrs, user) |
||
| 5722 | end |
||
| 5723 | end |
||
| 5724 | end |
||
| 5725 | 1464:261b3d9a4903 | Chris | # Redmine - project management software |
| 5726 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 5727 | 1464:261b3d9a4903 | Chris | # |
| 5728 | # This program is free software; you can redistribute it and/or |
||
| 5729 | # modify it under the terms of the GNU General Public License |
||
| 5730 | # as published by the Free Software Foundation; either version 2 |
||
| 5731 | # of the License, or (at your option) any later version. |
||
| 5732 | # |
||
| 5733 | # This program is distributed in the hope that it will be useful, |
||
| 5734 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 5735 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 5736 | # GNU General Public License for more details. |
||
| 5737 | # |
||
| 5738 | # You should have received a copy of the GNU General Public License |
||
| 5739 | # along with this program; if not, write to the Free Software |
||
| 5740 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 5741 | |||
| 5742 | module Redmine |
||
| 5743 | module Scm |
||
| 5744 | module Adapters |
||
| 5745 | class CommandFailed < StandardError #:nodoc: |
||
| 5746 | end |
||
| 5747 | end |
||
| 5748 | end |
||
| 5749 | end |
||
| 5750 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 5751 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 5752 | 0:513646585e45 | Chris | # |
| 5753 | # This program is free software; you can redistribute it and/or |
||
| 5754 | # modify it under the terms of the GNU General Public License |
||
| 5755 | # as published by the Free Software Foundation; either version 2 |
||
| 5756 | # of the License, or (at your option) any later version. |
||
| 5757 | 441:cbce1fd3b1b7 | Chris | # |
| 5758 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 5759 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 5760 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 5761 | # GNU General Public License for more details. |
||
| 5762 | 441:cbce1fd3b1b7 | Chris | # |
| 5763 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 5764 | # along with this program; if not, write to the Free Software |
||
| 5765 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 5766 | |||
| 5767 | require 'cgi' |
||
| 5768 | 1464:261b3d9a4903 | Chris | require 'redmine/scm/adapters' |
| 5769 | |||
| 5770 | if RUBY_VERSION < '1.9' |
||
| 5771 | require 'iconv' |
||
| 5772 | end |
||
| 5773 | 0:513646585e45 | Chris | |
| 5774 | module Redmine |
||
| 5775 | module Scm |
||
| 5776 | 245:051f544170fe | Chris | module Adapters |
| 5777 | 0:513646585e45 | Chris | class AbstractAdapter #:nodoc: |
| 5778 | 909:cbb26bc654de | Chris | |
| 5779 | # raised if scm command exited with error, e.g. unknown revision. |
||
| 5780 | class ScmCommandAborted < CommandFailed; end |
||
| 5781 | |||
| 5782 | 0:513646585e45 | Chris | class << self |
| 5783 | 245:051f544170fe | Chris | def client_command |
| 5784 | "" |
||
| 5785 | end |
||
| 5786 | |||
| 5787 | 909:cbb26bc654de | Chris | def shell_quote_command |
| 5788 | if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java' |
||
| 5789 | client_command |
||
| 5790 | else |
||
| 5791 | shell_quote(client_command) |
||
| 5792 | end |
||
| 5793 | end |
||
| 5794 | |||
| 5795 | 0:513646585e45 | Chris | # Returns the version of the scm client |
| 5796 | # Eg: [1, 5, 0] or [] if unknown |
||
| 5797 | def client_version |
||
| 5798 | [] |
||
| 5799 | end |
||
| 5800 | 245:051f544170fe | Chris | |
| 5801 | 0:513646585e45 | Chris | # Returns the version string of the scm client |
| 5802 | # Eg: '1.5.0' or 'Unknown version' if unknown |
||
| 5803 | def client_version_string |
||
| 5804 | v = client_version || 'Unknown version' |
||
| 5805 | v.is_a?(Array) ? v.join('.') : v.to_s
|
||
| 5806 | end |
||
| 5807 | 245:051f544170fe | Chris | |
| 5808 | 0:513646585e45 | Chris | # Returns true if the current client version is above |
| 5809 | # or equals the given one |
||
| 5810 | # If option is :unknown is set to true, it will return |
||
| 5811 | # true if the client version is unknown |
||
| 5812 | def client_version_above?(v, options={})
|
||
| 5813 | ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown]) |
||
| 5814 | end |
||
| 5815 | 245:051f544170fe | Chris | |
| 5816 | def client_available |
||
| 5817 | true |
||
| 5818 | end |
||
| 5819 | |||
| 5820 | def shell_quote(str) |
||
| 5821 | if Redmine::Platform.mswin? |
||
| 5822 | '"' + str.gsub(/"/, '\\"') + '"' |
||
| 5823 | else |
||
| 5824 | "'" + str.gsub(/'/, "'\"'\"'") + "'" |
||
| 5825 | end |
||
| 5826 | end |
||
| 5827 | 0:513646585e45 | Chris | end |
| 5828 | 245:051f544170fe | Chris | |
| 5829 | def initialize(url, root_url=nil, login=nil, password=nil, |
||
| 5830 | path_encoding=nil) |
||
| 5831 | 0:513646585e45 | Chris | @url = url |
| 5832 | @login = login if login && !login.empty? |
||
| 5833 | @password = (password || "") if @login |
||
| 5834 | @root_url = root_url.blank? ? retrieve_root_url : root_url |
||
| 5835 | end |
||
| 5836 | 245:051f544170fe | Chris | |
| 5837 | 0:513646585e45 | Chris | def adapter_name |
| 5838 | 'Abstract' |
||
| 5839 | end |
||
| 5840 | 245:051f544170fe | Chris | |
| 5841 | 0:513646585e45 | Chris | def supports_cat? |
| 5842 | true |
||
| 5843 | end |
||
| 5844 | |||
| 5845 | def supports_annotate? |
||
| 5846 | respond_to?('annotate')
|
||
| 5847 | end |
||
| 5848 | 245:051f544170fe | Chris | |
| 5849 | 0:513646585e45 | Chris | def root_url |
| 5850 | @root_url |
||
| 5851 | end |
||
| 5852 | 245:051f544170fe | Chris | |
| 5853 | 0:513646585e45 | Chris | def url |
| 5854 | @url |
||
| 5855 | end |
||
| 5856 | 441:cbce1fd3b1b7 | Chris | |
| 5857 | def path_encoding |
||
| 5858 | nil |
||
| 5859 | end |
||
| 5860 | |||
| 5861 | 0:513646585e45 | Chris | # get info about the svn repository |
| 5862 | def info |
||
| 5863 | return nil |
||
| 5864 | end |
||
| 5865 | 441:cbce1fd3b1b7 | Chris | |
| 5866 | 0:513646585e45 | Chris | # Returns the entry identified by path and revision identifier |
| 5867 | # or nil if entry doesn't exist in the repository |
||
| 5868 | def entry(path=nil, identifier=nil) |
||
| 5869 | parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
|
||
| 5870 | search_path = parts[0..-2].join('/')
|
||
| 5871 | search_name = parts[-1] |
||
| 5872 | if search_path.blank? && search_name.blank? |
||
| 5873 | # Root entry |
||
| 5874 | Entry.new(:path => '', :kind => 'dir') |
||
| 5875 | else |
||
| 5876 | # Search for the entry in the parent directory |
||
| 5877 | es = entries(search_path, identifier) |
||
| 5878 | es ? es.detect {|e| e.name == search_name} : nil
|
||
| 5879 | end |
||
| 5880 | end |
||
| 5881 | 441:cbce1fd3b1b7 | Chris | |
| 5882 | 0:513646585e45 | Chris | # Returns an Entries collection |
| 5883 | # or nil if the given path doesn't exist in the repository |
||
| 5884 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 5885 | 0:513646585e45 | Chris | return nil |
| 5886 | end |
||
| 5887 | |||
| 5888 | def branches |
||
| 5889 | return nil |
||
| 5890 | end |
||
| 5891 | |||
| 5892 | 441:cbce1fd3b1b7 | Chris | def tags |
| 5893 | 0:513646585e45 | Chris | return nil |
| 5894 | end |
||
| 5895 | |||
| 5896 | def default_branch |
||
| 5897 | return nil |
||
| 5898 | end |
||
| 5899 | 441:cbce1fd3b1b7 | Chris | |
| 5900 | 0:513646585e45 | Chris | def properties(path, identifier=nil) |
| 5901 | return nil |
||
| 5902 | end |
||
| 5903 | 441:cbce1fd3b1b7 | Chris | |
| 5904 | 0:513646585e45 | Chris | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 5905 | return nil |
||
| 5906 | end |
||
| 5907 | 441:cbce1fd3b1b7 | Chris | |
| 5908 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 5909 | return nil |
||
| 5910 | end |
||
| 5911 | 441:cbce1fd3b1b7 | Chris | |
| 5912 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 5913 | return nil |
||
| 5914 | end |
||
| 5915 | 441:cbce1fd3b1b7 | Chris | |
| 5916 | 0:513646585e45 | Chris | def with_leading_slash(path) |
| 5917 | path ||= '' |
||
| 5918 | (path[0,1]!="/") ? "/#{path}" : path
|
||
| 5919 | end |
||
| 5920 | |||
| 5921 | def with_trailling_slash(path) |
||
| 5922 | path ||= '' |
||
| 5923 | (path[-1,1] == "/") ? path : "#{path}/"
|
||
| 5924 | end |
||
| 5925 | 245:051f544170fe | Chris | |
| 5926 | 0:513646585e45 | Chris | def without_leading_slash(path) |
| 5927 | path ||= '' |
||
| 5928 | path.gsub(%r{^/+}, '')
|
||
| 5929 | end |
||
| 5930 | |||
| 5931 | def without_trailling_slash(path) |
||
| 5932 | path ||= '' |
||
| 5933 | (path[-1,1] == "/") ? path[0..-2] : path |
||
| 5934 | 1517:dffacf8a6908 | Chris | end |
| 5935 | 245:051f544170fe | Chris | |
| 5936 | 0:513646585e45 | Chris | def shell_quote(str) |
| 5937 | 245:051f544170fe | Chris | self.class.shell_quote(str) |
| 5938 | 0:513646585e45 | Chris | end |
| 5939 | |||
| 5940 | private |
||
| 5941 | def retrieve_root_url |
||
| 5942 | info = self.info |
||
| 5943 | info ? info.root_url : nil |
||
| 5944 | end |
||
| 5945 | 441:cbce1fd3b1b7 | Chris | |
| 5946 | 909:cbb26bc654de | Chris | def target(path, sq=true) |
| 5947 | 0:513646585e45 | Chris | path ||= '' |
| 5948 | base = path.match(/^\//) ? root_url : url |
||
| 5949 | 909:cbb26bc654de | Chris | str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
|
| 5950 | if sq |
||
| 5951 | str = shell_quote(str) |
||
| 5952 | end |
||
| 5953 | str |
||
| 5954 | 0:513646585e45 | Chris | end |
| 5955 | 245:051f544170fe | Chris | |
| 5956 | 0:513646585e45 | Chris | def logger |
| 5957 | self.class.logger |
||
| 5958 | end |
||
| 5959 | 245:051f544170fe | Chris | |
| 5960 | 1115:433d4f72a19b | Chris | def shellout(cmd, options = {}, &block)
|
| 5961 | self.class.shellout(cmd, options, &block) |
||
| 5962 | 0:513646585e45 | Chris | end |
| 5963 | 245:051f544170fe | Chris | |
| 5964 | 0:513646585e45 | Chris | def self.logger |
| 5965 | 909:cbb26bc654de | Chris | Rails.logger |
| 5966 | 0:513646585e45 | Chris | end |
| 5967 | 245:051f544170fe | Chris | |
| 5968 | 1464:261b3d9a4903 | Chris | # Path to the file where scm stderr output is logged |
| 5969 | # Returns nil if the log file is not writable |
||
| 5970 | def self.stderr_log_file |
||
| 5971 | if @stderr_log_file.nil? |
||
| 5972 | writable = false |
||
| 5973 | path = Redmine::Configuration['scm_stderr_log_file'].presence |
||
| 5974 | path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
|
||
| 5975 | if File.exists?(path) |
||
| 5976 | if File.file?(path) && File.writable?(path) |
||
| 5977 | writable = true |
||
| 5978 | else |
||
| 5979 | logger.warn("SCM log file (#{path}) is not writable")
|
||
| 5980 | end |
||
| 5981 | else |
||
| 5982 | begin |
||
| 5983 | File.open(path, "w") {}
|
||
| 5984 | writable = true |
||
| 5985 | rescue => e |
||
| 5986 | logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
|
||
| 5987 | end |
||
| 5988 | end |
||
| 5989 | @stderr_log_file = writable ? path : false |
||
| 5990 | end |
||
| 5991 | @stderr_log_file || nil |
||
| 5992 | end |
||
| 5993 | |||
| 5994 | 1115:433d4f72a19b | Chris | def self.shellout(cmd, options = {}, &block)
|
| 5995 | 507:0c939c159af4 | Chris | if logger && logger.debug? |
| 5996 | logger.debug "Shelling out: #{strip_credential(cmd)}"
|
||
| 5997 | 1464:261b3d9a4903 | Chris | # Capture stderr in a log file |
| 5998 | if stderr_log_file |
||
| 5999 | cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
|
||
| 6000 | end |
||
| 6001 | 0:513646585e45 | Chris | end |
| 6002 | begin |
||
| 6003 | 1115:433d4f72a19b | Chris | mode = "r+" |
| 6004 | 245:051f544170fe | Chris | IO.popen(cmd, mode) do |io| |
| 6005 | 1115:433d4f72a19b | Chris | io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
|
| 6006 | io.close_write unless options[:write_stdin] |
||
| 6007 | 0:513646585e45 | Chris | block.call(io) if block_given? |
| 6008 | end |
||
| 6009 | 909:cbb26bc654de | Chris | ## If scm command does not exist, |
| 6010 | ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException |
||
| 6011 | ## in production environment. |
||
| 6012 | # rescue Errno::ENOENT => e |
||
| 6013 | rescue Exception => e |
||
| 6014 | 0:513646585e45 | Chris | msg = strip_credential(e.message) |
| 6015 | # The command failed, log it and re-raise |
||
| 6016 | 507:0c939c159af4 | Chris | logmsg = "SCM command failed, " |
| 6017 | logmsg += "make sure that your SCM command (e.g. svn) is " |
||
| 6018 | logmsg += "in PATH (#{ENV['PATH']})\n"
|
||
| 6019 | logmsg += "You can configure your scm commands in config/configuration.yml.\n" |
||
| 6020 | logmsg += "#{strip_credential(cmd)}\n"
|
||
| 6021 | logmsg += "with: #{msg}"
|
||
| 6022 | logger.error(logmsg) |
||
| 6023 | 0:513646585e45 | Chris | raise CommandFailed.new(msg) |
| 6024 | end |
||
| 6025 | 245:051f544170fe | Chris | end |
| 6026 | |||
| 6027 | 0:513646585e45 | Chris | # Hides username/password in a given command |
| 6028 | def self.strip_credential(cmd) |
||
| 6029 | q = (Redmine::Platform.mswin? ? '"' : "'") |
||
| 6030 | cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
|
||
| 6031 | end |
||
| 6032 | 441:cbce1fd3b1b7 | Chris | |
| 6033 | 0:513646585e45 | Chris | def strip_credential(cmd) |
| 6034 | self.class.strip_credential(cmd) |
||
| 6035 | end |
||
| 6036 | 245:051f544170fe | Chris | |
| 6037 | def scm_iconv(to, from, str) |
||
| 6038 | return nil if str.nil? |
||
| 6039 | 922:ad295b270cd4 | Chris | # bug 446: non-utf8 paths in repositories blow up repo viewer and reposman |
| 6040 | # -- Remove this short-circuit: we want the conversion to |
||
| 6041 | # happen always, so we can trap the error here if the |
||
| 6042 | # source text happens not to be in the advertised |
||
| 6043 | # encoding (instead of having the database blow up later) |
||
| 6044 | # return str if to == from |
||
| 6045 | 1115:433d4f72a19b | Chris | if str.respond_to?(:force_encoding) |
| 6046 | str.force_encoding(from) |
||
| 6047 | begin |
||
| 6048 | str.encode(to) |
||
| 6049 | rescue Exception => err |
||
| 6050 | logger.error("failed to convert from #{from} to #{to}. #{err}")
|
||
| 6051 | nil |
||
| 6052 | end |
||
| 6053 | else |
||
| 6054 | begin |
||
| 6055 | Iconv.conv(to, from, str) |
||
| 6056 | rescue Iconv::Failure => err |
||
| 6057 | logger.error("failed to convert from #{from} to #{to}. #{err}")
|
||
| 6058 | nil |
||
| 6059 | end |
||
| 6060 | 245:051f544170fe | Chris | end |
| 6061 | end |
||
| 6062 | 1115:433d4f72a19b | Chris | |
| 6063 | def parse_xml(xml) |
||
| 6064 | if RUBY_PLATFORM == 'java' |
||
| 6065 | xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
|
||
| 6066 | end |
||
| 6067 | ActiveSupport::XmlMini.parse(xml) |
||
| 6068 | end |
||
| 6069 | 0:513646585e45 | Chris | end |
| 6070 | 245:051f544170fe | Chris | |
| 6071 | 0:513646585e45 | Chris | class Entries < Array |
| 6072 | def sort_by_name |
||
| 6073 | 1115:433d4f72a19b | Chris | dup.sort! {|x,y|
|
| 6074 | 0:513646585e45 | Chris | if x.kind == y.kind |
| 6075 | x.name.to_s <=> y.name.to_s |
||
| 6076 | else |
||
| 6077 | x.kind <=> y.kind |
||
| 6078 | end |
||
| 6079 | 245:051f544170fe | Chris | } |
| 6080 | 0:513646585e45 | Chris | end |
| 6081 | 441:cbce1fd3b1b7 | Chris | |
| 6082 | 0:513646585e45 | Chris | def revisions |
| 6083 | revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
|
||
| 6084 | end |
||
| 6085 | end |
||
| 6086 | 441:cbce1fd3b1b7 | Chris | |
| 6087 | 0:513646585e45 | Chris | class Info |
| 6088 | attr_accessor :root_url, :lastrev |
||
| 6089 | def initialize(attributes={})
|
||
| 6090 | self.root_url = attributes[:root_url] if attributes[:root_url] |
||
| 6091 | self.lastrev = attributes[:lastrev] |
||
| 6092 | end |
||
| 6093 | end |
||
| 6094 | 441:cbce1fd3b1b7 | Chris | |
| 6095 | 0:513646585e45 | Chris | class Entry |
| 6096 | 1115:433d4f72a19b | Chris | attr_accessor :name, :path, :kind, :size, :lastrev, :changeset |
| 6097 | |||
| 6098 | 0:513646585e45 | Chris | def initialize(attributes={})
|
| 6099 | self.name = attributes[:name] if attributes[:name] |
||
| 6100 | self.path = attributes[:path] if attributes[:path] |
||
| 6101 | self.kind = attributes[:kind] if attributes[:kind] |
||
| 6102 | self.size = attributes[:size].to_i if attributes[:size] |
||
| 6103 | self.lastrev = attributes[:lastrev] |
||
| 6104 | end |
||
| 6105 | 441:cbce1fd3b1b7 | Chris | |
| 6106 | 0:513646585e45 | Chris | def is_file? |
| 6107 | 'file' == self.kind |
||
| 6108 | end |
||
| 6109 | 441:cbce1fd3b1b7 | Chris | |
| 6110 | 0:513646585e45 | Chris | def is_dir? |
| 6111 | 'dir' == self.kind |
||
| 6112 | end |
||
| 6113 | 441:cbce1fd3b1b7 | Chris | |
| 6114 | 0:513646585e45 | Chris | def is_text? |
| 6115 | Redmine::MimeType.is_type?('text', name)
|
||
| 6116 | end |
||
| 6117 | 1115:433d4f72a19b | Chris | |
| 6118 | def author |
||
| 6119 | if changeset |
||
| 6120 | changeset.author.to_s |
||
| 6121 | elsif lastrev |
||
| 6122 | Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
|
||
| 6123 | end |
||
| 6124 | end |
||
| 6125 | 0:513646585e45 | Chris | end |
| 6126 | 441:cbce1fd3b1b7 | Chris | |
| 6127 | 0:513646585e45 | Chris | class Revisions < Array |
| 6128 | def latest |
||
| 6129 | sort {|x,y|
|
||
| 6130 | unless x.time.nil? or y.time.nil? |
||
| 6131 | x.time <=> y.time |
||
| 6132 | else |
||
| 6133 | 0 |
||
| 6134 | end |
||
| 6135 | }.last |
||
| 6136 | 441:cbce1fd3b1b7 | Chris | end |
| 6137 | 0:513646585e45 | Chris | end |
| 6138 | 441:cbce1fd3b1b7 | Chris | |
| 6139 | 0:513646585e45 | Chris | class Revision |
| 6140 | 441:cbce1fd3b1b7 | Chris | attr_accessor :scmid, :name, :author, :time, :message, |
| 6141 | 909:cbb26bc654de | Chris | :paths, :revision, :branch, :identifier, |
| 6142 | :parents |
||
| 6143 | 0:513646585e45 | Chris | |
| 6144 | def initialize(attributes={})
|
||
| 6145 | self.identifier = attributes[:identifier] |
||
| 6146 | 441:cbce1fd3b1b7 | Chris | self.scmid = attributes[:scmid] |
| 6147 | self.name = attributes[:name] || self.identifier |
||
| 6148 | self.author = attributes[:author] |
||
| 6149 | self.time = attributes[:time] |
||
| 6150 | self.message = attributes[:message] || "" |
||
| 6151 | self.paths = attributes[:paths] |
||
| 6152 | self.revision = attributes[:revision] |
||
| 6153 | self.branch = attributes[:branch] |
||
| 6154 | 909:cbb26bc654de | Chris | self.parents = attributes[:parents] |
| 6155 | 117:af80e5618e9b | Chris | end |
| 6156 | |||
| 6157 | # Returns the readable identifier. |
||
| 6158 | def format_identifier |
||
| 6159 | 441:cbce1fd3b1b7 | Chris | self.identifier.to_s |
| 6160 | 117:af80e5618e9b | Chris | end |
| 6161 | 1115:433d4f72a19b | Chris | |
| 6162 | def ==(other) |
||
| 6163 | if other.nil? |
||
| 6164 | false |
||
| 6165 | elsif scmid.present? |
||
| 6166 | scmid == other.scmid |
||
| 6167 | elsif identifier.present? |
||
| 6168 | identifier == other.identifier |
||
| 6169 | elsif revision.present? |
||
| 6170 | revision == other.revision |
||
| 6171 | end |
||
| 6172 | end |
||
| 6173 | 245:051f544170fe | Chris | end |
| 6174 | 117:af80e5618e9b | Chris | |
| 6175 | 0:513646585e45 | Chris | class Annotate |
| 6176 | attr_reader :lines, :revisions |
||
| 6177 | 441:cbce1fd3b1b7 | Chris | |
| 6178 | 0:513646585e45 | Chris | def initialize |
| 6179 | @lines = [] |
||
| 6180 | @revisions = [] |
||
| 6181 | end |
||
| 6182 | 441:cbce1fd3b1b7 | Chris | |
| 6183 | 0:513646585e45 | Chris | def add_line(line, revision) |
| 6184 | @lines << line |
||
| 6185 | @revisions << revision |
||
| 6186 | end |
||
| 6187 | 441:cbce1fd3b1b7 | Chris | |
| 6188 | 0:513646585e45 | Chris | def content |
| 6189 | content = lines.join("\n")
|
||
| 6190 | end |
||
| 6191 | 441:cbce1fd3b1b7 | Chris | |
| 6192 | 0:513646585e45 | Chris | def empty? |
| 6193 | lines.empty? |
||
| 6194 | end |
||
| 6195 | end |
||
| 6196 | 909:cbb26bc654de | Chris | |
| 6197 | class Branch < String |
||
| 6198 | attr_accessor :revision, :scmid |
||
| 6199 | end |
||
| 6200 | 0:513646585e45 | Chris | end |
| 6201 | end |
||
| 6202 | end |
||
| 6203 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 6204 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 6205 | 0:513646585e45 | Chris | # |
| 6206 | # This program is free software; you can redistribute it and/or |
||
| 6207 | # modify it under the terms of the GNU General Public License |
||
| 6208 | # as published by the Free Software Foundation; either version 2 |
||
| 6209 | # of the License, or (at your option) any later version. |
||
| 6210 | 441:cbce1fd3b1b7 | Chris | # |
| 6211 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 6212 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 6213 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 6214 | # GNU General Public License for more details. |
||
| 6215 | 441:cbce1fd3b1b7 | Chris | # |
| 6216 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 6217 | # along with this program; if not, write to the Free Software |
||
| 6218 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 6219 | |||
| 6220 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 6221 | 0:513646585e45 | Chris | |
| 6222 | module Redmine |
||
| 6223 | module Scm |
||
| 6224 | 245:051f544170fe | Chris | module Adapters |
| 6225 | 0:513646585e45 | Chris | class BazaarAdapter < AbstractAdapter |
| 6226 | 245:051f544170fe | Chris | |
| 6227 | 0:513646585e45 | Chris | # Bazaar executable name |
| 6228 | 210:0579821a129a | Chris | BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr" |
| 6229 | 245:051f544170fe | Chris | |
| 6230 | class << self |
||
| 6231 | def client_command |
||
| 6232 | @@bin ||= BZR_BIN |
||
| 6233 | end |
||
| 6234 | |||
| 6235 | def sq_bin |
||
| 6236 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command |
| 6237 | 245:051f544170fe | Chris | end |
| 6238 | |||
| 6239 | def client_version |
||
| 6240 | @@client_version ||= (scm_command_version || []) |
||
| 6241 | end |
||
| 6242 | |||
| 6243 | def client_available |
||
| 6244 | !client_version.empty? |
||
| 6245 | end |
||
| 6246 | |||
| 6247 | def scm_command_version |
||
| 6248 | scm_version = scm_version_from_command_line.dup |
||
| 6249 | if scm_version.respond_to?(:force_encoding) |
||
| 6250 | scm_version.force_encoding('ASCII-8BIT')
|
||
| 6251 | end |
||
| 6252 | if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||
| 6253 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 6254 | end |
||
| 6255 | end |
||
| 6256 | |||
| 6257 | def scm_version_from_command_line |
||
| 6258 | shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
||
| 6259 | end |
||
| 6260 | end |
||
| 6261 | |||
| 6262 | 1115:433d4f72a19b | Chris | def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
| 6263 | @url = url |
||
| 6264 | @root_url = url |
||
| 6265 | @path_encoding = 'UTF-8' |
||
| 6266 | # do not call *super* for non ASCII repository path |
||
| 6267 | end |
||
| 6268 | |||
| 6269 | def bzr_path_encodig=(encoding) |
||
| 6270 | @path_encoding = encoding |
||
| 6271 | end |
||
| 6272 | |||
| 6273 | 0:513646585e45 | Chris | # Get info about the repository |
| 6274 | def info |
||
| 6275 | 909:cbb26bc654de | Chris | cmd_args = %w|revno| |
| 6276 | cmd_args << bzr_target('')
|
||
| 6277 | 0:513646585e45 | Chris | info = nil |
| 6278 | 909:cbb26bc654de | Chris | scm_cmd(*cmd_args) do |io| |
| 6279 | 0:513646585e45 | Chris | if io.read =~ %r{^(\d+)\r?$}
|
| 6280 | info = Info.new({:root_url => url,
|
||
| 6281 | :lastrev => Revision.new({
|
||
| 6282 | :identifier => $1 |
||
| 6283 | }) |
||
| 6284 | }) |
||
| 6285 | end |
||
| 6286 | end |
||
| 6287 | info |
||
| 6288 | 909:cbb26bc654de | Chris | rescue ScmCommandAborted |
| 6289 | 0:513646585e45 | Chris | return nil |
| 6290 | end |
||
| 6291 | 245:051f544170fe | Chris | |
| 6292 | 0:513646585e45 | Chris | # Returns an Entries collection |
| 6293 | # or nil if the given path doesn't exist in the repository |
||
| 6294 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 6295 | 0:513646585e45 | Chris | path ||= '' |
| 6296 | entries = Entries.new |
||
| 6297 | 441:cbce1fd3b1b7 | Chris | identifier = -1 unless identifier && identifier.to_i > 0 |
| 6298 | 909:cbb26bc654de | Chris | cmd_args = %w|ls -v --show-ids| |
| 6299 | cmd_args << "-r#{identifier.to_i}"
|
||
| 6300 | cmd_args << bzr_target(path) |
||
| 6301 | scm_cmd(*cmd_args) do |io| |
||
| 6302 | 1115:433d4f72a19b | Chris | prefix_utf8 = "#{url}/#{path}".gsub('\\', '/')
|
| 6303 | logger.debug "PREFIX: #{prefix_utf8}"
|
||
| 6304 | prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8) |
||
| 6305 | prefix.force_encoding('ASCII-8BIT') if prefix.respond_to?(:force_encoding)
|
||
| 6306 | 0:513646585e45 | Chris | re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
|
| 6307 | io.each_line do |line| |
||
| 6308 | next unless line =~ re |
||
| 6309 | 1464:261b3d9a4903 | Chris | name_locale, slash, revision = $3.strip, $4, $5.strip |
| 6310 | 1115:433d4f72a19b | Chris | name = scm_iconv('UTF-8', @path_encoding, name_locale)
|
| 6311 | entries << Entry.new({:name => name,
|
||
| 6312 | :path => ((path.empty? ? "" : "#{path}/") + name),
|
||
| 6313 | 1464:261b3d9a4903 | Chris | :kind => (slash.blank? ? 'file' : 'dir'), |
| 6314 | 0:513646585e45 | Chris | :size => nil, |
| 6315 | 1464:261b3d9a4903 | Chris | :lastrev => Revision.new(:revision => revision) |
| 6316 | 0:513646585e45 | Chris | }) |
| 6317 | end |
||
| 6318 | end |
||
| 6319 | 909:cbb26bc654de | Chris | if logger && logger.debug? |
| 6320 | logger.debug("Found #{entries.size} entries in the repository for #{target(path)}")
|
||
| 6321 | end |
||
| 6322 | 0:513646585e45 | Chris | entries.sort_by_name |
| 6323 | 909:cbb26bc654de | Chris | rescue ScmCommandAborted |
| 6324 | return nil |
||
| 6325 | 0:513646585e45 | Chris | end |
| 6326 | 245:051f544170fe | Chris | |
| 6327 | 0:513646585e45 | Chris | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 6328 | path ||= '' |
||
| 6329 | 117:af80e5618e9b | Chris | identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1' |
| 6330 | identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1 |
||
| 6331 | 0:513646585e45 | Chris | revisions = Revisions.new |
| 6332 | 909:cbb26bc654de | Chris | cmd_args = %w|log -v --show-ids| |
| 6333 | cmd_args << "-r#{identifier_to}..#{identifier_from}"
|
||
| 6334 | cmd_args << bzr_target(path) |
||
| 6335 | scm_cmd(*cmd_args) do |io| |
||
| 6336 | 0:513646585e45 | Chris | revision = nil |
| 6337 | 909:cbb26bc654de | Chris | parsing = nil |
| 6338 | 0:513646585e45 | Chris | io.each_line do |line| |
| 6339 | if line =~ /^----/ |
||
| 6340 | revisions << revision if revision |
||
| 6341 | revision = Revision.new(:paths => [], :message => '') |
||
| 6342 | parsing = nil |
||
| 6343 | else |
||
| 6344 | next unless revision |
||
| 6345 | if line =~ /^revno: (\d+)($|\s\[merge\]$)/ |
||
| 6346 | revision.identifier = $1.to_i |
||
| 6347 | elsif line =~ /^committer: (.+)$/ |
||
| 6348 | revision.author = $1.strip |
||
| 6349 | elsif line =~ /^revision-id:(.+)$/ |
||
| 6350 | revision.scmid = $1.strip |
||
| 6351 | elsif line =~ /^timestamp: (.+)$/ |
||
| 6352 | revision.time = Time.parse($1).localtime |
||
| 6353 | elsif line =~ /^ -----/ |
||
| 6354 | # partial revisions |
||
| 6355 | parsing = nil unless parsing == 'message' |
||
| 6356 | elsif line =~ /^(message|added|modified|removed|renamed):/ |
||
| 6357 | parsing = $1 |
||
| 6358 | elsif line =~ /^ (.*)$/ |
||
| 6359 | if parsing == 'message' |
||
| 6360 | revision.message << "#{$1}\n"
|
||
| 6361 | else |
||
| 6362 | if $1 =~ /^(.*)\s+(\S+)$/ |
||
| 6363 | 1115:433d4f72a19b | Chris | path_locale = $1.strip |
| 6364 | path = scm_iconv('UTF-8', @path_encoding, path_locale)
|
||
| 6365 | 0:513646585e45 | Chris | revid = $2 |
| 6366 | case parsing |
||
| 6367 | when 'added' |
||
| 6368 | revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
|
||
| 6369 | when 'modified' |
||
| 6370 | revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
|
||
| 6371 | when 'removed' |
||
| 6372 | revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
|
||
| 6373 | when 'renamed' |
||
| 6374 | new_path = path.split('=>').last
|
||
| 6375 | 1115:433d4f72a19b | Chris | if new_path |
| 6376 | revision.paths << {:action => 'M', :path => "/#{new_path.strip}",
|
||
| 6377 | :revision => revid} |
||
| 6378 | end |
||
| 6379 | 0:513646585e45 | Chris | end |
| 6380 | end |
||
| 6381 | end |
||
| 6382 | else |
||
| 6383 | parsing = nil |
||
| 6384 | end |
||
| 6385 | end |
||
| 6386 | end |
||
| 6387 | revisions << revision if revision |
||
| 6388 | end |
||
| 6389 | revisions |
||
| 6390 | 909:cbb26bc654de | Chris | rescue ScmCommandAborted |
| 6391 | return nil |
||
| 6392 | 0:513646585e45 | Chris | end |
| 6393 | 245:051f544170fe | Chris | |
| 6394 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 6395 | path ||= '' |
||
| 6396 | if identifier_to |
||
| 6397 | 441:cbce1fd3b1b7 | Chris | identifier_to = identifier_to.to_i |
| 6398 | 0:513646585e45 | Chris | else |
| 6399 | identifier_to = identifier_from.to_i - 1 |
||
| 6400 | end |
||
| 6401 | 117:af80e5618e9b | Chris | if identifier_from |
| 6402 | identifier_from = identifier_from.to_i |
||
| 6403 | end |
||
| 6404 | 0:513646585e45 | Chris | diff = [] |
| 6405 | 909:cbb26bc654de | Chris | cmd_args = %w|diff| |
| 6406 | cmd_args << "-r#{identifier_to}..#{identifier_from}"
|
||
| 6407 | cmd_args << bzr_target(path) |
||
| 6408 | scm_cmd_no_raise(*cmd_args) do |io| |
||
| 6409 | 0:513646585e45 | Chris | io.each_line do |line| |
| 6410 | diff << line |
||
| 6411 | end |
||
| 6412 | end |
||
| 6413 | diff |
||
| 6414 | end |
||
| 6415 | 245:051f544170fe | Chris | |
| 6416 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 6417 | cat = nil |
||
| 6418 | 909:cbb26bc654de | Chris | cmd_args = %w|cat| |
| 6419 | cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
|
||
| 6420 | cmd_args << bzr_target(path) |
||
| 6421 | scm_cmd(*cmd_args) do |io| |
||
| 6422 | 0:513646585e45 | Chris | io.binmode |
| 6423 | cat = io.read |
||
| 6424 | end |
||
| 6425 | cat |
||
| 6426 | 909:cbb26bc654de | Chris | rescue ScmCommandAborted |
| 6427 | return nil |
||
| 6428 | 0:513646585e45 | Chris | end |
| 6429 | 245:051f544170fe | Chris | |
| 6430 | 0:513646585e45 | Chris | def annotate(path, identifier=nil) |
| 6431 | blame = Annotate.new |
||
| 6432 | 909:cbb26bc654de | Chris | cmd_args = %w|annotate -q --all| |
| 6433 | cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
|
||
| 6434 | cmd_args << bzr_target(path) |
||
| 6435 | scm_cmd(*cmd_args) do |io| |
||
| 6436 | author = nil |
||
| 6437 | 0:513646585e45 | Chris | identifier = nil |
| 6438 | io.each_line do |line| |
||
| 6439 | next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
|
||
| 6440 | 441:cbce1fd3b1b7 | Chris | rev = $1 |
| 6441 | blame.add_line($3.rstrip, |
||
| 6442 | Revision.new( |
||
| 6443 | :identifier => rev, |
||
| 6444 | :revision => rev, |
||
| 6445 | :author => $2.strip |
||
| 6446 | )) |
||
| 6447 | 0:513646585e45 | Chris | end |
| 6448 | end |
||
| 6449 | blame |
||
| 6450 | 909:cbb26bc654de | Chris | rescue ScmCommandAborted |
| 6451 | return nil |
||
| 6452 | 0:513646585e45 | Chris | end |
| 6453 | 909:cbb26bc654de | Chris | |
| 6454 | def self.branch_conf_path(path) |
||
| 6455 | bcp = nil |
||
| 6456 | m = path.match(%r{^(.*[/\\])\.bzr.*$})
|
||
| 6457 | if m |
||
| 6458 | bcp = m[1] |
||
| 6459 | else |
||
| 6460 | bcp = path |
||
| 6461 | end |
||
| 6462 | bcp.gsub!(%r{[\/\\]$}, "")
|
||
| 6463 | if bcp |
||
| 6464 | bcp = File.join(bcp, ".bzr", "branch", "branch.conf") |
||
| 6465 | end |
||
| 6466 | bcp |
||
| 6467 | end |
||
| 6468 | |||
| 6469 | def append_revisions_only |
||
| 6470 | return @aro if ! @aro.nil? |
||
| 6471 | @aro = false |
||
| 6472 | bcp = self.class.branch_conf_path(url) |
||
| 6473 | if bcp && File.exist?(bcp) |
||
| 6474 | begin |
||
| 6475 | f = File::open(bcp, "r") |
||
| 6476 | cnt = 0 |
||
| 6477 | f.each_line do |line| |
||
| 6478 | l = line.chomp.to_s |
||
| 6479 | if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/ |
||
| 6480 | str_aro = $1 |
||
| 6481 | if str_aro.upcase == "TRUE" |
||
| 6482 | @aro = true |
||
| 6483 | cnt += 1 |
||
| 6484 | elsif str_aro.upcase == "FALSE" |
||
| 6485 | @aro = false |
||
| 6486 | cnt += 1 |
||
| 6487 | end |
||
| 6488 | if cnt > 1 |
||
| 6489 | @aro = false |
||
| 6490 | break |
||
| 6491 | end |
||
| 6492 | end |
||
| 6493 | end |
||
| 6494 | ensure |
||
| 6495 | f.close |
||
| 6496 | end |
||
| 6497 | end |
||
| 6498 | @aro |
||
| 6499 | end |
||
| 6500 | |||
| 6501 | def scm_cmd(*args, &block) |
||
| 6502 | full_args = [] |
||
| 6503 | full_args += args |
||
| 6504 | 1115:433d4f72a19b | Chris | full_args_locale = [] |
| 6505 | full_args.map do |e| |
||
| 6506 | full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e) |
||
| 6507 | end |
||
| 6508 | 909:cbb26bc654de | Chris | ret = shellout( |
| 6509 | 1115:433d4f72a19b | Chris | self.class.sq_bin + ' ' + |
| 6510 | full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
|
||
| 6511 | 909:cbb26bc654de | Chris | &block |
| 6512 | ) |
||
| 6513 | if $? && $?.exitstatus != 0 |
||
| 6514 | raise ScmCommandAborted, "bzr exited with non-zero status: #{$?.exitstatus}"
|
||
| 6515 | end |
||
| 6516 | ret |
||
| 6517 | end |
||
| 6518 | private :scm_cmd |
||
| 6519 | |||
| 6520 | def scm_cmd_no_raise(*args, &block) |
||
| 6521 | full_args = [] |
||
| 6522 | full_args += args |
||
| 6523 | 1115:433d4f72a19b | Chris | full_args_locale = [] |
| 6524 | full_args.map do |e| |
||
| 6525 | full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e) |
||
| 6526 | end |
||
| 6527 | 909:cbb26bc654de | Chris | ret = shellout( |
| 6528 | 1115:433d4f72a19b | Chris | self.class.sq_bin + ' ' + |
| 6529 | full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
|
||
| 6530 | 909:cbb26bc654de | Chris | &block |
| 6531 | ) |
||
| 6532 | ret |
||
| 6533 | end |
||
| 6534 | private :scm_cmd_no_raise |
||
| 6535 | |||
| 6536 | def bzr_target(path) |
||
| 6537 | target(path, false) |
||
| 6538 | end |
||
| 6539 | private :bzr_target |
||
| 6540 | 0:513646585e45 | Chris | end |
| 6541 | end |
||
| 6542 | end |
||
| 6543 | end |
||
| 6544 | # redMine - project management software |
||
| 6545 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
||
| 6546 | # |
||
| 6547 | # This program is free software; you can redistribute it and/or |
||
| 6548 | # modify it under the terms of the GNU General Public License |
||
| 6549 | # as published by the Free Software Foundation; either version 2 |
||
| 6550 | # of the License, or (at your option) any later version. |
||
| 6551 | 441:cbce1fd3b1b7 | Chris | # |
| 6552 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 6553 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 6554 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 6555 | # GNU General Public License for more details. |
||
| 6556 | 441:cbce1fd3b1b7 | Chris | # |
| 6557 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 6558 | # along with this program; if not, write to the Free Software |
||
| 6559 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 6560 | |||
| 6561 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 6562 | 0:513646585e45 | Chris | |
| 6563 | module Redmine |
||
| 6564 | module Scm |
||
| 6565 | module Adapters |
||
| 6566 | class CvsAdapter < AbstractAdapter |
||
| 6567 | |||
| 6568 | # CVS executable name |
||
| 6569 | 210:0579821a129a | Chris | CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs" |
| 6570 | 245:051f544170fe | Chris | |
| 6571 | class << self |
||
| 6572 | def client_command |
||
| 6573 | @@bin ||= CVS_BIN |
||
| 6574 | end |
||
| 6575 | |||
| 6576 | def sq_bin |
||
| 6577 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command |
| 6578 | 245:051f544170fe | Chris | end |
| 6579 | |||
| 6580 | def client_version |
||
| 6581 | @@client_version ||= (scm_command_version || []) |
||
| 6582 | end |
||
| 6583 | |||
| 6584 | def client_available |
||
| 6585 | client_version_above?([1, 12]) |
||
| 6586 | end |
||
| 6587 | |||
| 6588 | def scm_command_version |
||
| 6589 | scm_version = scm_version_from_command_line.dup |
||
| 6590 | if scm_version.respond_to?(:force_encoding) |
||
| 6591 | scm_version.force_encoding('ASCII-8BIT')
|
||
| 6592 | end |
||
| 6593 | if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
|
||
| 6594 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 6595 | end |
||
| 6596 | end |
||
| 6597 | |||
| 6598 | def scm_version_from_command_line |
||
| 6599 | shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
||
| 6600 | end |
||
| 6601 | end |
||
| 6602 | |||
| 6603 | 0:513646585e45 | Chris | # Guidelines for the input: |
| 6604 | 441:cbce1fd3b1b7 | Chris | # url -> the project-path, relative to the cvsroot (eg. module name) |
| 6605 | 0:513646585e45 | Chris | # root_url -> the good old, sometimes damned, CVSROOT |
| 6606 | 441:cbce1fd3b1b7 | Chris | # login -> unnecessary |
| 6607 | 0:513646585e45 | Chris | # password -> unnecessary too |
| 6608 | 245:051f544170fe | Chris | def initialize(url, root_url=nil, login=nil, password=nil, |
| 6609 | path_encoding=nil) |
||
| 6610 | 441:cbce1fd3b1b7 | Chris | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
| 6611 | @url = url |
||
| 6612 | # TODO: better Exception here (IllegalArgumentException) |
||
| 6613 | raise CommandFailed if root_url.blank? |
||
| 6614 | @root_url = root_url |
||
| 6615 | |||
| 6616 | # These are unused. |
||
| 6617 | @login = login if login && !login.empty? |
||
| 6618 | 0:513646585e45 | Chris | @password = (password || "") if @login |
| 6619 | end |
||
| 6620 | 245:051f544170fe | Chris | |
| 6621 | 441:cbce1fd3b1b7 | Chris | def path_encoding |
| 6622 | @path_encoding |
||
| 6623 | 0:513646585e45 | Chris | end |
| 6624 | 245:051f544170fe | Chris | |
| 6625 | 0:513646585e45 | Chris | def info |
| 6626 | logger.debug "<cvs> info" |
||
| 6627 | Info.new({:root_url => @root_url, :lastrev => nil})
|
||
| 6628 | end |
||
| 6629 | 245:051f544170fe | Chris | |
| 6630 | 0:513646585e45 | Chris | def get_previous_revision(revision) |
| 6631 | CvsRevisionHelper.new(revision).prevRev |
||
| 6632 | end |
||
| 6633 | 245:051f544170fe | Chris | |
| 6634 | 0:513646585e45 | Chris | # Returns an Entries collection |
| 6635 | # or nil if the given path doesn't exist in the repository |
||
| 6636 | # this method is used by the repository-browser (aka LIST) |
||
| 6637 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 6638 | 0:513646585e45 | Chris | logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
|
| 6639 | 441:cbce1fd3b1b7 | Chris | path_locale = scm_iconv(@path_encoding, 'UTF-8', path) |
| 6640 | path_locale.force_encoding("ASCII-8BIT") if path_locale.respond_to?(:force_encoding)
|
||
| 6641 | 0:513646585e45 | Chris | entries = Entries.new |
| 6642 | 441:cbce1fd3b1b7 | Chris | cmd_args = %w|-q rls -e| |
| 6643 | cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier |
||
| 6644 | cmd_args << path_with_proj(path) |
||
| 6645 | scm_cmd(*cmd_args) do |io| |
||
| 6646 | io.each_line() do |line| |
||
| 6647 | fields = line.chop.split('/',-1)
|
||
| 6648 | 0:513646585e45 | Chris | logger.debug(">>InspectLine #{fields.inspect}")
|
| 6649 | if fields[0]!="D" |
||
| 6650 | 441:cbce1fd3b1b7 | Chris | time = nil |
| 6651 | # Thu Dec 13 16:27:22 2007 |
||
| 6652 | time_l = fields[-3].split(' ')
|
||
| 6653 | if time_l.size == 5 && time_l[4].length == 4 |
||
| 6654 | begin |
||
| 6655 | time = Time.parse( |
||
| 6656 | "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
|
||
| 6657 | rescue |
||
| 6658 | end |
||
| 6659 | end |
||
| 6660 | entries << Entry.new( |
||
| 6661 | {
|
||
| 6662 | :name => scm_iconv('UTF-8', @path_encoding, fields[-5]),
|
||
| 6663 | 0:513646585e45 | Chris | #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]), |
| 6664 | 441:cbce1fd3b1b7 | Chris | :path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[-5]}"),
|
| 6665 | 0:513646585e45 | Chris | :kind => 'file', |
| 6666 | :size => nil, |
||
| 6667 | 441:cbce1fd3b1b7 | Chris | :lastrev => Revision.new( |
| 6668 | {
|
||
| 6669 | :revision => fields[-4], |
||
| 6670 | :name => scm_iconv('UTF-8', @path_encoding, fields[-4]),
|
||
| 6671 | :time => time, |
||
| 6672 | :author => '' |
||
| 6673 | }) |
||
| 6674 | 0:513646585e45 | Chris | }) |
| 6675 | else |
||
| 6676 | 441:cbce1fd3b1b7 | Chris | entries << Entry.new( |
| 6677 | {
|
||
| 6678 | :name => scm_iconv('UTF-8', @path_encoding, fields[1]),
|
||
| 6679 | :path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[1]}"),
|
||
| 6680 | :kind => 'dir', |
||
| 6681 | :size => nil, |
||
| 6682 | 0:513646585e45 | Chris | :lastrev => nil |
| 6683 | 441:cbce1fd3b1b7 | Chris | }) |
| 6684 | 0:513646585e45 | Chris | end |
| 6685 | 441:cbce1fd3b1b7 | Chris | end |
| 6686 | 0:513646585e45 | Chris | end |
| 6687 | entries.sort_by_name |
||
| 6688 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 6689 | nil |
||
| 6690 | 245:051f544170fe | Chris | end |
| 6691 | 0:513646585e45 | Chris | |
| 6692 | STARTLOG="----------------------------" |
||
| 6693 | ENDLOG ="=============================================================================" |
||
| 6694 | 245:051f544170fe | Chris | |
| 6695 | 0:513646585e45 | Chris | # Returns all revisions found between identifier_from and identifier_to |
| 6696 | # in the repository. both identifier have to be dates or nil. |
||
| 6697 | # these method returns nothing but yield every result in block |
||
| 6698 | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
|
||
| 6699 | 441:cbce1fd3b1b7 | Chris | path_with_project_utf8 = path_with_proj(path) |
| 6700 | path_with_project_locale = scm_iconv(@path_encoding, 'UTF-8', path_with_project_utf8) |
||
| 6701 | logger.debug "<cvs> revisions path:" + |
||
| 6702 | "'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
|
||
| 6703 | cmd_args = %w|-q rlog| |
||
| 6704 | cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
|
||
| 6705 | cmd_args << path_with_project_utf8 |
||
| 6706 | scm_cmd(*cmd_args) do |io| |
||
| 6707 | state = "entry_start" |
||
| 6708 | commit_log = String.new |
||
| 6709 | revision = nil |
||
| 6710 | date = nil |
||
| 6711 | author = nil |
||
| 6712 | entry_path = nil |
||
| 6713 | entry_name = nil |
||
| 6714 | file_state = nil |
||
| 6715 | branch_map = nil |
||
| 6716 | 245:051f544170fe | Chris | io.each_line() do |line| |
| 6717 | 441:cbce1fd3b1b7 | Chris | if state != "revision" && /^#{ENDLOG}/ =~ line
|
| 6718 | commit_log = String.new |
||
| 6719 | revision = nil |
||
| 6720 | state = "entry_start" |
||
| 6721 | 0:513646585e45 | Chris | end |
| 6722 | 441:cbce1fd3b1b7 | Chris | if state == "entry_start" |
| 6723 | branch_map = Hash.new |
||
| 6724 | if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
|
||
| 6725 | 0:513646585e45 | Chris | entry_path = normalize_cvs_path($1) |
| 6726 | entry_name = normalize_path(File.basename($1)) |
||
| 6727 | logger.debug("Path #{entry_path} <=> Name #{entry_name}")
|
||
| 6728 | elsif /^head: (.+)$/ =~ line |
||
| 6729 | entry_headRev = $1 #unless entry.nil? |
||
| 6730 | elsif /^symbolic names:/ =~ line |
||
| 6731 | 441:cbce1fd3b1b7 | Chris | state = "symbolic" #unless entry.nil? |
| 6732 | 0:513646585e45 | Chris | elsif /^#{STARTLOG}/ =~ line
|
| 6733 | 441:cbce1fd3b1b7 | Chris | commit_log = String.new |
| 6734 | state = "revision" |
||
| 6735 | end |
||
| 6736 | 0:513646585e45 | Chris | next |
| 6737 | 441:cbce1fd3b1b7 | Chris | elsif state == "symbolic" |
| 6738 | if /^(.*):\s(.*)/ =~ (line.strip) |
||
| 6739 | branch_map[$1] = $2 |
||
| 6740 | 0:513646585e45 | Chris | else |
| 6741 | 441:cbce1fd3b1b7 | Chris | state = "tags" |
| 6742 | 0:513646585e45 | Chris | next |
| 6743 | 441:cbce1fd3b1b7 | Chris | end |
| 6744 | elsif state == "tags" |
||
| 6745 | 0:513646585e45 | Chris | if /^#{STARTLOG}/ =~ line
|
| 6746 | commit_log = "" |
||
| 6747 | 441:cbce1fd3b1b7 | Chris | state = "revision" |
| 6748 | 0:513646585e45 | Chris | elsif /^#{ENDLOG}/ =~ line
|
| 6749 | 441:cbce1fd3b1b7 | Chris | state = "head" |
| 6750 | 0:513646585e45 | Chris | end |
| 6751 | next |
||
| 6752 | 441:cbce1fd3b1b7 | Chris | elsif state == "revision" |
| 6753 | 245:051f544170fe | Chris | if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
|
| 6754 | 0:513646585e45 | Chris | if revision |
| 6755 | 441:cbce1fd3b1b7 | Chris | revHelper = CvsRevisionHelper.new(revision) |
| 6756 | revBranch = "HEAD" |
||
| 6757 | branch_map.each() do |branch_name, branch_point| |
||
| 6758 | 0:513646585e45 | Chris | if revHelper.is_in_branch_with_symbol(branch_point) |
| 6759 | 441:cbce1fd3b1b7 | Chris | revBranch = branch_name |
| 6760 | 0:513646585e45 | Chris | end |
| 6761 | end |
||
| 6762 | logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
|
||
| 6763 | 245:051f544170fe | Chris | yield Revision.new({
|
| 6764 | 441:cbce1fd3b1b7 | Chris | :time => date, |
| 6765 | :author => author, |
||
| 6766 | :message => commit_log.chomp, |
||
| 6767 | 0:513646585e45 | Chris | :paths => [{
|
| 6768 | 1115:433d4f72a19b | Chris | :revision => revision.dup, |
| 6769 | :branch => revBranch.dup, |
||
| 6770 | 441:cbce1fd3b1b7 | Chris | :path => scm_iconv('UTF-8', @path_encoding, entry_path),
|
| 6771 | :name => scm_iconv('UTF-8', @path_encoding, entry_name),
|
||
| 6772 | :kind => 'file', |
||
| 6773 | :action => file_state |
||
| 6774 | }] |
||
| 6775 | }) |
||
| 6776 | 0:513646585e45 | Chris | end |
| 6777 | 441:cbce1fd3b1b7 | Chris | commit_log = String.new |
| 6778 | revision = nil |
||
| 6779 | 0:513646585e45 | Chris | if /^#{ENDLOG}/ =~ line
|
| 6780 | 441:cbce1fd3b1b7 | Chris | state = "entry_start" |
| 6781 | 0:513646585e45 | Chris | end |
| 6782 | next |
||
| 6783 | end |
||
| 6784 | 245:051f544170fe | Chris | |
| 6785 | 0:513646585e45 | Chris | if /^branches: (.+)$/ =~ line |
| 6786 | 441:cbce1fd3b1b7 | Chris | # TODO: version.branch = $1 |
| 6787 | 0:513646585e45 | Chris | elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line |
| 6788 | 441:cbce1fd3b1b7 | Chris | revision = $1 |
| 6789 | 0:513646585e45 | Chris | elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line |
| 6790 | 441:cbce1fd3b1b7 | Chris | date = Time.parse($1) |
| 6791 | line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line)
|
||
| 6792 | author_utf8 = /author: ([^;]+)/.match(line_utf8)[1] |
||
| 6793 | author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8) |
||
| 6794 | file_state = /state: ([^;]+)/.match(line)[1] |
||
| 6795 | # TODO: |
||
| 6796 | # linechanges only available in CVS.... |
||
| 6797 | # maybe a feature our SVN implementation. |
||
| 6798 | # I'm sure, they are useful for stats or something else |
||
| 6799 | 0:513646585e45 | Chris | # linechanges =/lines: \+(\d+) -(\d+)/.match(line) |
| 6800 | # unless linechanges.nil? |
||
| 6801 | # version.line_plus = linechanges[1] |
||
| 6802 | # version.line_minus = linechanges[2] |
||
| 6803 | # else |
||
| 6804 | # version.line_plus = 0 |
||
| 6805 | 245:051f544170fe | Chris | # version.line_minus = 0 |
| 6806 | # end |
||
| 6807 | else |
||
| 6808 | 0:513646585e45 | Chris | commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/ |
| 6809 | 245:051f544170fe | Chris | end |
| 6810 | end |
||
| 6811 | 0:513646585e45 | Chris | end |
| 6812 | end |
||
| 6813 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 6814 | Revisions.new |
||
| 6815 | 245:051f544170fe | Chris | end |
| 6816 | |||
| 6817 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 6818 | 441:cbce1fd3b1b7 | Chris | logger.debug "<cvs> diff path:'#{path}'" +
|
| 6819 | ",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
|
||
| 6820 | cmd_args = %w|rdiff -u| |
||
| 6821 | cmd_args << "-r#{identifier_to}"
|
||
| 6822 | cmd_args << "-r#{identifier_from}"
|
||
| 6823 | cmd_args << path_with_proj(path) |
||
| 6824 | 0:513646585e45 | Chris | diff = [] |
| 6825 | 441:cbce1fd3b1b7 | Chris | scm_cmd(*cmd_args) do |io| |
| 6826 | 0:513646585e45 | Chris | io.each_line do |line| |
| 6827 | diff << line |
||
| 6828 | end |
||
| 6829 | end |
||
| 6830 | diff |
||
| 6831 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 6832 | nil |
||
| 6833 | 245:051f544170fe | Chris | end |
| 6834 | |||
| 6835 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 6836 | identifier = (identifier) ? identifier : "HEAD" |
||
| 6837 | logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
|
||
| 6838 | 441:cbce1fd3b1b7 | Chris | cmd_args = %w|-q co| |
| 6839 | cmd_args << "-D" << time_to_cvstime(identifier) if identifier |
||
| 6840 | cmd_args << "-p" << path_with_proj(path) |
||
| 6841 | 0:513646585e45 | Chris | cat = nil |
| 6842 | 441:cbce1fd3b1b7 | Chris | scm_cmd(*cmd_args) do |io| |
| 6843 | 245:051f544170fe | Chris | io.binmode |
| 6844 | 0:513646585e45 | Chris | cat = io.read |
| 6845 | end |
||
| 6846 | cat |
||
| 6847 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 6848 | nil |
||
| 6849 | 245:051f544170fe | Chris | end |
| 6850 | 0:513646585e45 | Chris | |
| 6851 | def annotate(path, identifier=nil) |
||
| 6852 | 441:cbce1fd3b1b7 | Chris | identifier = (identifier) ? identifier : "HEAD" |
| 6853 | 0:513646585e45 | Chris | logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
|
| 6854 | 441:cbce1fd3b1b7 | Chris | cmd_args = %w|rannotate| |
| 6855 | cmd_args << "-D" << time_to_cvstime(identifier) if identifier |
||
| 6856 | cmd_args << path_with_proj(path) |
||
| 6857 | 0:513646585e45 | Chris | blame = Annotate.new |
| 6858 | 441:cbce1fd3b1b7 | Chris | scm_cmd(*cmd_args) do |io| |
| 6859 | 0:513646585e45 | Chris | io.each_line do |line| |
| 6860 | next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
|
||
| 6861 | 441:cbce1fd3b1b7 | Chris | blame.add_line( |
| 6862 | $3.rstrip, |
||
| 6863 | Revision.new( |
||
| 6864 | :revision => $1, |
||
| 6865 | :identifier => nil, |
||
| 6866 | :author => $2.strip |
||
| 6867 | )) |
||
| 6868 | 0:513646585e45 | Chris | end |
| 6869 | end |
||
| 6870 | blame |
||
| 6871 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 6872 | Annotate.new |
||
| 6873 | 0:513646585e45 | Chris | end |
| 6874 | 245:051f544170fe | Chris | |
| 6875 | 0:513646585e45 | Chris | private |
| 6876 | 245:051f544170fe | Chris | |
| 6877 | 0:513646585e45 | Chris | # Returns the root url without the connexion string |
| 6878 | # :pserver:anonymous@foo.bar:/path => /path |
||
| 6879 | # :ext:cvsservername:/path => /path |
||
| 6880 | def root_url_path |
||
| 6881 | 1464:261b3d9a4903 | Chris | root_url.to_s.gsub(%r{^:.+?(?=/)}, '')
|
| 6882 | 0:513646585e45 | Chris | end |
| 6883 | |||
| 6884 | # convert a date/time into the CVS-format |
||
| 6885 | def time_to_cvstime(time) |
||
| 6886 | return nil if time.nil? |
||
| 6887 | 441:cbce1fd3b1b7 | Chris | time = Time.now if time == 'HEAD' |
| 6888 | |||
| 6889 | 0:513646585e45 | Chris | unless time.kind_of? Time |
| 6890 | time = Time.parse(time) |
||
| 6891 | end |
||
| 6892 | 441:cbce1fd3b1b7 | Chris | return time_to_cvstime_rlog(time) |
| 6893 | 0:513646585e45 | Chris | end |
| 6894 | 210:0579821a129a | Chris | |
| 6895 | def time_to_cvstime_rlog(time) |
||
| 6896 | return nil if time.nil? |
||
| 6897 | t1 = time.clone.localtime |
||
| 6898 | return t1.strftime("%Y-%m-%d %H:%M:%S")
|
||
| 6899 | end |
||
| 6900 | 441:cbce1fd3b1b7 | Chris | |
| 6901 | 0:513646585e45 | Chris | def normalize_cvs_path(path) |
| 6902 | normalize_path(path.gsub(/Attic\//,'')) |
||
| 6903 | end |
||
| 6904 | 441:cbce1fd3b1b7 | Chris | |
| 6905 | 0:513646585e45 | Chris | def normalize_path(path) |
| 6906 | path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1') |
||
| 6907 | 441:cbce1fd3b1b7 | Chris | end |
| 6908 | |||
| 6909 | def path_with_proj(path) |
||
| 6910 | "#{url}#{with_leading_slash(path)}"
|
||
| 6911 | end |
||
| 6912 | private :path_with_proj |
||
| 6913 | |||
| 6914 | class Revision < Redmine::Scm::Adapters::Revision |
||
| 6915 | # Returns the readable identifier |
||
| 6916 | def format_identifier |
||
| 6917 | revision.to_s |
||
| 6918 | end |
||
| 6919 | end |
||
| 6920 | |||
| 6921 | def scm_cmd(*args, &block) |
||
| 6922 | 909:cbb26bc654de | Chris | full_args = ['-d', root_url] |
| 6923 | 441:cbce1fd3b1b7 | Chris | full_args += args |
| 6924 | full_args_locale = [] |
||
| 6925 | full_args.map do |e| |
||
| 6926 | full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e) |
||
| 6927 | end |
||
| 6928 | 909:cbb26bc654de | Chris | ret = shellout( |
| 6929 | self.class.sq_bin + ' ' + full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
|
||
| 6930 | &block |
||
| 6931 | ) |
||
| 6932 | 441:cbce1fd3b1b7 | Chris | if $? && $?.exitstatus != 0 |
| 6933 | raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
|
||
| 6934 | end |
||
| 6935 | ret |
||
| 6936 | end |
||
| 6937 | private :scm_cmd |
||
| 6938 | end |
||
| 6939 | |||
| 6940 | 0:513646585e45 | Chris | class CvsRevisionHelper |
| 6941 | attr_accessor :complete_rev, :revision, :base, :branchid |
||
| 6942 | 441:cbce1fd3b1b7 | Chris | |
| 6943 | 0:513646585e45 | Chris | def initialize(complete_rev) |
| 6944 | @complete_rev = complete_rev |
||
| 6945 | parseRevision() |
||
| 6946 | end |
||
| 6947 | 441:cbce1fd3b1b7 | Chris | |
| 6948 | 0:513646585e45 | Chris | def branchPoint |
| 6949 | return @base |
||
| 6950 | end |
||
| 6951 | 441:cbce1fd3b1b7 | Chris | |
| 6952 | 0:513646585e45 | Chris | def branchVersion |
| 6953 | if isBranchRevision |
||
| 6954 | return @base+"."+@branchid |
||
| 6955 | end |
||
| 6956 | return @base |
||
| 6957 | end |
||
| 6958 | 441:cbce1fd3b1b7 | Chris | |
| 6959 | 0:513646585e45 | Chris | def isBranchRevision |
| 6960 | !@branchid.nil? |
||
| 6961 | end |
||
| 6962 | 441:cbce1fd3b1b7 | Chris | |
| 6963 | 0:513646585e45 | Chris | def prevRev |
| 6964 | 441:cbce1fd3b1b7 | Chris | unless @revision == 0 |
| 6965 | return buildRevision( @revision - 1 ) |
||
| 6966 | 0:513646585e45 | Chris | end |
| 6967 | 441:cbce1fd3b1b7 | Chris | return buildRevision( @revision ) |
| 6968 | 0:513646585e45 | Chris | end |
| 6969 | 441:cbce1fd3b1b7 | Chris | |
| 6970 | 0:513646585e45 | Chris | def is_in_branch_with_symbol(branch_symbol) |
| 6971 | 441:cbce1fd3b1b7 | Chris | bpieces = branch_symbol.split(".")
|
| 6972 | branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
|
||
| 6973 | return ( branchVersion == branch_start ) |
||
| 6974 | 0:513646585e45 | Chris | end |
| 6975 | 441:cbce1fd3b1b7 | Chris | |
| 6976 | 0:513646585e45 | Chris | private |
| 6977 | def buildRevision(rev) |
||
| 6978 | 441:cbce1fd3b1b7 | Chris | if rev == 0 |
| 6979 | 245:051f544170fe | Chris | if @branchid.nil? |
| 6980 | 441:cbce1fd3b1b7 | Chris | @base + ".0" |
| 6981 | 245:051f544170fe | Chris | else |
| 6982 | @base |
||
| 6983 | end |
||
| 6984 | 441:cbce1fd3b1b7 | Chris | elsif @branchid.nil? |
| 6985 | @base + "." + rev.to_s |
||
| 6986 | 0:513646585e45 | Chris | else |
| 6987 | 441:cbce1fd3b1b7 | Chris | @base + "." + @branchid + "." + rev.to_s |
| 6988 | 0:513646585e45 | Chris | end |
| 6989 | end |
||
| 6990 | 441:cbce1fd3b1b7 | Chris | |
| 6991 | 0:513646585e45 | Chris | # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15 |
| 6992 | def parseRevision() |
||
| 6993 | 441:cbce1fd3b1b7 | Chris | pieces = @complete_rev.split(".")
|
| 6994 | @revision = pieces.last.to_i |
||
| 6995 | baseSize = 1 |
||
| 6996 | baseSize += (pieces.size / 2) |
||
| 6997 | @base = pieces[0..-baseSize].join(".")
|
||
| 6998 | 0:513646585e45 | Chris | if baseSize > 2 |
| 6999 | 441:cbce1fd3b1b7 | Chris | @branchid = pieces[-2] |
| 7000 | end |
||
| 7001 | 0:513646585e45 | Chris | end |
| 7002 | end |
||
| 7003 | end |
||
| 7004 | end |
||
| 7005 | end |
||
| 7006 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 7007 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 7008 | 0:513646585e45 | Chris | # |
| 7009 | # This program is free software; you can redistribute it and/or |
||
| 7010 | # modify it under the terms of the GNU General Public License |
||
| 7011 | # as published by the Free Software Foundation; either version 2 |
||
| 7012 | # of the License, or (at your option) any later version. |
||
| 7013 | 441:cbce1fd3b1b7 | Chris | # |
| 7014 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 7015 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 7016 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 7017 | # GNU General Public License for more details. |
||
| 7018 | 441:cbce1fd3b1b7 | Chris | # |
| 7019 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 7020 | # along with this program; if not, write to the Free Software |
||
| 7021 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 7022 | |||
| 7023 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 7024 | 0:513646585e45 | Chris | require 'rexml/document' |
| 7025 | |||
| 7026 | module Redmine |
||
| 7027 | module Scm |
||
| 7028 | 245:051f544170fe | Chris | module Adapters |
| 7029 | class DarcsAdapter < AbstractAdapter |
||
| 7030 | 0:513646585e45 | Chris | # Darcs executable name |
| 7031 | 210:0579821a129a | Chris | DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs" |
| 7032 | 245:051f544170fe | Chris | |
| 7033 | 0:513646585e45 | Chris | class << self |
| 7034 | 245:051f544170fe | Chris | def client_command |
| 7035 | @@bin ||= DARCS_BIN |
||
| 7036 | end |
||
| 7037 | |||
| 7038 | def sq_bin |
||
| 7039 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command |
| 7040 | 245:051f544170fe | Chris | end |
| 7041 | |||
| 7042 | 0:513646585e45 | Chris | def client_version |
| 7043 | @@client_version ||= (darcs_binary_version || []) |
||
| 7044 | end |
||
| 7045 | 245:051f544170fe | Chris | |
| 7046 | def client_available |
||
| 7047 | !client_version.empty? |
||
| 7048 | end |
||
| 7049 | |||
| 7050 | 0:513646585e45 | Chris | def darcs_binary_version |
| 7051 | 245:051f544170fe | Chris | darcsversion = darcs_binary_version_from_command_line.dup |
| 7052 | if darcsversion.respond_to?(:force_encoding) |
||
| 7053 | darcsversion.force_encoding('ASCII-8BIT')
|
||
| 7054 | end |
||
| 7055 | 210:0579821a129a | Chris | if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
|
| 7056 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 7057 | 0:513646585e45 | Chris | end |
| 7058 | 210:0579821a129a | Chris | end |
| 7059 | |||
| 7060 | def darcs_binary_version_from_command_line |
||
| 7061 | 245:051f544170fe | Chris | shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
| 7062 | 0:513646585e45 | Chris | end |
| 7063 | end |
||
| 7064 | |||
| 7065 | 245:051f544170fe | Chris | def initialize(url, root_url=nil, login=nil, password=nil, |
| 7066 | path_encoding=nil) |
||
| 7067 | 0:513646585e45 | Chris | @url = url |
| 7068 | @root_url = url |
||
| 7069 | end |
||
| 7070 | |||
| 7071 | def supports_cat? |
||
| 7072 | # cat supported in darcs 2.0.0 and higher |
||
| 7073 | self.class.client_version_above?([2, 0, 0]) |
||
| 7074 | end |
||
| 7075 | |||
| 7076 | # Get info about the darcs repository |
||
| 7077 | def info |
||
| 7078 | rev = revisions(nil,nil,nil,{:limit => 1})
|
||
| 7079 | rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
|
||
| 7080 | end |
||
| 7081 | 245:051f544170fe | Chris | |
| 7082 | 0:513646585e45 | Chris | # Returns an Entries collection |
| 7083 | # or nil if the given path doesn't exist in the repository |
||
| 7084 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 7085 | 0:513646585e45 | Chris | path_prefix = (path.blank? ? '' : "#{path}/")
|
| 7086 | 210:0579821a129a | Chris | if path.blank? |
| 7087 | path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' ) |
||
| 7088 | end |
||
| 7089 | 245:051f544170fe | Chris | entries = Entries.new |
| 7090 | cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
|
||
| 7091 | 0:513646585e45 | Chris | cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
|
| 7092 | cmd << " #{shell_quote path}"
|
||
| 7093 | shellout(cmd) do |io| |
||
| 7094 | begin |
||
| 7095 | doc = REXML::Document.new(io) |
||
| 7096 | if doc.root.name == 'directory' |
||
| 7097 | doc.elements.each('directory/*') do |element|
|
||
| 7098 | next unless ['file', 'directory'].include? element.name |
||
| 7099 | entries << entry_from_xml(element, path_prefix) |
||
| 7100 | end |
||
| 7101 | elsif doc.root.name == 'file' |
||
| 7102 | entries << entry_from_xml(doc.root, path_prefix) |
||
| 7103 | end |
||
| 7104 | rescue |
||
| 7105 | end |
||
| 7106 | end |
||
| 7107 | return nil if $? && $?.exitstatus != 0 |
||
| 7108 | 1115:433d4f72a19b | Chris | entries.compact! |
| 7109 | entries.sort_by_name |
||
| 7110 | 0:513646585e45 | Chris | end |
| 7111 | 245:051f544170fe | Chris | |
| 7112 | 0:513646585e45 | Chris | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 7113 | path = '.' if path.blank? |
||
| 7114 | revisions = Revisions.new |
||
| 7115 | 245:051f544170fe | Chris | cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
|
| 7116 | 0:513646585e45 | Chris | cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
|
| 7117 | cmd << " --last #{options[:limit].to_i}" if options[:limit]
|
||
| 7118 | shellout(cmd) do |io| |
||
| 7119 | begin |
||
| 7120 | doc = REXML::Document.new(io) |
||
| 7121 | doc.elements.each("changelog/patch") do |patch|
|
||
| 7122 | message = patch.elements['name'].text |
||
| 7123 | message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment'] |
||
| 7124 | revisions << Revision.new({:identifier => nil,
|
||
| 7125 | :author => patch.attributes['author'], |
||
| 7126 | :scmid => patch.attributes['hash'], |
||
| 7127 | :time => Time.parse(patch.attributes['local_date']), |
||
| 7128 | :message => message, |
||
| 7129 | :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil) |
||
| 7130 | }) |
||
| 7131 | end |
||
| 7132 | rescue |
||
| 7133 | end |
||
| 7134 | end |
||
| 7135 | return nil if $? && $?.exitstatus != 0 |
||
| 7136 | revisions |
||
| 7137 | end |
||
| 7138 | 245:051f544170fe | Chris | |
| 7139 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 7140 | path = '*' if path.blank? |
||
| 7141 | 245:051f544170fe | Chris | cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
|
| 7142 | 0:513646585e45 | Chris | if identifier_to.nil? |
| 7143 | cmd << " --match #{shell_quote("hash #{identifier_from}")}"
|
||
| 7144 | else |
||
| 7145 | cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
|
||
| 7146 | cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
|
||
| 7147 | end |
||
| 7148 | cmd << " -u #{shell_quote path}"
|
||
| 7149 | diff = [] |
||
| 7150 | shellout(cmd) do |io| |
||
| 7151 | io.each_line do |line| |
||
| 7152 | diff << line |
||
| 7153 | end |
||
| 7154 | end |
||
| 7155 | return nil if $? && $?.exitstatus != 0 |
||
| 7156 | diff |
||
| 7157 | end |
||
| 7158 | 245:051f544170fe | Chris | |
| 7159 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 7160 | 245:051f544170fe | Chris | cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
|
| 7161 | 0:513646585e45 | Chris | cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
|
| 7162 | cmd << " #{shell_quote path}"
|
||
| 7163 | cat = nil |
||
| 7164 | shellout(cmd) do |io| |
||
| 7165 | io.binmode |
||
| 7166 | cat = io.read |
||
| 7167 | end |
||
| 7168 | return nil if $? && $?.exitstatus != 0 |
||
| 7169 | cat |
||
| 7170 | end |
||
| 7171 | |||
| 7172 | private |
||
| 7173 | 245:051f544170fe | Chris | |
| 7174 | 0:513646585e45 | Chris | # Returns an Entry from the given XML element |
| 7175 | # or nil if the entry was deleted |
||
| 7176 | def entry_from_xml(element, path_prefix) |
||
| 7177 | modified_element = element.elements['modified'] |
||
| 7178 | if modified_element.elements['modified_how'].text.match(/removed/) |
||
| 7179 | return nil |
||
| 7180 | end |
||
| 7181 | 245:051f544170fe | Chris | |
| 7182 | 0:513646585e45 | Chris | Entry.new({:name => element.attributes['name'],
|
| 7183 | :path => path_prefix + element.attributes['name'], |
||
| 7184 | :kind => element.name == 'file' ? 'file' : 'dir', |
||
| 7185 | :size => nil, |
||
| 7186 | :lastrev => Revision.new({
|
||
| 7187 | :identifier => nil, |
||
| 7188 | :scmid => modified_element.elements['patch'].attributes['hash'] |
||
| 7189 | }) |
||
| 7190 | 245:051f544170fe | Chris | }) |
| 7191 | 0:513646585e45 | Chris | end |
| 7192 | 210:0579821a129a | Chris | |
| 7193 | def get_paths_for_patch(hash) |
||
| 7194 | paths = get_paths_for_patch_raw(hash) |
||
| 7195 | if self.class.client_version_above?([2, 4]) |
||
| 7196 | orig_paths = paths |
||
| 7197 | paths = [] |
||
| 7198 | add_paths = [] |
||
| 7199 | add_paths_name = [] |
||
| 7200 | mod_paths = [] |
||
| 7201 | other_paths = [] |
||
| 7202 | orig_paths.each do |path| |
||
| 7203 | if path[:action] == 'A' |
||
| 7204 | add_paths << path |
||
| 7205 | add_paths_name << path[:path] |
||
| 7206 | elsif path[:action] == 'M' |
||
| 7207 | mod_paths << path |
||
| 7208 | else |
||
| 7209 | other_paths << path |
||
| 7210 | end |
||
| 7211 | end |
||
| 7212 | add_paths_name.each do |add_path| |
||
| 7213 | mod_paths.delete_if { |m| m[:path] == add_path }
|
||
| 7214 | end |
||
| 7215 | paths.concat add_paths |
||
| 7216 | paths.concat mod_paths |
||
| 7217 | paths.concat other_paths |
||
| 7218 | end |
||
| 7219 | paths |
||
| 7220 | end |
||
| 7221 | 245:051f544170fe | Chris | |
| 7222 | 0:513646585e45 | Chris | # Retrieve changed paths for a single patch |
| 7223 | 210:0579821a129a | Chris | def get_paths_for_patch_raw(hash) |
| 7224 | 245:051f544170fe | Chris | cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
|
| 7225 | 0:513646585e45 | Chris | cmd << " --match #{shell_quote("hash #{hash}")} "
|
| 7226 | paths = [] |
||
| 7227 | shellout(cmd) do |io| |
||
| 7228 | begin |
||
| 7229 | # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7) |
||
| 7230 | # A root element is added so that REXML doesn't raise an error |
||
| 7231 | doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
|
||
| 7232 | doc.elements.each('fake_root/summary/*') do |modif|
|
||
| 7233 | paths << {:action => modif.name[0,1].upcase,
|
||
| 7234 | :path => "/" + modif.text.chomp.gsub(/^\s*/, '') |
||
| 7235 | } |
||
| 7236 | end |
||
| 7237 | rescue |
||
| 7238 | end |
||
| 7239 | end |
||
| 7240 | paths |
||
| 7241 | rescue CommandFailed |
||
| 7242 | paths |
||
| 7243 | end |
||
| 7244 | end |
||
| 7245 | end |
||
| 7246 | end |
||
| 7247 | end |
||
| 7248 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 7249 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 7250 | 0:513646585e45 | Chris | # |
| 7251 | # FileSystem adapter |
||
| 7252 | # File written by Paul Rivier, at Demotera. |
||
| 7253 | # |
||
| 7254 | # This program is free software; you can redistribute it and/or |
||
| 7255 | # modify it under the terms of the GNU General Public License |
||
| 7256 | # as published by the Free Software Foundation; either version 2 |
||
| 7257 | # of the License, or (at your option) any later version. |
||
| 7258 | 441:cbce1fd3b1b7 | Chris | # |
| 7259 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 7260 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 7261 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 7262 | # GNU General Public License for more details. |
||
| 7263 | 441:cbce1fd3b1b7 | Chris | # |
| 7264 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 7265 | # along with this program; if not, write to the Free Software |
||
| 7266 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 7267 | |||
| 7268 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 7269 | 0:513646585e45 | Chris | require 'find' |
| 7270 | |||
| 7271 | module Redmine |
||
| 7272 | module Scm |
||
| 7273 | 245:051f544170fe | Chris | module Adapters |
| 7274 | 0:513646585e45 | Chris | class FilesystemAdapter < AbstractAdapter |
| 7275 | |||
| 7276 | 245:051f544170fe | Chris | class << self |
| 7277 | def client_available |
||
| 7278 | true |
||
| 7279 | end |
||
| 7280 | end |
||
| 7281 | |||
| 7282 | def initialize(url, root_url=nil, login=nil, password=nil, |
||
| 7283 | path_encoding=nil) |
||
| 7284 | 0:513646585e45 | Chris | @url = with_trailling_slash(url) |
| 7285 | 441:cbce1fd3b1b7 | Chris | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
| 7286 | end |
||
| 7287 | |||
| 7288 | def path_encoding |
||
| 7289 | @path_encoding |
||
| 7290 | 0:513646585e45 | Chris | end |
| 7291 | |||
| 7292 | def format_path_ends(path, leading=true, trailling=true) |
||
| 7293 | 441:cbce1fd3b1b7 | Chris | path = leading ? with_leading_slash(path) : |
| 7294 | 0:513646585e45 | Chris | without_leading_slash(path) |
| 7295 | 441:cbce1fd3b1b7 | Chris | trailling ? with_trailling_slash(path) : |
| 7296 | without_trailling_slash(path) |
||
| 7297 | 0:513646585e45 | Chris | end |
| 7298 | |||
| 7299 | def info |
||
| 7300 | info = Info.new({:root_url => target(),
|
||
| 7301 | :lastrev => nil |
||
| 7302 | }) |
||
| 7303 | info |
||
| 7304 | rescue CommandFailed |
||
| 7305 | return nil |
||
| 7306 | end |
||
| 7307 | 245:051f544170fe | Chris | |
| 7308 | 441:cbce1fd3b1b7 | Chris | def entries(path="", identifier=nil, options={})
|
| 7309 | 0:513646585e45 | Chris | entries = Entries.new |
| 7310 | 245:051f544170fe | Chris | trgt_utf8 = target(path) |
| 7311 | trgt = scm_iconv(@path_encoding, 'UTF-8', trgt_utf8) |
||
| 7312 | Dir.new(trgt).each do |e1| |
||
| 7313 | e_utf8 = scm_iconv('UTF-8', @path_encoding, e1)
|
||
| 7314 | 441:cbce1fd3b1b7 | Chris | next if e_utf8.blank? |
| 7315 | relative_path_utf8 = format_path_ends( |
||
| 7316 | (format_path_ends(path,false,true) + e_utf8),false,false) |
||
| 7317 | 245:051f544170fe | Chris | t1_utf8 = target(relative_path_utf8) |
| 7318 | t1 = scm_iconv(@path_encoding, 'UTF-8', t1_utf8) |
||
| 7319 | relative_path = scm_iconv(@path_encoding, 'UTF-8', relative_path_utf8) |
||
| 7320 | e1 = scm_iconv(@path_encoding, 'UTF-8', e_utf8) |
||
| 7321 | if File.exist?(t1) and # paranoid test |
||
| 7322 | %w{file directory}.include?(File.ftype(t1)) and # avoid special types
|
||
| 7323 | not File.basename(e1).match(/^\.+$/) # avoid . and .. |
||
| 7324 | p1 = File.readable?(t1) ? relative_path : "" |
||
| 7325 | utf_8_path = scm_iconv('UTF-8', @path_encoding, p1)
|
||
| 7326 | entries << |
||
| 7327 | Entry.new({ :name => scm_iconv('UTF-8', @path_encoding, File.basename(e1)),
|
||
| 7328 | 0:513646585e45 | Chris | # below : list unreadable files, but dont link them. |
| 7329 | 245:051f544170fe | Chris | :path => utf_8_path, |
| 7330 | :kind => (File.directory?(t1) ? 'dir' : 'file'), |
||
| 7331 | :size => (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first),
|
||
| 7332 | 441:cbce1fd3b1b7 | Chris | :lastrev => |
| 7333 | 245:051f544170fe | Chris | Revision.new({:time => (File.mtime(t1)) })
|
| 7334 | }) |
||
| 7335 | end |
||
| 7336 | 0:513646585e45 | Chris | end |
| 7337 | entries.sort_by_name |
||
| 7338 | 245:051f544170fe | Chris | rescue => err |
| 7339 | logger.error "scm: filesystem: error: #{err.message}"
|
||
| 7340 | raise CommandFailed.new(err.message) |
||
| 7341 | 0:513646585e45 | Chris | end |
| 7342 | 245:051f544170fe | Chris | |
| 7343 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 7344 | 245:051f544170fe | Chris | p = scm_iconv(@path_encoding, 'UTF-8', target(path)) |
| 7345 | File.new(p, "rb").read |
||
| 7346 | rescue => err |
||
| 7347 | logger.error "scm: filesystem: error: #{err.message}"
|
||
| 7348 | raise CommandFailed.new(err.message) |
||
| 7349 | 0:513646585e45 | Chris | end |
| 7350 | |||
| 7351 | private |
||
| 7352 | 245:051f544170fe | Chris | |
| 7353 | 0:513646585e45 | Chris | # AbstractAdapter::target is implicitly made to quote paths. |
| 7354 | # Here we do not shell-out, so we do not want quotes. |
||
| 7355 | def target(path=nil) |
||
| 7356 | 245:051f544170fe | Chris | # Prevent the use of .. |
| 7357 | 0:513646585e45 | Chris | if path and !path.match(/(^|\/)\.\.(\/|$)/) |
| 7358 | return "#{self.url}#{without_leading_slash(path)}"
|
||
| 7359 | end |
||
| 7360 | return self.url |
||
| 7361 | end |
||
| 7362 | end |
||
| 7363 | end |
||
| 7364 | end |
||
| 7365 | end |
||
| 7366 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 7367 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 7368 | 0:513646585e45 | Chris | # |
| 7369 | # This program is free software; you can redistribute it and/or |
||
| 7370 | # modify it under the terms of the GNU General Public License |
||
| 7371 | # as published by the Free Software Foundation; either version 2 |
||
| 7372 | # of the License, or (at your option) any later version. |
||
| 7373 | 441:cbce1fd3b1b7 | Chris | # |
| 7374 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 7375 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 7376 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 7377 | # GNU General Public License for more details. |
||
| 7378 | 441:cbce1fd3b1b7 | Chris | # |
| 7379 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 7380 | # along with this program; if not, write to the Free Software |
||
| 7381 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 7382 | |||
| 7383 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 7384 | 0:513646585e45 | Chris | |
| 7385 | module Redmine |
||
| 7386 | module Scm |
||
| 7387 | 245:051f544170fe | Chris | module Adapters |
| 7388 | 0:513646585e45 | Chris | class GitAdapter < AbstractAdapter |
| 7389 | 245:051f544170fe | Chris | |
| 7390 | 0:513646585e45 | Chris | # Git executable name |
| 7391 | 210:0579821a129a | Chris | GIT_BIN = Redmine::Configuration['scm_git_command'] || "git" |
| 7392 | 0:513646585e45 | Chris | |
| 7393 | 1115:433d4f72a19b | Chris | class GitBranch < Branch |
| 7394 | attr_accessor :is_default |
||
| 7395 | end |
||
| 7396 | |||
| 7397 | 245:051f544170fe | Chris | class << self |
| 7398 | def client_command |
||
| 7399 | @@bin ||= GIT_BIN |
||
| 7400 | end |
||
| 7401 | |||
| 7402 | def sq_bin |
||
| 7403 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command |
| 7404 | 245:051f544170fe | Chris | end |
| 7405 | |||
| 7406 | def client_version |
||
| 7407 | @@client_version ||= (scm_command_version || []) |
||
| 7408 | end |
||
| 7409 | |||
| 7410 | def client_available |
||
| 7411 | !client_version.empty? |
||
| 7412 | end |
||
| 7413 | |||
| 7414 | def scm_command_version |
||
| 7415 | scm_version = scm_version_from_command_line.dup |
||
| 7416 | if scm_version.respond_to?(:force_encoding) |
||
| 7417 | scm_version.force_encoding('ASCII-8BIT')
|
||
| 7418 | end |
||
| 7419 | if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||
| 7420 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 7421 | end |
||
| 7422 | end |
||
| 7423 | |||
| 7424 | def scm_version_from_command_line |
||
| 7425 | shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
|
||
| 7426 | end |
||
| 7427 | end |
||
| 7428 | |||
| 7429 | def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
||
| 7430 | super |
||
| 7431 | 441:cbce1fd3b1b7 | Chris | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
| 7432 | end |
||
| 7433 | |||
| 7434 | def path_encoding |
||
| 7435 | @path_encoding |
||
| 7436 | 245:051f544170fe | Chris | end |
| 7437 | |||
| 7438 | 0:513646585e45 | Chris | def info |
| 7439 | begin |
||
| 7440 | Info.new(:root_url => url, :lastrev => lastrev('',nil))
|
||
| 7441 | rescue |
||
| 7442 | nil |
||
| 7443 | end |
||
| 7444 | end |
||
| 7445 | |||
| 7446 | def branches |
||
| 7447 | return @branches if @branches |
||
| 7448 | @branches = [] |
||
| 7449 | 909:cbb26bc654de | Chris | cmd_args = %w|branch --no-color --verbose --no-abbrev| |
| 7450 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) do |io| |
| 7451 | 0:513646585e45 | Chris | io.each_line do |line| |
| 7452 | 1115:433d4f72a19b | Chris | branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
|
| 7453 | bran = GitBranch.new(branch_rev[2]) |
||
| 7454 | bran.revision = branch_rev[3] |
||
| 7455 | bran.scmid = branch_rev[3] |
||
| 7456 | bran.is_default = ( branch_rev[1] == '*' ) |
||
| 7457 | 909:cbb26bc654de | Chris | @branches << bran |
| 7458 | 0:513646585e45 | Chris | end |
| 7459 | end |
||
| 7460 | @branches.sort! |
||
| 7461 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 7462 | nil |
||
| 7463 | 0:513646585e45 | Chris | end |
| 7464 | |||
| 7465 | def tags |
||
| 7466 | return @tags if @tags |
||
| 7467 | 441:cbce1fd3b1b7 | Chris | cmd_args = %w|tag| |
| 7468 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) do |io| |
| 7469 | 0:513646585e45 | Chris | @tags = io.readlines.sort!.map{|t| t.strip}
|
| 7470 | end |
||
| 7471 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 7472 | nil |
||
| 7473 | end |
||
| 7474 | |||
| 7475 | def default_branch |
||
| 7476 | bras = self.branches |
||
| 7477 | return nil if bras.nil? |
||
| 7478 | 1115:433d4f72a19b | Chris | default_bras = bras.select{|x| x.is_default == true}
|
| 7479 | return default_bras.first.to_s if ! default_bras.empty? |
||
| 7480 | master_bras = bras.select{|x| x.to_s == 'master'}
|
||
| 7481 | master_bras.empty? ? bras.first.to_s : 'master' |
||
| 7482 | 441:cbce1fd3b1b7 | Chris | end |
| 7483 | |||
| 7484 | def entry(path=nil, identifier=nil) |
||
| 7485 | parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
|
||
| 7486 | search_path = parts[0..-2].join('/')
|
||
| 7487 | search_name = parts[-1] |
||
| 7488 | if search_path.blank? && search_name.blank? |
||
| 7489 | # Root entry |
||
| 7490 | Entry.new(:path => '', :kind => 'dir') |
||
| 7491 | else |
||
| 7492 | # Search for the entry in the parent directory |
||
| 7493 | es = entries(search_path, identifier, |
||
| 7494 | options = {:report_last_commit => false})
|
||
| 7495 | es ? es.detect {|e| e.name == search_name} : nil
|
||
| 7496 | end |
||
| 7497 | 0:513646585e45 | Chris | end |
| 7498 | |||
| 7499 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 7500 | 0:513646585e45 | Chris | path ||= '' |
| 7501 | 441:cbce1fd3b1b7 | Chris | p = scm_iconv(@path_encoding, 'UTF-8', path) |
| 7502 | 0:513646585e45 | Chris | entries = Entries.new |
| 7503 | 441:cbce1fd3b1b7 | Chris | cmd_args = %w|ls-tree -l| |
| 7504 | cmd_args << "HEAD:#{p}" if identifier.nil?
|
||
| 7505 | cmd_args << "#{identifier}:#{p}" if identifier
|
||
| 7506 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) do |io| |
| 7507 | 0:513646585e45 | Chris | io.each_line do |line| |
| 7508 | e = line.chomp.to_s |
||
| 7509 | 37:94944d00e43c | chris | if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
|
| 7510 | 0:513646585e45 | Chris | type = $1 |
| 7511 | 441:cbce1fd3b1b7 | Chris | sha = $2 |
| 7512 | 0:513646585e45 | Chris | size = $3 |
| 7513 | name = $4 |
||
| 7514 | 441:cbce1fd3b1b7 | Chris | if name.respond_to?(:force_encoding) |
| 7515 | name.force_encoding(@path_encoding) |
||
| 7516 | end |
||
| 7517 | full_path = p.empty? ? name : "#{p}/#{name}"
|
||
| 7518 | n = scm_iconv('UTF-8', @path_encoding, name)
|
||
| 7519 | full_p = scm_iconv('UTF-8', @path_encoding, full_path)
|
||
| 7520 | entries << Entry.new({:name => n,
|
||
| 7521 | :path => full_p, |
||
| 7522 | 0:513646585e45 | Chris | :kind => (type == "tree") ? 'dir' : 'file', |
| 7523 | :size => (type == "tree") ? nil : size, |
||
| 7524 | 441:cbce1fd3b1b7 | Chris | :lastrev => options[:report_last_commit] ? |
| 7525 | lastrev(full_path, identifier) : Revision.new |
||
| 7526 | 0:513646585e45 | Chris | }) unless entries.detect{|entry| entry.name == name}
|
| 7527 | end |
||
| 7528 | end |
||
| 7529 | end |
||
| 7530 | entries.sort_by_name |
||
| 7531 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 7532 | nil |
||
| 7533 | 0:513646585e45 | Chris | end |
| 7534 | |||
| 7535 | 245:051f544170fe | Chris | def lastrev(path, rev) |
| 7536 | 0:513646585e45 | Chris | return nil if path.nil? |
| 7537 | 245:051f544170fe | Chris | cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1| |
| 7538 | 441:cbce1fd3b1b7 | Chris | cmd_args << rev if rev |
| 7539 | 245:051f544170fe | Chris | cmd_args << "--" << path unless path.empty? |
| 7540 | 117:af80e5618e9b | Chris | lines = [] |
| 7541 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) { |io| lines = io.readlines }
|
| 7542 | 117:af80e5618e9b | Chris | begin |
| 7543 | id = lines[0].split[1] |
||
| 7544 | author = lines[1].match('Author:\s+(.*)$')[1]
|
||
| 7545 | 245:051f544170fe | Chris | time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
|
| 7546 | 0:513646585e45 | Chris | |
| 7547 | Revision.new({
|
||
| 7548 | :identifier => id, |
||
| 7549 | 441:cbce1fd3b1b7 | Chris | :scmid => id, |
| 7550 | :author => author, |
||
| 7551 | :time => time, |
||
| 7552 | :message => nil, |
||
| 7553 | :paths => nil |
||
| 7554 | 245:051f544170fe | Chris | }) |
| 7555 | 117:af80e5618e9b | Chris | rescue NoMethodError => e |
| 7556 | 0:513646585e45 | Chris | logger.error("The revision '#{path}' has a wrong format")
|
| 7557 | return nil |
||
| 7558 | end |
||
| 7559 | 245:051f544170fe | Chris | rescue ScmCommandAborted |
| 7560 | nil |
||
| 7561 | 0:513646585e45 | Chris | end |
| 7562 | |||
| 7563 | def revisions(path, identifier_from, identifier_to, options={})
|
||
| 7564 | 441:cbce1fd3b1b7 | Chris | revs = Revisions.new |
| 7565 | 1115:433d4f72a19b | Chris | cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin| |
| 7566 | 245:051f544170fe | Chris | cmd_args << "--reverse" if options[:reverse] |
| 7567 | cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
|
||
| 7568 | 441:cbce1fd3b1b7 | Chris | cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty? |
| 7569 | 1115:433d4f72a19b | Chris | revisions = [] |
| 7570 | if identifier_from || identifier_to |
||
| 7571 | revisions << "" |
||
| 7572 | revisions[0] << "#{identifier_from}.." if identifier_from
|
||
| 7573 | revisions[0] << "#{identifier_to}" if identifier_to
|
||
| 7574 | else |
||
| 7575 | unless options[:includes].blank? |
||
| 7576 | revisions += options[:includes] |
||
| 7577 | end |
||
| 7578 | unless options[:excludes].blank? |
||
| 7579 | revisions += options[:excludes].map{|r| "^#{r}"}
|
||
| 7580 | end |
||
| 7581 | end |
||
| 7582 | 0:513646585e45 | Chris | |
| 7583 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args, {:write_stdin => true}) do |io|
|
| 7584 | io.binmode |
||
| 7585 | io.puts(revisions.join("\n"))
|
||
| 7586 | io.close_write |
||
| 7587 | 0:513646585e45 | Chris | files=[] |
| 7588 | changeset = {}
|
||
| 7589 | parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files |
||
| 7590 | |||
| 7591 | io.each_line do |line| |
||
| 7592 | 909:cbb26bc654de | Chris | if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
|
| 7593 | 0:513646585e45 | Chris | key = "commit" |
| 7594 | value = $1 |
||
| 7595 | 909:cbb26bc654de | Chris | parents_str = $2 |
| 7596 | 0:513646585e45 | Chris | if (parsing_descr == 1 || parsing_descr == 2) |
| 7597 | parsing_descr = 0 |
||
| 7598 | revision = Revision.new({
|
||
| 7599 | :identifier => changeset[:commit], |
||
| 7600 | 441:cbce1fd3b1b7 | Chris | :scmid => changeset[:commit], |
| 7601 | :author => changeset[:author], |
||
| 7602 | :time => Time.parse(changeset[:date]), |
||
| 7603 | :message => changeset[:description], |
||
| 7604 | 909:cbb26bc654de | Chris | :paths => files, |
| 7605 | :parents => changeset[:parents] |
||
| 7606 | 0:513646585e45 | Chris | }) |
| 7607 | if block_given? |
||
| 7608 | yield revision |
||
| 7609 | else |
||
| 7610 | 441:cbce1fd3b1b7 | Chris | revs << revision |
| 7611 | 0:513646585e45 | Chris | end |
| 7612 | changeset = {}
|
||
| 7613 | files = [] |
||
| 7614 | end |
||
| 7615 | changeset[:commit] = $1 |
||
| 7616 | 909:cbb26bc654de | Chris | unless parents_str.nil? or parents_str == "" |
| 7617 | changeset[:parents] = parents_str.strip.split(' ')
|
||
| 7618 | end |
||
| 7619 | 0:513646585e45 | Chris | elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/ |
| 7620 | key = $1 |
||
| 7621 | value = $2 |
||
| 7622 | if key == "Author" |
||
| 7623 | changeset[:author] = value |
||
| 7624 | elsif key == "CommitDate" |
||
| 7625 | changeset[:date] = value |
||
| 7626 | end |
||
| 7627 | elsif (parsing_descr == 0) && line.chomp.to_s == "" |
||
| 7628 | parsing_descr = 1 |
||
| 7629 | changeset[:description] = "" |
||
| 7630 | elsif (parsing_descr == 1 || parsing_descr == 2) \ |
||
| 7631 | 441:cbce1fd3b1b7 | Chris | && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/ |
| 7632 | 0:513646585e45 | Chris | parsing_descr = 2 |
| 7633 | 441:cbce1fd3b1b7 | Chris | fileaction = $1 |
| 7634 | filepath = $2 |
||
| 7635 | p = scm_iconv('UTF-8', @path_encoding, filepath)
|
||
| 7636 | files << {:action => fileaction, :path => p}
|
||
| 7637 | 0:513646585e45 | Chris | elsif (parsing_descr == 1 || parsing_descr == 2) \ |
| 7638 | 441:cbce1fd3b1b7 | Chris | && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/ |
| 7639 | 0:513646585e45 | Chris | parsing_descr = 2 |
| 7640 | 441:cbce1fd3b1b7 | Chris | fileaction = $1 |
| 7641 | filepath = $3 |
||
| 7642 | p = scm_iconv('UTF-8', @path_encoding, filepath)
|
||
| 7643 | files << {:action => fileaction, :path => p}
|
||
| 7644 | 0:513646585e45 | Chris | elsif (parsing_descr == 1) && line.chomp.to_s == "" |
| 7645 | parsing_descr = 2 |
||
| 7646 | elsif (parsing_descr == 1) |
||
| 7647 | changeset[:description] << line[4..-1] |
||
| 7648 | end |
||
| 7649 | 441:cbce1fd3b1b7 | Chris | end |
| 7650 | 0:513646585e45 | Chris | |
| 7651 | if changeset[:commit] |
||
| 7652 | revision = Revision.new({
|
||
| 7653 | :identifier => changeset[:commit], |
||
| 7654 | 441:cbce1fd3b1b7 | Chris | :scmid => changeset[:commit], |
| 7655 | :author => changeset[:author], |
||
| 7656 | :time => Time.parse(changeset[:date]), |
||
| 7657 | :message => changeset[:description], |
||
| 7658 | 909:cbb26bc654de | Chris | :paths => files, |
| 7659 | :parents => changeset[:parents] |
||
| 7660 | 441:cbce1fd3b1b7 | Chris | }) |
| 7661 | 0:513646585e45 | Chris | if block_given? |
| 7662 | yield revision |
||
| 7663 | else |
||
| 7664 | 441:cbce1fd3b1b7 | Chris | revs << revision |
| 7665 | 0:513646585e45 | Chris | end |
| 7666 | end |
||
| 7667 | end |
||
| 7668 | 441:cbce1fd3b1b7 | Chris | revs |
| 7669 | rescue ScmCommandAborted => e |
||
| 7670 | 1115:433d4f72a19b | Chris | err_msg = "git log error: #{e.message}"
|
| 7671 | logger.error(err_msg) |
||
| 7672 | if block_given? |
||
| 7673 | raise CommandFailed, err_msg |
||
| 7674 | else |
||
| 7675 | revs |
||
| 7676 | end |
||
| 7677 | 0:513646585e45 | Chris | end |
| 7678 | |||
| 7679 | def diff(path, identifier_from, identifier_to=nil) |
||
| 7680 | path ||= '' |
||
| 7681 | 441:cbce1fd3b1b7 | Chris | cmd_args = [] |
| 7682 | 0:513646585e45 | Chris | if identifier_to |
| 7683 | 441:cbce1fd3b1b7 | Chris | cmd_args << "diff" << "--no-color" << identifier_to << identifier_from |
| 7684 | 0:513646585e45 | Chris | else |
| 7685 | 441:cbce1fd3b1b7 | Chris | cmd_args << "show" << "--no-color" << identifier_from |
| 7686 | 0:513646585e45 | Chris | end |
| 7687 | 441:cbce1fd3b1b7 | Chris | cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty? |
| 7688 | 0:513646585e45 | Chris | diff = [] |
| 7689 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) do |io| |
| 7690 | 0:513646585e45 | Chris | io.each_line do |line| |
| 7691 | diff << line |
||
| 7692 | end |
||
| 7693 | end |
||
| 7694 | diff |
||
| 7695 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 7696 | nil |
||
| 7697 | 0:513646585e45 | Chris | end |
| 7698 | 441:cbce1fd3b1b7 | Chris | |
| 7699 | 0:513646585e45 | Chris | def annotate(path, identifier=nil) |
| 7700 | identifier = 'HEAD' if identifier.blank? |
||
| 7701 | 1464:261b3d9a4903 | Chris | cmd_args = %w|blame --encoding=UTF-8| |
| 7702 | 441:cbce1fd3b1b7 | Chris | cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path) |
| 7703 | 0:513646585e45 | Chris | blame = Annotate.new |
| 7704 | content = nil |
||
| 7705 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) { |io| io.binmode; content = io.read }
|
| 7706 | 0:513646585e45 | Chris | # git annotates binary files |
| 7707 | return nil if content.is_binary_data? |
||
| 7708 | identifier = '' |
||
| 7709 | # git shows commit author on the first occurrence only |
||
| 7710 | authors_by_commit = {}
|
||
| 7711 | content.split("\n").each do |line|
|
||
| 7712 | if line =~ /^([0-9a-f]{39,40})\s.*/
|
||
| 7713 | identifier = $1 |
||
| 7714 | elsif line =~ /^author (.+)/ |
||
| 7715 | authors_by_commit[identifier] = $1.strip |
||
| 7716 | elsif line =~ /^\t(.*)/ |
||
| 7717 | 441:cbce1fd3b1b7 | Chris | blame.add_line($1, Revision.new( |
| 7718 | :identifier => identifier, |
||
| 7719 | :revision => identifier, |
||
| 7720 | :scmid => identifier, |
||
| 7721 | :author => authors_by_commit[identifier] |
||
| 7722 | )) |
||
| 7723 | 0:513646585e45 | Chris | identifier = '' |
| 7724 | author = '' |
||
| 7725 | end |
||
| 7726 | end |
||
| 7727 | blame |
||
| 7728 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 7729 | nil |
||
| 7730 | 0:513646585e45 | Chris | end |
| 7731 | 245:051f544170fe | Chris | |
| 7732 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 7733 | if identifier.nil? |
||
| 7734 | identifier = 'HEAD' |
||
| 7735 | end |
||
| 7736 | 441:cbce1fd3b1b7 | Chris | cmd_args = %w|show --no-color| |
| 7737 | cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
|
||
| 7738 | 0:513646585e45 | Chris | cat = nil |
| 7739 | 1115:433d4f72a19b | Chris | git_cmd(cmd_args) do |io| |
| 7740 | 0:513646585e45 | Chris | io.binmode |
| 7741 | cat = io.read |
||
| 7742 | end |
||
| 7743 | cat |
||
| 7744 | 441:cbce1fd3b1b7 | Chris | rescue ScmCommandAborted |
| 7745 | nil |
||
| 7746 | 0:513646585e45 | Chris | end |
| 7747 | 117:af80e5618e9b | Chris | |
| 7748 | class Revision < Redmine::Scm::Adapters::Revision |
||
| 7749 | # Returns the readable identifier |
||
| 7750 | def format_identifier |
||
| 7751 | identifier[0,8] |
||
| 7752 | end |
||
| 7753 | end |
||
| 7754 | 245:051f544170fe | Chris | |
| 7755 | 1115:433d4f72a19b | Chris | def git_cmd(args, options = {}, &block)
|
| 7756 | 245:051f544170fe | Chris | repo_path = root_url || url |
| 7757 | 909:cbb26bc654de | Chris | full_args = ['--git-dir', repo_path] |
| 7758 | 441:cbce1fd3b1b7 | Chris | if self.class.client_version_above?([1, 7, 2]) |
| 7759 | full_args << '-c' << 'core.quotepath=false' |
||
| 7760 | full_args << '-c' << 'log.decorate=no' |
||
| 7761 | end |
||
| 7762 | 245:051f544170fe | Chris | full_args += args |
| 7763 | 909:cbb26bc654de | Chris | ret = shellout( |
| 7764 | self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
|
||
| 7765 | 1115:433d4f72a19b | Chris | options, |
| 7766 | 909:cbb26bc654de | Chris | &block |
| 7767 | ) |
||
| 7768 | 245:051f544170fe | Chris | if $? && $?.exitstatus != 0 |
| 7769 | raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
|
||
| 7770 | end |
||
| 7771 | ret |
||
| 7772 | end |
||
| 7773 | 1115:433d4f72a19b | Chris | private :git_cmd |
| 7774 | 0:513646585e45 | Chris | end |
| 7775 | end |
||
| 7776 | end |
||
| 7777 | end |
||
| 7778 | changeset = 'This template must be used with --debug option\n' |
||
| 7779 | changeset_quiet = 'This template must be used with --debug option\n' |
||
| 7780 | changeset_verbose = 'This template must be used with --debug option\n' |
||
| 7781 | changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
|
||
| 7782 | |||
| 7783 | 117:af80e5618e9b | Chris | file = '<path action="M">{file|urlescape}</path>\n'
|
| 7784 | file_add = '<path action="A">{file_add|urlescape}</path>\n'
|
||
| 7785 | file_del = '<path action="D">{file_del|urlescape}</path>\n'
|
||
| 7786 | file_copy = '<path-copied copyfrom-path="{source|urlescape}">{name|urlescape}</path-copied>\n'
|
||
| 7787 | 0:513646585e45 | Chris | tag = '<tag>{tag|escape}</tag>\n'
|
| 7788 | header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' |
||
| 7789 | 3:7c48bad7d85d | Chris | footer='</log>' |
| 7790 | 0:513646585e45 | Chris | changeset = 'This template must be used with --debug option\n' |
| 7791 | changeset_quiet = 'This template must be used with --debug option\n' |
||
| 7792 | changeset_verbose = 'This template must be used with --debug option\n' |
||
| 7793 | 1517:dffacf8a6908 | Chris | changeset_debug = '<logentry revision="{rev}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodatesec}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n<parents>\n{parents}</parents>\n</logentry>\n\n'
|
| 7794 | 0:513646585e45 | Chris | |
| 7795 | 117:af80e5618e9b | Chris | file_mod = '<path action="M">{file_mod|urlescape}</path>\n'
|
| 7796 | file_add = '<path action="A">{file_add|urlescape}</path>\n'
|
||
| 7797 | file_del = '<path action="D">{file_del|urlescape}</path>\n'
|
||
| 7798 | file_copy = '<path-copied copyfrom-path="{source|urlescape}">{name|urlescape}</path-copied>\n'
|
||
| 7799 | 1517:dffacf8a6908 | Chris | parent = '<parent>{node}</parent>\n'
|
| 7800 | 0:513646585e45 | Chris | header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' |
| 7801 | 3:7c48bad7d85d | Chris | footer='</log>' |
| 7802 | 245:051f544170fe | Chris | # redminehelper: Redmine helper extension for Mercurial |
| 7803 | # |
||
| 7804 | # Copyright 2010 Alessio Franceschelli (alefranz.net) |
||
| 7805 | # Copyright 2010-2011 Yuya Nishihara <yuya@tcha.org> |
||
| 7806 | # |
||
| 7807 | # This software may be used and distributed according to the terms of the |
||
| 7808 | # GNU General Public License version 2 or any later version. |
||
| 7809 | """helper commands for Redmine to reduce the number of hg calls |
||
| 7810 | |||
| 7811 | To test this extension, please try:: |
||
| 7812 | |||
| 7813 | $ hg --config extensions.redminehelper=redminehelper.py rhsummary |
||
| 7814 | |||
| 7815 | I/O encoding: |
||
| 7816 | |||
| 7817 | :file path: urlencoded, raw string |
||
| 7818 | :tag name: utf-8 |
||
| 7819 | :branch name: utf-8 |
||
| 7820 | 1517:dffacf8a6908 | Chris | :node: hex string |
| 7821 | 245:051f544170fe | Chris | |
| 7822 | Output example of rhsummary:: |
||
| 7823 | |||
| 7824 | <?xml version="1.0"?> |
||
| 7825 | <rhsummary> |
||
| 7826 | <repository root="/foo/bar"> |
||
| 7827 | <tip revision="1234" node="abcdef0123..."/> |
||
| 7828 | <tag revision="123" node="34567abc..." name="1.1.1"/> |
||
| 7829 | <branch .../> |
||
| 7830 | ... |
||
| 7831 | </repository> |
||
| 7832 | </rhsummary> |
||
| 7833 | |||
| 7834 | Output example of rhmanifest:: |
||
| 7835 | |||
| 7836 | <?xml version="1.0"?> |
||
| 7837 | <rhmanifest> |
||
| 7838 | <repository root="/foo/bar"> |
||
| 7839 | <manifest revision="1234" path="lib"> |
||
| 7840 | <file name="diff.rb" revision="123" node="34567abc..." time="12345" |
||
| 7841 | size="100"/> |
||
| 7842 | ... |
||
| 7843 | <dir name="redmine"/> |
||
| 7844 | ... |
||
| 7845 | </manifest> |
||
| 7846 | </repository> |
||
| 7847 | </rhmanifest> |
||
| 7848 | """ |
||
| 7849 | import re, time, cgi, urllib |
||
| 7850 | 909:cbb26bc654de | Chris | from mercurial import cmdutil, commands, node, error, hg |
| 7851 | 245:051f544170fe | Chris | |
| 7852 | _x = cgi.escape |
||
| 7853 | _u = lambda s: cgi.escape(urllib.quote(s)) |
||
| 7854 | |||
| 7855 | def _tip(ui, repo): |
||
| 7856 | # see mercurial/commands.py:tip |
||
| 7857 | def tiprev(): |
||
| 7858 | try: |
||
| 7859 | return len(repo) - 1 |
||
| 7860 | except TypeError: # Mercurial < 1.1 |
||
| 7861 | return repo.changelog.count() - 1 |
||
| 7862 | tipctx = repo.changectx(tiprev()) |
||
| 7863 | ui.write('<tip revision="%d" node="%s"/>\n'
|
||
| 7864 | 1517:dffacf8a6908 | Chris | % (tipctx.rev(), _x(node.hex(tipctx.node())))) |
| 7865 | 245:051f544170fe | Chris | |
| 7866 | _SPECIAL_TAGS = ('tip',)
|
||
| 7867 | |||
| 7868 | def _tags(ui, repo): |
||
| 7869 | # see mercurial/commands.py:tags |
||
| 7870 | for t, n in reversed(repo.tagslist()): |
||
| 7871 | if t in _SPECIAL_TAGS: |
||
| 7872 | continue |
||
| 7873 | try: |
||
| 7874 | r = repo.changelog.rev(n) |
||
| 7875 | except error.LookupError: |
||
| 7876 | continue |
||
| 7877 | ui.write('<tag revision="%d" node="%s" name="%s"/>\n'
|
||
| 7878 | 1517:dffacf8a6908 | Chris | % (r, _x(node.hex(n)), _x(t))) |
| 7879 | 245:051f544170fe | Chris | |
| 7880 | def _branches(ui, repo): |
||
| 7881 | # see mercurial/commands.py:branches |
||
| 7882 | def iterbranches(): |
||
| 7883 | 1494:e248c7af89ec | Chris | if getattr(repo, 'branchtags', None) is not None: |
| 7884 | # Mercurial < 2.9 |
||
| 7885 | for t, n in repo.branchtags().iteritems(): |
||
| 7886 | yield t, n, repo.changelog.rev(n) |
||
| 7887 | else: |
||
| 7888 | for tag, heads, tip, isclosed in repo.branchmap().iterbranches(): |
||
| 7889 | yield tag, tip, repo.changelog.rev(tip) |
||
| 7890 | 245:051f544170fe | Chris | def branchheads(branch): |
| 7891 | try: |
||
| 7892 | return repo.branchheads(branch, closed=False) |
||
| 7893 | except TypeError: # Mercurial < 1.2 |
||
| 7894 | return repo.branchheads(branch) |
||
| 7895 | for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True): |
||
| 7896 | if repo.lookup(r) in branchheads(t): |
||
| 7897 | ui.write('<branch revision="%d" node="%s" name="%s"/>\n'
|
||
| 7898 | 1517:dffacf8a6908 | Chris | % (r, _x(node.hex(n)), _x(t))) |
| 7899 | 245:051f544170fe | Chris | |
| 7900 | def _manifest(ui, repo, path, rev): |
||
| 7901 | ctx = repo.changectx(rev) |
||
| 7902 | ui.write('<manifest revision="%d" path="%s">\n'
|
||
| 7903 | % (ctx.rev(), _u(path))) |
||
| 7904 | |||
| 7905 | known = set() |
||
| 7906 | pathprefix = (path.rstrip('/') + '/').lstrip('/')
|
||
| 7907 | for f, n in sorted(ctx.manifest().iteritems(), key=lambda e: e[0]): |
||
| 7908 | if not f.startswith(pathprefix): |
||
| 7909 | continue |
||
| 7910 | name = re.sub(r'/.*', '/', f[len(pathprefix):]) |
||
| 7911 | if name in known: |
||
| 7912 | continue |
||
| 7913 | known.add(name) |
||
| 7914 | |||
| 7915 | if name.endswith('/'):
|
||
| 7916 | ui.write('<dir name="%s"/>\n'
|
||
| 7917 | % _x(urllib.quote(name[:-1]))) |
||
| 7918 | else: |
||
| 7919 | fctx = repo.filectx(f, fileid=n) |
||
| 7920 | tm, tzoffset = fctx.date() |
||
| 7921 | ui.write('<file name="%s" revision="%d" node="%s" '
|
||
| 7922 | 'time="%d" size="%d"/>\n' |
||
| 7923 | 1517:dffacf8a6908 | Chris | % (_u(name), fctx.rev(), _x(node.hex(fctx.node())), |
| 7924 | 245:051f544170fe | Chris | tm, fctx.size(), )) |
| 7925 | |||
| 7926 | ui.write('</manifest>\n')
|
||
| 7927 | |||
| 7928 | def rhannotate(ui, repo, *pats, **opts): |
||
| 7929 | 441:cbce1fd3b1b7 | Chris | rev = urllib.unquote_plus(opts.pop('rev', None))
|
| 7930 | opts['rev'] = rev |
||
| 7931 | 245:051f544170fe | Chris | return commands.annotate(ui, repo, *map(urllib.unquote_plus, pats), **opts) |
| 7932 | |||
| 7933 | def rhcat(ui, repo, file1, *pats, **opts): |
||
| 7934 | 441:cbce1fd3b1b7 | Chris | rev = urllib.unquote_plus(opts.pop('rev', None))
|
| 7935 | opts['rev'] = rev |
||
| 7936 | 245:051f544170fe | Chris | return commands.cat(ui, repo, urllib.unquote_plus(file1), *map(urllib.unquote_plus, pats), **opts) |
| 7937 | |||
| 7938 | def rhdiff(ui, repo, *pats, **opts): |
||
| 7939 | """diff repository (or selected files)""" |
||
| 7940 | change = opts.pop('change', None)
|
||
| 7941 | if change: # add -c option for Mercurial<1.1 |
||
| 7942 | base = repo.changectx(change).parents()[0].rev() |
||
| 7943 | opts['rev'] = [str(base), change] |
||
| 7944 | opts['nodates'] = True |
||
| 7945 | return commands.diff(ui, repo, *map(urllib.unquote_plus, pats), **opts) |
||
| 7946 | |||
| 7947 | 441:cbce1fd3b1b7 | Chris | def rhlog(ui, repo, *pats, **opts): |
| 7948 | rev = opts.pop('rev')
|
||
| 7949 | bra0 = opts.pop('branch')
|
||
| 7950 | from_rev = urllib.unquote_plus(opts.pop('from', None))
|
||
| 7951 | to_rev = urllib.unquote_plus(opts.pop('to' , None))
|
||
| 7952 | bra = urllib.unquote_plus(opts.pop('rhbranch', None))
|
||
| 7953 | from_rev = from_rev.replace('"', '\\"')
|
||
| 7954 | to_rev = to_rev.replace('"', '\\"')
|
||
| 7955 | 909:cbb26bc654de | Chris | if hg.util.version() >= '1.6': |
| 7956 | opts['rev'] = ['"%s":"%s"' % (from_rev, to_rev)] |
||
| 7957 | else: |
||
| 7958 | opts['rev'] = ['%s:%s' % (from_rev, to_rev)] |
||
| 7959 | 441:cbce1fd3b1b7 | Chris | opts['branch'] = [bra] |
| 7960 | return commands.log(ui, repo, *map(urllib.unquote_plus, pats), **opts) |
||
| 7961 | |||
| 7962 | 245:051f544170fe | Chris | def rhmanifest(ui, repo, path='', **opts): |
| 7963 | """output the sub-manifest of the specified directory""" |
||
| 7964 | ui.write('<?xml version="1.0"?>\n')
|
||
| 7965 | ui.write('<rhmanifest>\n')
|
||
| 7966 | ui.write('<repository root="%s">\n' % _u(repo.root))
|
||
| 7967 | try: |
||
| 7968 | _manifest(ui, repo, urllib.unquote_plus(path), urllib.unquote_plus(opts.get('rev')))
|
||
| 7969 | finally: |
||
| 7970 | ui.write('</repository>\n')
|
||
| 7971 | ui.write('</rhmanifest>\n')
|
||
| 7972 | |||
| 7973 | def rhsummary(ui, repo, **opts): |
||
| 7974 | """output the summary of the repository""" |
||
| 7975 | ui.write('<?xml version="1.0"?>\n')
|
||
| 7976 | ui.write('<rhsummary>\n')
|
||
| 7977 | ui.write('<repository root="%s">\n' % _u(repo.root))
|
||
| 7978 | try: |
||
| 7979 | _tip(ui, repo) |
||
| 7980 | _tags(ui, repo) |
||
| 7981 | _branches(ui, repo) |
||
| 7982 | # TODO: bookmarks in core (Mercurial>=1.8) |
||
| 7983 | finally: |
||
| 7984 | ui.write('</repository>\n')
|
||
| 7985 | ui.write('</rhsummary>\n')
|
||
| 7986 | |||
| 7987 | cmdtable = {
|
||
| 7988 | 'rhannotate': (rhannotate, |
||
| 7989 | [('r', 'rev', '', 'revision'),
|
||
| 7990 | ('u', 'user', None, 'list the author (long with -v)'),
|
||
| 7991 | ('n', 'number', None, 'list the revision number (default)'),
|
||
| 7992 | ('c', 'changeset', None, 'list the changeset'),
|
||
| 7993 | ], |
||
| 7994 | 'hg rhannotate [-r REV] [-u] [-n] [-c] FILE...'), |
||
| 7995 | 'rhcat': (rhcat, |
||
| 7996 | [('r', 'rev', '', 'revision')],
|
||
| 7997 | 'hg rhcat ([-r REV] ...) FILE...'), |
||
| 7998 | 'rhdiff': (rhdiff, |
||
| 7999 | [('r', 'rev', [], 'revision'),
|
||
| 8000 | ('c', 'change', '', 'change made by revision')],
|
||
| 8001 | 'hg rhdiff ([-c REV] | [-r REV] ...) [FILE]...'), |
||
| 8002 | 441:cbce1fd3b1b7 | Chris | 'rhlog': (rhlog, |
| 8003 | [ |
||
| 8004 | ('r', 'rev', [], 'show the specified revision'),
|
||
| 8005 | ('b', 'branch', [],
|
||
| 8006 | 909:cbb26bc654de | Chris | 'show changesets within the given named branch'), |
| 8007 | 441:cbce1fd3b1b7 | Chris | ('l', 'limit', '',
|
| 8008 | 909:cbb26bc654de | Chris | 'limit number of changes displayed'), |
| 8009 | 441:cbce1fd3b1b7 | Chris | ('d', 'date', '',
|
| 8010 | 909:cbb26bc654de | Chris | 'show revisions matching date spec'), |
| 8011 | 441:cbce1fd3b1b7 | Chris | ('u', 'user', [],
|
| 8012 | 909:cbb26bc654de | Chris | 'revisions committed by user'), |
| 8013 | 441:cbce1fd3b1b7 | Chris | ('', 'from', '',
|
| 8014 | 909:cbb26bc654de | Chris | ''), |
| 8015 | 441:cbce1fd3b1b7 | Chris | ('', 'to', '',
|
| 8016 | 909:cbb26bc654de | Chris | ''), |
| 8017 | 441:cbce1fd3b1b7 | Chris | ('', 'rhbranch', '',
|
| 8018 | 909:cbb26bc654de | Chris | ''), |
| 8019 | 441:cbce1fd3b1b7 | Chris | ('', 'template', '',
|
| 8020 | 909:cbb26bc654de | Chris | 'display with template')], |
| 8021 | 441:cbce1fd3b1b7 | Chris | 'hg rhlog [OPTION]... [FILE]'), |
| 8022 | 245:051f544170fe | Chris | 'rhmanifest': (rhmanifest, |
| 8023 | [('r', 'rev', '', 'show the specified revision')],
|
||
| 8024 | 'hg rhmanifest [-r REV] [PATH]'), |
||
| 8025 | 'rhsummary': (rhsummary, [], 'hg rhsummary'), |
||
| 8026 | } |
||
| 8027 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 8028 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 8029 | 0:513646585e45 | Chris | # |
| 8030 | # This program is free software; you can redistribute it and/or |
||
| 8031 | # modify it under the terms of the GNU General Public License |
||
| 8032 | # as published by the Free Software Foundation; either version 2 |
||
| 8033 | # of the License, or (at your option) any later version. |
||
| 8034 | 441:cbce1fd3b1b7 | Chris | # |
| 8035 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 8036 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 8037 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 8038 | # GNU General Public License for more details. |
||
| 8039 | 441:cbce1fd3b1b7 | Chris | # |
| 8040 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 8041 | # along with this program; if not, write to the Free Software |
||
| 8042 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 8043 | |||
| 8044 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 8045 | 119:8661b858af72 | Chris | require 'cgi' |
| 8046 | 0:513646585e45 | Chris | |
| 8047 | module Redmine |
||
| 8048 | module Scm |
||
| 8049 | 245:051f544170fe | Chris | module Adapters |
| 8050 | 0:513646585e45 | Chris | class MercurialAdapter < AbstractAdapter |
| 8051 | 119:8661b858af72 | Chris | |
| 8052 | 0:513646585e45 | Chris | # Mercurial executable name |
| 8053 | 210:0579821a129a | Chris | HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg" |
| 8054 | 245:051f544170fe | Chris | HELPERS_DIR = File.dirname(__FILE__) + "/mercurial" |
| 8055 | HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
|
||
| 8056 | 0:513646585e45 | Chris | TEMPLATE_NAME = "hg-template" |
| 8057 | TEMPLATE_EXTENSION = "tmpl" |
||
| 8058 | 119:8661b858af72 | Chris | |
| 8059 | 245:051f544170fe | Chris | # raised if hg command exited with error, e.g. unknown revision. |
| 8060 | class HgCommandAborted < CommandFailed; end |
||
| 8061 | |||
| 8062 | 0:513646585e45 | Chris | class << self |
| 8063 | 245:051f544170fe | Chris | def client_command |
| 8064 | @@bin ||= HG_BIN |
||
| 8065 | end |
||
| 8066 | |||
| 8067 | def sq_bin |
||
| 8068 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command |
| 8069 | 245:051f544170fe | Chris | end |
| 8070 | |||
| 8071 | 0:513646585e45 | Chris | def client_version |
| 8072 | @@client_version ||= (hgversion || []) |
||
| 8073 | end |
||
| 8074 | 119:8661b858af72 | Chris | |
| 8075 | 245:051f544170fe | Chris | def client_available |
| 8076 | 909:cbb26bc654de | Chris | client_version_above?([1, 2]) |
| 8077 | 245:051f544170fe | Chris | end |
| 8078 | |||
| 8079 | def hgversion |
||
| 8080 | 0:513646585e45 | Chris | # The hg version is expressed either as a |
| 8081 | # release number (eg 0.9.5 or 1.0) or as a revision |
||
| 8082 | # id composed of 12 hexa characters. |
||
| 8083 | 245:051f544170fe | Chris | theversion = hgversion_from_command_line.dup |
| 8084 | if theversion.respond_to?(:force_encoding) |
||
| 8085 | theversion.force_encoding('ASCII-8BIT')
|
||
| 8086 | end |
||
| 8087 | 119:8661b858af72 | Chris | if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
|
| 8088 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 8089 | 0:513646585e45 | Chris | end |
| 8090 | end |
||
| 8091 | 119:8661b858af72 | Chris | |
| 8092 | 0:513646585e45 | Chris | def hgversion_from_command_line |
| 8093 | 245:051f544170fe | Chris | shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
| 8094 | 0:513646585e45 | Chris | end |
| 8095 | 119:8661b858af72 | Chris | |
| 8096 | 0:513646585e45 | Chris | def template_path |
| 8097 | @@template_path ||= template_path_for(client_version) |
||
| 8098 | end |
||
| 8099 | 119:8661b858af72 | Chris | |
| 8100 | 0:513646585e45 | Chris | def template_path_for(version) |
| 8101 | 909:cbb26bc654de | Chris | "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
|
| 8102 | 0:513646585e45 | Chris | end |
| 8103 | end |
||
| 8104 | 119:8661b858af72 | Chris | |
| 8105 | 245:051f544170fe | Chris | def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
| 8106 | super |
||
| 8107 | 441:cbce1fd3b1b7 | Chris | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
| 8108 | end |
||
| 8109 | |||
| 8110 | def path_encoding |
||
| 8111 | @path_encoding |
||
| 8112 | 0:513646585e45 | Chris | end |
| 8113 | 119:8661b858af72 | Chris | |
| 8114 | 245:051f544170fe | Chris | def info |
| 8115 | tip = summary['repository']['tip'] |
||
| 8116 | Info.new(:root_url => CGI.unescape(summary['repository']['root']), |
||
| 8117 | :lastrev => Revision.new(:revision => tip['revision'], |
||
| 8118 | :scmid => tip['node'])) |
||
| 8119 | 507:0c939c159af4 | Chris | # rescue HgCommandAborted |
| 8120 | rescue Exception => e |
||
| 8121 | logger.error "hg: error during getting info: #{e.message}"
|
||
| 8122 | nil |
||
| 8123 | 245:051f544170fe | Chris | end |
| 8124 | |||
| 8125 | def tags |
||
| 8126 | as_ary(summary['repository']['tag']).map { |e| e['name'] }
|
||
| 8127 | end |
||
| 8128 | |||
| 8129 | # Returns map of {'tag' => 'nodeid', ...}
|
||
| 8130 | def tagmap |
||
| 8131 | alist = as_ary(summary['repository']['tag']).map do |e| |
||
| 8132 | e.values_at('name', 'node')
|
||
| 8133 | end |
||
| 8134 | Hash[*alist.flatten] |
||
| 8135 | end |
||
| 8136 | |||
| 8137 | def branches |
||
| 8138 | 909:cbb26bc654de | Chris | brs = [] |
| 8139 | as_ary(summary['repository']['branch']).each do |e| |
||
| 8140 | br = Branch.new(e['name']) |
||
| 8141 | br.revision = e['revision'] |
||
| 8142 | br.scmid = e['node'] |
||
| 8143 | brs << br |
||
| 8144 | end |
||
| 8145 | brs |
||
| 8146 | 245:051f544170fe | Chris | end |
| 8147 | |||
| 8148 | # Returns map of {'branch' => 'nodeid', ...}
|
||
| 8149 | def branchmap |
||
| 8150 | alist = as_ary(summary['repository']['branch']).map do |e| |
||
| 8151 | e.values_at('name', 'node')
|
||
| 8152 | end |
||
| 8153 | Hash[*alist.flatten] |
||
| 8154 | end |
||
| 8155 | |||
| 8156 | def summary |
||
| 8157 | 441:cbce1fd3b1b7 | Chris | return @summary if @summary |
| 8158 | 245:051f544170fe | Chris | hg 'rhsummary' do |io| |
| 8159 | output = io.read |
||
| 8160 | if output.respond_to?(:force_encoding) |
||
| 8161 | output.force_encoding('UTF-8')
|
||
| 8162 | end |
||
| 8163 | begin |
||
| 8164 | 1115:433d4f72a19b | Chris | @summary = parse_xml(output)['rhsummary'] |
| 8165 | 245:051f544170fe | Chris | rescue |
| 8166 | 0:513646585e45 | Chris | end |
| 8167 | end |
||
| 8168 | 245:051f544170fe | Chris | end |
| 8169 | private :summary |
||
| 8170 | |||
| 8171 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 8172 | 245:051f544170fe | Chris | p1 = scm_iconv(@path_encoding, 'UTF-8', path) |
| 8173 | manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
|
||
| 8174 | CGI.escape(without_leading_slash(p1.to_s))) do |io| |
||
| 8175 | output = io.read |
||
| 8176 | if output.respond_to?(:force_encoding) |
||
| 8177 | output.force_encoding('UTF-8')
|
||
| 8178 | end |
||
| 8179 | begin |
||
| 8180 | 1115:433d4f72a19b | Chris | parse_xml(output)['rhmanifest']['repository']['manifest'] |
| 8181 | 245:051f544170fe | Chris | rescue |
| 8182 | end |
||
| 8183 | end |
||
| 8184 | path_prefix = path.blank? ? '' : with_trailling_slash(path) |
||
| 8185 | |||
| 8186 | entries = Entries.new |
||
| 8187 | as_ary(manifest['dir']).each do |e| |
||
| 8188 | n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
|
||
| 8189 | p = "#{path_prefix}#{n}"
|
||
| 8190 | entries << Entry.new(:name => n, :path => p, :kind => 'dir') |
||
| 8191 | end |
||
| 8192 | |||
| 8193 | as_ary(manifest['file']).each do |e| |
||
| 8194 | n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
|
||
| 8195 | p = "#{path_prefix}#{n}"
|
||
| 8196 | lr = Revision.new(:revision => e['revision'], :scmid => e['node'], |
||
| 8197 | :identifier => e['node'], |
||
| 8198 | :time => Time.at(e['time'].to_i)) |
||
| 8199 | entries << Entry.new(:name => n, :path => p, :kind => 'file', |
||
| 8200 | :size => e['size'].to_i, :lastrev => lr) |
||
| 8201 | end |
||
| 8202 | |||
| 8203 | entries |
||
| 8204 | rescue HgCommandAborted |
||
| 8205 | nil # means not found |
||
| 8206 | 0:513646585e45 | Chris | end |
| 8207 | 119:8661b858af72 | Chris | |
| 8208 | 245:051f544170fe | Chris | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 8209 | revs = Revisions.new |
||
| 8210 | each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
|
||
| 8211 | revs |
||
| 8212 | end |
||
| 8213 | |||
| 8214 | # Iterates the revisions by using a template file that |
||
| 8215 | 0:513646585e45 | Chris | # makes Mercurial produce a xml output. |
| 8216 | 245:051f544170fe | Chris | def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 8217 | hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] |
||
| 8218 | hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
|
||
| 8219 | hg_args << '--limit' << options[:limit] if options[:limit] |
||
| 8220 | hg_args << hgtarget(path) unless path.blank? |
||
| 8221 | log = hg(*hg_args) do |io| |
||
| 8222 | output = io.read |
||
| 8223 | if output.respond_to?(:force_encoding) |
||
| 8224 | output.force_encoding('UTF-8')
|
||
| 8225 | end |
||
| 8226 | 0:513646585e45 | Chris | begin |
| 8227 | 1116:bb32da3bea34 | Chris | parse_xml("#{output}")['log']
|
| 8228 | 0:513646585e45 | Chris | rescue |
| 8229 | end |
||
| 8230 | end |
||
| 8231 | 245:051f544170fe | Chris | as_ary(log['logentry']).each do |le| |
| 8232 | cpalist = as_ary(le['paths']['path-copied']).map do |e| |
||
| 8233 | 441:cbce1fd3b1b7 | Chris | [e['__content__'], e['copyfrom-path']].map do |s| |
| 8234 | scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
|
||
| 8235 | end |
||
| 8236 | 245:051f544170fe | Chris | end |
| 8237 | cpmap = Hash[*cpalist.flatten] |
||
| 8238 | paths = as_ary(le['paths']['path']).map do |e| |
||
| 8239 | p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
|
||
| 8240 | 441:cbce1fd3b1b7 | Chris | {:action => e['action'],
|
| 8241 | :path => with_leading_slash(p), |
||
| 8242 | :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil), |
||
| 8243 | :from_revision => (cpmap.member?(p) ? le['node'] : nil)} |
||
| 8244 | 245:051f544170fe | Chris | end.sort { |a, b| a[:path] <=> b[:path] }
|
| 8245 | 909:cbb26bc654de | Chris | parents_ary = [] |
| 8246 | as_ary(le['parents']['parent']).map do |par| |
||
| 8247 | 1517:dffacf8a6908 | Chris | parents_ary << par['__content__'] if par['__content__'] != "0000000000000000000000000000000000000000" |
| 8248 | 909:cbb26bc654de | Chris | end |
| 8249 | 245:051f544170fe | Chris | yield Revision.new(:revision => le['revision'], |
| 8250 | 441:cbce1fd3b1b7 | Chris | :scmid => le['node'], |
| 8251 | :author => (le['author']['__content__'] rescue ''), |
||
| 8252 | :time => Time.parse(le['date']['__content__']), |
||
| 8253 | :message => le['msg']['__content__'], |
||
| 8254 | 909:cbb26bc654de | Chris | :paths => paths, |
| 8255 | :parents => parents_ary) |
||
| 8256 | 245:051f544170fe | Chris | end |
| 8257 | self |
||
| 8258 | 0:513646585e45 | Chris | end |
| 8259 | 119:8661b858af72 | Chris | |
| 8260 | 441:cbce1fd3b1b7 | Chris | # Returns list of nodes in the specified branch |
| 8261 | def nodes_in_branch(branch, options={})
|
||
| 8262 | 1517:dffacf8a6908 | Chris | hg_args = ['rhlog', '--template', '{node}\n', '--rhbranch', CGI.escape(branch)]
|
| 8263 | 441:cbce1fd3b1b7 | Chris | hg_args << '--from' << CGI.escape(branch) |
| 8264 | hg_args << '--to' << '0' |
||
| 8265 | hg_args << '--limit' << options[:limit] if options[:limit] |
||
| 8266 | hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
|
||
| 8267 | end |
||
| 8268 | |||
| 8269 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 8270 | 245:051f544170fe | Chris | hg_args = %w|rhdiff| |
| 8271 | if identifier_to |
||
| 8272 | hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) |
||
| 8273 | else |
||
| 8274 | hg_args << '-c' << hgrev(identifier_from) |
||
| 8275 | end |
||
| 8276 | unless path.blank? |
||
| 8277 | p = scm_iconv(@path_encoding, 'UTF-8', path) |
||
| 8278 | hg_args << CGI.escape(hgtarget(p)) |
||
| 8279 | end |
||
| 8280 | 119:8661b858af72 | Chris | diff = [] |
| 8281 | 245:051f544170fe | Chris | hg *hg_args do |io| |
| 8282 | 0:513646585e45 | Chris | io.each_line do |line| |
| 8283 | diff << line |
||
| 8284 | end |
||
| 8285 | end |
||
| 8286 | diff |
||
| 8287 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 8288 | nil # means not found |
||
| 8289 | 0:513646585e45 | Chris | end |
| 8290 | 119:8661b858af72 | Chris | |
| 8291 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 8292 | 245:051f544170fe | Chris | p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
| 8293 | 441:cbce1fd3b1b7 | Chris | hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
| 8294 | 0:513646585e45 | Chris | io.binmode |
| 8295 | 245:051f544170fe | Chris | io.read |
| 8296 | 0:513646585e45 | Chris | end |
| 8297 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 8298 | nil # means not found |
||
| 8299 | 0:513646585e45 | Chris | end |
| 8300 | 119:8661b858af72 | Chris | |
| 8301 | 0:513646585e45 | Chris | def annotate(path, identifier=nil) |
| 8302 | 245:051f544170fe | Chris | p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
| 8303 | 0:513646585e45 | Chris | blame = Annotate.new |
| 8304 | 441:cbce1fd3b1b7 | Chris | hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
| 8305 | 0:513646585e45 | Chris | io.each_line do |line| |
| 8306 | 245:051f544170fe | Chris | line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
|
| 8307 | 119:8661b858af72 | Chris | next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
|
| 8308 | r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, |
||
| 8309 | :identifier => $3) |
||
| 8310 | blame.add_line($4.rstrip, r) |
||
| 8311 | 0:513646585e45 | Chris | end |
| 8312 | end |
||
| 8313 | blame |
||
| 8314 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 8315 | 507:0c939c159af4 | Chris | # means not found or cannot be annotated |
| 8316 | Annotate.new |
||
| 8317 | 0:513646585e45 | Chris | end |
| 8318 | 119:8661b858af72 | Chris | |
| 8319 | class Revision < Redmine::Scm::Adapters::Revision |
||
| 8320 | # Returns the readable identifier |
||
| 8321 | def format_identifier |
||
| 8322 | "#{revision}:#{scmid}"
|
||
| 8323 | end |
||
| 8324 | end |
||
| 8325 | |||
| 8326 | 245:051f544170fe | Chris | # Runs 'hg' command with the given args |
| 8327 | def hg(*args, &block) |
||
| 8328 | repo_path = root_url || url |
||
| 8329 | 909:cbb26bc654de | Chris | full_args = ['-R', repo_path, '--encoding', 'utf-8'] |
| 8330 | 245:051f544170fe | Chris | full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
|
| 8331 | full_args << '--config' << 'diff.git=false' |
||
| 8332 | full_args += args |
||
| 8333 | 909:cbb26bc654de | Chris | ret = shellout( |
| 8334 | self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
|
||
| 8335 | &block |
||
| 8336 | ) |
||
| 8337 | 245:051f544170fe | Chris | if $? && $?.exitstatus != 0 |
| 8338 | raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
|
||
| 8339 | end |
||
| 8340 | ret |
||
| 8341 | end |
||
| 8342 | private :hg |
||
| 8343 | |||
| 8344 | 119:8661b858af72 | Chris | # Returns correct revision identifier |
| 8345 | 245:051f544170fe | Chris | def hgrev(identifier, sq=false) |
| 8346 | rev = identifier.blank? ? 'tip' : identifier.to_s |
||
| 8347 | rev = shell_quote(rev) if sq |
||
| 8348 | rev |
||
| 8349 | 119:8661b858af72 | Chris | end |
| 8350 | private :hgrev |
||
| 8351 | 245:051f544170fe | Chris | |
| 8352 | def hgtarget(path) |
||
| 8353 | path ||= '' |
||
| 8354 | root_url + '/' + without_leading_slash(path) |
||
| 8355 | end |
||
| 8356 | private :hgtarget |
||
| 8357 | |||
| 8358 | def as_ary(o) |
||
| 8359 | return [] unless o |
||
| 8360 | o.is_a?(Array) ? o : Array[o] |
||
| 8361 | end |
||
| 8362 | private :as_ary |
||
| 8363 | 0:513646585e45 | Chris | end |
| 8364 | end |
||
| 8365 | end |
||
| 8366 | end |
||
| 8367 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 8368 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 8369 | 441:cbce1fd3b1b7 | Chris | # |
| 8370 | # This program is free software; you can redistribute it and/or |
||
| 8371 | # modify it under the terms of the GNU General Public License |
||
| 8372 | # as published by the Free Software Foundation; either version 2 |
||
| 8373 | # of the License, or (at your option) any later version. |
||
| 8374 | # |
||
| 8375 | # This program is distributed in the hope that it will be useful, |
||
| 8376 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 8377 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 8378 | # GNU General Public License for more details. |
||
| 8379 | # |
||
| 8380 | # You should have received a copy of the GNU General Public License |
||
| 8381 | # along with this program; if not, write to the Free Software |
||
| 8382 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 8383 | |||
| 8384 | 1136:51d7f3e06556 | chris | require_dependency 'redmine/scm/adapters/abstract_adapter' |
| 8385 | 441:cbce1fd3b1b7 | Chris | require 'uri' |
| 8386 | |||
| 8387 | module Redmine |
||
| 8388 | module Scm |
||
| 8389 | module Adapters |
||
| 8390 | class SubversionAdapter < AbstractAdapter |
||
| 8391 | |||
| 8392 | # SVN executable name |
||
| 8393 | SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn" |
||
| 8394 | |||
| 8395 | class << self |
||
| 8396 | def client_command |
||
| 8397 | @@bin ||= SVN_BIN |
||
| 8398 | end |
||
| 8399 | |||
| 8400 | def sq_bin |
||
| 8401 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command |
| 8402 | 441:cbce1fd3b1b7 | Chris | end |
| 8403 | |||
| 8404 | def client_version |
||
| 8405 | @@client_version ||= (svn_binary_version || []) |
||
| 8406 | end |
||
| 8407 | |||
| 8408 | def client_available |
||
| 8409 | # --xml options are introduced in 1.3. |
||
| 8410 | # http://subversion.apache.org/docs/release-notes/1.3.html |
||
| 8411 | client_version_above?([1, 3]) |
||
| 8412 | end |
||
| 8413 | |||
| 8414 | def svn_binary_version |
||
| 8415 | scm_version = scm_version_from_command_line.dup |
||
| 8416 | if scm_version.respond_to?(:force_encoding) |
||
| 8417 | scm_version.force_encoding('ASCII-8BIT')
|
||
| 8418 | end |
||
| 8419 | if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||
| 8420 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 8421 | end |
||
| 8422 | end |
||
| 8423 | |||
| 8424 | def scm_version_from_command_line |
||
| 8425 | shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
||
| 8426 | end |
||
| 8427 | end |
||
| 8428 | |||
| 8429 | # Get info about the svn repository |
||
| 8430 | def info |
||
| 8431 | cmd = "#{self.class.sq_bin} info --xml #{target}"
|
||
| 8432 | cmd << credentials_string |
||
| 8433 | info = nil |
||
| 8434 | shellout(cmd) do |io| |
||
| 8435 | output = io.read |
||
| 8436 | if output.respond_to?(:force_encoding) |
||
| 8437 | output.force_encoding('UTF-8')
|
||
| 8438 | end |
||
| 8439 | begin |
||
| 8440 | 1115:433d4f72a19b | Chris | doc = parse_xml(output) |
| 8441 | 441:cbce1fd3b1b7 | Chris | # root_url = doc.elements["info/entry/repository/root"].text |
| 8442 | info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
|
||
| 8443 | :lastrev => Revision.new({
|
||
| 8444 | :identifier => doc['info']['entry']['commit']['revision'], |
||
| 8445 | :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime, |
||
| 8446 | :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "") |
||
| 8447 | }) |
||
| 8448 | }) |
||
| 8449 | rescue |
||
| 8450 | end |
||
| 8451 | end |
||
| 8452 | return nil if $? && $?.exitstatus != 0 |
||
| 8453 | info |
||
| 8454 | rescue CommandFailed |
||
| 8455 | return nil |
||
| 8456 | end |
||
| 8457 | |||
| 8458 | # Returns an Entries collection |
||
| 8459 | # or nil if the given path doesn't exist in the repository |
||
| 8460 | def entries(path=nil, identifier=nil, options={})
|
||
| 8461 | path ||= '' |
||
| 8462 | identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" |
||
| 8463 | entries = Entries.new |
||
| 8464 | cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
|
||
| 8465 | cmd << credentials_string |
||
| 8466 | shellout(cmd) do |io| |
||
| 8467 | output = io.read |
||
| 8468 | if output.respond_to?(:force_encoding) |
||
| 8469 | output.force_encoding('UTF-8')
|
||
| 8470 | end |
||
| 8471 | begin |
||
| 8472 | 1115:433d4f72a19b | Chris | doc = parse_xml(output) |
| 8473 | 441:cbce1fd3b1b7 | Chris | each_xml_element(doc['lists']['list'], 'entry') do |entry| |
| 8474 | commit = entry['commit'] |
||
| 8475 | commit_date = commit['date'] |
||
| 8476 | # Skip directory if there is no commit date (usually that |
||
| 8477 | # means that we don't have read access to it) |
||
| 8478 | next if entry['kind'] == 'dir' && commit_date.nil? |
||
| 8479 | name = entry['name']['__content__'] |
||
| 8480 | entries << Entry.new({:name => URI.unescape(name),
|
||
| 8481 | :path => ((path.empty? ? "" : "#{path}/") + name),
|
||
| 8482 | :kind => entry['kind'], |
||
| 8483 | :size => ((s = entry['size']) ? s['__content__'].to_i : nil), |
||
| 8484 | :lastrev => Revision.new({
|
||
| 8485 | :identifier => commit['revision'], |
||
| 8486 | :time => Time.parse(commit_date['__content__'].to_s).localtime, |
||
| 8487 | :author => ((a = commit['author']) ? a['__content__'] : nil) |
||
| 8488 | }) |
||
| 8489 | }) |
||
| 8490 | end |
||
| 8491 | rescue Exception => e |
||
| 8492 | logger.error("Error parsing svn output: #{e.message}")
|
||
| 8493 | logger.error("Output was:\n #{output}")
|
||
| 8494 | end |
||
| 8495 | end |
||
| 8496 | return nil if $? && $?.exitstatus != 0 |
||
| 8497 | logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
|
||
| 8498 | entries.sort_by_name |
||
| 8499 | end |
||
| 8500 | |||
| 8501 | def properties(path, identifier=nil) |
||
| 8502 | # proplist xml output supported in svn 1.5.0 and higher |
||
| 8503 | return nil unless self.class.client_version_above?([1, 5, 0]) |
||
| 8504 | |||
| 8505 | identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" |
||
| 8506 | cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
|
||
| 8507 | cmd << credentials_string |
||
| 8508 | properties = {}
|
||
| 8509 | shellout(cmd) do |io| |
||
| 8510 | output = io.read |
||
| 8511 | if output.respond_to?(:force_encoding) |
||
| 8512 | output.force_encoding('UTF-8')
|
||
| 8513 | end |
||
| 8514 | begin |
||
| 8515 | 1115:433d4f72a19b | Chris | doc = parse_xml(output) |
| 8516 | 441:cbce1fd3b1b7 | Chris | each_xml_element(doc['properties']['target'], 'property') do |property| |
| 8517 | properties[ property['name'] ] = property['__content__'].to_s |
||
| 8518 | end |
||
| 8519 | rescue |
||
| 8520 | end |
||
| 8521 | end |
||
| 8522 | return nil if $? && $?.exitstatus != 0 |
||
| 8523 | properties |
||
| 8524 | end |
||
| 8525 | |||
| 8526 | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
||
| 8527 | path ||= '' |
||
| 8528 | identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" |
||
| 8529 | identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1 |
||
| 8530 | revisions = Revisions.new |
||
| 8531 | cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
|
||
| 8532 | cmd << credentials_string |
||
| 8533 | cmd << " --verbose " if options[:with_paths] |
||
| 8534 | cmd << " --limit #{options[:limit].to_i}" if options[:limit]
|
||
| 8535 | cmd << ' ' + target(path) |
||
| 8536 | shellout(cmd) do |io| |
||
| 8537 | output = io.read |
||
| 8538 | if output.respond_to?(:force_encoding) |
||
| 8539 | output.force_encoding('UTF-8')
|
||
| 8540 | end |
||
| 8541 | begin |
||
| 8542 | 1115:433d4f72a19b | Chris | doc = parse_xml(output) |
| 8543 | 441:cbce1fd3b1b7 | Chris | each_xml_element(doc['log'], 'logentry') do |logentry| |
| 8544 | paths = [] |
||
| 8545 | each_xml_element(logentry['paths'], 'path') do |path| |
||
| 8546 | paths << {:action => path['action'],
|
||
| 8547 | :path => path['__content__'], |
||
| 8548 | :from_path => path['copyfrom-path'], |
||
| 8549 | :from_revision => path['copyfrom-rev'] |
||
| 8550 | } |
||
| 8551 | end if logentry['paths'] && logentry['paths']['path'] |
||
| 8552 | paths.sort! { |x,y| x[:path] <=> y[:path] }
|
||
| 8553 | |||
| 8554 | revisions << Revision.new({:identifier => logentry['revision'],
|
||
| 8555 | :author => (logentry['author'] ? logentry['author']['__content__'] : ""), |
||
| 8556 | :time => Time.parse(logentry['date']['__content__'].to_s).localtime, |
||
| 8557 | :message => logentry['msg']['__content__'], |
||
| 8558 | :paths => paths |
||
| 8559 | }) |
||
| 8560 | end |
||
| 8561 | rescue |
||
| 8562 | end |
||
| 8563 | end |
||
| 8564 | return nil if $? && $?.exitstatus != 0 |
||
| 8565 | revisions |
||
| 8566 | end |
||
| 8567 | |||
| 8568 | 1115:433d4f72a19b | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 8569 | 441:cbce1fd3b1b7 | Chris | path ||= '' |
| 8570 | identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : '' |
||
| 8571 | |||
| 8572 | identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) |
||
| 8573 | |||
| 8574 | cmd = "#{self.class.sq_bin} diff -r "
|
||
| 8575 | cmd << "#{identifier_to}:"
|
||
| 8576 | cmd << "#{identifier_from}"
|
||
| 8577 | cmd << " #{target(path)}@#{identifier_from}"
|
||
| 8578 | cmd << credentials_string |
||
| 8579 | diff = [] |
||
| 8580 | shellout(cmd) do |io| |
||
| 8581 | io.each_line do |line| |
||
| 8582 | diff << line |
||
| 8583 | end |
||
| 8584 | end |
||
| 8585 | return nil if $? && $?.exitstatus != 0 |
||
| 8586 | diff |
||
| 8587 | end |
||
| 8588 | |||
| 8589 | def cat(path, identifier=nil) |
||
| 8590 | identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" |
||
| 8591 | cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
|
||
| 8592 | cmd << credentials_string |
||
| 8593 | cat = nil |
||
| 8594 | shellout(cmd) do |io| |
||
| 8595 | io.binmode |
||
| 8596 | cat = io.read |
||
| 8597 | end |
||
| 8598 | return nil if $? && $?.exitstatus != 0 |
||
| 8599 | cat |
||
| 8600 | end |
||
| 8601 | |||
| 8602 | def annotate(path, identifier=nil) |
||
| 8603 | identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" |
||
| 8604 | cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
|
||
| 8605 | cmd << credentials_string |
||
| 8606 | blame = Annotate.new |
||
| 8607 | shellout(cmd) do |io| |
||
| 8608 | io.each_line do |line| |
||
| 8609 | next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
|
||
| 8610 | rev = $1 |
||
| 8611 | blame.add_line($3.rstrip, |
||
| 8612 | Revision.new( |
||
| 8613 | :identifier => rev, |
||
| 8614 | :revision => rev, |
||
| 8615 | :author => $2.strip |
||
| 8616 | )) |
||
| 8617 | end |
||
| 8618 | end |
||
| 8619 | return nil if $? && $?.exitstatus != 0 |
||
| 8620 | blame |
||
| 8621 | end |
||
| 8622 | |||
| 8623 | private |
||
| 8624 | |||
| 8625 | def credentials_string |
||
| 8626 | str = '' |
||
| 8627 | str << " --username #{shell_quote(@login)}" unless @login.blank?
|
||
| 8628 | str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
|
||
| 8629 | str << " --no-auth-cache --non-interactive" |
||
| 8630 | str |
||
| 8631 | end |
||
| 8632 | |||
| 8633 | # Helper that iterates over the child elements of a xml node |
||
| 8634 | # MiniXml returns a hash when a single child is found |
||
| 8635 | # or an array of hashes for multiple children |
||
| 8636 | def each_xml_element(node, name) |
||
| 8637 | if node && node[name] |
||
| 8638 | if node[name].is_a?(Hash) |
||
| 8639 | yield node[name] |
||
| 8640 | else |
||
| 8641 | node[name].each do |element| |
||
| 8642 | yield element |
||
| 8643 | end |
||
| 8644 | end |
||
| 8645 | end |
||
| 8646 | end |
||
| 8647 | |||
| 8648 | def target(path = '') |
||
| 8649 | base = path.match(/^\//) ? root_url : url |
||
| 8650 | uri = "#{base}/#{path}"
|
||
| 8651 | uri = URI.escape(URI.escape(uri), '[]') |
||
| 8652 | shell_quote(uri.gsub(/[?<>\*]/, '')) |
||
| 8653 | end |
||
| 8654 | end |
||
| 8655 | end |
||
| 8656 | end |
||
| 8657 | end |
||
| 8658 | 0:513646585e45 | Chris | module Redmine |
| 8659 | module Scm |
||
| 8660 | class Base |
||
| 8661 | class << self |
||
| 8662 | |||
| 8663 | def all |
||
| 8664 | 1464:261b3d9a4903 | Chris | @scms || [] |
| 8665 | 0:513646585e45 | Chris | end |
| 8666 | |||
| 8667 | # Add a new SCM adapter and repository |
||
| 8668 | def add(scm_name) |
||
| 8669 | @scms ||= [] |
||
| 8670 | @scms << scm_name |
||
| 8671 | end |
||
| 8672 | |||
| 8673 | # Remove a SCM adapter from Redmine's list of supported scms |
||
| 8674 | def delete(scm_name) |
||
| 8675 | @scms.delete(scm_name) |
||
| 8676 | end |
||
| 8677 | end |
||
| 8678 | end |
||
| 8679 | end |
||
| 8680 | end |
||
| 8681 | # Redmine - project management software |
||
| 8682 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 8683 | 0:513646585e45 | Chris | # |
| 8684 | # This program is free software; you can redistribute it and/or |
||
| 8685 | # modify it under the terms of the GNU General Public License |
||
| 8686 | # as published by the Free Software Foundation; either version 2 |
||
| 8687 | # of the License, or (at your option) any later version. |
||
| 8688 | 909:cbb26bc654de | Chris | # |
| 8689 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 8690 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 8691 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 8692 | # GNU General Public License for more details. |
||
| 8693 | 909:cbb26bc654de | Chris | # |
| 8694 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 8695 | # along with this program; if not, write to the Free Software |
||
| 8696 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 8697 | |||
| 8698 | module Redmine |
||
| 8699 | module Search |
||
| 8700 | 909:cbb26bc654de | Chris | |
| 8701 | 0:513646585e45 | Chris | mattr_accessor :available_search_types |
| 8702 | 909:cbb26bc654de | Chris | |
| 8703 | 0:513646585e45 | Chris | @@available_search_types = [] |
| 8704 | |||
| 8705 | class << self |
||
| 8706 | def map(&block) |
||
| 8707 | yield self |
||
| 8708 | end |
||
| 8709 | 909:cbb26bc654de | Chris | |
| 8710 | 0:513646585e45 | Chris | # Registers a search provider |
| 8711 | def register(search_type, options={})
|
||
| 8712 | search_type = search_type.to_s |
||
| 8713 | @@available_search_types << search_type unless @@available_search_types.include?(search_type) |
||
| 8714 | end |
||
| 8715 | end |
||
| 8716 | 909:cbb26bc654de | Chris | |
| 8717 | 0:513646585e45 | Chris | module Controller |
| 8718 | def self.included(base) |
||
| 8719 | base.extend(ClassMethods) |
||
| 8720 | end |
||
| 8721 | |||
| 8722 | module ClassMethods |
||
| 8723 | @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
|
||
| 8724 | mattr_accessor :default_search_scopes |
||
| 8725 | 909:cbb26bc654de | Chris | |
| 8726 | 0:513646585e45 | Chris | # Set the default search scope for a controller or specific actions |
| 8727 | # Examples: |
||
| 8728 | # * search_scope :issues # => sets the search scope to :issues for the whole controller |
||
| 8729 | # * search_scope :issues, :only => :index |
||
| 8730 | # * search_scope :issues, :only => [:index, :show] |
||
| 8731 | def default_search_scope(id, options = {})
|
||
| 8732 | if actions = options[:only] |
||
| 8733 | actions = [] << actions unless actions.is_a?(Array) |
||
| 8734 | actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
|
||
| 8735 | else |
||
| 8736 | default_search_scopes[controller_name.to_sym][:default] = id.to_s |
||
| 8737 | end |
||
| 8738 | end |
||
| 8739 | end |
||
| 8740 | |||
| 8741 | def default_search_scopes |
||
| 8742 | self.class.default_search_scopes |
||
| 8743 | end |
||
| 8744 | |||
| 8745 | # Returns the default search scope according to the current action |
||
| 8746 | def default_search_scope |
||
| 8747 | @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] || |
||
| 8748 | default_search_scopes[controller_name.to_sym][:default] |
||
| 8749 | end |
||
| 8750 | end |
||
| 8751 | end |
||
| 8752 | end |
||
| 8753 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 8754 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 8755 | 1115:433d4f72a19b | Chris | # |
| 8756 | # This program is free software; you can redistribute it and/or |
||
| 8757 | # modify it under the terms of the GNU General Public License |
||
| 8758 | # as published by the Free Software Foundation; either version 2 |
||
| 8759 | # of the License, or (at your option) any later version. |
||
| 8760 | # |
||
| 8761 | # This program is distributed in the hope that it will be useful, |
||
| 8762 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 8763 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 8764 | # GNU General Public License for more details. |
||
| 8765 | # |
||
| 8766 | # You should have received a copy of the GNU General Public License |
||
| 8767 | # along with this program; if not, write to the Free Software |
||
| 8768 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 8769 | |||
| 8770 | module Redmine |
||
| 8771 | module SubclassFactory |
||
| 8772 | def self.included(base) |
||
| 8773 | base.extend ClassMethods |
||
| 8774 | end |
||
| 8775 | |||
| 8776 | module ClassMethods |
||
| 8777 | def get_subclass(class_name) |
||
| 8778 | klass = nil |
||
| 8779 | begin |
||
| 8780 | klass = class_name.to_s.classify.constantize |
||
| 8781 | rescue |
||
| 8782 | # invalid class name |
||
| 8783 | end |
||
| 8784 | unless subclasses.include? klass |
||
| 8785 | klass = nil |
||
| 8786 | end |
||
| 8787 | klass |
||
| 8788 | end |
||
| 8789 | |||
| 8790 | # Returns an instance of the given subclass name |
||
| 8791 | def new_subclass_instance(class_name, *args) |
||
| 8792 | klass = get_subclass(class_name) |
||
| 8793 | if klass |
||
| 8794 | klass.new(*args) |
||
| 8795 | end |
||
| 8796 | end |
||
| 8797 | end |
||
| 8798 | end |
||
| 8799 | end |
||
| 8800 | 0:513646585e45 | Chris | # Redmine - project management software |
| 8801 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 8802 | 0:513646585e45 | Chris | # |
| 8803 | # This program is free software; you can redistribute it and/or |
||
| 8804 | # modify it under the terms of the GNU General Public License |
||
| 8805 | # as published by the Free Software Foundation; either version 2 |
||
| 8806 | # of the License, or (at your option) any later version. |
||
| 8807 | 909:cbb26bc654de | Chris | # |
| 8808 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 8809 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 8810 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 8811 | # GNU General Public License for more details. |
||
| 8812 | 909:cbb26bc654de | Chris | # |
| 8813 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 8814 | # along with this program; if not, write to the Free Software |
||
| 8815 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 8816 | |||
| 8817 | module Redmine |
||
| 8818 | module SyntaxHighlighting |
||
| 8819 | 909:cbb26bc654de | Chris | |
| 8820 | 0:513646585e45 | Chris | class << self |
| 8821 | attr_reader :highlighter |
||
| 8822 | 909:cbb26bc654de | Chris | |
| 8823 | 0:513646585e45 | Chris | def highlighter=(name) |
| 8824 | if name.is_a?(Module) |
||
| 8825 | @highlighter = name |
||
| 8826 | else |
||
| 8827 | @highlighter = const_get(name) |
||
| 8828 | end |
||
| 8829 | end |
||
| 8830 | 1517:dffacf8a6908 | Chris | |
| 8831 | def highlight_by_filename(text, filename) |
||
| 8832 | highlighter.highlight_by_filename(text, filename) |
||
| 8833 | rescue |
||
| 8834 | ERB::Util.h(text) |
||
| 8835 | end |
||
| 8836 | |||
| 8837 | def highlight_by_language(text, language) |
||
| 8838 | highlighter.highlight_by_language(text, language) |
||
| 8839 | rescue |
||
| 8840 | ERB::Util.h(text) |
||
| 8841 | end |
||
| 8842 | 0:513646585e45 | Chris | end |
| 8843 | 909:cbb26bc654de | Chris | |
| 8844 | 0:513646585e45 | Chris | module CodeRay |
| 8845 | require 'coderay' |
||
| 8846 | 909:cbb26bc654de | Chris | |
| 8847 | 0:513646585e45 | Chris | class << self |
| 8848 | # Highlights +text+ as the content of +filename+ |
||
| 8849 | # Should not return line numbers nor outer pre tag |
||
| 8850 | def highlight_by_filename(text, filename) |
||
| 8851 | language = ::CodeRay::FileType[filename] |
||
| 8852 | 1115:433d4f72a19b | Chris | language ? ::CodeRay.scan(text, language).html(:break_lines => true) : ERB::Util.h(text) |
| 8853 | 0:513646585e45 | Chris | end |
| 8854 | 909:cbb26bc654de | Chris | |
| 8855 | 0:513646585e45 | Chris | # Highlights +text+ using +language+ syntax |
| 8856 | # Should not return outer pre tag |
||
| 8857 | def highlight_by_language(text, language) |
||
| 8858 | 1115:433d4f72a19b | Chris | ::CodeRay.scan(text, language).html(:wrap => :span) |
| 8859 | 0:513646585e45 | Chris | end |
| 8860 | end |
||
| 8861 | end |
||
| 8862 | end |
||
| 8863 | 909:cbb26bc654de | Chris | |
| 8864 | 0:513646585e45 | Chris | SyntaxHighlighting.highlighter = 'CodeRay' |
| 8865 | end |
||
| 8866 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 8867 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 8868 | 0:513646585e45 | Chris | # |
| 8869 | # This program is free software; you can redistribute it and/or |
||
| 8870 | # modify it under the terms of the GNU General Public License |
||
| 8871 | # as published by the Free Software Foundation; either version 2 |
||
| 8872 | # of the License, or (at your option) any later version. |
||
| 8873 | 909:cbb26bc654de | Chris | # |
| 8874 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 8875 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 8876 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 8877 | # GNU General Public License for more details. |
||
| 8878 | 909:cbb26bc654de | Chris | # |
| 8879 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 8880 | # along with this program; if not, write to the Free Software |
||
| 8881 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 8882 | |||
| 8883 | module Redmine |
||
| 8884 | module Themes |
||
| 8885 | 909:cbb26bc654de | Chris | |
| 8886 | 0:513646585e45 | Chris | # Return an array of installed themes |
| 8887 | def self.themes |
||
| 8888 | @@installed_themes ||= scan_themes |
||
| 8889 | end |
||
| 8890 | 909:cbb26bc654de | Chris | |
| 8891 | 0:513646585e45 | Chris | # Rescan themes directory |
| 8892 | def self.rescan |
||
| 8893 | @@installed_themes = scan_themes |
||
| 8894 | end |
||
| 8895 | 909:cbb26bc654de | Chris | |
| 8896 | 0:513646585e45 | Chris | # Return theme for given id, or nil if it's not found |
| 8897 | 119:8661b858af72 | Chris | def self.theme(id, options={})
|
| 8898 | return nil if id.blank? |
||
| 8899 | 909:cbb26bc654de | Chris | |
| 8900 | 119:8661b858af72 | Chris | found = themes.find {|t| t.id == id}
|
| 8901 | if found.nil? && options[:rescan] != false |
||
| 8902 | rescan |
||
| 8903 | found = theme(id, :rescan => false) |
||
| 8904 | end |
||
| 8905 | found |
||
| 8906 | 0:513646585e45 | Chris | end |
| 8907 | 909:cbb26bc654de | Chris | |
| 8908 | 0:513646585e45 | Chris | # Class used to represent a theme |
| 8909 | class Theme |
||
| 8910 | 119:8661b858af72 | Chris | attr_reader :path, :name, :dir |
| 8911 | 909:cbb26bc654de | Chris | |
| 8912 | 0:513646585e45 | Chris | def initialize(path) |
| 8913 | 119:8661b858af72 | Chris | @path = path |
| 8914 | 0:513646585e45 | Chris | @dir = File.basename(path) |
| 8915 | @name = @dir.humanize |
||
| 8916 | 119:8661b858af72 | Chris | @stylesheets = nil |
| 8917 | @javascripts = nil |
||
| 8918 | 0:513646585e45 | Chris | end |
| 8919 | 909:cbb26bc654de | Chris | |
| 8920 | 0:513646585e45 | Chris | # Directory name used as the theme id |
| 8921 | def id; dir end |
||
| 8922 | 909:cbb26bc654de | Chris | |
| 8923 | 119:8661b858af72 | Chris | def ==(theme) |
| 8924 | theme.is_a?(Theme) && theme.dir == dir |
||
| 8925 | end |
||
| 8926 | 909:cbb26bc654de | Chris | |
| 8927 | 0:513646585e45 | Chris | def <=>(theme) |
| 8928 | name <=> theme.name |
||
| 8929 | end |
||
| 8930 | 909:cbb26bc654de | Chris | |
| 8931 | 119:8661b858af72 | Chris | def stylesheets |
| 8932 | @stylesheets ||= assets("stylesheets", "css")
|
||
| 8933 | end |
||
| 8934 | 909:cbb26bc654de | Chris | |
| 8935 | 1115:433d4f72a19b | Chris | def images |
| 8936 | @images ||= assets("images")
|
||
| 8937 | end |
||
| 8938 | |||
| 8939 | 119:8661b858af72 | Chris | def javascripts |
| 8940 | @javascripts ||= assets("javascripts", "js")
|
||
| 8941 | end |
||
| 8942 | 909:cbb26bc654de | Chris | |
| 8943 | 1517:dffacf8a6908 | Chris | def favicons |
| 8944 | @favicons ||= assets("favicon")
|
||
| 8945 | end |
||
| 8946 | |||
| 8947 | def favicon |
||
| 8948 | favicons.first |
||
| 8949 | end |
||
| 8950 | |||
| 8951 | def favicon? |
||
| 8952 | favicon.present? |
||
| 8953 | end |
||
| 8954 | |||
| 8955 | 119:8661b858af72 | Chris | def stylesheet_path(source) |
| 8956 | "/themes/#{dir}/stylesheets/#{source}"
|
||
| 8957 | end |
||
| 8958 | 909:cbb26bc654de | Chris | |
| 8959 | 1115:433d4f72a19b | Chris | def image_path(source) |
| 8960 | "/themes/#{dir}/images/#{source}"
|
||
| 8961 | end |
||
| 8962 | |||
| 8963 | 119:8661b858af72 | Chris | def javascript_path(source) |
| 8964 | "/themes/#{dir}/javascripts/#{source}"
|
||
| 8965 | end |
||
| 8966 | 909:cbb26bc654de | Chris | |
| 8967 | 1517:dffacf8a6908 | Chris | def favicon_path |
| 8968 | "/themes/#{dir}/favicon/#{favicon}"
|
||
| 8969 | end |
||
| 8970 | |||
| 8971 | 119:8661b858af72 | Chris | private |
| 8972 | 909:cbb26bc654de | Chris | |
| 8973 | 1115:433d4f72a19b | Chris | def assets(dir, ext=nil) |
| 8974 | if ext |
||
| 8975 | Dir.glob("#{path}/#{dir}/*.#{ext}").collect {|f| File.basename(f).gsub(/\.#{ext}$/, '')}
|
||
| 8976 | else |
||
| 8977 | Dir.glob("#{path}/#{dir}/*").collect {|f| File.basename(f)}
|
||
| 8978 | end |
||
| 8979 | 119:8661b858af72 | Chris | end |
| 8980 | 0:513646585e45 | Chris | end |
| 8981 | 909:cbb26bc654de | Chris | |
| 8982 | 0:513646585e45 | Chris | private |
| 8983 | 909:cbb26bc654de | Chris | |
| 8984 | 0:513646585e45 | Chris | def self.scan_themes |
| 8985 | dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f|
|
||
| 8986 | # A theme should at least override application.css |
||
| 8987 | File.directory?(f) && File.exist?("#{f}/stylesheets/application.css")
|
||
| 8988 | end |
||
| 8989 | dirs.collect {|dir| Theme.new(dir)}.sort
|
||
| 8990 | end |
||
| 8991 | end |
||
| 8992 | end |
||
| 8993 | |||
| 8994 | module ApplicationHelper |
||
| 8995 | 119:8661b858af72 | Chris | def current_theme |
| 8996 | unless instance_variable_defined?(:@current_theme) |
||
| 8997 | @current_theme = Redmine::Themes.theme(Setting.ui_theme) |
||
| 8998 | end |
||
| 8999 | @current_theme |
||
| 9000 | end |
||
| 9001 | 909:cbb26bc654de | Chris | |
| 9002 | 119:8661b858af72 | Chris | # Returns the header tags for the current theme |
| 9003 | def heads_for_theme |
||
| 9004 | if current_theme && current_theme.javascripts.include?('theme')
|
||
| 9005 | javascript_include_tag current_theme.javascript_path('theme')
|
||
| 9006 | end |
||
| 9007 | end |
||
| 9008 | 0:513646585e45 | Chris | end |
| 9009 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 9010 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9011 | 1115:433d4f72a19b | Chris | # |
| 9012 | # This program is free software; you can redistribute it and/or |
||
| 9013 | # modify it under the terms of the GNU General Public License |
||
| 9014 | # as published by the Free Software Foundation; either version 2 |
||
| 9015 | # of the License, or (at your option) any later version. |
||
| 9016 | # |
||
| 9017 | # This program is distributed in the hope that it will be useful, |
||
| 9018 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9019 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9020 | # GNU General Public License for more details. |
||
| 9021 | # |
||
| 9022 | # You should have received a copy of the GNU General Public License |
||
| 9023 | # along with this program; if not, write to the Free Software |
||
| 9024 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9025 | |||
| 9026 | require 'fileutils' |
||
| 9027 | |||
| 9028 | module Redmine |
||
| 9029 | module Thumbnail |
||
| 9030 | extend Redmine::Utils::Shell |
||
| 9031 | |||
| 9032 | CONVERT_BIN = (Redmine::Configuration['imagemagick_convert_command'] || 'convert').freeze |
||
| 9033 | |||
| 9034 | # Generates a thumbnail for the source image to target |
||
| 9035 | def self.generate(source, target, size) |
||
| 9036 | return nil unless convert_available? |
||
| 9037 | unless File.exists?(target) |
||
| 9038 | directory = File.dirname(target) |
||
| 9039 | unless File.exists?(directory) |
||
| 9040 | FileUtils.mkdir_p directory |
||
| 9041 | end |
||
| 9042 | size_option = "#{size}x#{size}>"
|
||
| 9043 | cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -thumbnail #{shell_quote size_option} #{shell_quote target}"
|
||
| 9044 | unless system(cmd) |
||
| 9045 | logger.error("Creating thumbnail failed (#{$?}):\nCommand: #{cmd}")
|
||
| 9046 | return nil |
||
| 9047 | end |
||
| 9048 | end |
||
| 9049 | target |
||
| 9050 | end |
||
| 9051 | |||
| 9052 | def self.convert_available? |
||
| 9053 | return @convert_available if defined?(@convert_available) |
||
| 9054 | @convert_available = system("#{shell_quote CONVERT_BIN} -version") rescue false
|
||
| 9055 | logger.warn("Imagemagick's convert binary (#{CONVERT_BIN}) not available") unless @convert_available
|
||
| 9056 | @convert_available |
||
| 9057 | end |
||
| 9058 | |||
| 9059 | def self.logger |
||
| 9060 | Rails.logger |
||
| 9061 | end |
||
| 9062 | end |
||
| 9063 | end |
||
| 9064 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software |
| 9065 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9066 | 0:513646585e45 | Chris | # |
| 9067 | # This program is free software; you can redistribute it and/or |
||
| 9068 | # modify it under the terms of the GNU General Public License |
||
| 9069 | # as published by the Free Software Foundation; either version 2 |
||
| 9070 | # of the License, or (at your option) any later version. |
||
| 9071 | 909:cbb26bc654de | Chris | # |
| 9072 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 9073 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9074 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9075 | # GNU General Public License for more details. |
||
| 9076 | 909:cbb26bc654de | Chris | # |
| 9077 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 9078 | # along with this program; if not, write to the Free Software |
||
| 9079 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9080 | |||
| 9081 | module Redmine |
||
| 9082 | # Class used to parse unified diffs |
||
| 9083 | 441:cbce1fd3b1b7 | Chris | class UnifiedDiff < Array |
| 9084 | 1115:433d4f72a19b | Chris | attr_reader :diff_type, :diff_style |
| 9085 | 909:cbb26bc654de | Chris | |
| 9086 | 0:513646585e45 | Chris | def initialize(diff, options={})
|
| 9087 | 1115:433d4f72a19b | Chris | options.assert_valid_keys(:type, :style, :max_lines) |
| 9088 | 0:513646585e45 | Chris | diff = diff.split("\n") if diff.is_a?(String)
|
| 9089 | 441:cbce1fd3b1b7 | Chris | @diff_type = options[:type] || 'inline' |
| 9090 | 1115:433d4f72a19b | Chris | @diff_style = options[:style] |
| 9091 | 0:513646585e45 | Chris | lines = 0 |
| 9092 | @truncated = false |
||
| 9093 | 1115:433d4f72a19b | Chris | diff_table = DiffTable.new(diff_type, diff_style) |
| 9094 | 1464:261b3d9a4903 | Chris | diff.each do |line_raw| |
| 9095 | line = Redmine::CodesetUtil.to_utf8_by_setting(line_raw) |
||
| 9096 | unless diff_table.add_line(line) |
||
| 9097 | 245:051f544170fe | Chris | self << diff_table if diff_table.length > 0 |
| 9098 | 1115:433d4f72a19b | Chris | diff_table = DiffTable.new(diff_type, diff_style) |
| 9099 | 0:513646585e45 | Chris | end |
| 9100 | lines += 1 |
||
| 9101 | if options[:max_lines] && lines > options[:max_lines] |
||
| 9102 | @truncated = true |
||
| 9103 | break |
||
| 9104 | end |
||
| 9105 | end |
||
| 9106 | self << diff_table unless diff_table.empty? |
||
| 9107 | self |
||
| 9108 | end |
||
| 9109 | 245:051f544170fe | Chris | |
| 9110 | 0:513646585e45 | Chris | def truncated?; @truncated; end |
| 9111 | end |
||
| 9112 | |||
| 9113 | # Class that represents a file diff |
||
| 9114 | 909:cbb26bc654de | Chris | class DiffTable < Array |
| 9115 | 441:cbce1fd3b1b7 | Chris | attr_reader :file_name |
| 9116 | 0:513646585e45 | Chris | |
| 9117 | # Initialize with a Diff file and the type of Diff View |
||
| 9118 | # The type view must be inline or sbs (side_by_side) |
||
| 9119 | 1115:433d4f72a19b | Chris | def initialize(type="inline", style=nil) |
| 9120 | 0:513646585e45 | Chris | @parsing = false |
| 9121 | 441:cbce1fd3b1b7 | Chris | @added = 0 |
| 9122 | @removed = 0 |
||
| 9123 | 0:513646585e45 | Chris | @type = type |
| 9124 | 1115:433d4f72a19b | Chris | @style = style |
| 9125 | @file_name = nil |
||
| 9126 | @git_diff = false |
||
| 9127 | 0:513646585e45 | Chris | end |
| 9128 | |||
| 9129 | # Function for add a line of this Diff |
||
| 9130 | # Returns false when the diff ends |
||
| 9131 | def add_line(line) |
||
| 9132 | unless @parsing |
||
| 9133 | if line =~ /^(---|\+\+\+) (.*)$/ |
||
| 9134 | 1115:433d4f72a19b | Chris | self.file_name = $2 |
| 9135 | 0:513646585e45 | Chris | elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ |
| 9136 | @line_num_l = $2.to_i |
||
| 9137 | @line_num_r = $5.to_i |
||
| 9138 | @parsing = true |
||
| 9139 | end |
||
| 9140 | else |
||
| 9141 | 1464:261b3d9a4903 | Chris | if line =~ %r{^[^\+\-\s@\\]}
|
| 9142 | 0:513646585e45 | Chris | @parsing = false |
| 9143 | return false |
||
| 9144 | elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ |
||
| 9145 | @line_num_l = $2.to_i |
||
| 9146 | @line_num_r = $5.to_i |
||
| 9147 | else |
||
| 9148 | 909:cbb26bc654de | Chris | parse_line(line, @type) |
| 9149 | 0:513646585e45 | Chris | end |
| 9150 | end |
||
| 9151 | return true |
||
| 9152 | end |
||
| 9153 | 909:cbb26bc654de | Chris | |
| 9154 | 441:cbce1fd3b1b7 | Chris | def each_line |
| 9155 | prev_line_left, prev_line_right = nil, nil |
||
| 9156 | each do |line| |
||
| 9157 | spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1) |
||
| 9158 | yield spacing, line |
||
| 9159 | prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0 |
||
| 9160 | prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0 |
||
| 9161 | end |
||
| 9162 | end |
||
| 9163 | 0:513646585e45 | Chris | |
| 9164 | def inspect |
||
| 9165 | puts '### DIFF TABLE ###' |
||
| 9166 | puts "file : #{file_name}"
|
||
| 9167 | self.each do |d| |
||
| 9168 | d.inspect |
||
| 9169 | end |
||
| 9170 | end |
||
| 9171 | |||
| 9172 | 441:cbce1fd3b1b7 | Chris | private |
| 9173 | 0:513646585e45 | Chris | |
| 9174 | 1115:433d4f72a19b | Chris | def file_name=(arg) |
| 9175 | both_git_diff = false |
||
| 9176 | if file_name.nil? |
||
| 9177 | @git_diff = true if arg =~ %r{^(a/|/dev/null)}
|
||
| 9178 | else |
||
| 9179 | both_git_diff = (@git_diff && arg =~ %r{^(b/|/dev/null)})
|
||
| 9180 | end |
||
| 9181 | if both_git_diff |
||
| 9182 | if file_name && arg == "/dev/null" |
||
| 9183 | # keep the original file name |
||
| 9184 | @file_name = file_name.sub(%r{^a/}, '')
|
||
| 9185 | else |
||
| 9186 | # remove leading b/ |
||
| 9187 | @file_name = arg.sub(%r{^b/}, '')
|
||
| 9188 | end |
||
| 9189 | elsif @style == "Subversion" |
||
| 9190 | # removing trailing "(revision nn)" |
||
| 9191 | @file_name = arg.sub(%r{\t+\(.*\)$}, '')
|
||
| 9192 | else |
||
| 9193 | @file_name = arg |
||
| 9194 | end |
||
| 9195 | end |
||
| 9196 | |||
| 9197 | 441:cbce1fd3b1b7 | Chris | def diff_for_added_line |
| 9198 | if @type == 'sbs' && @removed > 0 && @added < @removed |
||
| 9199 | self[-(@removed - @added)] |
||
| 9200 | else |
||
| 9201 | diff = Diff.new |
||
| 9202 | self << diff |
||
| 9203 | diff |
||
| 9204 | end |
||
| 9205 | end |
||
| 9206 | 0:513646585e45 | Chris | |
| 9207 | def parse_line(line, type="inline") |
||
| 9208 | if line[0, 1] == "+" |
||
| 9209 | 441:cbce1fd3b1b7 | Chris | diff = diff_for_added_line |
| 9210 | 929:5f33065ddc4b | Chris | diff.line_right = line[1..-1] |
| 9211 | 0:513646585e45 | Chris | diff.nb_line_right = @line_num_r |
| 9212 | diff.type_diff_right = 'diff_in' |
||
| 9213 | @line_num_r += 1 |
||
| 9214 | 441:cbce1fd3b1b7 | Chris | @added += 1 |
| 9215 | 0:513646585e45 | Chris | true |
| 9216 | elsif line[0, 1] == "-" |
||
| 9217 | 441:cbce1fd3b1b7 | Chris | diff = Diff.new |
| 9218 | 929:5f33065ddc4b | Chris | diff.line_left = line[1..-1] |
| 9219 | 0:513646585e45 | Chris | diff.nb_line_left = @line_num_l |
| 9220 | diff.type_diff_left = 'diff_out' |
||
| 9221 | 441:cbce1fd3b1b7 | Chris | self << diff |
| 9222 | 0:513646585e45 | Chris | @line_num_l += 1 |
| 9223 | 441:cbce1fd3b1b7 | Chris | @removed += 1 |
| 9224 | 0:513646585e45 | Chris | true |
| 9225 | 441:cbce1fd3b1b7 | Chris | else |
| 9226 | write_offsets |
||
| 9227 | if line[0, 1] =~ /\s/ |
||
| 9228 | diff = Diff.new |
||
| 9229 | 929:5f33065ddc4b | Chris | diff.line_right = line[1..-1] |
| 9230 | 441:cbce1fd3b1b7 | Chris | diff.nb_line_right = @line_num_r |
| 9231 | 929:5f33065ddc4b | Chris | diff.line_left = line[1..-1] |
| 9232 | 441:cbce1fd3b1b7 | Chris | diff.nb_line_left = @line_num_l |
| 9233 | self << diff |
||
| 9234 | @line_num_l += 1 |
||
| 9235 | @line_num_r += 1 |
||
| 9236 | true |
||
| 9237 | elsif line[0, 1] = "\\" |
||
| 9238 | 0:513646585e45 | Chris | true |
| 9239 | else |
||
| 9240 | false |
||
| 9241 | end |
||
| 9242 | end |
||
| 9243 | end |
||
| 9244 | 909:cbb26bc654de | Chris | |
| 9245 | 441:cbce1fd3b1b7 | Chris | def write_offsets |
| 9246 | if @added > 0 && @added == @removed |
||
| 9247 | @added.times do |i| |
||
| 9248 | line = self[-(1 + i)] |
||
| 9249 | removed = (@type == 'sbs') ? line : self[-(1 + @added + i)] |
||
| 9250 | offsets = offsets(removed.line_left, line.line_right) |
||
| 9251 | removed.offsets = line.offsets = offsets |
||
| 9252 | end |
||
| 9253 | end |
||
| 9254 | @added = 0 |
||
| 9255 | @removed = 0 |
||
| 9256 | end |
||
| 9257 | 909:cbb26bc654de | Chris | |
| 9258 | 441:cbce1fd3b1b7 | Chris | def offsets(line_left, line_right) |
| 9259 | if line_left.present? && line_right.present? && line_left != line_right |
||
| 9260 | max = [line_left.size, line_right.size].min |
||
| 9261 | starting = 0 |
||
| 9262 | while starting < max && line_left[starting] == line_right[starting] |
||
| 9263 | starting += 1 |
||
| 9264 | end |
||
| 9265 | 1464:261b3d9a4903 | Chris | if (! "".respond_to?(:force_encoding)) && starting < line_left.size |
| 9266 | while line_left[starting].ord.between?(128, 191) && starting > 0 |
||
| 9267 | starting -= 1 |
||
| 9268 | end |
||
| 9269 | end |
||
| 9270 | 441:cbce1fd3b1b7 | Chris | ending = -1 |
| 9271 | 1464:261b3d9a4903 | Chris | while ending >= -(max - starting) && (line_left[ending] == line_right[ending]) |
| 9272 | 441:cbce1fd3b1b7 | Chris | ending -= 1 |
| 9273 | end |
||
| 9274 | 1464:261b3d9a4903 | Chris | if (! "".respond_to?(:force_encoding)) && ending > (-1 * line_left.size) |
| 9275 | while line_left[ending].ord.between?(128, 255) && ending < -1 |
||
| 9276 | if line_left[ending].ord.between?(128, 191) |
||
| 9277 | if line_left[ending + 1].ord.between?(128, 191) |
||
| 9278 | ending += 1 |
||
| 9279 | else |
||
| 9280 | break |
||
| 9281 | end |
||
| 9282 | else |
||
| 9283 | ending += 1 |
||
| 9284 | end |
||
| 9285 | end |
||
| 9286 | end |
||
| 9287 | 441:cbce1fd3b1b7 | Chris | unless starting == 0 && ending == -1 |
| 9288 | [starting, ending] |
||
| 9289 | end |
||
| 9290 | end |
||
| 9291 | end |
||
| 9292 | end |
||
| 9293 | 0:513646585e45 | Chris | |
| 9294 | # A line of diff |
||
| 9295 | 909:cbb26bc654de | Chris | class Diff |
| 9296 | 0:513646585e45 | Chris | attr_accessor :nb_line_left |
| 9297 | attr_accessor :line_left |
||
| 9298 | attr_accessor :nb_line_right |
||
| 9299 | attr_accessor :line_right |
||
| 9300 | attr_accessor :type_diff_right |
||
| 9301 | attr_accessor :type_diff_left |
||
| 9302 | 441:cbce1fd3b1b7 | Chris | attr_accessor :offsets |
| 9303 | 909:cbb26bc654de | Chris | |
| 9304 | 0:513646585e45 | Chris | def initialize() |
| 9305 | self.nb_line_left = '' |
||
| 9306 | self.nb_line_right = '' |
||
| 9307 | self.line_left = '' |
||
| 9308 | self.line_right = '' |
||
| 9309 | self.type_diff_right = '' |
||
| 9310 | self.type_diff_left = '' |
||
| 9311 | end |
||
| 9312 | 909:cbb26bc654de | Chris | |
| 9313 | 441:cbce1fd3b1b7 | Chris | def type_diff |
| 9314 | type_diff_right == 'diff_in' ? type_diff_right : type_diff_left |
||
| 9315 | end |
||
| 9316 | 909:cbb26bc654de | Chris | |
| 9317 | 441:cbce1fd3b1b7 | Chris | def line |
| 9318 | type_diff_right == 'diff_in' ? line_right : line_left |
||
| 9319 | end |
||
| 9320 | 909:cbb26bc654de | Chris | |
| 9321 | 441:cbce1fd3b1b7 | Chris | def html_line_left |
| 9322 | 929:5f33065ddc4b | Chris | line_to_html(line_left, offsets) |
| 9323 | 441:cbce1fd3b1b7 | Chris | end |
| 9324 | 909:cbb26bc654de | Chris | |
| 9325 | 441:cbce1fd3b1b7 | Chris | def html_line_right |
| 9326 | 929:5f33065ddc4b | Chris | line_to_html(line_right, offsets) |
| 9327 | 441:cbce1fd3b1b7 | Chris | end |
| 9328 | 909:cbb26bc654de | Chris | |
| 9329 | 441:cbce1fd3b1b7 | Chris | def html_line |
| 9330 | 929:5f33065ddc4b | Chris | line_to_html(line, offsets) |
| 9331 | 441:cbce1fd3b1b7 | Chris | end |
| 9332 | 0:513646585e45 | Chris | |
| 9333 | def inspect |
||
| 9334 | puts '### Start Line Diff ###' |
||
| 9335 | puts self.nb_line_left |
||
| 9336 | puts self.line_left |
||
| 9337 | puts self.nb_line_right |
||
| 9338 | puts self.line_right |
||
| 9339 | end |
||
| 9340 | 929:5f33065ddc4b | Chris | |
| 9341 | private |
||
| 9342 | |||
| 9343 | def line_to_html(line, offsets) |
||
| 9344 | 1464:261b3d9a4903 | Chris | html = line_to_html_raw(line, offsets) |
| 9345 | html.force_encoding('UTF-8') if html.respond_to?(:force_encoding)
|
||
| 9346 | html |
||
| 9347 | end |
||
| 9348 | |||
| 9349 | def line_to_html_raw(line, offsets) |
||
| 9350 | 929:5f33065ddc4b | Chris | if offsets |
| 9351 | s = '' |
||
| 9352 | unless offsets.first == 0 |
||
| 9353 | s << CGI.escapeHTML(line[0..offsets.first-1]) |
||
| 9354 | end |
||
| 9355 | s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>' |
||
| 9356 | unless offsets.last == -1 |
||
| 9357 | s << CGI.escapeHTML(line[offsets.last+1..-1]) |
||
| 9358 | end |
||
| 9359 | s |
||
| 9360 | else |
||
| 9361 | CGI.escapeHTML(line) |
||
| 9362 | end |
||
| 9363 | end |
||
| 9364 | 0:513646585e45 | Chris | end |
| 9365 | end |
||
| 9366 | # Redmine - project management software |
||
| 9367 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9368 | 0:513646585e45 | Chris | # |
| 9369 | # This program is free software; you can redistribute it and/or |
||
| 9370 | # modify it under the terms of the GNU General Public License |
||
| 9371 | # as published by the Free Software Foundation; either version 2 |
||
| 9372 | # of the License, or (at your option) any later version. |
||
| 9373 | # |
||
| 9374 | # This program is distributed in the hope that it will be useful, |
||
| 9375 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9376 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9377 | # GNU General Public License for more details. |
||
| 9378 | # |
||
| 9379 | # You should have received a copy of the GNU General Public License |
||
| 9380 | # along with this program; if not, write to the Free Software |
||
| 9381 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9382 | |||
| 9383 | module Redmine |
||
| 9384 | module Utils |
||
| 9385 | class << self |
||
| 9386 | # Returns the relative root url of the application |
||
| 9387 | def relative_url_root |
||
| 9388 | ActionController::Base.respond_to?('relative_url_root') ?
|
||
| 9389 | ActionController::Base.relative_url_root.to_s : |
||
| 9390 | 1115:433d4f72a19b | Chris | ActionController::Base.config.relative_url_root.to_s |
| 9391 | 0:513646585e45 | Chris | end |
| 9392 | 909:cbb26bc654de | Chris | |
| 9393 | 0:513646585e45 | Chris | # Sets the relative root url of the application |
| 9394 | def relative_url_root=(arg) |
||
| 9395 | if ActionController::Base.respond_to?('relative_url_root=')
|
||
| 9396 | ActionController::Base.relative_url_root=arg |
||
| 9397 | else |
||
| 9398 | 1115:433d4f72a19b | Chris | ActionController::Base.config.relative_url_root = arg |
| 9399 | end |
||
| 9400 | end |
||
| 9401 | |||
| 9402 | # Generates a n bytes random hex string |
||
| 9403 | # Example: |
||
| 9404 | # random_hex(4) # => "89b8c729" |
||
| 9405 | def random_hex(n) |
||
| 9406 | SecureRandom.hex(n) |
||
| 9407 | end |
||
| 9408 | end |
||
| 9409 | |||
| 9410 | module Shell |
||
| 9411 | def shell_quote(str) |
||
| 9412 | if Redmine::Platform.mswin? |
||
| 9413 | '"' + str.gsub(/"/, '\\"') + '"' |
||
| 9414 | else |
||
| 9415 | "'" + str.gsub(/'/, "'\"'\"'") + "'" |
||
| 9416 | end |
||
| 9417 | end |
||
| 9418 | end |
||
| 9419 | |||
| 9420 | module DateCalculation |
||
| 9421 | # Returns the number of working days between from and to |
||
| 9422 | def working_days(from, to) |
||
| 9423 | days = (to - from).to_i |
||
| 9424 | if days > 0 |
||
| 9425 | weeks = days / 7 |
||
| 9426 | result = weeks * (7 - non_working_week_days.size) |
||
| 9427 | days_left = days - weeks * 7 |
||
| 9428 | start_cwday = from.cwday |
||
| 9429 | days_left.times do |i| |
||
| 9430 | unless non_working_week_days.include?(((start_cwday + i - 1) % 7) + 1) |
||
| 9431 | result += 1 |
||
| 9432 | end |
||
| 9433 | end |
||
| 9434 | result |
||
| 9435 | else |
||
| 9436 | 0 |
||
| 9437 | end |
||
| 9438 | end |
||
| 9439 | |||
| 9440 | # Adds working days to the given date |
||
| 9441 | def add_working_days(date, working_days) |
||
| 9442 | if working_days > 0 |
||
| 9443 | weeks = working_days / (7 - non_working_week_days.size) |
||
| 9444 | result = weeks * 7 |
||
| 9445 | days_left = working_days - weeks * (7 - non_working_week_days.size) |
||
| 9446 | cwday = date.cwday |
||
| 9447 | while days_left > 0 |
||
| 9448 | cwday += 1 |
||
| 9449 | unless non_working_week_days.include?(((cwday - 1) % 7) + 1) |
||
| 9450 | days_left -= 1 |
||
| 9451 | end |
||
| 9452 | result += 1 |
||
| 9453 | end |
||
| 9454 | next_working_date(date + result) |
||
| 9455 | else |
||
| 9456 | date |
||
| 9457 | end |
||
| 9458 | end |
||
| 9459 | |||
| 9460 | # Returns the date of the first day on or after the given date that is a working day |
||
| 9461 | def next_working_date(date) |
||
| 9462 | cwday = date.cwday |
||
| 9463 | days = 0 |
||
| 9464 | while non_working_week_days.include?(((cwday + days - 1) % 7) + 1) |
||
| 9465 | days += 1 |
||
| 9466 | end |
||
| 9467 | date + days |
||
| 9468 | end |
||
| 9469 | |||
| 9470 | # Returns the index of non working week days (1=monday, 7=sunday) |
||
| 9471 | def non_working_week_days |
||
| 9472 | @non_working_week_days ||= begin |
||
| 9473 | days = Setting.non_working_week_days |
||
| 9474 | if days.is_a?(Array) && days.size < 7 |
||
| 9475 | days.map(&:to_i) |
||
| 9476 | else |
||
| 9477 | [] |
||
| 9478 | end |
||
| 9479 | 0:513646585e45 | Chris | end |
| 9480 | end |
||
| 9481 | end |
||
| 9482 | end |
||
| 9483 | end |
||
| 9484 | require 'rexml/document' |
||
| 9485 | |||
| 9486 | module Redmine |
||
| 9487 | module VERSION #:nodoc: |
||
| 9488 | 1115:433d4f72a19b | Chris | MAJOR = 2 |
| 9489 | 1517:dffacf8a6908 | Chris | MINOR = 5 |
| 9490 | TINY = 2 |
||
| 9491 | 441:cbce1fd3b1b7 | Chris | |
| 9492 | 0:513646585e45 | Chris | # Branch values: |
| 9493 | # * official release: nil |
||
| 9494 | # * stable branch: stable |
||
| 9495 | # * trunk: devel |
||
| 9496 | 441:cbce1fd3b1b7 | Chris | BRANCH = 'stable' |
| 9497 | 0:513646585e45 | Chris | |
| 9498 | 1115:433d4f72a19b | Chris | # Retrieves the revision from the working copy |
| 9499 | 0:513646585e45 | Chris | def self.revision |
| 9500 | 1568:bc47b68a9487 | Chris | return 0 |
| 9501 | # if File.directory?(File.join(Rails.root, '.svn')) |
||
| 9502 | # begin |
||
| 9503 | # path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s) |
||
| 9504 | # if `svn info --xml #{path}` =~ /revision="(\d+)"/
|
||
| 9505 | # return $1.to_i |
||
| 9506 | # end |
||
| 9507 | # rescue |
||
| 9508 | 441:cbce1fd3b1b7 | Chris | # Could not find the current revision |
| 9509 | 1568:bc47b68a9487 | Chris | # end |
| 9510 | # end |
||
| 9511 | # nil |
||
| 9512 | 0:513646585e45 | Chris | end |
| 9513 | |||
| 9514 | REVISION = self.revision |
||
| 9515 | 441:cbce1fd3b1b7 | Chris | ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact |
| 9516 | STRING = ARRAY.join('.')
|
||
| 9517 | 909:cbb26bc654de | Chris | |
| 9518 | 441:cbce1fd3b1b7 | Chris | def self.to_a; ARRAY end |
| 9519 | 909:cbb26bc654de | Chris | def self.to_s; STRING end |
| 9520 | 0:513646585e45 | Chris | end |
| 9521 | end |
||
| 9522 | 119:8661b858af72 | Chris | # Redmine - project management software |
| 9523 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9524 | 119:8661b858af72 | Chris | # |
| 9525 | # This program is free software; you can redistribute it and/or |
||
| 9526 | # modify it under the terms of the GNU General Public License |
||
| 9527 | # as published by the Free Software Foundation; either version 2 |
||
| 9528 | # of the License, or (at your option) any later version. |
||
| 9529 | 909:cbb26bc654de | Chris | # |
| 9530 | 119:8661b858af72 | Chris | # This program is distributed in the hope that it will be useful, |
| 9531 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9532 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9533 | # GNU General Public License for more details. |
||
| 9534 | 909:cbb26bc654de | Chris | # |
| 9535 | 119:8661b858af72 | Chris | # You should have received a copy of the GNU General Public License |
| 9536 | # along with this program; if not, write to the Free Software |
||
| 9537 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9538 | |||
| 9539 | module Redmine |
||
| 9540 | module Views |
||
| 9541 | 1115:433d4f72a19b | Chris | class ApiTemplateHandler |
| 9542 | def self.call(template) |
||
| 9543 | "Redmine::Views::Builders.for(params[:format], request, response) do |api|; #{template.source}; self.output_buffer = api.output; end"
|
||
| 9544 | 119:8661b858af72 | Chris | end |
| 9545 | end |
||
| 9546 | end |
||
| 9547 | end |
||
| 9548 | # Redmine - project management software |
||
| 9549 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9550 | 119:8661b858af72 | Chris | # |
| 9551 | # This program is free software; you can redistribute it and/or |
||
| 9552 | # modify it under the terms of the GNU General Public License |
||
| 9553 | # as published by the Free Software Foundation; either version 2 |
||
| 9554 | # of the License, or (at your option) any later version. |
||
| 9555 | 909:cbb26bc654de | Chris | # |
| 9556 | 119:8661b858af72 | Chris | # This program is distributed in the hope that it will be useful, |
| 9557 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9558 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9559 | # GNU General Public License for more details. |
||
| 9560 | 909:cbb26bc654de | Chris | # |
| 9561 | 119:8661b858af72 | Chris | # You should have received a copy of the GNU General Public License |
| 9562 | # along with this program; if not, write to the Free Software |
||
| 9563 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9564 | |||
| 9565 | 1464:261b3d9a4903 | Chris | require 'redmine/views/builders/json' |
| 9566 | require 'redmine/views/builders/xml' |
||
| 9567 | |||
| 9568 | 119:8661b858af72 | Chris | module Redmine |
| 9569 | module Views |
||
| 9570 | module Builders |
||
| 9571 | 1115:433d4f72a19b | Chris | def self.for(format, request, response, &block) |
| 9572 | 119:8661b858af72 | Chris | builder = case format |
| 9573 | 1115:433d4f72a19b | Chris | when 'xml', :xml; Builders::Xml.new(request, response) |
| 9574 | when 'json', :json; Builders::Json.new(request, response) |
||
| 9575 | 119:8661b858af72 | Chris | else; raise "No builder for format #{format}"
|
| 9576 | end |
||
| 9577 | if block |
||
| 9578 | block.call(builder) |
||
| 9579 | else |
||
| 9580 | builder |
||
| 9581 | end |
||
| 9582 | end |
||
| 9583 | end |
||
| 9584 | end |
||
| 9585 | end |
||
| 9586 | # Redmine - project management software |
||
| 9587 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9588 | 119:8661b858af72 | Chris | # |
| 9589 | # This program is free software; you can redistribute it and/or |
||
| 9590 | # modify it under the terms of the GNU General Public License |
||
| 9591 | # as published by the Free Software Foundation; either version 2 |
||
| 9592 | # of the License, or (at your option) any later version. |
||
| 9593 | 909:cbb26bc654de | Chris | # |
| 9594 | 119:8661b858af72 | Chris | # This program is distributed in the hope that it will be useful, |
| 9595 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9596 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9597 | # GNU General Public License for more details. |
||
| 9598 | 909:cbb26bc654de | Chris | # |
| 9599 | 119:8661b858af72 | Chris | # You should have received a copy of the GNU General Public License |
| 9600 | # along with this program; if not, write to the Free Software |
||
| 9601 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9602 | |||
| 9603 | 1464:261b3d9a4903 | Chris | require 'redmine/views/builders/structure' |
| 9604 | 119:8661b858af72 | Chris | |
| 9605 | module Redmine |
||
| 9606 | module Views |
||
| 9607 | module Builders |
||
| 9608 | class Json < Structure |
||
| 9609 | 1115:433d4f72a19b | Chris | attr_accessor :jsonp |
| 9610 | |||
| 9611 | def initialize(request, response) |
||
| 9612 | super |
||
| 9613 | 1464:261b3d9a4903 | Chris | callback = request.params[:callback] || request.params[:jsonp] |
| 9614 | if callback && Setting.jsonp_enabled? |
||
| 9615 | self.jsonp = callback.to_s.gsub(/[^a-zA-Z0-9_]/, '') |
||
| 9616 | end |
||
| 9617 | 1115:433d4f72a19b | Chris | end |
| 9618 | |||
| 9619 | 119:8661b858af72 | Chris | def output |
| 9620 | 1115:433d4f72a19b | Chris | json = @struct.first.to_json |
| 9621 | if jsonp.present? |
||
| 9622 | json = "#{jsonp}(#{json})"
|
||
| 9623 | response.content_type = 'application/javascript' |
||
| 9624 | end |
||
| 9625 | json |
||
| 9626 | 119:8661b858af72 | Chris | end |
| 9627 | end |
||
| 9628 | end |
||
| 9629 | end |
||
| 9630 | end |
||
| 9631 | # Redmine - project management software |
||
| 9632 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9633 | 119:8661b858af72 | Chris | # |
| 9634 | # This program is free software; you can redistribute it and/or |
||
| 9635 | # modify it under the terms of the GNU General Public License |
||
| 9636 | # as published by the Free Software Foundation; either version 2 |
||
| 9637 | # of the License, or (at your option) any later version. |
||
| 9638 | 909:cbb26bc654de | Chris | # |
| 9639 | 119:8661b858af72 | Chris | # This program is distributed in the hope that it will be useful, |
| 9640 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9641 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9642 | # GNU General Public License for more details. |
||
| 9643 | 909:cbb26bc654de | Chris | # |
| 9644 | 119:8661b858af72 | Chris | # You should have received a copy of the GNU General Public License |
| 9645 | # along with this program; if not, write to the Free Software |
||
| 9646 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9647 | |||
| 9648 | require 'blankslate' |
||
| 9649 | |||
| 9650 | module Redmine |
||
| 9651 | module Views |
||
| 9652 | module Builders |
||
| 9653 | class Structure < BlankSlate |
||
| 9654 | 1115:433d4f72a19b | Chris | attr_accessor :request, :response |
| 9655 | |||
| 9656 | def initialize(request, response) |
||
| 9657 | 119:8661b858af72 | Chris | @struct = [{}]
|
| 9658 | 1115:433d4f72a19b | Chris | self.request = request |
| 9659 | self.response = response |
||
| 9660 | 119:8661b858af72 | Chris | end |
| 9661 | 909:cbb26bc654de | Chris | |
| 9662 | 119:8661b858af72 | Chris | def array(tag, options={}, &block)
|
| 9663 | @struct << [] |
||
| 9664 | block.call(self) |
||
| 9665 | ret = @struct.pop |
||
| 9666 | @struct.last[tag] = ret |
||
| 9667 | @struct.last.merge!(options) if options |
||
| 9668 | end |
||
| 9669 | 909:cbb26bc654de | Chris | |
| 9670 | 119:8661b858af72 | Chris | def method_missing(sym, *args, &block) |
| 9671 | if args.any? |
||
| 9672 | if args.first.is_a?(Hash) |
||
| 9673 | if @struct.last.is_a?(Array) |
||
| 9674 | @struct.last << args.first unless block |
||
| 9675 | else |
||
| 9676 | @struct.last[sym] = args.first |
||
| 9677 | end |
||
| 9678 | else |
||
| 9679 | if @struct.last.is_a?(Array) |
||
| 9680 | 1115:433d4f72a19b | Chris | if args.size == 1 && !block_given? |
| 9681 | @struct.last << args.first |
||
| 9682 | else |
||
| 9683 | @struct.last << (args.last || {}).merge(:value => args.first)
|
||
| 9684 | end |
||
| 9685 | 119:8661b858af72 | Chris | else |
| 9686 | @struct.last[sym] = args.first |
||
| 9687 | end |
||
| 9688 | end |
||
| 9689 | end |
||
| 9690 | 909:cbb26bc654de | Chris | |
| 9691 | 119:8661b858af72 | Chris | if block |
| 9692 | @struct << (args.first.is_a?(Hash) ? args.first : {})
|
||
| 9693 | block.call(self) |
||
| 9694 | ret = @struct.pop |
||
| 9695 | if @struct.last.is_a?(Array) |
||
| 9696 | @struct.last << ret |
||
| 9697 | else |
||
| 9698 | if @struct.last.has_key?(sym) && @struct.last[sym].is_a?(Hash) |
||
| 9699 | @struct.last[sym].merge! ret |
||
| 9700 | else |
||
| 9701 | @struct.last[sym] = ret |
||
| 9702 | end |
||
| 9703 | end |
||
| 9704 | end |
||
| 9705 | end |
||
| 9706 | 909:cbb26bc654de | Chris | |
| 9707 | 119:8661b858af72 | Chris | def output |
| 9708 | raise "Need to implement #{self.class.name}#output"
|
||
| 9709 | end |
||
| 9710 | end |
||
| 9711 | end |
||
| 9712 | end |
||
| 9713 | end |
||
| 9714 | # Redmine - project management software |
||
| 9715 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9716 | 119:8661b858af72 | Chris | # |
| 9717 | # This program is free software; you can redistribute it and/or |
||
| 9718 | # modify it under the terms of the GNU General Public License |
||
| 9719 | # as published by the Free Software Foundation; either version 2 |
||
| 9720 | # of the License, or (at your option) any later version. |
||
| 9721 | 909:cbb26bc654de | Chris | # |
| 9722 | 119:8661b858af72 | Chris | # This program is distributed in the hope that it will be useful, |
| 9723 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9724 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9725 | # GNU General Public License for more details. |
||
| 9726 | 909:cbb26bc654de | Chris | # |
| 9727 | 119:8661b858af72 | Chris | # You should have received a copy of the GNU General Public License |
| 9728 | # along with this program; if not, write to the Free Software |
||
| 9729 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9730 | |||
| 9731 | 1115:433d4f72a19b | Chris | require 'builder' |
| 9732 | |||
| 9733 | 119:8661b858af72 | Chris | module Redmine |
| 9734 | module Views |
||
| 9735 | module Builders |
||
| 9736 | class Xml < ::Builder::XmlMarkup |
||
| 9737 | 1115:433d4f72a19b | Chris | def initialize(request, response) |
| 9738 | super() |
||
| 9739 | 119:8661b858af72 | Chris | instruct! |
| 9740 | end |
||
| 9741 | 909:cbb26bc654de | Chris | |
| 9742 | 119:8661b858af72 | Chris | def output |
| 9743 | target! |
||
| 9744 | end |
||
| 9745 | 909:cbb26bc654de | Chris | |
| 9746 | 119:8661b858af72 | Chris | def method_missing(sym, *args, &block) |
| 9747 | 1115:433d4f72a19b | Chris | if args.size == 1 && args.first.is_a?(::Time) |
| 9748 | 119:8661b858af72 | Chris | __send__ sym, args.first.xmlschema, &block |
| 9749 | else |
||
| 9750 | super |
||
| 9751 | end |
||
| 9752 | end |
||
| 9753 | 909:cbb26bc654de | Chris | |
| 9754 | 119:8661b858af72 | Chris | def array(name, options={}, &block)
|
| 9755 | __send__ name, (options || {}).merge(:type => 'array'), &block
|
||
| 9756 | end |
||
| 9757 | end |
||
| 9758 | end |
||
| 9759 | end |
||
| 9760 | end |
||
| 9761 | 1115:433d4f72a19b | Chris | # Redmine - project management software |
| 9762 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9763 | 1115:433d4f72a19b | Chris | # |
| 9764 | # This program is free software; you can redistribute it and/or |
||
| 9765 | # modify it under the terms of the GNU General Public License |
||
| 9766 | # as published by the Free Software Foundation; either version 2 |
||
| 9767 | # of the License, or (at your option) any later version. |
||
| 9768 | # |
||
| 9769 | # This program is distributed in the hope that it will be useful, |
||
| 9770 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9771 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9772 | # GNU General Public License for more details. |
||
| 9773 | # |
||
| 9774 | # You should have received a copy of the GNU General Public License |
||
| 9775 | # along with this program; if not, write to the Free Software |
||
| 9776 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9777 | |||
| 9778 | require 'action_view/helpers/form_helper' |
||
| 9779 | |||
| 9780 | class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder |
||
| 9781 | include Redmine::I18n |
||
| 9782 | |||
| 9783 | 1517:dffacf8a6908 | Chris | (field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for check_box) + |
| 9784 | 1115:433d4f72a19b | Chris | %w(date_select)).each do |selector| |
| 9785 | src = <<-END_SRC |
||
| 9786 | def #{selector}(field, options = {})
|
||
| 9787 | label_for_field(field, options) + super(field, options.except(:label)).html_safe |
||
| 9788 | end |
||
| 9789 | END_SRC |
||
| 9790 | class_eval src, __FILE__, __LINE__ |
||
| 9791 | end |
||
| 9792 | |||
| 9793 | 1517:dffacf8a6908 | Chris | def check_box(field, options={}, checked_value="1", unchecked_value="0")
|
| 9794 | label_for_field(field, options) + super(field, options.except(:label), checked_value, unchecked_value).html_safe |
||
| 9795 | end |
||
| 9796 | |||
| 9797 | 1115:433d4f72a19b | Chris | def select(field, choices, options = {}, html_options = {})
|
| 9798 | label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe |
||
| 9799 | end |
||
| 9800 | |||
| 9801 | def time_zone_select(field, priority_zones = nil, options = {}, html_options = {})
|
||
| 9802 | label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe |
||
| 9803 | end |
||
| 9804 | |||
| 9805 | # Returns a label tag for the given field |
||
| 9806 | def label_for_field(field, options = {})
|
||
| 9807 | return ''.html_safe if options.delete(:no_label) |
||
| 9808 | text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label] |
||
| 9809 | text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
|
||
| 9810 | text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
|
||
| 9811 | @template.content_tag("label", text.html_safe,
|
||
| 9812 | :class => (@object && @object.errors[field].present? ? "error" : nil), |
||
| 9813 | :for => (@object_name.to_s + "_" + field.to_s)) |
||
| 9814 | end |
||
| 9815 | end |
||
| 9816 | 0:513646585e45 | Chris | # Redmine - project management software |
| 9817 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9818 | 0:513646585e45 | Chris | # |
| 9819 | # This program is free software; you can redistribute it and/or |
||
| 9820 | # modify it under the terms of the GNU General Public License |
||
| 9821 | # as published by the Free Software Foundation; either version 2 |
||
| 9822 | # of the License, or (at your option) any later version. |
||
| 9823 | 909:cbb26bc654de | Chris | # |
| 9824 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 9825 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9826 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9827 | # GNU General Public License for more details. |
||
| 9828 | 909:cbb26bc654de | Chris | # |
| 9829 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 9830 | # along with this program; if not, write to the Free Software |
||
| 9831 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9832 | |||
| 9833 | module Redmine |
||
| 9834 | module Views |
||
| 9835 | module MyPage |
||
| 9836 | module Block |
||
| 9837 | def self.additional_blocks |
||
| 9838 | 1115:433d4f72a19b | Chris | @@additional_blocks ||= Dir.glob("#{Redmine::Plugin.directory}/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
|
| 9839 | 0:513646585e45 | Chris | name = File.basename(file).split('.').first.gsub(/^_/, '')
|
| 9840 | h[name] = name.to_sym |
||
| 9841 | h |
||
| 9842 | end |
||
| 9843 | end |
||
| 9844 | end |
||
| 9845 | end |
||
| 9846 | end |
||
| 9847 | end |
||
| 9848 | # Redmine - project management software |
||
| 9849 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9850 | 0:513646585e45 | Chris | # |
| 9851 | # This program is free software; you can redistribute it and/or |
||
| 9852 | # modify it under the terms of the GNU General Public License |
||
| 9853 | # as published by the Free Software Foundation; either version 2 |
||
| 9854 | # of the License, or (at your option) any later version. |
||
| 9855 | 909:cbb26bc654de | Chris | # |
| 9856 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 9857 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9858 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9859 | # GNU General Public License for more details. |
||
| 9860 | 909:cbb26bc654de | Chris | # |
| 9861 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 9862 | # along with this program; if not, write to the Free Software |
||
| 9863 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9864 | |||
| 9865 | module Redmine |
||
| 9866 | module Views |
||
| 9867 | class OtherFormatsBuilder |
||
| 9868 | def initialize(view) |
||
| 9869 | @view = view |
||
| 9870 | end |
||
| 9871 | 909:cbb26bc654de | Chris | |
| 9872 | 0:513646585e45 | Chris | def link_to(name, options={})
|
| 9873 | 1115:433d4f72a19b | Chris | url = { :format => name.to_s.downcase }.merge(options.delete(:url) || {}).except('page')
|
| 9874 | 0:513646585e45 | Chris | caption = options.delete(:caption) || name |
| 9875 | html_options = { :class => name.to_s.downcase, :rel => 'nofollow' }.merge(options)
|
||
| 9876 | @view.content_tag('span', @view.link_to(caption, url, html_options))
|
||
| 9877 | end |
||
| 9878 | end |
||
| 9879 | end |
||
| 9880 | end |
||
| 9881 | # Redmine - project management software |
||
| 9882 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 9883 | 0:513646585e45 | Chris | # |
| 9884 | # This program is free software; you can redistribute it and/or |
||
| 9885 | # modify it under the terms of the GNU General Public License |
||
| 9886 | # as published by the Free Software Foundation; either version 2 |
||
| 9887 | # of the License, or (at your option) any later version. |
||
| 9888 | 909:cbb26bc654de | Chris | # |
| 9889 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 9890 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 9891 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 9892 | # GNU General Public License for more details. |
||
| 9893 | 909:cbb26bc654de | Chris | # |
| 9894 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 9895 | # along with this program; if not, write to the Free Software |
||
| 9896 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 9897 | |||
| 9898 | 1115:433d4f72a19b | Chris | require 'digest/md5' |
| 9899 | |||
| 9900 | 0:513646585e45 | Chris | module Redmine |
| 9901 | module WikiFormatting |
||
| 9902 | 909:cbb26bc654de | Chris | class StaleSectionError < Exception; end |
| 9903 | |||
| 9904 | 0:513646585e45 | Chris | @@formatters = {}
|
| 9905 | |||
| 9906 | class << self |
||
| 9907 | def map |
||
| 9908 | yield self |
||
| 9909 | end |
||
| 9910 | 909:cbb26bc654de | Chris | |
| 9911 | 1517:dffacf8a6908 | Chris | def register(name, formatter, helper, options={})
|
| 9912 | name = name.to_s |
||
| 9913 | raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
|
||
| 9914 | @@formatters[name] = {
|
||
| 9915 | :formatter => formatter, |
||
| 9916 | :helper => helper, |
||
| 9917 | :label => options[:label] || name.humanize |
||
| 9918 | } |
||
| 9919 | 0:513646585e45 | Chris | end |
| 9920 | 909:cbb26bc654de | Chris | |
| 9921 | def formatter |
||
| 9922 | formatter_for(Setting.text_formatting) |
||
| 9923 | end |
||
| 9924 | |||
| 9925 | 0:513646585e45 | Chris | def formatter_for(name) |
| 9926 | entry = @@formatters[name.to_s] |
||
| 9927 | (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter |
||
| 9928 | end |
||
| 9929 | 909:cbb26bc654de | Chris | |
| 9930 | 0:513646585e45 | Chris | def helper_for(name) |
| 9931 | entry = @@formatters[name.to_s] |
||
| 9932 | (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper |
||
| 9933 | end |
||
| 9934 | 909:cbb26bc654de | Chris | |
| 9935 | 0:513646585e45 | Chris | def format_names |
| 9936 | @@formatters.keys.map |
||
| 9937 | end |
||
| 9938 | 909:cbb26bc654de | Chris | |
| 9939 | 1517:dffacf8a6908 | Chris | def formats_for_select |
| 9940 | @@formatters.map {|name, options| [options[:label], name]}
|
||
| 9941 | end |
||
| 9942 | |||
| 9943 | 909:cbb26bc654de | Chris | def to_html(format, text, options = {})
|
| 9944 | 1115:433d4f72a19b | Chris | text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute]) |
| 9945 | 0:513646585e45 | Chris | # Text retrieved from the cache store may be frozen |
| 9946 | # We need to dup it so we can do in-place substitutions with gsub! |
||
| 9947 | cache_store.fetch cache_key do |
||
| 9948 | formatter_for(format).new(text).to_html |
||
| 9949 | end.dup |
||
| 9950 | else |
||
| 9951 | formatter_for(format).new(text).to_html |
||
| 9952 | end |
||
| 9953 | text |
||
| 9954 | end |
||
| 9955 | |||
| 9956 | 909:cbb26bc654de | Chris | # Returns true if the text formatter supports single section edit |
| 9957 | def supports_section_edit? |
||
| 9958 | (formatter.instance_methods & ['update_section', :update_section]).any? |
||
| 9959 | end |
||
| 9960 | |||
| 9961 | 1115:433d4f72a19b | Chris | # Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done |
| 9962 | def cache_key_for(format, text, object, attribute) |
||
| 9963 | if object && attribute && !object.new_record? && format.present? |
||
| 9964 | "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}"
|
||
| 9965 | 0:513646585e45 | Chris | end |
| 9966 | end |
||
| 9967 | 909:cbb26bc654de | Chris | |
| 9968 | 0:513646585e45 | Chris | # Returns the cache store used to cache HTML output |
| 9969 | def cache_store |
||
| 9970 | ActionController::Base.cache_store |
||
| 9971 | end |
||
| 9972 | end |
||
| 9973 | 909:cbb26bc654de | Chris | |
| 9974 | 1115:433d4f72a19b | Chris | module LinksHelper |
| 9975 | AUTO_LINK_RE = %r{
|
||
| 9976 | ( # leading text |
||
| 9977 | <\w+.*?>| # leading HTML tag, or |
||
| 9978 | 1464:261b3d9a4903 | Chris | [\s\(\[,;]| # leading punctuation, or |
| 9979 | 1115:433d4f72a19b | Chris | ^ # beginning of line |
| 9980 | ) |
||
| 9981 | ( |
||
| 9982 | (?:https?://)| # protocol spec, or |
||
| 9983 | (?:s?ftps?://)| |
||
| 9984 | (?:www\.) # www.* |
||
| 9985 | ) |
||
| 9986 | ( |
||
| 9987 | 1464:261b3d9a4903 | Chris | ([^<]\S*?) # url |
| 9988 | 1115:433d4f72a19b | Chris | (\/)? # slash |
| 9989 | ) |
||
| 9990 | ((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post |
||
| 9991 | (?=<|\s|$) |
||
| 9992 | }x unless const_defined?(:AUTO_LINK_RE) |
||
| 9993 | |||
| 9994 | # Destructively remplaces urls into clickable links |
||
| 9995 | def auto_link!(text) |
||
| 9996 | text.gsub!(AUTO_LINK_RE) do |
||
| 9997 | all, leading, proto, url, post = $&, $1, $2, $3, $6 |
||
| 9998 | if leading =~ /<a\s/i || leading =~ /![<>=]?/ |
||
| 9999 | # don't replace URL's that are already linked |
||
| 10000 | # and URL's prefixed with ! !> !< != (textile images) |
||
| 10001 | all |
||
| 10002 | else |
||
| 10003 | # Idea below : an URL with unbalanced parethesis and |
||
| 10004 | # ending by ')' is put into external parenthesis |
||
| 10005 | if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
|
||
| 10006 | url=url[0..-2] # discard closing parenth from url |
||
| 10007 | post = ")"+post # add closing parenth to post |
||
| 10008 | end |
||
| 10009 | content = proto + url |
||
| 10010 | href = "#{proto=="www."?"http://www.":proto}#{url}"
|
||
| 10011 | %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
|
||
| 10012 | end |
||
| 10013 | end |
||
| 10014 | end |
||
| 10015 | |||
| 10016 | # Destructively remplaces email addresses into clickable links |
||
| 10017 | def auto_mailto!(text) |
||
| 10018 | text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do |
||
| 10019 | mail = $1 |
||
| 10020 | if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
|
||
| 10021 | |||
| 10022 | else |
||
| 10023 | %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
|
||
| 10024 | end |
||
| 10025 | end |
||
| 10026 | end |
||
| 10027 | end |
||
| 10028 | |||
| 10029 | 0:513646585e45 | Chris | # Default formatter module |
| 10030 | module NullFormatter |
||
| 10031 | class Formatter |
||
| 10032 | include ActionView::Helpers::TagHelper |
||
| 10033 | include ActionView::Helpers::TextHelper |
||
| 10034 | include ActionView::Helpers::UrlHelper |
||
| 10035 | 1115:433d4f72a19b | Chris | include Redmine::WikiFormatting::LinksHelper |
| 10036 | 909:cbb26bc654de | Chris | |
| 10037 | 0:513646585e45 | Chris | def initialize(text) |
| 10038 | @text = text |
||
| 10039 | end |
||
| 10040 | 909:cbb26bc654de | Chris | |
| 10041 | 0:513646585e45 | Chris | def to_html(*args) |
| 10042 | 1115:433d4f72a19b | Chris | t = CGI::escapeHTML(@text) |
| 10043 | auto_link!(t) |
||
| 10044 | auto_mailto!(t) |
||
| 10045 | simple_format(t, {}, :sanitize => false)
|
||
| 10046 | 0:513646585e45 | Chris | end |
| 10047 | end |
||
| 10048 | 909:cbb26bc654de | Chris | |
| 10049 | 0:513646585e45 | Chris | module Helper |
| 10050 | def wikitoolbar_for(field_id) |
||
| 10051 | end |
||
| 10052 | 909:cbb26bc654de | Chris | |
| 10053 | 0:513646585e45 | Chris | def heads_for_wiki_formatter |
| 10054 | end |
||
| 10055 | 909:cbb26bc654de | Chris | |
| 10056 | 0:513646585e45 | Chris | def initial_page_content(page) |
| 10057 | page.pretty_title.to_s |
||
| 10058 | end |
||
| 10059 | end |
||
| 10060 | end |
||
| 10061 | end |
||
| 10062 | end |
||
| 10063 | 909:cbb26bc654de | Chris | # Redmine - project management software |
| 10064 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 10065 | 0:513646585e45 | Chris | # |
| 10066 | # This program is free software; you can redistribute it and/or |
||
| 10067 | # modify it under the terms of the GNU General Public License |
||
| 10068 | # as published by the Free Software Foundation; either version 2 |
||
| 10069 | # of the License, or (at your option) any later version. |
||
| 10070 | 909:cbb26bc654de | Chris | # |
| 10071 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 10072 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 10073 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 10074 | # GNU General Public License for more details. |
||
| 10075 | 909:cbb26bc654de | Chris | # |
| 10076 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 10077 | # along with this program; if not, write to the Free Software |
||
| 10078 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 10079 | |||
| 10080 | module Redmine |
||
| 10081 | module WikiFormatting |
||
| 10082 | module Macros |
||
| 10083 | module Definitions |
||
| 10084 | 1115:433d4f72a19b | Chris | # Returns true if +name+ is the name of an existing macro |
| 10085 | def macro_exists?(name) |
||
| 10086 | Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym) |
||
| 10087 | end |
||
| 10088 | |||
| 10089 | def exec_macro(name, obj, args, text) |
||
| 10090 | macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym] |
||
| 10091 | return unless macro_options |
||
| 10092 | |||
| 10093 | 0:513646585e45 | Chris | method_name = "macro_#{name}"
|
| 10094 | 1115:433d4f72a19b | Chris | unless macro_options[:parse_args] == false |
| 10095 | args = args.split(',').map(&:strip)
|
||
| 10096 | end |
||
| 10097 | |||
| 10098 | begin |
||
| 10099 | if self.class.instance_method(method_name).arity == 3 |
||
| 10100 | send(method_name, obj, args, text) |
||
| 10101 | elsif text |
||
| 10102 | raise "This macro does not accept a block of text" |
||
| 10103 | else |
||
| 10104 | send(method_name, obj, args) |
||
| 10105 | end |
||
| 10106 | rescue => e |
||
| 10107 | "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
|
||
| 10108 | end |
||
| 10109 | 0:513646585e45 | Chris | end |
| 10110 | 909:cbb26bc654de | Chris | |
| 10111 | 0:513646585e45 | Chris | def extract_macro_options(args, *keys) |
| 10112 | options = {}
|
||
| 10113 | 1115:433d4f72a19b | Chris | while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym)
|
| 10114 | 0:513646585e45 | Chris | options[$1.downcase.to_sym] = $2 |
| 10115 | args.pop |
||
| 10116 | end |
||
| 10117 | return [args, options] |
||
| 10118 | end |
||
| 10119 | end |
||
| 10120 | 909:cbb26bc654de | Chris | |
| 10121 | 0:513646585e45 | Chris | @@available_macros = {}
|
| 10122 | 1115:433d4f72a19b | Chris | mattr_accessor :available_macros |
| 10123 | 909:cbb26bc654de | Chris | |
| 10124 | 0:513646585e45 | Chris | class << self |
| 10125 | # Plugins can use this method to define new macros: |
||
| 10126 | 909:cbb26bc654de | Chris | # |
| 10127 | 0:513646585e45 | Chris | # Redmine::WikiFormatting::Macros.register do |
| 10128 | # desc "This is my macro" |
||
| 10129 | # macro :my_macro do |obj, args| |
||
| 10130 | # "My macro output" |
||
| 10131 | # end |
||
| 10132 | 1115:433d4f72a19b | Chris | # |
| 10133 | # desc "This is my macro that accepts a block of text" |
||
| 10134 | # macro :my_macro do |obj, args, text| |
||
| 10135 | # "My macro output" |
||
| 10136 | # end |
||
| 10137 | 0:513646585e45 | Chris | # end |
| 10138 | def register(&block) |
||
| 10139 | class_eval(&block) if block_given? |
||
| 10140 | end |
||
| 10141 | 909:cbb26bc654de | Chris | |
| 10142 | 1115:433d4f72a19b | Chris | # Defines a new macro with the given name, options and block. |
| 10143 | # |
||
| 10144 | # Options: |
||
| 10145 | # * :desc - A description of the macro |
||
| 10146 | # * :parse_args => false - Disables arguments parsing (the whole arguments |
||
| 10147 | # string is passed to the macro) |
||
| 10148 | # |
||
| 10149 | # Macro blocks accept 2 or 3 arguments: |
||
| 10150 | # * obj: the object that is rendered (eg. an Issue, a WikiContent...) |
||
| 10151 | # * args: macro arguments |
||
| 10152 | # * text: the block of text given to the macro (should be present only if the |
||
| 10153 | # macro accepts a block of text). text is a String or nil if the macro is |
||
| 10154 | # invoked without a block of text. |
||
| 10155 | # |
||
| 10156 | # Examples: |
||
| 10157 | # By default, when the macro is invoked, the coma separated list of arguments |
||
| 10158 | # is split and passed to the macro block as an array. If no argument is given |
||
| 10159 | # the macro will be invoked with an empty array: |
||
| 10160 | # |
||
| 10161 | # macro :my_macro do |obj, args| |
||
| 10162 | # # args is an array |
||
| 10163 | # # and this macro do not accept a block of text |
||
| 10164 | # end |
||
| 10165 | # |
||
| 10166 | # You can disable arguments spliting with the :parse_args => false option. In |
||
| 10167 | # this case, the full string of arguments is passed to the macro: |
||
| 10168 | # |
||
| 10169 | # macro :my_macro, :parse_args => false do |obj, args| |
||
| 10170 | # # args is a string |
||
| 10171 | # end |
||
| 10172 | # |
||
| 10173 | # Macro can optionally accept a block of text: |
||
| 10174 | # |
||
| 10175 | # macro :my_macro do |obj, args, text| |
||
| 10176 | # # this macro accepts a block of text |
||
| 10177 | # end |
||
| 10178 | # |
||
| 10179 | # Macros are invoked in formatted text using double curly brackets. Arguments |
||
| 10180 | # must be enclosed in parenthesis if any. A new line after the macro name or the |
||
| 10181 | # arguments starts the block of text that will be passe to the macro (invoking |
||
| 10182 | # a macro that do not accept a block of text with some text will fail). |
||
| 10183 | # Examples: |
||
| 10184 | # |
||
| 10185 | # No arguments: |
||
| 10186 | # {{my_macro}}
|
||
| 10187 | # |
||
| 10188 | # With arguments: |
||
| 10189 | # {{my_macro(arg1, arg2)}}
|
||
| 10190 | # |
||
| 10191 | # With a block of text: |
||
| 10192 | # {{my_macro
|
||
| 10193 | # multiple lines |
||
| 10194 | # of text |
||
| 10195 | # }} |
||
| 10196 | # |
||
| 10197 | # With arguments and a block of text |
||
| 10198 | # {{my_macro(arg1, arg2)
|
||
| 10199 | # multiple lines |
||
| 10200 | # of text |
||
| 10201 | # }} |
||
| 10202 | # |
||
| 10203 | # If a block of text is given, the closing tag }} must be at the start of a new line. |
||
| 10204 | def macro(name, options={}, &block)
|
||
| 10205 | options.assert_valid_keys(:desc, :parse_args) |
||
| 10206 | unless name.to_s.match(/\A\w+\z/) |
||
| 10207 | raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
|
||
| 10208 | end |
||
| 10209 | unless block_given? |
||
| 10210 | raise "Can not create a macro without a block!" |
||
| 10211 | end |
||
| 10212 | 1294:3e4c3460b6ca | Chris | name = name.to_s.downcase.to_sym |
| 10213 | 1115:433d4f72a19b | Chris | available_macros[name] = {:desc => @@desc || ''}.merge(options)
|
| 10214 | 0:513646585e45 | Chris | @@desc = nil |
| 10215 | 1294:3e4c3460b6ca | Chris | Definitions.send :define_method, "macro_#{name}", &block
|
| 10216 | 0:513646585e45 | Chris | end |
| 10217 | 909:cbb26bc654de | Chris | |
| 10218 | 0:513646585e45 | Chris | # Sets description for the next macro to be defined |
| 10219 | def desc(txt) |
||
| 10220 | @@desc = txt |
||
| 10221 | end |
||
| 10222 | end |
||
| 10223 | 909:cbb26bc654de | Chris | |
| 10224 | 0:513646585e45 | Chris | # Builtin macros |
| 10225 | desc "Sample macro." |
||
| 10226 | 1115:433d4f72a19b | Chris | macro :hello_world do |obj, args, text| |
| 10227 | h("Hello world! Object: #{obj.class.name}, " +
|
||
| 10228 | (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
|
||
| 10229 | " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
|
||
| 10230 | ) |
||
| 10231 | 0:513646585e45 | Chris | end |
| 10232 | 909:cbb26bc654de | Chris | |
| 10233 | 0:513646585e45 | Chris | desc "Displays a list of all available macros, including description if available." |
| 10234 | 909:cbb26bc654de | Chris | macro :macro_list do |obj, args| |
| 10235 | 1115:433d4f72a19b | Chris | out = ''.html_safe |
| 10236 | @@available_macros.each do |macro, options| |
||
| 10237 | out << content_tag('dt', content_tag('code', macro.to_s))
|
||
| 10238 | out << content_tag('dd', textilizable(options[:desc]))
|
||
| 10239 | 0:513646585e45 | Chris | end |
| 10240 | content_tag('dl', out)
|
||
| 10241 | end |
||
| 10242 | 909:cbb26bc654de | Chris | |
| 10243 | 0:513646585e45 | Chris | desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" + |
| 10244 | " !{{child_pages}} -- can be used from a wiki page only\n" +
|
||
| 10245 | 1115:433d4f72a19b | Chris | " !{{child_pages(depth=2)}} -- display 2 levels nesting only\n"
|
| 10246 | 0:513646585e45 | Chris | " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
|
| 10247 | " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
|
||
| 10248 | macro :child_pages do |obj, args| |
||
| 10249 | 1115:433d4f72a19b | Chris | args, options = extract_macro_options(args, :parent, :depth) |
| 10250 | options[:depth] = options[:depth].to_i if options[:depth].present? |
||
| 10251 | |||
| 10252 | 0:513646585e45 | Chris | page = nil |
| 10253 | if args.size > 0 |
||
| 10254 | page = Wiki.find_page(args.first.to_s, :project => @project) |
||
| 10255 | elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version) |
||
| 10256 | page = obj.page |
||
| 10257 | else |
||
| 10258 | raise 'With no argument, this macro can be called from wiki pages only.' |
||
| 10259 | end |
||
| 10260 | raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) |
||
| 10261 | 1115:433d4f72a19b | Chris | pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id) |
| 10262 | 0:513646585e45 | Chris | render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id) |
| 10263 | end |
||
| 10264 | 909:cbb26bc654de | Chris | |
| 10265 | 0:513646585e45 | Chris | desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
|
| 10266 | macro :include do |obj, args| |
||
| 10267 | page = Wiki.find_page(args.first.to_s, :project => @project) |
||
| 10268 | raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) |
||
| 10269 | @included_wiki_pages ||= [] |
||
| 10270 | raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) |
||
| 10271 | @included_wiki_pages << page.title |
||
| 10272 | 37:94944d00e43c | chris | out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false) |
| 10273 | 0:513646585e45 | Chris | @included_wiki_pages.pop |
| 10274 | out |
||
| 10275 | end |
||
| 10276 | 1115:433d4f72a19b | Chris | |
| 10277 | desc "Inserts of collapsed block of text. Example:\n\n {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}"
|
||
| 10278 | macro :collapse do |obj, args, text| |
||
| 10279 | html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
|
||
| 10280 | show_label = args[0] || l(:button_show) |
||
| 10281 | hide_label = args[1] || args[0] || l(:button_hide) |
||
| 10282 | js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
|
||
| 10283 | out = ''.html_safe |
||
| 10284 | out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
|
||
| 10285 | out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
|
||
| 10286 | 1517:dffacf8a6908 | Chris | out << content_tag('div', textilizable(text, :object => obj, :headings => false), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
|
| 10287 | 1115:433d4f72a19b | Chris | out |
| 10288 | end |
||
| 10289 | |||
| 10290 | desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
|
||
| 10291 | macro :thumbnail do |obj, args| |
||
| 10292 | args, options = extract_macro_options(args, :size, :title) |
||
| 10293 | filename = args.first |
||
| 10294 | raise 'Filename required' unless filename.present? |
||
| 10295 | size = options[:size] |
||
| 10296 | raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/) |
||
| 10297 | size = size.to_i |
||
| 10298 | size = nil unless size > 0 |
||
| 10299 | if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename) |
||
| 10300 | title = options[:title] || attachment.title |
||
| 10301 | 1517:dffacf8a6908 | Chris | thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => false) |
| 10302 | image_url = url_for(:controller => 'attachments', :action => 'show', :id => attachment, :only_path => false) |
||
| 10303 | |||
| 10304 | img = image_tag(thumbnail_url, :alt => attachment.filename) |
||
| 10305 | link_to(img, image_url, :class => 'thumbnail', :title => title) |
||
| 10306 | 1115:433d4f72a19b | Chris | else |
| 10307 | raise "Attachment #{filename} not found"
|
||
| 10308 | end |
||
| 10309 | end |
||
| 10310 | 0:513646585e45 | Chris | end |
| 10311 | end |
||
| 10312 | end |
||
| 10313 | 1517:dffacf8a6908 | Chris | # Redmine - project management software |
| 10314 | # Copyright (C) 2006-2014 Jean-Philippe Lang |
||
| 10315 | # |
||
| 10316 | # This program is free software; you can redistribute it and/or |
||
| 10317 | # modify it under the terms of the GNU General Public License |
||
| 10318 | # as published by the Free Software Foundation; either version 2 |
||
| 10319 | # of the License, or (at your option) any later version. |
||
| 10320 | # |
||
| 10321 | # This program is distributed in the hope that it will be useful, |
||
| 10322 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 10323 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 10324 | # GNU General Public License for more details. |
||
| 10325 | # |
||
| 10326 | # You should have received a copy of the GNU General Public License |
||
| 10327 | # along with this program; if not, write to the Free Software |
||
| 10328 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 10329 | |||
| 10330 | require 'cgi' |
||
| 10331 | |||
| 10332 | module Redmine |
||
| 10333 | module WikiFormatting |
||
| 10334 | module Markdown |
||
| 10335 | class HTML < Redcarpet::Render::HTML |
||
| 10336 | include ActionView::Helpers::TagHelper |
||
| 10337 | |||
| 10338 | def link(link, title, content) |
||
| 10339 | css = nil |
||
| 10340 | unless link && link.starts_with?('/')
|
||
| 10341 | css = 'external' |
||
| 10342 | end |
||
| 10343 | content_tag('a', content.to_s.html_safe, :href => link, :title => title, :class => css)
|
||
| 10344 | end |
||
| 10345 | |||
| 10346 | def block_code(code, language) |
||
| 10347 | if language.present? |
||
| 10348 | "<pre><code class=\"#{CGI.escapeHTML language} syntaxhl\">" +
|
||
| 10349 | Redmine::SyntaxHighlighting.highlight_by_language(code, language) + |
||
| 10350 | "</code></pre>" |
||
| 10351 | else |
||
| 10352 | "<pre>" + CGI.escapeHTML(code) + "</pre>" |
||
| 10353 | end |
||
| 10354 | end |
||
| 10355 | end |
||
| 10356 | |||
| 10357 | class Formatter |
||
| 10358 | def initialize(text) |
||
| 10359 | @text = text |
||
| 10360 | end |
||
| 10361 | |||
| 10362 | def to_html(*args) |
||
| 10363 | html = formatter.render(@text) |
||
| 10364 | # restore wiki links eg. [[Foo]] |
||
| 10365 | html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
|
||
| 10366 | "[[#{$2}]]"
|
||
| 10367 | end |
||
| 10368 | # restore Redmine links with double-quotes, eg. version:"1.0" |
||
| 10369 | html.gsub!(/(\w):"(.+?)"/) do |
||
| 10370 | "#{$1}:\"#{$2}\""
|
||
| 10371 | end |
||
| 10372 | html |
||
| 10373 | end |
||
| 10374 | |||
| 10375 | def get_section(index) |
||
| 10376 | section = extract_sections(index)[1] |
||
| 10377 | hash = Digest::MD5.hexdigest(section) |
||
| 10378 | return section, hash |
||
| 10379 | end |
||
| 10380 | |||
| 10381 | def update_section(index, update, hash=nil) |
||
| 10382 | t = extract_sections(index) |
||
| 10383 | if hash.present? && hash != Digest::MD5.hexdigest(t[1]) |
||
| 10384 | raise Redmine::WikiFormatting::StaleSectionError |
||
| 10385 | end |
||
| 10386 | t[1] = update unless t[1].blank? |
||
| 10387 | t.reject(&:blank?).join "\n\n" |
||
| 10388 | end |
||
| 10389 | |||
| 10390 | def extract_sections(index) |
||
| 10391 | sections = ['', '', ''] |
||
| 10392 | offset = 0 |
||
| 10393 | i = 0 |
||
| 10394 | l = 1 |
||
| 10395 | inside_pre = false |
||
| 10396 | @text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|~~~.*)\s*$)/).each do |part| |
||
| 10397 | level = nil |
||
| 10398 | if part =~ /\A~{3,}(\S+)?\s*$/
|
||
| 10399 | if $1 |
||
| 10400 | if !inside_pre |
||
| 10401 | inside_pre = true |
||
| 10402 | end |
||
| 10403 | else |
||
| 10404 | inside_pre = !inside_pre |
||
| 10405 | end |
||
| 10406 | elsif inside_pre |
||
| 10407 | # nop |
||
| 10408 | elsif part =~ /\A(#+).+/ |
||
| 10409 | level = $1.size |
||
| 10410 | elsif part =~ /\A.+\r?\n\r?(\=+|\-+)\s*$/ |
||
| 10411 | level = $1.include?('=') ? 1 : 2
|
||
| 10412 | end |
||
| 10413 | if level |
||
| 10414 | i += 1 |
||
| 10415 | if offset == 0 && i == index |
||
| 10416 | # entering the requested section |
||
| 10417 | offset = 1 |
||
| 10418 | l = level |
||
| 10419 | elsif offset == 1 && i > index && level <= l |
||
| 10420 | # leaving the requested section |
||
| 10421 | offset = 2 |
||
| 10422 | end |
||
| 10423 | end |
||
| 10424 | sections[offset] << part |
||
| 10425 | end |
||
| 10426 | sections.map(&:strip) |
||
| 10427 | end |
||
| 10428 | |||
| 10429 | private |
||
| 10430 | |||
| 10431 | def formatter |
||
| 10432 | @@formatter ||= Redcarpet::Markdown.new( |
||
| 10433 | Redmine::WikiFormatting::Markdown::HTML.new( |
||
| 10434 | :filter_html => true, |
||
| 10435 | :hard_wrap => true |
||
| 10436 | ), |
||
| 10437 | :autolink => true, |
||
| 10438 | :fenced_code_blocks => true, |
||
| 10439 | :space_after_headers => true, |
||
| 10440 | :tables => true, |
||
| 10441 | :strikethrough => true, |
||
| 10442 | :superscript => true, |
||
| 10443 | :no_intra_emphasis => true |
||
| 10444 | ) |
||
| 10445 | end |
||
| 10446 | end |
||
| 10447 | end |
||
| 10448 | end |
||
| 10449 | end |
||
| 10450 | # Redmine - project management software |
||
| 10451 | # Copyright (C) 2006-2014 Jean-Philippe Lang |
||
| 10452 | # |
||
| 10453 | # This program is free software; you can redistribute it and/or |
||
| 10454 | # modify it under the terms of the GNU General Public License |
||
| 10455 | # as published by the Free Software Foundation; either version 2 |
||
| 10456 | # of the License, or (at your option) any later version. |
||
| 10457 | # |
||
| 10458 | # This program is distributed in the hope that it will be useful, |
||
| 10459 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 10460 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 10461 | # GNU General Public License for more details. |
||
| 10462 | # |
||
| 10463 | # You should have received a copy of the GNU General Public License |
||
| 10464 | # along with this program; if not, write to the Free Software |
||
| 10465 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 10466 | |||
| 10467 | module Redmine |
||
| 10468 | module WikiFormatting |
||
| 10469 | module Markdown |
||
| 10470 | module Helper |
||
| 10471 | def wikitoolbar_for(field_id) |
||
| 10472 | heads_for_wiki_formatter |
||
| 10473 | javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.draw();")
|
||
| 10474 | end |
||
| 10475 | |||
| 10476 | def initial_page_content(page) |
||
| 10477 | "# #{@page.pretty_title}"
|
||
| 10478 | end |
||
| 10479 | |||
| 10480 | def heads_for_wiki_formatter |
||
| 10481 | unless @heads_for_wiki_formatter_included |
||
| 10482 | content_for :header_tags do |
||
| 10483 | javascript_include_tag('jstoolbar/jstoolbar') +
|
||
| 10484 | javascript_include_tag('jstoolbar/markdown') +
|
||
| 10485 | javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
|
||
| 10486 | stylesheet_link_tag('jstoolbar')
|
||
| 10487 | end |
||
| 10488 | @heads_for_wiki_formatter_included = true |
||
| 10489 | end |
||
| 10490 | end |
||
| 10491 | end |
||
| 10492 | end |
||
| 10493 | end |
||
| 10494 | end |
||
| 10495 | 0:513646585e45 | Chris | # Redmine - project management software |
| 10496 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 10497 | 0:513646585e45 | Chris | # |
| 10498 | # This program is free software; you can redistribute it and/or |
||
| 10499 | # modify it under the terms of the GNU General Public License |
||
| 10500 | # as published by the Free Software Foundation; either version 2 |
||
| 10501 | # of the License, or (at your option) any later version. |
||
| 10502 | 909:cbb26bc654de | Chris | # |
| 10503 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 10504 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 10505 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 10506 | # GNU General Public License for more details. |
||
| 10507 | 909:cbb26bc654de | Chris | # |
| 10508 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 10509 | # along with this program; if not, write to the Free Software |
||
| 10510 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 10511 | |||
| 10512 | require 'redcloth3' |
||
| 10513 | 909:cbb26bc654de | Chris | require 'digest/md5' |
| 10514 | 0:513646585e45 | Chris | |
| 10515 | module Redmine |
||
| 10516 | module WikiFormatting |
||
| 10517 | module Textile |
||
| 10518 | class Formatter < RedCloth3 |
||
| 10519 | include ActionView::Helpers::TagHelper |
||
| 10520 | 1115:433d4f72a19b | Chris | include Redmine::WikiFormatting::LinksHelper |
| 10521 | |||
| 10522 | alias :inline_auto_link :auto_link! |
||
| 10523 | alias :inline_auto_mailto :auto_mailto! |
||
| 10524 | 909:cbb26bc654de | Chris | |
| 10525 | 0:513646585e45 | Chris | # auto_link rule after textile rules so that it doesn't break !image_url! tags |
| 10526 | 37:94944d00e43c | chris | RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] |
| 10527 | 909:cbb26bc654de | Chris | |
| 10528 | 0:513646585e45 | Chris | def initialize(*args) |
| 10529 | super |
||
| 10530 | self.hard_breaks=true |
||
| 10531 | self.no_span_caps=true |
||
| 10532 | 1115:433d4f72a19b | Chris | self.filter_styles=false |
| 10533 | 0:513646585e45 | Chris | end |
| 10534 | 909:cbb26bc654de | Chris | |
| 10535 | 0:513646585e45 | Chris | def to_html(*rules) |
| 10536 | @toc = [] |
||
| 10537 | super(*RULES).to_s |
||
| 10538 | end |
||
| 10539 | 909:cbb26bc654de | Chris | |
| 10540 | def get_section(index) |
||
| 10541 | section = extract_sections(index)[1] |
||
| 10542 | hash = Digest::MD5.hexdigest(section) |
||
| 10543 | return section, hash |
||
| 10544 | end |
||
| 10545 | |||
| 10546 | def update_section(index, update, hash=nil) |
||
| 10547 | t = extract_sections(index) |
||
| 10548 | if hash.present? && hash != Digest::MD5.hexdigest(t[1]) |
||
| 10549 | raise Redmine::WikiFormatting::StaleSectionError |
||
| 10550 | end |
||
| 10551 | t[1] = update unless t[1].blank? |
||
| 10552 | t.reject(&:blank?).join "\n\n" |
||
| 10553 | end |
||
| 10554 | |||
| 10555 | def extract_sections(index) |
||
| 10556 | @pre_list = [] |
||
| 10557 | text = self.dup |
||
| 10558 | rip_offtags text, false, false |
||
| 10559 | before = '' |
||
| 10560 | s = '' |
||
| 10561 | after = '' |
||
| 10562 | i = 0 |
||
| 10563 | l = 1 |
||
| 10564 | started = false |
||
| 10565 | ended = false |
||
| 10566 | 1294:3e4c3460b6ca | Chris | text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level|
|
| 10567 | 909:cbb26bc654de | Chris | if heading.nil? |
| 10568 | if ended |
||
| 10569 | after << all |
||
| 10570 | elsif started |
||
| 10571 | s << all |
||
| 10572 | else |
||
| 10573 | before << all |
||
| 10574 | end |
||
| 10575 | break |
||
| 10576 | end |
||
| 10577 | i += 1 |
||
| 10578 | if ended |
||
| 10579 | after << all |
||
| 10580 | elsif i == index |
||
| 10581 | l = level.to_i |
||
| 10582 | before << content |
||
| 10583 | s << heading |
||
| 10584 | started = true |
||
| 10585 | elsif i > index |
||
| 10586 | s << content |
||
| 10587 | if level.to_i > l |
||
| 10588 | s << heading |
||
| 10589 | else |
||
| 10590 | after << heading |
||
| 10591 | ended = true |
||
| 10592 | end |
||
| 10593 | else |
||
| 10594 | before << all |
||
| 10595 | end |
||
| 10596 | end |
||
| 10597 | sections = [before.strip, s.strip, after.strip] |
||
| 10598 | sections.each {|section| smooth_offtags_without_code_highlighting section}
|
||
| 10599 | sections |
||
| 10600 | end |
||
| 10601 | |||
| 10602 | 0:513646585e45 | Chris | private |
| 10603 | 909:cbb26bc654de | Chris | |
| 10604 | 0:513646585e45 | Chris | # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. |
| 10605 | # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> |
||
| 10606 | 909:cbb26bc654de | Chris | def hard_break( text ) |
| 10607 | 441:cbce1fd3b1b7 | Chris | text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
|
| 10608 | 0:513646585e45 | Chris | end |
| 10609 | 909:cbb26bc654de | Chris | |
| 10610 | alias :smooth_offtags_without_code_highlighting :smooth_offtags |
||
| 10611 | 0:513646585e45 | Chris | # Patch to add code highlighting support to RedCloth |
| 10612 | def smooth_offtags( text ) |
||
| 10613 | unless @pre_list.empty? |
||
| 10614 | ## replace <pre> content |
||
| 10615 | text.gsub!(/<redpre#(\d+)>/) do |
||
| 10616 | content = @pre_list[$1.to_i] |
||
| 10617 | if content.match(/<code\s+class="(\w+)">\s?(.+)/m) |
||
| 10618 | 909:cbb26bc654de | Chris | content = "<code class=\"#{$1} syntaxhl\">" +
|
| 10619 | 0:513646585e45 | Chris | Redmine::SyntaxHighlighting.highlight_by_language($2, $1) |
| 10620 | end |
||
| 10621 | content |
||
| 10622 | end |
||
| 10623 | end |
||
| 10624 | end |
||
| 10625 | end |
||
| 10626 | end |
||
| 10627 | end |
||
| 10628 | end |
||
| 10629 | # Redmine - project management software |
||
| 10630 | 1494:e248c7af89ec | Chris | # Copyright (C) 2006-2014 Jean-Philippe Lang |
| 10631 | 0:513646585e45 | Chris | # |
| 10632 | # This program is free software; you can redistribute it and/or |
||
| 10633 | # modify it under the terms of the GNU General Public License |
||
| 10634 | # as published by the Free Software Foundation; either version 2 |
||
| 10635 | # of the License, or (at your option) any later version. |
||
| 10636 | 909:cbb26bc654de | Chris | # |
| 10637 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful, |
| 10638 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 10639 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 10640 | # GNU General Public License for more details. |
||
| 10641 | 909:cbb26bc654de | Chris | # |
| 10642 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License |
| 10643 | # along with this program; if not, write to the Free Software |
||
| 10644 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 10645 | |||
| 10646 | module Redmine |
||
| 10647 | module WikiFormatting |
||
| 10648 | module Textile |
||
| 10649 | module Helper |
||
| 10650 | def wikitoolbar_for(field_id) |
||
| 10651 | 441:cbce1fd3b1b7 | Chris | heads_for_wiki_formatter |
| 10652 | 0:513646585e45 | Chris | # Is there a simple way to link to a public resource? |
| 10653 | 1464:261b3d9a4903 | Chris | url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax.html"
|
| 10654 | javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();")
|
||
| 10655 | 0:513646585e45 | Chris | end |
| 10656 | 909:cbb26bc654de | Chris | |
| 10657 | 0:513646585e45 | Chris | def initial_page_content(page) |
| 10658 | "h1. #{@page.pretty_title}"
|
||
| 10659 | end |
||
| 10660 | 909:cbb26bc654de | Chris | |
| 10661 | 0:513646585e45 | Chris | def heads_for_wiki_formatter |
| 10662 | 441:cbce1fd3b1b7 | Chris | unless @heads_for_wiki_formatter_included |
| 10663 | content_for :header_tags do |
||
| 10664 | 1115:433d4f72a19b | Chris | javascript_include_tag('jstoolbar/jstoolbar-textile.min') +
|
| 10665 | 441:cbce1fd3b1b7 | Chris | javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
|
| 10666 | stylesheet_link_tag('jstoolbar')
|
||
| 10667 | end |
||
| 10668 | @heads_for_wiki_formatter_included = true |
||
| 10669 | end |
||
| 10670 | 0:513646585e45 | Chris | end |
| 10671 | end |
||
| 10672 | end |
||
| 10673 | end |
||
| 10674 | end |