Soks 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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