Soks 0.0.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. data/LICENSE.txt +2 -0
  2. data/README.txt +3 -2
  3. data/TODO.txt +31 -0
  4. data/bin/soks-create-wiki.rb +0 -1
  5. data/lib/authenticators.rb +30 -4
  6. data/lib/helpers/counter-helpers.rb +132 -0
  7. data/lib/helpers/default-helpers.rb +170 -169
  8. data/lib/helpers/mail2wiki-helper.rb +18 -22
  9. data/lib/helpers/maintenance-helpers.rb +149 -0
  10. data/lib/helpers/rss2wiki-helper.rb +7 -8
  11. data/lib/soks-model.rb +82 -54
  12. data/lib/soks-servlet.rb +126 -108
  13. data/lib/soks-storage.rb +74 -11
  14. data/lib/soks-utils.rb +77 -3
  15. data/lib/soks-view.rb +169 -103
  16. data/lib/soks.rb +5 -23
  17. data/templates/default/attachment/newpage.js +4 -13
  18. data/templates/default/attachment/print_stylesheet.css +2 -7
  19. data/templates/default/caches/readme.txt +1 -0
  20. data/templates/default/content/Api%20for%20classes%20to%20modify%20the%20wiki.textile +2 -0
  21. data/templates/default/content/Author.textile +4 -1
  22. data/templates/default/content/Automatic%20Summaries.textile +16 -53
  23. data/templates/default/content/Automatic%20linking%20between%20pages.textile +3 -3
  24. data/templates/default/content/{bug%3A%20competing%20edits.textile → Bug%3A%20Competing%20edits.textile} +9 -0
  25. data/templates/default/content/Bug%3A%20Does%20not%20make%20use%20of%20if%2Dmodified%2Dsince%20r.textile +2 -0
  26. data/templates/default/content/Bug%3A%20E%2Dmail%20addresses%20with%20hyphens%20not%20recognised.textile +17 -0
  27. data/templates/default/content/Bug%3A%20Email%20adresses%20in%20page%20titles%20cause%20incorrec.textile +3 -0
  28. data/templates/default/content/Bug%3A%20GEM%20limits%20title%20lengths.textile +3 -1
  29. data/templates/default/content/Bug%3A%20Memory%20leak.textile +13 -0
  30. data/templates/default/content/Bug%3A%20Pages%20that%20link%20here%20may%20not%20appear%20on%20r.textile +13 -0
  31. data/templates/default/content/Bug%3A%20Textile%20mishandles%20paragraphs.textile +4 -0
  32. data/templates/default/content/Bug%3A%20Unanticipated%20Rollbacks.textile +2 -0
  33. data/templates/default/content/Bug%3A%20notextile%20does%20not%20prevent%20page%20inserts.textile +2 -0
  34. data/templates/default/content/Home%20Page.textile +3 -1
  35. data/templates/default/content/How%20to%20administrate%20this%20wiki.textile +23 -13
  36. data/templates/default/content/How%20to%20change%20the%20way%20this%20wiki%20looks.textile +3 -1
  37. data/templates/default/content/How%20to%20export%20a%20site%20from%20this%20wiki.textile +22 -0
  38. data/templates/default/content/How%20to%20get%20the%20latest%20Soks%20from%20cvs.textile +2 -0
  39. data/templates/default/content/How%20to%20hack%20soks.textile +2 -0
  40. data/templates/default/content/How%20to%20import%20a%20site%20from%20instiki.textile +2 -0
  41. data/templates/default/content/{How%20to%20import%20data%20to%20this%20wiki.textile → How%20to%20import%20data.textile} +3 -7
  42. data/templates/default/content/How%20to%20install%20Soks.textile +2 -0
  43. data/templates/default/content/How%20to%20password%20protect%20your%20wiki.textile +21 -11
  44. data/templates/default/content/How%20to%20report%20a%20bug.textile +2 -1
  45. data/templates/default/content/How%20to%20upgrade%20soks.textile +22 -0
  46. data/templates/default/content/How%20to%20use%20the%20keyboard%20shortcuts.textile +2 -2
  47. data/templates/default/content/How%20to%20use%20this%20wiki.textile +3 -1
  48. data/templates/default/content/List%20of%20changes.textile +84 -118
  49. data/templates/default/content/News%3A%20Version%201%2D0%2D0%20released.textile +19 -0
  50. data/templates/default/content/Pages%20to%20include%20in%20the%20distribution.textile +51 -0
  51. data/templates/default/content/Per%20Wiki%20Templates.textile +2 -0
  52. data/templates/default/content/Planned%20Features.textile +30 -9
  53. data/templates/default/content/README.textile +3 -2
  54. data/templates/default/content/RSS%20feed.textile +1 -1
  55. data/templates/default/content/Recent%20changes%20to%20this%20site.textile +283 -0
  56. data/templates/default/content/SOKS%20features.textile +3 -0
  57. data/templates/default/content/Site%20Index.textile +202 -0
  58. data/templates/default/content/Soks%20Licence.textile +2 -0
  59. data/templates/default/content/Tag%3A%20Include%20this%20page%20in%20the%20distribution.textile +6 -0
  60. data/templates/default/start.rb +67 -123
  61. data/templates/default/version.txt +1 -1
  62. data/templates/default/views/Page_edit.rhtml +7 -7
  63. data/templates/default/views/{Page_search_results.rhtml → Page_find.rhtml} +9 -3
  64. data/templates/default/views/Page_linksfromrss.rhtml +24 -0
  65. data/templates/default/views/Page_listrss.rhtml +46 -0
  66. data/templates/default/views/Page_meta.rhtml +1 -1
  67. data/templates/default/views/Page_revision.rhtml +39 -0
  68. data/templates/default/views/Page_revisions.rhtml +13 -5
  69. data/templates/default/views/Page_rss.rhtml +8 -8
  70. data/templates/default/views/Page_view.rhtml +3 -3
  71. data/templates/default/views/UploadPage_edit.rhtml +8 -8
  72. data/templates/default/views/frame.rhtml +8 -8
  73. data/templates/default/views/messages.yaml +1 -0
  74. data/test/html/2006Mar.html +66 -0
  75. data/test/html/poignant.html +36 -0
  76. data/test/html/poignant.textile +36 -0
  77. data/test/mock-objects.rb +69 -0
  78. data/test/stress_url_calls.rb +33 -0
  79. data/test/stress_urls.txt +68 -0
  80. data/test/test_counter-helper.rb +158 -0
  81. data/test/test_soks-helper-maintenance.rb +106 -0
  82. data/test/test_soks-helpers.rb +104 -0
  83. data/test/test_soks-model.rb +144 -0
  84. data/test/test_soks-servlet.rb +231 -0
  85. data/test/test_soks-storage.rb +70 -31
  86. data/test/test_soks-utils.rb +112 -13
  87. data/test/test_soks-view.rb +141 -3
  88. metadata +38 -27
  89. data/templates/default/content/A%20page%20with%20an%20umlaut%20%F6%20in%20its%20title.textile +0 -1
  90. data/templates/default/content/All%20News.textile +0 -26
  91. data/templates/default/content/Bil%20Kleb.textile +0 -1
  92. data/templates/default/content/Bil.textile +0 -1
  93. data/templates/default/content/Bill%20Wood.textile +0 -3
  94. data/templates/default/content/Bug%3A%20RSS%20feed%20does%20not%20validate.textile +0 -10
  95. data/templates/default/content/Bug%3A%20Type%20a%20title%20here.textile +0 -31
  96. data/templates/default/content/Instructions%20and%20Howtos.textile +0 -21
  97. data/templates/default/content/Latest%20News.textile +0 -26
  98. data/templates/default/content/New%20Recent%20Changes%20class.textile +0 -68
  99. data/templates/default/content/New%20page%20templates%20or%20categories%20code.textile +0 -68
  100. data/templates/default/content/News%3A%20Version%200%2E0%2E6%20Released.textile +0 -13
  101. data/templates/default/content/Recent%20Blog%20Entries.textile +0 -5
  102. data/templates/default/content/Recent%20Changes%20to%20This%20Site.textile +0 -286
  103. data/templates/default/content/Ruby.textile +0 -9
  104. data/templates/default/content/Skorgu.textile +0 -3
  105. data/templates/default/content/ctrl%2Dn.textile +0 -1
  106. data/templates/default/content/let%20me%20know.textile +0 -1
  107. data/templates/default/content/sandbox.textile +0 -20
  108. data/templates/default/content/tamc.textile +0 -1
  109. data/templates/default/content/tamc2.textile +0 -1
data/lib/soks-servlet.rb CHANGED
@@ -2,158 +2,176 @@
2
2
  require 'authenticators'
3
3
  require 'yaml'
4
4
 
5
- # Default settings moved to soks.rb
5
+ class ServletSettings
6
+
7
+ attr_accessor :view_controller, :wiki
8
+ attr_accessor :home_page
9
+ attr_accessor :default_view
10
+ attr_accessor :upload_directory
11
+ attr_accessor :authenticators
12
+ attr_accessor :static_file_directories
13
+ attr_accessor :force_no_cache
14
+ attr_accessor :content_types
15
+ attr_accessor :wiki_directory
16
+
17
+ def initialize( wiki, view_controller )
18
+ @wiki, @view_controller = wiki, view_controller
19
+ @home_page = 'Home Page'
20
+ @default_view = 'view'
21
+ @authenticators = []
22
+ @static_file_directories = {}
23
+ @content_types = {}
24
+ @force_no_cache = false
25
+ end
26
+
27
+ def url
28
+ @view_controller.root_url
29
+ end
30
+
31
+ def page_name_for_url_name( url_name )
32
+ @view_controller.page_name_for_url_name( url_name )
33
+ end
34
+
35
+ def url_name_for_page_name( page_name )
36
+ @view_controller.url_name_for_page_name( page_name )
37
+ end
38
+ end
6
39
 
7
40
  class WikiServlet < WEBrick::HTTPServlet::AbstractServlet
8
-
9
- def initialize( server, view, settings )
10
- @server, @view, @wikiurl, @authenticators = server, view, settings[:url] , settings[:authenticators]
41
+
42
+ attr_accessor :server, :settings
43
+
44
+ def initialize( server, servlet_settings )
45
+ @server, @settings = server, servlet_settings
11
46
  end
12
47
 
13
48
  def service( request, response )
14
- case request.path
15
- when '/robots.txt', '/favicon.ico' # Catch some requests to static files
16
- doAttachment( request, response, request.path[1..-1] )
17
- when /\/(\w+?)\/(.+)/ # If request of the form /verb/pagename then doVerb on this class
18
- verb, pagename = $1, make_pagename_valid( $2 )
19
- authenticate request, response
20
- make_username_valid( request )
21
- if self.respond_to?( "do#{verb.capitalize}" )
22
- self.send( "do#{verb.capitalize}", request, response, pagename, request.user )
23
- else
24
- unknownView( request, response, pagename, verb, request.user )
25
- end
26
- when /\/(.+)/ ; redirect( response, $1, 'view' ) # No verb given
27
- when "/" ; redirect( response, $SETTINGS[:home_page], 'view' ) # No page or verb given
28
- end
49
+ case request.path_info
50
+
51
+ # Pass some requests directly to static files
52
+ when '/robots.txt', '/favicon.ico'; serveStaticFile( request, response, request.path[1..-1], 'Attachment' )
29
53
 
30
- if $SETTINGS[:force_no_cache]
31
- response['Cache-control'] ||= 'no-cache'
32
- response['Pragma'] ||= 'no-cache'
54
+ # If request of the form /verb/pagename then do it
55
+ when /\/(\w+?)\/(.+)/; wiki_service( request, response, $1.capitalize, $2 )
56
+
57
+ # If request of the form /pagename then redirect to /view/pagename
58
+ when /\/(.+)/ ; redirect( response, $1, settings.default_view )
59
+
60
+ # If request of the form / then redirect to /view/home%20page
61
+ when "/" ; redirect( response, settings.home_page, settings.default_view )
62
+
33
63
  end
34
- end
64
+ end
65
+
66
+ def wiki_service( request, response, verb, url_name )
67
+ authenticate request, response
68
+ make_username_valid( request )
69
+ if settings.static_file_directories.include? verb
70
+ serveStaticFile( request, response, url_name, verb )
71
+ elsif self.respond_to?( "do#{verb}" )
72
+ self.send( "do#{verb}", request, response,settings.page_name_for_url_name(url_name), request.user )
73
+ set_cache_settings(response)
74
+ else
75
+ renderView( request, response, settings.page_name_for_url_name(url_name), verb, request.user )
76
+ set_cache_settings(response)
77
+ end
78
+ end
35
79
 
36
80
  def authenticate( request, response )
37
- @authenticators.each do |path_regex,authenticator|
38
- if request.path.downcase =~ path_regex
81
+ settings.authenticators.each do |path_regex,authenticator|
82
+ if request.path_info.downcase =~ path_regex
39
83
  authenticator.authenticate( request, response )
40
84
  break
41
85
  end
42
86
  end
43
87
  end
44
88
 
45
- def redirect( response, pagename, verb = 'view' )
46
- pagename = pagename.url_encode # Reformat into url encoding
47
- response.set_redirect(WEBrick::HTTPStatus::Found, verb ? "#{@wikiurl}/#{verb}/#{pagename}" : "#{@wikiurl}/#{pagename}")
48
- end
49
-
50
- # These methods deal with the various views
51
-
52
- def doAttachment( request, response, pagename, person = nil )
53
- request.script_name = 'attachment'
54
- request.path_info = "/#{pagename}"
55
- WEBrick::HTTPServlet::FileHandler.get_instance(@server, "#{$SETTINGS[:root_directory] }/attachment",true).service(request, response)
56
- end
57
-
58
- def doFind( request, response, pagename, person )
59
- response.body = @view.find( request.query['regex'].strip )
60
- response['Content-Type'] ||= "text/html"
89
+ # A special redirect to allow WikiLink style urls
90
+ # Not sure if used by anyone, so may delete
91
+ def doWiki( request, response, pagename, person )
92
+ redirect( response, pagename.gsub(/([a-z])([A-Z])/,'\1 \2') )
61
93
  end
62
94
 
63
- def doRss( request, response, pagename, person )
64
- response.body = @view.view( pagename, 'rss', person )
65
- response["Content-Type"] = "application/xml"
95
+ # This passes any requests for static files onto a FileHandler
96
+ def serveStaticFile( request, response, url_name, view )
97
+ request.script_name = view
98
+ request.path_info = "/#{url_name}"
99
+ WEBrick::HTTPServlet::FileHandler.get_instance(@server, settings.static_file_directories[view], true).service(request, response)
66
100
  end
67
101
 
68
- # This captures all unkown view types
69
- def unknownView( request, response, pagename, view, person )
70
- response.body = @view.view( pagename, view, person )
71
- response['Content-Type'] ||= "text/html"
102
+ # This passes any rendering of the page onto the view class
103
+ def renderView( request, response, pagename, view, person )
104
+ response.body = view_controller.render( pagename, view, person, request.query )
105
+ response['Content-Type'] = settings.content_types[view] || 'text/html'
72
106
  end
73
107
 
74
108
  # All the following methods change the wiki, then redirect
75
-
76
- def doWiki( request, response, pagename, person )
77
- redirect( response, pagename.gsub(/([a-z])([A-Z])/,'\1 \2') )
78
- end
79
109
 
80
110
  def doSave( request, response, pagename, person )
81
- content = request.query["content"].to_s
82
- newpagename = make_pagename_valid( request.query["titleprefix"].to_s + request.query["newtitle"].to_s)
83
- @view.revise( pagename, content.gsub(/\r\n/,"\n"), person, newpagename) if content
84
- redirect( response, newpagename )
111
+ pagename = move_page_as_required( request, response, pagename, person )
112
+ content = request.query["content"].to_s.gsub(/\r\n/,"\n")
113
+ wiki.revise( pagename, content, person ) if content
114
+ redirect( response, pagename )
85
115
  end
86
116
 
87
117
  def doRollback( request, response, pagename, person )
88
- revision = request.query['revision'] ? request.query['revision'].to_i : nil
89
- @view.rollback( pagename, revision, person ) if revision
118
+ if request.query['revision']
119
+ wiki.rollback( pagename, request.query['revision'].to_i, person )
120
+ end
90
121
  redirect( response, pagename )
91
122
  end
92
123
 
93
124
  def doDelete( request, response, pagename, person )
94
- @view.delete( pagename, person )
125
+ wiki.delete( pagename, person )
95
126
  redirect( response, pagename )
96
127
  end
97
128
 
98
129
  def doUpload( request, response, pagename, person )
99
- newpagename = make_pagename_valid( request.query["titleprefix"].to_s + request.query["newtitle"].to_s)
130
+ pagename = move_page_as_required( request, response, pagename, person )
100
131
  unless request.query['file'] == ""
101
- @view.revise( pagename, upload_file_data( request.query['file'] ), person, newpagename )
102
- else
103
- @view.move( pagename, person, newpagename )
104
- end
105
- redirect( response, newpagename )
132
+ filename = upload_file_data( request.query['file'] )
133
+ wiki.revise( pagename, filename, person )
134
+ end
135
+ redirect( response, pagename )
106
136
  end
107
137
 
108
138
  private
139
+
140
+ def redirect( response, pagename, verb = settings.default_view )
141
+ response.set_redirect( WEBrick::HTTPStatus::Found, "#{settings.url}/#{verb}/#{settings.url_name_for_page_name(pagename)}" )
142
+ end
143
+
144
+ # Moves a page if there is a newtitle in the query
145
+ # If the original page had 'type a title' in its tile, then it is assumed to be a template
146
+ # and therefore is not moved.
147
+ def move_page_as_required( request, response, pagename, person )
148
+ new_pagename = "#{request.query["titleprefix"]}#{request.query["newtitle"]}"
149
+ return new_pagename if pagename =~ /#{$MESSAGES[:Type_a_title_here]}/io
150
+ return pagename if new_pagename == pagename
151
+ wiki.move( pagename, new_pagename, person )
152
+ new_pagename
153
+ end
109
154
 
110
- def upload_file_data( upload_data )
111
- path = "#{$SETTINGS[:root_directory]}/attachment/"
155
+ def upload_file_data( upload_data, destination = settings.upload_directory )
156
+ return "Uploads prohibited" unless destination
157
+ path = settings.static_file_directories[ destination ]
112
158
  filename = File.unique_filename( path , upload_data.filename )
113
159
  File.open( File.join( path, filename ), 'wb' ) { |file| upload_data.list.each { |data| file << data } }
114
- "/attachment/#{filename}"
160
+ "/#{destination}/#{filename}"
115
161
  end
116
162
 
117
- # Certain characters cause problems in titles, so delete
118
- def make_pagename_valid( pagename )
119
- pagename.to_valid_pagename
163
+ # Make sure the username doesn't start with Automatic
164
+ def make_username_valid( request )
165
+ request.user = "User: #{request.user}" if request.user =~ /^Automatic/i
120
166
  end
121
167
 
122
- # Because the username is also used as a page title, also delete troublesome characters from them
123
- # and make sure the name doesn't start with Automatic
124
- def make_username_valid( request )
125
- request.user = request.user.to_valid_pagename
126
- request.user = "External #{request.user}" if request.user =~ /^Automatic/i
168
+ def set_cache_settings(response)
169
+ return unless @settings.force_no_cache
170
+ response['Cache-control'] ||= 'no-cache'
171
+ response['Pragma'] ||= 'no-cache'
127
172
  end
128
- end
129
-
130
- def start_wiki( settings = {}, &automatic_agents )
131
-
132
- $SETTINGS.merge! settings
133
- $MESSAGES = YAML.load( IO.readlines("#{$SETTINGS[:root_directory]}/views/messages.yaml").join )
134
-
135
- wiki = Wiki.new( "#{$SETTINGS[:root_directory]}/content" )
136
- view = View.new( wiki, $SETTINGS[:name] )
137
-
138
- Thread.new( automatic_agents, wiki, view ) do |block, wiki, view|
139
- $LOG.info "Starting helper classes"
140
- block.call( wiki, view )
141
- $LOG.info "Finished starting helper classes"
142
- end.priority = -2
143
-
144
- server = WEBrick::HTTPServer.new( { :Port => $SETTINGS[:port],
145
- :ServerType=> $SETTINGS[:server_type]
146
- } )
147
-
148
- server.mount("/", WikiServlet, view, $SETTINGS )
149
-
150
- trap("INT") {
151
- $LOG.warn "Trying to shutdown gracefully"
152
- server.shutdown
153
- wiki.shutdown
154
- $LOG.info "Shutdown."
155
- }
156
-
157
- $LOG.warn "Starting server"
158
- server.start
159
- end
173
+
174
+ def wiki; @settings.wiki end
175
+ def view_controller; @settings.view_controller end
176
+
177
+ end
data/lib/soks-storage.rb CHANGED
@@ -1,3 +1,48 @@
1
+ module WikiCacheStore
2
+
3
+ CACHE_EXTENSION = ".marshal"
4
+
5
+ def load_cache( cache_name )
6
+ return nil unless @cache_folder
7
+
8
+ cache = nil
9
+
10
+ File.open( cache_filename_for( cache_name ) ) do |f|
11
+ cache = Marshal.load(f)
12
+ end
13
+
14
+ File.delete( cache_filename_for( cache_name ) )
15
+
16
+ $LOG.info "Loaded #{cache_name} cache"
17
+
18
+ return cache
19
+
20
+ rescue ArgumentError
21
+ $LOG.warn "#{cache_name} cache corrupt (bad characters in file)"
22
+ return nil
23
+
24
+ rescue EOFError
25
+ $LOG.warn "#{cache_name} cache corrupt (unexpected end of file)"
26
+ return nil
27
+
28
+ rescue Errno::ENOENT
29
+ $LOG.warn "#{cache_name} cache not found"
30
+ return nil
31
+ end
32
+
33
+ def save_cache( cache_name, cache_object )
34
+ return nil unless @cache_folder
35
+ File.open( cache_filename_for( cache_name ), 'w' ) do |f|
36
+ f.puts Marshal.dump(cache_object)
37
+ end
38
+ end
39
+
40
+ def cache_filename_for( name )
41
+ File.join( @cache_folder, "#{name}#{CACHE_EXTENSION}")
42
+ end
43
+
44
+ end
45
+
1
46
  module WikiFlatFileStore
2
47
 
3
48
  CONTENT_EXTENSION = '.textile'
@@ -15,8 +60,22 @@ module WikiFlatFileStore
15
60
  end
16
61
  end
17
62
 
63
+ def save( page )
64
+ save_content( page )
65
+ save_last_revision( page )
66
+ end
67
+
68
+ def delete_files_for_page( page_name )
69
+ File.delete( filename_for_content( page_name ), filename_for_revisions( page_name ) )
70
+ end
71
+
72
+ def move_files_for_page( old_page_name, new_page_name )
73
+ File.rename( filename_for_content( old_page_name ), filename_for_content( new_page_name ) )
74
+ File.rename( filename_for_revisions( old_page_name ), filename_for_revisions( new_page_name ) )
75
+ end
76
+
18
77
  def check_disk_for_updated_page( pagename, force = false )
19
- return unless force || $SETTINGS[:check_files_every] # We don't care about file changes
78
+ return unless force || self.check_files_every # We don't care about file changes
20
79
  filename = filename_for_content( pagename )
21
80
  return :file_does_not_exist unless File.exists?( filename ) # File doesn't exist on disk
22
81
  return load_page( filename ) unless page_named( pagename )# File is new on the disk, but not yet in memory
@@ -38,7 +97,7 @@ module WikiFlatFileStore
38
97
  if content_newer_than_revisions?( page ) # The textile file has been modified, but the array file has not been updated to match
39
98
  page.content = reconstruct_content_from_revisions( page.revisions )
40
99
  page.revise( disk_content, DEFAULT_AUTHOR )
41
- save_revisions( page )
100
+ save_last_revision( page )
42
101
  else # The textile file and the array file are in sync.
43
102
  page.content = disk_content
44
103
  end
@@ -84,24 +143,18 @@ module WikiFlatFileStore
84
143
  def move_files_if_names_are_not_url_encoded
85
144
  Dir[ File.join( @folder, "*#{CONTENT_EXTENSION}" ) ].each do |filename|
86
145
  basename = File.basename( filename, '.*')
87
- next if basename.url_decode.to_valid_pagename.url_encode == basename # All ok, so no worry
88
- new_name = File.join( File.dirname(filename), File.unique_filename( File.dirname(filename), basename.url_decode.to_valid_pagename.url_encode + File.extname( filename) ) )
146
+ next if basename.url_decode.url_encode == basename # All ok, so no worry
147
+ new_name = File.join( File.dirname(filename), File.unique_filename( File.dirname(filename), basename.url_decode.url_encode + File.extname( filename) ) )
89
148
  File.rename(filename, new_name )
90
149
  end
91
150
  end
92
-
93
- def save( page )
94
- add_page_to_index( page )
95
- save_content( page )
96
- save_revisions( page )
97
- end
98
151
 
99
152
  def save_content( page )
100
153
  File.open(filename_for_content( page.name ), 'w' ) { |file| file.puts page.content }
101
154
  end
102
155
 
103
156
  # Appends the last revision onto the yaml file
104
- def save_revisions( page )
157
+ def save_last_revision( page )
105
158
  $LOG.info "Saving revisions for #{page.name}"
106
159
  File.open(filename_for_revisions( page.name ), 'a' ) do |file|
107
160
  YAML.dump( page.revisions.last.to_a, file )
@@ -109,6 +162,16 @@ module WikiFlatFileStore
109
162
  end
110
163
  end
111
164
 
165
+ def save_all_revisions( page )
166
+ $LOG.warn "Saving all revisions for #{page.name}"
167
+ File.open(filename_for_revisions( page.name ), 'w' ) do |file|
168
+ page.revisions.each do |revision|
169
+ YAML.dump( revision.to_a, file )
170
+ file.puts # Needed to ensure that documents are separated
171
+ end
172
+ end
173
+ end
174
+
112
175
  def page_name_for( filename )
113
176
  File.basename( filename, '.*').url_decode
114
177
  end
data/lib/soks-utils.rb CHANGED
@@ -30,8 +30,10 @@ class EventQueue
30
30
  end
31
31
 
32
32
  def event( event, messages )
33
+ # $LOG.warn "#{event}, #{messages}"
33
34
  check_thread_ok
34
35
  @queue.enq [ event, messages ]
36
+ $LOG.warn "Notification queue backlog of #{@queue.size}" if @queue.size > 100
35
37
  notify_attentive_watchers( event, *messages )
36
38
  end
37
39
 
@@ -74,7 +76,7 @@ class EventQueue
74
76
  begin
75
77
  action_block.call(event, *messages)
76
78
  rescue StandardError => err
77
- $stderr.puts "ERROR #{err}: #{event} - #{messages.join(' ')}"
79
+ $LOG.warn "ERROR #{err}: #{event} - #{messages.join(' ')}"
78
80
  err.backtrace.each { |s| $stderr.puts s }
79
81
  end
80
82
  }
@@ -106,7 +108,31 @@ class EventQueue
106
108
  def attentive_watchers
107
109
  @attentive_watchers ||= {}
108
110
  end
111
+ end
112
+
113
+ class PeriodicNotification
114
+
115
+ def initialize( *notify_about, &block)
116
+ @block = block
117
+ notify_about.each do |period|
118
+ start_thread( period )
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def start_thread( period )
125
+ Thread.new( period ) do |period|
126
+ while true
127
+ sleep seconds_to_next_period( period )
128
+ @block.call( period )
129
+ end
130
+ end
131
+ end
109
132
 
133
+ def seconds_to_next_period( period )
134
+ Time.now.next( period ) - Time.now
135
+ end
110
136
  end
111
137
 
112
138
  class String
@@ -145,9 +171,15 @@ class String
145
171
  end
146
172
 
147
173
  # Removes punctuation and spaces that can cause problems with page names
148
- def to_valid_pagename
149
- self.tr('\\\[]?{}&^`<>/','').strip
174
+ #def to_valid_pagename
175
+ # self.tr('\\\[]?{}#&^`<>/','').strip
176
+ #end
177
+
178
+ #Returns the changes between the lines of this string and another
179
+ def changes_from( other_string )
180
+ other_string.split("\n").diff( self.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } }
150
181
  end
182
+
151
183
  end
152
184
 
153
185
  class FiniteUniqueList
@@ -223,13 +255,55 @@ class Numeric
223
255
  end
224
256
 
225
257
  class Time
258
+
259
+ # Returns 'yesterday', 'tomorrow' for date relative to now
260
+ def relative_day
261
+ # Days difference
262
+ case self.days_from( Time.now )
263
+ when -7..-2 ; strftime('Last %A')
264
+ when -1 ; "Yesterday"
265
+ when 0 ; "Today"
266
+ when 1 ; "Tomorrow"
267
+ when 2..7 ; strftime('%A')
268
+ else ; strftime( (Time.now.year == self.year) ? '%d %b' :'%d %b %Y')
269
+ end
270
+ end
226
271
 
272
+ def days_from( other_time )
273
+ ((Time.local(self.year, self.month, self.day)-Time.local(other_time.year, other_time.month, other_time.day))/(24*60*60)).round
274
+ end
275
+
227
276
  # Checks whether two times are on the same day
228
277
  def same_day?( other_time )
229
278
  return false unless other_time.year == self.year
230
279
  return false unless other_time.yday == self.yday
231
280
  return true
232
281
  end
282
+
283
+ # Returns the Time at the next occurance of the period
284
+ def next( period )
285
+ case period
286
+ when :sec, :second
287
+ next_time = self + 1
288
+ Time.local( next_time.year, next_time.month, next_time.day, next_time.hour, next_time.min, next_time.sec )
289
+ when :min, :minute
290
+ next_time = self + 60
291
+ Time.local( next_time.year, next_time.month, next_time.day, next_time.hour, next_time.min )
292
+ when :hour
293
+ next_time = self + ( 60*60 )
294
+ Time.local( next_time.year, next_time.month, next_time.day, next_time.hour)
295
+ when :day
296
+ next_time = self + ( 60*60*24 )
297
+ Time.local( next_time.year, next_time.month, next_time.day)
298
+ when :mon, :month
299
+ next_time = self + ( 60*60*24*(32-self.day) )
300
+ Time.local( next_time.year, next_time.month)
301
+ when :year
302
+ next_time = self + ( 60*60*24*(367-self.yday) )
303
+ Time.local( next_time.year )
304
+ end
305
+ end
306
+
233
307
  end
234
308
 
235
309
  class File