Revision 1297:0a574315af3e .svn/pristine/bc

View differences:

.svn/pristine/bc/bc30ce4931d78e6c793accaf2d15ef5a8dea2204.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require 'cgi'
19

  
20
module Redmine
21
  module Scm
22
    module Adapters
23
      class CommandFailed < StandardError #:nodoc:
24
      end
25

  
26
      class AbstractAdapter #:nodoc:
27

  
28
        # raised if scm command exited with error, e.g. unknown revision.
29
        class ScmCommandAborted < CommandFailed; end
30

  
31
        class << self
32
          def client_command
33
            ""
34
          end
35

  
36
          def shell_quote_command
37
            if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
38
              client_command
39
            else
40
              shell_quote(client_command)
41
            end
42
          end
43

  
44
          # Returns the version of the scm client
45
          # Eg: [1, 5, 0] or [] if unknown
46
          def client_version
47
            []
48
          end
49

  
50
          # Returns the version string of the scm client
51
          # Eg: '1.5.0' or 'Unknown version' if unknown
52
          def client_version_string
53
            v = client_version || 'Unknown version'
54
            v.is_a?(Array) ? v.join('.') : v.to_s
55
          end
56

  
57
          # Returns true if the current client version is above
58
          # or equals the given one
59
          # If option is :unknown is set to true, it will return
60
          # true if the client version is unknown
61
          def client_version_above?(v, options={})
62
            ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
63
          end
64

  
65
          def client_available
66
            true
67
          end
68

  
69
          def shell_quote(str)
70
            if Redmine::Platform.mswin?
71
              '"' + str.gsub(/"/, '\\"') + '"'
72
            else
73
              "'" + str.gsub(/'/, "'\"'\"'") + "'"
74
            end
75
          end
76
        end
77

  
78
        def initialize(url, root_url=nil, login=nil, password=nil,
79
                       path_encoding=nil)
80
          @url = url
81
          @login = login if login && !login.empty?
82
          @password = (password || "") if @login
83
          @root_url = root_url.blank? ? retrieve_root_url : root_url
84
        end
85

  
86
        def adapter_name
87
          'Abstract'
88
        end
89

  
90
        def supports_cat?
91
          true
92
        end
93

  
94
        def supports_annotate?
95
          respond_to?('annotate')
96
        end
97

  
98
        def root_url
99
          @root_url
100
        end
101

  
102
        def url
103
          @url
104
        end
105

  
106
        def path_encoding
107
          nil
108
        end
109

  
110
        # get info about the svn repository
111
        def info
112
          return nil
113
        end
114

  
115
        # Returns the entry identified by path and revision identifier
116
        # or nil if entry doesn't exist in the repository
117
        def entry(path=nil, identifier=nil)
118
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
119
          search_path = parts[0..-2].join('/')
120
          search_name = parts[-1]
121
          if search_path.blank? && search_name.blank?
122
            # Root entry
123
            Entry.new(:path => '', :kind => 'dir')
124
          else
125
            # Search for the entry in the parent directory
126
            es = entries(search_path, identifier)
127
            es ? es.detect {|e| e.name == search_name} : nil
128
          end
129
        end
130

  
131
        # Returns an Entries collection
132
        # or nil if the given path doesn't exist in the repository
133
        def entries(path=nil, identifier=nil, options={})
134
          return nil
135
        end
136

  
137
        def branches
138
          return nil
139
        end
140

  
141
        def tags
142
          return nil
143
        end
144

  
145
        def default_branch
146
          return nil
147
        end
148

  
149
        def properties(path, identifier=nil)
150
          return nil
151
        end
152

  
153
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
154
          return nil
155
        end
156

  
157
        def diff(path, identifier_from, identifier_to=nil)
158
          return nil
159
        end
160

  
161
        def cat(path, identifier=nil)
162
          return nil
163
        end
164

  
165
        def with_leading_slash(path)
166
          path ||= ''
167
          (path[0,1]!="/") ? "/#{path}" : path
168
        end
169

  
170
        def with_trailling_slash(path)
171
          path ||= ''
172
          (path[-1,1] == "/") ? path : "#{path}/"
173
        end
174

  
175
        def without_leading_slash(path)
176
          path ||= ''
177
          path.gsub(%r{^/+}, '')
178
        end
179

  
180
        def without_trailling_slash(path)
181
          path ||= ''
182
          (path[-1,1] == "/") ? path[0..-2] : path
183
         end
184

  
185
        def shell_quote(str)
186
          self.class.shell_quote(str)
187
        end
188

  
189
      private
190
        def retrieve_root_url
191
          info = self.info
192
          info ? info.root_url : nil
193
        end
194

  
195
        def target(path, sq=true)
196
          path ||= ''
197
          base = path.match(/^\//) ? root_url : url
198
          str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
199
          if sq
200
            str = shell_quote(str)
201
          end
202
          str
203
        end
204

  
205
        def logger
206
          self.class.logger
207
        end
208

  
209
        def shellout(cmd, options = {}, &block)
210
          self.class.shellout(cmd, options, &block)
211
        end
212

  
213
        def self.logger
214
          Rails.logger
215
        end
216

  
217
        def self.shellout(cmd, options = {}, &block)
218
          if logger && logger.debug?
219
            logger.debug "Shelling out: #{strip_credential(cmd)}"
220
          end
221
          if Rails.env == 'development'
222
            # Capture stderr when running in dev environment
223
            cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
224
          end
225
          begin
226
            mode = "r+"
227
            IO.popen(cmd, mode) do |io|
228
              io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
229
              io.close_write unless options[:write_stdin]
230
              block.call(io) if block_given?
231
            end
232
          ## If scm command does not exist,
233
          ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
234
          ## in production environment.
235
          # rescue Errno::ENOENT => e
236
          rescue Exception => e
237
            msg = strip_credential(e.message)
238
            # The command failed, log it and re-raise
239
            logmsg = "SCM command failed, "
240
            logmsg += "make sure that your SCM command (e.g. svn) is "
241
            logmsg += "in PATH (#{ENV['PATH']})\n"
242
            logmsg += "You can configure your scm commands in config/configuration.yml.\n"
243
            logmsg += "#{strip_credential(cmd)}\n"
244
            logmsg += "with: #{msg}"
245
            logger.error(logmsg)
246
            raise CommandFailed.new(msg)
247
          end
248
        end
249

  
250
        # Hides username/password in a given command
251
        def self.strip_credential(cmd)
252
          q = (Redmine::Platform.mswin? ? '"' : "'")
253
          cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
254
        end
255

  
256
        def strip_credential(cmd)
257
          self.class.strip_credential(cmd)
258
        end
259

  
260
        def scm_iconv(to, from, str)
261
          return nil if str.nil?
262
          return str if to == from
263
          if str.respond_to?(:force_encoding)
264
            str.force_encoding(from)
265
            begin
266
              str.encode(to)
267
            rescue Exception => err
268
              logger.error("failed to convert from #{from} to #{to}. #{err}")
269
              nil
270
            end
271
          else
272
            begin
273
              Iconv.conv(to, from, str)
274
            rescue Iconv::Failure => err
275
              logger.error("failed to convert from #{from} to #{to}. #{err}")
276
              nil
277
            end
278
          end
279
        end
280

  
281
        def parse_xml(xml)
282
          if RUBY_PLATFORM == 'java'
283
            xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
284
          end
285
          ActiveSupport::XmlMini.parse(xml)
286
        end
287
      end
288

  
289
      class Entries < Array
290
        def sort_by_name
291
          dup.sort! {|x,y|
292
            if x.kind == y.kind
293
              x.name.to_s <=> y.name.to_s
294
            else
295
              x.kind <=> y.kind
296
            end
297
          }
298
        end
299

  
300
        def revisions
301
          revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
302
        end
303
      end
304

  
305
      class Info
306
        attr_accessor :root_url, :lastrev
307
        def initialize(attributes={})
308
          self.root_url = attributes[:root_url] if attributes[:root_url]
309
          self.lastrev = attributes[:lastrev]
310
        end
311
      end
312

  
313
      class Entry
314
        attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
315

  
316
        def initialize(attributes={})
317
          self.name = attributes[:name] if attributes[:name]
318
          self.path = attributes[:path] if attributes[:path]
319
          self.kind = attributes[:kind] if attributes[:kind]
320
          self.size = attributes[:size].to_i if attributes[:size]
321
          self.lastrev = attributes[:lastrev]
322
        end
323

  
324
        def is_file?
325
          'file' == self.kind
326
        end
327

  
328
        def is_dir?
329
          'dir' == self.kind
330
        end
331

  
332
        def is_text?
333
          Redmine::MimeType.is_type?('text', name)
334
        end
335

  
336
        def author
337
          if changeset
338
            changeset.author.to_s
339
          elsif lastrev
340
            Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
341
          end
342
        end
343
      end
344

  
345
      class Revisions < Array
346
        def latest
347
          sort {|x,y|
348
            unless x.time.nil? or y.time.nil?
349
              x.time <=> y.time
350
            else
351
              0
352
            end
353
          }.last
354
        end
355
      end
356

  
357
      class Revision
358
        attr_accessor :scmid, :name, :author, :time, :message,
359
                      :paths, :revision, :branch, :identifier,
360
                      :parents
361

  
362
        def initialize(attributes={})
363
          self.identifier = attributes[:identifier]
364
          self.scmid      = attributes[:scmid]
365
          self.name       = attributes[:name] || self.identifier
366
          self.author     = attributes[:author]
367
          self.time       = attributes[:time]
368
          self.message    = attributes[:message] || ""
369
          self.paths      = attributes[:paths]
370
          self.revision   = attributes[:revision]
371
          self.branch     = attributes[:branch]
372
          self.parents    = attributes[:parents]
373
        end
374

  
375
        # Returns the readable identifier.
376
        def format_identifier
377
          self.identifier.to_s
378
        end
379

  
380
        def ==(other)
381
          if other.nil?
382
            false
383
          elsif scmid.present?
384
            scmid == other.scmid
385
          elsif identifier.present?
386
            identifier == other.identifier
387
          elsif revision.present?
388
            revision == other.revision
389
          end
390
        end
391
      end
392

  
393
      class Annotate
394
        attr_reader :lines, :revisions
395

  
396
        def initialize
397
          @lines = []
398
          @revisions = []
399
        end
400

  
401
        def add_line(line, revision)
402
          @lines << line
403
          @revisions << revision
404
        end
405

  
406
        def content
407
          content = lines.join("\n")
408
        end
409

  
410
        def empty?
411
          lines.empty?
412
        end
413
      end
414

  
415
      class Branch < String
416
        attr_accessor :revision, :scmid
417
      end
418
    end
419
  end
420
end
.svn/pristine/bc/bc5154e6e901520f6590eecc5a9c5e2a04a8c2f1.svn-base
1
/* Turkish initialisation for the jQuery UI date picker plugin. */
2
/* Written by Izzet Emre Erkan (kara@karalamalar.net). */
3
jQuery(function($){
4
	$.datepicker.regional['tr'] = {
5
		closeText: 'kapat',
6
		prevText: '&#x3c;geri',
7
		nextText: 'ileri&#x3e',
8
		currentText: 'bugün',
9
		monthNames: ['Ocak','Şubat','Mart','Nisan','Mayıs','Haziran',
10
		'Temmuz','Ağustos','Eylül','Ekim','Kasım','Aralık'],
11
		monthNamesShort: ['Oca','Åžub','Mar','Nis','May','Haz',
12
		'Tem','AÄŸu','Eyl','Eki','Kas','Ara'],
13
		dayNames: ['Pazar','Pazartesi','Salı','Çarşamba','Perşembe','Cuma','Cumartesi'],
14
		dayNamesShort: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'],
15
		dayNamesMin: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'],
16
		weekHeader: 'Hf',
17
		dateFormat: 'dd.mm.yy',
18
		firstDay: 1,
19
		isRTL: false,
20
		showMonthAfterYear: false,
21
		yearSuffix: ''};
22
	$.datepicker.setDefaults($.datepicker.regional['tr']);
23
});
.svn/pristine/bc/bc8bccaed2502b05fe9e07abbe378d2850bf83c3.svn-base
1
desc "Run the Continous Integration tests for Redmine"
2
task :ci do
3
  # RAILS_ENV and ENV[] can diverge so force them both to test
4
  ENV['RAILS_ENV'] = 'test'
5
  RAILS_ENV = 'test'
6
  Rake::Task["ci:setup"].invoke
7
  Rake::Task["ci:build"].invoke
8
  Rake::Task["ci:teardown"].invoke
9
end
10

  
11
# Tasks can be hooked into by redefining them in a plugin
12
namespace :ci do
13
  desc "Setup Redmine for a new build."
14
  task :setup do
15
    Rake::Task["ci:dump_environment"].invoke
16
    Rake::Task["db:create"].invoke
17
    Rake::Task["db:migrate"].invoke
18
    Rake::Task["db:schema:dump"].invoke
19
    Rake::Task["test:scm:update"].invoke
20
  end
21

  
22
  desc "Build Redmine"
23
  task :build do
24
    Rake::Task["test"].invoke
25
  end
26

  
27
  # Use this to cleanup after building or run post-build analysis.
28
  desc "Finish the build"
29
  task :teardown do
30
  end
31

  
32
  desc "Creates and configures the databases for the CI server"
33
  task :database do
34
    path = 'config/database.yml'
35
    unless File.exists?(path)
36
      database = ENV['DATABASE_ADAPTER']
37
      ruby = ENV['RUBY_VER'].gsub('.', '').gsub('-', '')
38
      branch = ENV['BRANCH'].gsub('.', '').gsub('-', '')
39
      dev_db_name = "ci_#{branch}_#{ruby}_dev"
40
      test_db_name = "ci_#{branch}_#{ruby}_test"
41

  
42
      case database
43
      when 'mysql'
44
        raise "Error creating databases" unless
45
          system(%|mysql -u jenkins --password=jenkins -e 'create database #{dev_db_name} character set utf8;'|) &&
46
          system(%|mysql -u jenkins --password=jenkins -e 'create database #{test_db_name} character set utf8;'|)
47
        dev_conf =  { 'adapter' => (RUBY_VERSION >= '1.9' ? 'mysql2' : 'mysql'), 'database' => dev_db_name, 'host' => 'localhost', 'username' => 'jenkins', 'password' => 'jenkins', 'encoding' => 'utf8' }
48
        test_conf = { 'adapter' => (RUBY_VERSION >= '1.9' ? 'mysql2' : 'mysql'), 'database' => test_db_name, 'host' => 'localhost', 'username' => 'jenkins', 'password' => 'jenkins', 'encoding' => 'utf8' }
49
      when 'postgresql'
50
        raise "Error creating databases" unless
51
          system(%|psql -U jenkins -d postgres -c "create database #{dev_db_name} owner jenkins encoding 'UTF8';"|) &&
52
          system(%|psql -U jenkins -d postgres -c "create database #{test_db_name} owner jenkins encoding 'UTF8';"|)
53
        dev_conf =  { 'adapter' => 'postgresql', 'database' => dev_db_name, 'host' => 'localhost', 'username' => 'jenkins', 'password' => 'jenkins' }
54
        test_conf = { 'adapter' => 'postgresql', 'database' => test_db_name, 'host' => 'localhost', 'username' => 'jenkins', 'password' => 'jenkins' }
55
      when 'sqlite3'
56
        dev_conf =  { 'adapter' => 'sqlite3', 'database' => "db/#{dev_db_name}.sqlite3" }
57
        test_conf = { 'adapter' => 'sqlite3', 'database' => "db/#{test_db_name}.sqlite3" }
58
      else
59
        raise "Unknown database"
60
      end
61

  
62
      File.open(path, 'w') do |f|
63
        f.write YAML.dump({'development' => dev_conf, 'test' => test_conf})
64
      end
65
    end
66
  end
67

  
68
  desc "Dump the environment information to a BUILD_ENVIRONMENT ENV variable for debugging"
69
  task :dump_environment do
70

  
71
    ENV['BUILD_ENVIRONMENT'] = ['ruby -v', 'gem -v', 'gem list'].collect do |command|
72
      result = `#{command}`
73
      "$ #{command}\n#{result}"
74
    end.join("\n")
75

  
76
  end
77
end
78

  
.svn/pristine/bc/bcc554e7744ab29cc1c8ca57d857bed00d539a54.svn-base
1
<div class="contextual">
2
<%= link_to(l(:label_news_new),
3
            new_project_news_path(@project),
4
            :class => 'icon icon-add',
5
            :onclick => 'showAndScrollTo("add-news", "news_title"); return false;') if @project && User.current.allowed_to?(:manage_news, @project) %>
6
</div>
7

  
8
<div id="add-news" style="display:none;">
9
<h2><%=l(:label_news_new)%></h2>
10
<%= labelled_form_for @news, :url => project_news_index_path(@project),
11
                                           :html => { :id => 'news-form', :multipart => true } do |f| %>
12
<%= render :partial => 'news/form', :locals => { :f => f } %>
13
<%= submit_tag l(:button_create) %>
14
<%= preview_link preview_news_path(:project_id => @project), 'news-form' %> |
15
<%= link_to l(:button_cancel), "#", :onclick => '$("#add-news").hide()' %>
16
<% end if @project %>
17
<div id="preview" class="wiki"></div>
18
</div>
19

  
20
<h2><%=l(:label_news_plural)%></h2>
21

  
22
<% if @newss.empty? %>
23
<p class="nodata"><%= l(:label_no_data) %></p>
24
<% else %>
25
<% @newss.each do |news| %>
26
    <h3><%= avatar(news.author, :size => "24") %><%= link_to_project(news.project) + ': ' unless news.project == @project %>
27
    <%= link_to h(news.title), news_path(news) %>
28
    <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
29
    <p class="author"><%= authoring news.created_on, news.author %></p>
30
    <div class="wiki">
31
    <%= textilizable(news, :description) %>
32
    </div>
33
<% end %>
34
<% end %>
35
<p class="pagination"><%= pagination_links_full @news_pages %></p>
36

  
37
<% other_formats_links do |f| %>
38
  <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
39
<% end %>
40

  
41
<% content_for :header_tags do %>
42
  <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
43
  <%= stylesheet_link_tag 'scm' %>
44
<% end %>
45

  
46
<% html_title(l(:label_news_plural)) -%>
.svn/pristine/bc/bcd855d1f37773ea6cbe38e91cc6db056d7aef2e.svn-base
1
<div class="contextual">
2
<%= link_to(l(:button_edit), edit_user_path(@user), :class => 'icon icon-edit') if User.current.admin? %>
3
</div>
4

  
5
<h2><%= avatar @user, :size => "50" %> <%=h @user.name %></h2>
6

  
7
<div class="splitcontentleft">
8
<ul>
9
  <% unless @user.pref.hide_mail %>
10
    <li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li>
11
  <% end %>
12
  <% @user.visible_custom_field_values.each do |custom_value| %>
13
  <% if !custom_value.value.blank? %>
14
    <li><%=h custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
15
  <% end %>
16
  <% end %>
17
    <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
18
  <% unless @user.last_login_on.nil? %>
19
    <li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li>
20
  <% end %>
21
</ul>
22

  
23
<% unless @memberships.empty? %>
24
<h3><%=l(:label_project_plural)%></h3>
25
<ul>
26
<% for membership in @memberships %>
27
  <li><%= link_to_project(membership.project) %>
28
    (<%=h membership.roles.sort.collect(&:to_s).join(', ') %>, <%= format_date(membership.created_on) %>)</li>
29
<% end %>
30
</ul>
31
<% end %>
32
<%= call_hook :view_account_left_bottom, :user => @user %>
33
</div>
34

  
35
<div class="splitcontentright">
36

  
37
<% unless @events_by_day.empty? %>
38
<h3><%= link_to l(:label_activity), :controller => 'activities',
39
                :action => 'index', :id => nil, :user_id => @user,
40
                :from => @events_by_day.keys.first %></h3>
41

  
42
<p>
43
<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
44
</p>
45

  
46
<div id="activity">
47
<% @events_by_day.keys.sort.reverse.each do |day| %>
48
<h4><%= format_activity_day(day) %></h4>
49
<dl>
50
<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
51
  <dt class="<%= e.event_type %>">
52
  <span class="time"><%= format_time(e.event_datetime, false) %></span>
53
  <%= content_tag('span', h(e.project), :class => 'project') %>
54
  <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
55
  <dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>
56
<% end -%>
57
</dl>
58
<% end -%>
59
</div>
60

  
61
<% other_formats_links do |f| %>
62
  <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => nil, :user_id => @user, :key => User.current.rss_key} %>
63
<% end %>
64

  
65
<% content_for :header_tags do %>
66
  <%= auto_discovery_link_tag(:atom, :controller => 'activities', :action => 'index', :user_id => @user, :format => :atom, :key => User.current.rss_key) %>
67
<% end %>
68
<% end %>
69
<%= call_hook :view_account_right_bottom, :user => @user %>
70
</div>
71

  
72
<% html_title @user.name %>
.svn/pristine/bc/bcefb9a2bbe2d9c2d605eef1915f4d25e465456c.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require 'active_record'
19
require 'iconv'
20
require 'pp'
21

  
22
namespace :redmine do
23
  desc 'Trac migration script'
24
  task :migrate_from_trac => :environment do
25

  
26
    module TracMigrate
27
        TICKET_MAP = []
28

  
29
        DEFAULT_STATUS = IssueStatus.default
30
        assigned_status = IssueStatus.find_by_position(2)
31
        resolved_status = IssueStatus.find_by_position(3)
32
        feedback_status = IssueStatus.find_by_position(4)
33
        closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34
        STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35
                          'reopened' => feedback_status,
36
                          'assigned' => assigned_status,
37
                          'closed' => closed_status
38
                          }
39

  
40
        priorities = IssuePriority.all
41
        DEFAULT_PRIORITY = priorities[0]
42
        PRIORITY_MAPPING = {'lowest' => priorities[0],
43
                            'low' => priorities[0],
44
                            'normal' => priorities[1],
45
                            'high' => priorities[2],
46
                            'highest' => priorities[3],
47
                            # ---
48
                            'trivial' => priorities[0],
49
                            'minor' => priorities[1],
50
                            'major' => priorities[2],
51
                            'critical' => priorities[3],
52
                            'blocker' => priorities[4]
53
                            }
54

  
55
        TRACKER_BUG = Tracker.find_by_position(1)
56
        TRACKER_FEATURE = Tracker.find_by_position(2)
57
        DEFAULT_TRACKER = TRACKER_BUG
58
        TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59
                           'enhancement' => TRACKER_FEATURE,
60
                           'task' => TRACKER_FEATURE,
61
                           'patch' =>TRACKER_FEATURE
62
                           }
63

  
64
        roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
65
        manager_role = roles[0]
66
        developer_role = roles[1]
67
        DEFAULT_ROLE = roles.last
68
        ROLE_MAPPING = {'admin' => manager_role,
69
                        'developer' => developer_role
70
                        }
71

  
72
      class ::Time
73
        class << self
74
          alias :real_now :now
75
          def now
76
            real_now - @fake_diff.to_i
77
          end
78
          def fake(time)
79
            @fake_diff = real_now - time
80
            res = yield
81
            @fake_diff = 0
82
           res
83
          end
84
        end
85
      end
86

  
87
      class TracComponent < ActiveRecord::Base
88
        self.table_name = :component
89
      end
90

  
91
      class TracMilestone < ActiveRecord::Base
92
        self.table_name = :milestone
93
        # If this attribute is set a milestone has a defined target timepoint
94
        def due
95
          if read_attribute(:due) && read_attribute(:due) > 0
96
            Time.at(read_attribute(:due)).to_date
97
          else
98
            nil
99
          end
100
        end
101
        # This is the real timepoint at which the milestone has finished.
102
        def completed
103
          if read_attribute(:completed) && read_attribute(:completed) > 0
104
            Time.at(read_attribute(:completed)).to_date
105
          else
106
            nil
107
          end
108
        end
109

  
110
        def description
111
          # Attribute is named descr in Trac v0.8.x
112
          has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113
        end
114
      end
115

  
116
      class TracTicketCustom < ActiveRecord::Base
117
        self.table_name = :ticket_custom
118
      end
119

  
120
      class TracAttachment < ActiveRecord::Base
121
        self.table_name = :attachment
122
        set_inheritance_column :none
123

  
124
        def time; Time.at(read_attribute(:time)) end
125

  
126
        def original_filename
127
          filename
128
        end
129

  
130
        def content_type
131
          ''
132
        end
133

  
134
        def exist?
135
          File.file? trac_fullpath
136
        end
137

  
138
        def open
139
          File.open("#{trac_fullpath}", 'rb') {|f|
140
            @file = f
141
            yield self
142
          }
143
        end
144

  
145
        def read(*args)
146
          @file.read(*args)
147
        end
148

  
149
        def description
150
          read_attribute(:description).to_s.slice(0,255)
151
        end
152

  
153
      private
154
        def trac_fullpath
155
          attachment_type = read_attribute(:type)
156
          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157
          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158
        end
159
      end
160

  
161
      class TracTicket < ActiveRecord::Base
162
        self.table_name = :ticket
163
        set_inheritance_column :none
164

  
165
        # ticket changes: only migrate status changes and comments
166
        has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167
        has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168

  
169
        def attachments
170
          TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171
        end
172

  
173
        def ticket_type
174
          read_attribute(:type)
175
        end
176

  
177
        def summary
178
          read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179
        end
180

  
181
        def description
182
          read_attribute(:description).blank? ? summary : read_attribute(:description)
183
        end
184

  
185
        def time; Time.at(read_attribute(:time)) end
186
        def changetime; Time.at(read_attribute(:changetime)) end
187
      end
188

  
189
      class TracTicketChange < ActiveRecord::Base
190
        self.table_name = :ticket_change
191

  
192
        def self.columns
193
          # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194
          super.select {|column| column.name.to_s != 'field'}
195
        end
196

  
197
        def time; Time.at(read_attribute(:time)) end
198
      end
199

  
200
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201
                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202
                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203
                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204
                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205
                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206
                           CamelCase TitleIndex)
207

  
208
      class TracWikiPage < ActiveRecord::Base
209
        self.table_name = :wiki
210
        set_primary_key :name
211

  
212
        def self.columns
213
          # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214
          super.select {|column| column.name.to_s != 'readonly'}
215
        end
216

  
217
        def attachments
218
          TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219
        end
220

  
221
        def time; Time.at(read_attribute(:time)) end
222
      end
223

  
224
      class TracPermission < ActiveRecord::Base
225
        self.table_name = :permission
226
      end
227

  
228
      class TracSessionAttribute < ActiveRecord::Base
229
        self.table_name = :session_attribute
230
      end
231

  
232
      def self.find_or_create_user(username, project_member = false)
233
        return User.anonymous if username.blank?
234

  
235
        u = User.find_by_login(username)
236
        if !u
237
          # Create a new user if not found
238
          mail = username[0, User::MAIL_LENGTH_LIMIT]
239
          if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240
            mail = mail_attr.value
241
          end
242
          mail = "#{mail}@foo.bar" unless mail.include?("@")
243

  
244
          name = username
245
          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246
            name = name_attr.value
247
          end
248
          name =~ (/(.*)(\s+\w+)?/)
249
          fn = $1.strip
250
          ln = ($2 || '-').strip
251

  
252
          u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253
                       :firstname => fn[0, limit_for(User, 'firstname')],
254
                       :lastname => ln[0, limit_for(User, 'lastname')]
255

  
256
          u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257
          u.password = 'trac'
258
          u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259
          # finally, a default user is used if the new user is not valid
260
          u = User.find(:first) unless u.save
261
        end
262
        # Make sure he is a member of the project
263
        if project_member && !u.member_of?(@target_project)
264
          role = DEFAULT_ROLE
265
          if u.admin
266
            role = ROLE_MAPPING['admin']
267
          elsif TracPermission.find_by_username_and_action(username, 'developer')
268
            role = ROLE_MAPPING['developer']
269
          end
270
          Member.create(:user => u, :project => @target_project, :roles => [role])
271
          u.reload
272
        end
273
        u
274
      end
275

  
276
      # Basic wiki syntax conversion
277
      def self.convert_wiki_text(text)
278
        # Titles
279
        text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280
        # External Links
281
        text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282
        # Ticket links:
283
        #      [ticket:234 Text],[ticket:234 This is a test]
284
        text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285
        #      ticket:1234
286
        #      #1 is working cause Redmine uses the same syntax.
287
        text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288
        # Milestone links:
289
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290
        #      The text "Milestone 0.1.0 (Mercury)" is not converted,
291
        #      cause Redmine's wiki does not support this.
292
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293
        #      [milestone:"0.1.0 Mercury"]
294
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295
        text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296
        #      milestone:0.1.0
297
        text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298
        text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299
        # Internal Links
300
        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301
        text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304
        text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305
        text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306

  
307
  # Links to pages UsingJustWikiCaps
308
  text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309
  # Normalize things that were supposed to not be links
310
  # like !NotALink
311
  text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312
        # Revisions links
313
        text = text.gsub(/\[(\d+)\]/, 'r\1')
314
        # Ticket number re-writing
315
        text = text.gsub(/#(\d+)/) do |s|
316
          if $1.length < 10
317
#            TICKET_MAP[$1.to_i] ||= $1
318
            "\##{TICKET_MAP[$1.to_i] || $1}"
319
          else
320
            s
321
          end
322
        end
323
        # We would like to convert the Code highlighting too
324
        # This will go into the next line.
325
        shebang_line = false
326
        # Reguar expression for start of code
327
        pre_re = /\{\{\{/
328
        # Code hightlighing...
329
        shebang_re = /^\#\!([a-z]+)/
330
        # Regular expression for end of code
331
        pre_end_re = /\}\}\}/
332

  
333
        # Go through the whole text..extract it line by line
334
        text = text.gsub(/^(.*)$/) do |line|
335
          m_pre = pre_re.match(line)
336
          if m_pre
337
            line = '<pre>'
338
          else
339
            m_sl = shebang_re.match(line)
340
            if m_sl
341
              shebang_line = true
342
              line = '<code class="' + m_sl[1] + '">'
343
            end
344
            m_pre_end = pre_end_re.match(line)
345
            if m_pre_end
346
              line = '</pre>'
347
              if shebang_line
348
                line = '</code>' + line
349
              end
350
            end
351
          end
352
          line
353
        end
354

  
355
        # Highlighting
356
        text = text.gsub(/'''''([^\s])/, '_*\1')
357
        text = text.gsub(/([^\s])'''''/, '\1*_')
358
        text = text.gsub(/'''/, '*')
359
        text = text.gsub(/''/, '_')
360
        text = text.gsub(/__/, '+')
361
        text = text.gsub(/~~/, '-')
362
        text = text.gsub(/`/, '@')
363
        text = text.gsub(/,,/, '~')
364
        # Lists
365
        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366

  
367
        text
368
      end
369

  
370
      def self.migrate
371
        establish_connection
372

  
373
        # Quick database test
374
        TracComponent.count
375

  
376
        migrated_components = 0
377
        migrated_milestones = 0
378
        migrated_tickets = 0
379
        migrated_custom_values = 0
380
        migrated_ticket_attachments = 0
381
        migrated_wiki_edits = 0
382
        migrated_wiki_attachments = 0
383

  
384
        #Wiki system initializing...
385
        @target_project.wiki.destroy if @target_project.wiki
386
        @target_project.reload
387
        wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388
        wiki_edit_count = 0
389

  
390
        # Components
391
        print "Migrating components"
392
        issues_category_map = {}
393
        TracComponent.find(:all).each do |component|
394
        print '.'
395
        STDOUT.flush
396
          c = IssueCategory.new :project => @target_project,
397
                                :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398
        next unless c.save
399
        issues_category_map[component.name] = c
400
        migrated_components += 1
401
        end
402
        puts
403

  
404
        # Milestones
405
        print "Migrating milestones"
406
        version_map = {}
407
        TracMilestone.find(:all).each do |milestone|
408
          print '.'
409
          STDOUT.flush
410
          # First we try to find the wiki page...
411
          p = wiki.find_or_new_page(milestone.name.to_s)
412
          p.content = WikiContent.new(:page => p) if p.new_record?
413
          p.content.text = milestone.description.to_s
414
          p.content.author = find_or_create_user('trac')
415
          p.content.comments = 'Milestone'
416
          p.save
417

  
418
          v = Version.new :project => @target_project,
419
                          :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420
                          :description => nil,
421
                          :wiki_page_title => milestone.name.to_s,
422
                          :effective_date => milestone.completed
423

  
424
          next unless v.save
425
          version_map[milestone.name] = v
426
          migrated_milestones += 1
427
        end
428
        puts
429

  
430
        # Custom fields
431
        # TODO: read trac.ini instead
432
        print "Migrating custom fields"
433
        custom_field_map = {}
434
        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435
          print '.'
436
          STDOUT.flush
437
          # Redmine custom field name
438
          field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439
          # Find if the custom already exists in Redmine
440
          f = IssueCustomField.find_by_name(field_name)
441
          # Or create a new one
442
          f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443
                                        :field_format => 'string')
444

  
445
          next if f.new_record?
446
          f.trackers = Tracker.find(:all)
447
          f.projects << @target_project
448
          custom_field_map[field.name] = f
449
        end
450
        puts
451

  
452
        # Trac 'resolution' field as a Redmine custom field
453
        r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
454
        r = IssueCustomField.new(:name => 'Resolution',
455
                                 :field_format => 'list',
456
                                 :is_filter => true) if r.nil?
457
        r.trackers = Tracker.find(:all)
458
        r.projects << @target_project
459
        r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460
        r.save!
461
        custom_field_map['resolution'] = r
462

  
463
        # Tickets
464
        print "Migrating tickets"
465
          TracTicket.find_each(:batch_size => 200) do |ticket|
466
          print '.'
467
          STDOUT.flush
468
          i = Issue.new :project => @target_project,
469
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470
                          :description => convert_wiki_text(encode(ticket.description)),
471
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472
                          :created_on => ticket.time
473
          i.author = find_or_create_user(ticket.reporter)
474
          i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475
          i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476
          i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477
          i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478
          i.id = ticket.id unless Issue.exists?(ticket.id)
479
          next unless Time.fake(ticket.changetime) { i.save }
480
          TICKET_MAP[ticket.id] = i.id
481
          migrated_tickets += 1
482

  
483
          # Owner
484
            unless ticket.owner.blank?
485
              i.assigned_to = find_or_create_user(ticket.owner, true)
486
              Time.fake(ticket.changetime) { i.save }
487
            end
488

  
489
          # Comments and status/resolution changes
490
          ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491
              status_change = changeset.select {|change| change.field == 'status'}.first
492
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493
              comment_change = changeset.select {|change| change.field == 'comment'}.first
494

  
495
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496
                              :created_on => time
497
              n.user = find_or_create_user(changeset.first.author)
498
              n.journalized = i
499
              if status_change &&
500
                   STATUS_MAPPING[status_change.oldvalue] &&
501
                   STATUS_MAPPING[status_change.newvalue] &&
502
                   (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503
                n.details << JournalDetail.new(:property => 'attr',
504
                                               :prop_key => 'status_id',
505
                                               :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506
                                               :value => STATUS_MAPPING[status_change.newvalue].id)
507
              end
508
              if resolution_change
509
                n.details << JournalDetail.new(:property => 'cf',
510
                                               :prop_key => custom_field_map['resolution'].id,
511
                                               :old_value => resolution_change.oldvalue,
512
                                               :value => resolution_change.newvalue)
513
              end
514
              n.save unless n.details.empty? && n.notes.blank?
515
          end
516

  
517
          # Attachments
518
          ticket.attachments.each do |attachment|
519
            next unless attachment.exist?
520
              attachment.open {
521
                a = Attachment.new :created_on => attachment.time
522
                a.file = attachment
523
                a.author = find_or_create_user(attachment.author)
524
                a.container = i
525
                a.description = attachment.description
526
                migrated_ticket_attachments += 1 if a.save
527
              }
528
          end
529

  
530
          # Custom fields
531
          custom_values = ticket.customs.inject({}) do |h, custom|
532
            if custom_field = custom_field_map[custom.name]
533
              h[custom_field.id] = custom.value
534
              migrated_custom_values += 1
535
            end
536
            h
537
          end
538
          if custom_field_map['resolution'] && !ticket.resolution.blank?
539
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
540
          end
541
          i.custom_field_values = custom_values
542
          i.save_custom_field_values
543
        end
544

  
545
        # update issue id sequence if needed (postgresql)
546
        Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547
        puts
548

  
549
        # Wiki
550
        print "Migrating wiki"
551
        if wiki.save
552
          TracWikiPage.find(:all, :order => 'name, version').each do |page|
553
            # Do not migrate Trac manual wiki pages
554
            next if TRAC_WIKI_PAGES.include?(page.name)
555
            wiki_edit_count += 1
556
            print '.'
557
            STDOUT.flush
558
            p = wiki.find_or_new_page(page.name)
559
            p.content = WikiContent.new(:page => p) if p.new_record?
560
            p.content.text = page.text
561
            p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562
            p.content.comments = page.comment
563
            Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564

  
565
            next if p.content.new_record?
566
            migrated_wiki_edits += 1
567

  
568
            # Attachments
569
            page.attachments.each do |attachment|
570
              next unless attachment.exist?
571
              next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572
              attachment.open {
573
                a = Attachment.new :created_on => attachment.time
574
                a.file = attachment
575
                a.author = find_or_create_user(attachment.author)
576
                a.description = attachment.description
577
                a.container = p
578
                migrated_wiki_attachments += 1 if a.save
579
              }
580
            end
581
          end
582

  
583
          wiki.reload
584
          wiki.pages.each do |page|
585
            page.content.text = convert_wiki_text(page.content.text)
586
            Time.fake(page.content.updated_on) { page.content.save }
587
          end
588
        end
589
        puts
590

  
591
        puts
592
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
593
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
594
        puts "Tickets:         #{migrated_tickets}/#{TracTicket.count}"
595
        puts "Ticket files:    #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596
        puts "Custom values:   #{migrated_custom_values}/#{TracTicketCustom.count}"
597
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
598
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599
      end
600

  
601
      def self.limit_for(klass, attribute)
602
        klass.columns_hash[attribute.to_s].limit
603
      end
604

  
605
      def self.encoding(charset)
606
        @ic = Iconv.new('UTF-8', charset)
607
      rescue Iconv::InvalidEncoding
608
        puts "Invalid encoding!"
609
        return false
610
      end
611

  
612
      def self.set_trac_directory(path)
613
        @@trac_directory = path
614
        raise "This directory doesn't exist!" unless File.directory?(path)
615
        raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616
        @@trac_directory
617
      rescue Exception => e
618
        puts e
619
        return false
620
      end
621

  
622
      def self.trac_directory
623
        @@trac_directory
624
      end
625

  
626
      def self.set_trac_adapter(adapter)
627
        return false if adapter.blank?
628
        raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629
        # If adapter is sqlite or sqlite3, make sure that trac.db exists
630
        raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631
        @@trac_adapter = adapter
632
      rescue Exception => e
633
        puts e
634
        return false
635
      end
636

  
637
      def self.set_trac_db_host(host)
638
        return nil if host.blank?
639
        @@trac_db_host = host
640
      end
641

  
642
      def self.set_trac_db_port(port)
643
        return nil if port.to_i == 0
644
        @@trac_db_port = port.to_i
645
      end
646

  
647
      def self.set_trac_db_name(name)
648
        return nil if name.blank?
649
        @@trac_db_name = name
650
      end
651

  
652
      def self.set_trac_db_username(username)
653
        @@trac_db_username = username
654
      end
655

  
656
      def self.set_trac_db_password(password)
657
        @@trac_db_password = password
658
      end
659

  
660
      def self.set_trac_db_schema(schema)
661
        @@trac_db_schema = schema
662
      end
663

  
664
      mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
665

  
666
      def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667
      def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668

  
669
      def self.target_project_identifier(identifier)
670
        project = Project.find_by_identifier(identifier)
671
        if !project
672
          # create the target project
673
          project = Project.new :name => identifier.humanize,
674
                                :description => ''
675
          project.identifier = identifier
676
          puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677
          # enable issues and wiki for the created project
678
          project.enabled_module_names = ['issue_tracking', 'wiki']
679
        else
680
          puts
681
          puts "This project already exists in your Redmine database."
682
          print "Are you sure you want to append data to this project ? [Y/n] "
683
          STDOUT.flush
684
          exit if STDIN.gets.match(/^n$/i)
685
        end
686
        project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687
        project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688
        @target_project = project.new_record? ? nil : project
689
        @target_project.reload
690
      end
691

  
692
      def self.connection_params
693
        if trac_adapter == 'sqlite3'
694
          {:adapter => 'sqlite3',
695
           :database => trac_db_path}
696
        else
697
          {:adapter => trac_adapter,
698
           :database => trac_db_name,
699
           :host => trac_db_host,
700
           :port => trac_db_port,
701
           :username => trac_db_username,
702
           :password => trac_db_password,
703
           :schema_search_path => trac_db_schema
704
          }
705
        end
706
      end
707

  
708
      def self.establish_connection
709
        constants.each do |const|
710
          klass = const_get(const)
711
          next unless klass.respond_to? 'establish_connection'
712
          klass.establish_connection connection_params
713
        end
714
      end
715

  
716
    private
717
      def self.encode(text)
718
        @ic.iconv text
719
      rescue
720
        text
721
      end
722
    end
723

  
724
    puts
725
    if Redmine::DefaultData::Loader.no_data?
726
      puts "Redmine configuration need to be loaded before importing data."
727
      puts "Please, run this first:"
728
      puts
729
      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
730
      exit
731
    end
732

  
733
    puts "WARNING: a new project will be added to Redmine during this process."
734
    print "Are you sure you want to continue ? [y/N] "
735
    STDOUT.flush
736
    break unless STDIN.gets.match(/^y$/i)
737
    puts
738

  
739
    def prompt(text, options = {}, &block)
740
      default = options[:default] || ''
741
      while true
742
        print "#{text} [#{default}]: "
743
        STDOUT.flush
744
        value = STDIN.gets.chomp!
745
        value = default if value.blank?
746
        break if yield value
747
      end
748
    end
749

  
750
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
751

  
752
    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
753
    prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
754
    unless %w(sqlite3).include?(TracMigrate.trac_adapter)
755
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
756
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
757
      prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
758
      prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
759
      prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
760
      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
761
    end
762
    prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763
    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764
    puts
765

  
766
    # Turn off email notifications
767
    Setting.notified_events = []
768

  
769
    TracMigrate.migrate
770
  end
771
end
772

  
.svn/pristine/bc/bcf601e4c2eab267682f5366299899e20f6d08d2.svn-base
1
OpenIdAuthentication
2
====================
3

  
4
Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:
5

  
6
  gem install ruby-openid
7

  
8
To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
9
from that gem.
10

  
11
The specification used is http://openid.net/specs/openid-authentication-2_0.html.
12

  
13

  
14
Prerequisites
15
=============
16

  
17
OpenID authentication uses the session, so be sure that you haven't turned that off.
18

  
19
Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
20

  
21
  OpenIdAuthentication.store = :file
22

  
23
This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
24
If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
25

  
26
The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:
27

  
28
  map.root :controller => 'articles'
29

  
30
This plugin relies on Rails Edge revision 6317 or newer.
31

  
32

  
33
Example
34
=======
35

  
36
This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
37
salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
38
not a destination.
39

  
40
Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
41
model you are using for authentication.
42

  
43
Also of note is the following code block used in the example below:
44

  
45
  authenticate_with_open_id do |result, identity_url|
46
    ...
47
  end
48

  
49
In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
50
If you are storing just 'example.com' with your user, the lookup will fail.
51

  
52
There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.
53

  
54
  OpenIdAuthentication.normalize_url(user.identity_url)
55

  
56
The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
57
It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
58
Use the above code in your User model and validate OpenID URLs before saving them.
59

  
60
config/routes.rb
61

  
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff