Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 358
- Log:
Add the I2 to Instiki migration script (and yes, the "changeme"
default Web password for Instiki *was* changed!).
- Author:
- rool
- Date:
- Sat Mar 19 21:59:17 +0000 2011
- Size:
- 8716 Bytes
1 | # |
2 | # migrate.rb |
3 | # ========== |
4 | # |
5 | # Migrate an I2 Wiki to Instiki. There must be only one 'book' in I2. A default |
6 | # 'Web' is created in Instiki for this, with a system password of "changeme", |
7 | # configured for Textile with CamelCase Wiki words disabled - only the |
8 | # '[[foo]]' syntax is allowed. |
9 | # |
10 | # Run this script using "script/runner" inside the target Instiki installation. |
11 | # |
12 | |
13 | # Create a base class which establishes the alternative database connection. |
14 | # Update the connection details as required to read the source Wiki data. |
15 | |
16 | class I2Database < ActiveRecord::Base |
17 | establish_connection( |
18 | "adapter"=>"postgresql", |
19 | "host"=>"/home/rool/postgres/", |
20 | "database"=>"i2", |
21 | "username"=>"rool", |
22 | "password"=>"set-password-here" |
23 | ) |
24 | end |
25 | |
26 | # We're going to use namespaced models to avoid name clashes below. This |
27 | # provokes problems with ActiveRecord; see: |
28 | # |
29 | # https://rails.lighthouseapp.com/projects/8994/tickets/2283-unnecessary-exception-raised-in-asdependenciesload_missing_constant |
30 | # |
31 | # The ugly but effective monkey patch given in the above ticket is applied |
32 | # below. More structural changes were eventually considered, but this is |
33 | # sufficient for our needs here. |
34 | |
35 | module ActiveSupport |
36 | module Dependencies |
37 | extend self |
38 | |
39 | #def load_missing_constant(from_mod, const_name) |
40 | |
41 | def forgiving_load_missing_constant( from_mod, const_name ) |
42 | begin |
43 | old_load_missing_constant(from_mod, const_name) |
44 | rescue ArgumentError => arg_err |
45 | if arg_err.message == "#{from_mod} is not missing constant #{const_name}!" |
46 | return from_mod.const_get(const_name) |
47 | else |
48 | raise |
49 | end |
50 | end |
51 | end |
52 | alias :old_load_missing_constant :load_missing_constant |
53 | alias :load_missing_constant :forgiving_load_missing_constant |
54 | end |
55 | end |
56 | |
57 | # Create a module to avoid namespace clashes (with 'Page' in particular) - |
58 | # use "I2::Page" for an I2 page model, c.f. just "Page" for an Instiki model. |
59 | |
60 | module I2 |
61 | class Page < I2Database |
62 | set_table_name 'pages' |
63 | |
64 | has_many :versions |
65 | end |
66 | |
67 | class Version < I2Database |
68 | set_table_name 'versions' |
69 | |
70 | belongs_to :author |
71 | belongs_to :page |
72 | end |
73 | |
74 | class Author < I2Database |
75 | set_table_name 'authors' |
76 | |
77 | has_many :versions |
78 | has_many :pages, :through => :versions |
79 | end |
80 | end |
81 | |
82 | # Create the basic System and Web objects. |
83 | |
84 | system = System.new |
85 | system.password = "changeme" |
86 | system.save! |
87 | |
88 | web = Web.new |
89 | web.name = "Documentation" |
90 | web.address = "documentation" |
91 | web.additional_style = "" |
92 | web.allow_uploads = 1 |
93 | web.published = 0 |
94 | web.count_pages = 0 |
95 | web.markup = "textile" |
96 | web.color = "008B26" |
97 | web.max_upload_size = 100 |
98 | web.safe_mode = 0 |
99 | web.brackets_only = 1 |
100 | web.save! |
101 | |
102 | # Given an I2::Version instance, return body text suitable for Instiki. |
103 | # |
104 | def process_body( i2version ) |
105 | |
106 | # Complications: |
107 | # |
108 | # I2 allowed a nil revision body for some of its Versions. Instiki fails |
109 | # if you have that, so make sure at least an empty string is present. |
110 | |
111 | content = ( i2version.body ) || '' |
112 | |
113 | # New RedCloth versions don't like it if you try to use an ID/anchor with |
114 | # capital letters, so use a rather permissive gsub to fix these - it |
115 | # risks catching other "#foo" cases than just anchor/IDs in links or |
116 | # definitions, but (at least in ROOL's case) that seems to be very rare |
117 | # (visual examination of about 1700 matches didn't spot any). We also |
118 | # need to change "+"s to "%20" - see the next complication for why. |
119 | |
120 | content.gsub!(/\#[A-Za-z\-\+_]+/) { | str | str.downcase.gsub('+', '%20') } |
121 | |
122 | # # Textilised references of the form "a+b+c" were interpreted as "a b c" |
123 | # # under I2 (i.e. "+" mapped to " ") but this is not the case under |
124 | # # Instiki. Replacing the "+"s with "%20" solves the problem albeit in an |
125 | # # ugly fashion - but it's less trouble than patching Instiki and having |
126 | # # difficulty with upgrades later. |
127 | # |
128 | # 2011-03-13 (ADH): Commented out as changes to the Page model within |
129 | # Instiki handle this case via a different method. |
130 | # |
131 | # content.gsub!(/((\"\:)|(\]))[A-Za-z0-9_\-\%]+\+([A-Za-z0-9_\-\%]+(\+)?)+/) { | str | str.gsub('+', '%20') } |
132 | |
133 | # A common vertical spacing hack in the ROOL wiki to separate tables was |
134 | # to use <br> just above the table definition, but in new Textile any |
135 | # block element must have a blank line above it else the block text is |
136 | # rendered 'raw'. CSS improvements for the new Wiki make sure that such |
137 | # blocks don't vertically abut so the line breaks are useless - remove, |
138 | # but keep the line feed to make sure we don't let Textile block markup |
139 | # touch without an intervening blank line, provoking the problem we're |
140 | # trying to solve. |
141 | |
142 | return content.gsub(/\n\<br\ ?\/?\>/, "\n") |
143 | end |
144 | |
145 | # Set the ID of the Web all pages are to be placed into here; set the ID of the |
146 | # I2 Book to copy over here as well. |
147 | |
148 | target_instiki_web = Web.first |
149 | target_instiki_web_id = target_instiki_web.id |
150 | source_i2_book_id = 1 # Get this e.g. from observation of SQL. |
151 | |
152 | #Web.transaction do |
153 | begin |
154 | |
155 | # Pages |
156 | # ===== |
157 | # |
158 | # Instiki Page maps to I2 Page |
159 | # |
160 | # I2 field Instiki field |
161 | # ====================================== |
162 | # title name |
163 | # created_at created_at |
164 | # updated_at updated_at |
165 | # book_id 1 (well, Web.first.id) |
166 | # locked_by and locked_at => nil or empty or whatever |
167 | # |
168 | # Revisons |
169 | # ======== |
170 | # |
171 | # Instiki Version maps to I2 Revision |
172 | # |
173 | # I2 field Instiki field |
174 | # ====================================== |
175 | # page_id mapped equivalent "page_id" |
176 | # author_id get author and write name to "author" |
177 | # created_at created_at |
178 | # body content |
179 | # book_id N/A (redundant; Revision has Web through Page) |
180 | # updated_at => copy created_at |
181 | # revised_at => copy created_at |
182 | # ip => Make up (e.g. 127.0.0.1) |
183 | |
184 | page_count = 0 |
185 | page_total = I2::Page.count |
186 | version_count = 0 |
187 | version_total = I2::Version.count |
188 | |
189 | wiki = Wiki.new |
190 | |
191 | app = ActionController::Integration::Session.new |
192 | app.host = "www.riscosopen.org" |
193 | app.https! |
194 | |
195 | PageRenderer.setup_url_generator( UrlGenerator.new( app ) ) |
196 | |
197 | I2::Page.all( :order => 'id ASC' ).each do | i2page | |
198 | next unless i2page.book_id == source_i2_book_id |
199 | |
200 | # Create and save the equivalent Instiki page and revisions. I2 has a very |
201 | # shaky use of |
202 | |
203 | i2versions = i2page.versions.find( :all, :order => 'created_at ASC' ) |
204 | i2version = i2versions.try( :shift ) |
205 | |
206 | if ( i2version.nil? ) |
207 | puts "WARNING: No versions found for page '#{ i2page.title }' (#{ i2page.id }) - skipping" |
208 | next |
209 | end |
210 | |
211 | # Instiki assumes a name of "HomePage" for the default starting page in |
212 | # any given 'Web'. |
213 | |
214 | name = i2page.title |
215 | name = "HomePage" if ( name == "Home Page" ) |
216 | |
217 | # I2 on the ROOL site has had one or two nasty hiccups wherein a page with |
218 | # a duplicate title was created. We can't handle that. Instiki just adds |
219 | # a new revision to whatever existing page has a matching title, even if |
220 | # you use its "write_page" call (see below). The best solution based on |
221 | # observation of the data at hand is to simply skip the duplicate page; the |
222 | # original page of a given title always takes precedence and any later |
223 | # same-name copies are ignored. |
224 | |
225 | next unless ( Page.find_by_name( name ).nil? ) |
226 | |
227 | # Create the page and associated first revision. |
228 | |
229 | begin |
230 | page_count += 1 |
231 | version_count += 1 |
232 | |
233 | puts "Page #{ page_count } of #{ page_total } ('#{ name }')..." |
234 | |
235 | wiki.write_page( |
236 | target_instiki_web.address, |
237 | name, |
238 | process_body( i2version ), |
239 | i2page.created_at, |
240 | Author.new( i2version.author.name, '127.0.0.1' ), |
241 | PageRenderer.new |
242 | ) |
243 | |
244 | rescue => e |
245 | puts "WARNING: Could not save I2 page ID #{ i2page.id }:" |
246 | puts e.message |
247 | next # Must not try to process other revisions since we have no page |
248 | |
249 | end |
250 | |
251 | # Now handle any other revisions. |
252 | |
253 | i2versions.each do | i2version | |
254 | |
255 | begin |
256 | version_count += 1 |
257 | |
258 | puts "version #{ version_count } of #{ version_total } done..." |
259 | |
260 | wiki.revise_page( |
261 | target_instiki_web.address, |
262 | name, |
263 | name, |
264 | process_body( i2version ), |
265 | i2version.created_at, |
266 | Author.new( i2version.author.name, '127.0.0.1' ), |
267 | PageRenderer.new |
268 | ) |
269 | |
270 | rescue => e |
271 | puts "WARNING: Could not save I2 version ID #{ i2version.id }:" |
272 | puts e.message |
273 | |
274 | end |
275 | end |
276 | end |
277 | |
278 | puts "Finished" |
279 | |
280 | end |
281 | |
282 | # Done! |