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
@@ -3,7 +3,7 @@ require 'net/imap'
3
3
  # From Dave Burt on comp.lang.ruby
4
4
  class String
5
5
  def from_quoted_printable
6
- self.gsub(/\r\n/, "\n").unpack("M").first
6
+ self.gsub(/\r\n/, "\n").gsub(/=(?![\dA-F]{2})/,'=3D').unpack("M").first
7
7
  end
8
8
  end
9
9
 
@@ -14,20 +14,16 @@ class Message
14
14
  def initialize( imap, message_id )
15
15
  @imap, @message_id = imap, message_id
16
16
  envelope = @imap.fetch( @message_id, 'ENVELOPE' ).first.attr['ENVELOPE']
17
- @subject = envelope['subject']
18
- @sender_name = envelope['from'].first['name']
17
+ @subject = envelope['subject'].gsub(/^(Fw|Re):?/i,'').strip
18
+ @sender_name = envelope['from'].first['name'].gsub(/@/,' at ')
19
19
  @date = envelope['date']
20
- @sender_email = envelope['from'].first['mailbox'] + '@' + envelope['from'].first['host']
20
+ @sender_email = envelope['from'].first['mailbox'] + ' at ' + envelope['from'].first['host']
21
+ @sender_name = @sender_email unless @sender_name && @sender_name.size > 1
21
22
  @text = plain_text_content_from_message( message_id )
22
23
  end
23
24
 
24
25
  def plain_text_content_from_message( id )
25
- text = @imap.fetch( id, 'BODY[1]' ).first.attr['BODY[1]'].from_quoted_printable
26
- text
27
- end
28
-
29
- def textile
30
- text.gsub(/[\*-]{2,}/,'').gsub(/([^\n])\n([^\n])/,'\1\2').gsub(/^[ \t]+/,'')
26
+ @imap.fetch( id, 'BODY[1]' ).first.attr['BODY[1]'].from_quoted_printable
31
27
  end
32
28
 
33
29
  end
@@ -38,31 +34,29 @@ class Mail2WikiHelper
38
34
  :server => 'imap.hermes.cam.ac.uk',
39
35
  :username => 'tamc2',
40
36
  :password => 'missing_a_password',
41
- :mailbox => 'mcr notices',
42
- :check_period => 600, # Seconds
37
+ :mailbox => 'test',
38
+ :check_event => :hour,
43
39
  :subject_regexp => /.*/,
40
+ :keyword => 'PutInWiki'
44
41
  }
45
42
 
46
43
  def initialize( wiki, settings = {} )
47
44
  @settings = DEFAULT_SETTINGS.merge( settings )
48
45
  @wiki = wiki
49
46
  check_mailbox
50
- Thread.new do
51
- loop do
52
- sleep @settings[:check_period ]
53
- check_mailbox
54
- end
55
- end
47
+ @wiki.watch_for(@settings[:check_event]) { check_mailbox }
56
48
  end
57
49
 
58
50
  private
59
51
 
60
52
  def check_mailbox
53
+ $LOG.info "Checking #{@settings[:mailbox]} on #{@settings[:server]}"
61
54
  login
62
55
  select_mailbox
63
56
  new_messages_for_wiki do |message_id|
64
57
  this_message = Message.new( @imap, message_id )
65
58
  if this_message.subject =~ @settings[:subject_regexp]
59
+ $LOG.info "Adding '#{this_message.subject}' to wiki"
66
60
  add_message_to_wiki( this_message )
67
61
  mark_as_added( message_id )
68
62
  end
@@ -78,8 +72,10 @@ class Mail2WikiHelper
78
72
  text = current_page.textile
79
73
  end
80
74
  text << "\n\n"
81
- text << "#{message.date} from #{message.sender_name} #{message.sender_email}\n"
82
- text << message.text
75
+ text << "*Copied from Email on #{message.date} from #{message.sender_name} (#{message.sender_email})*\n\n"
76
+ text << "<pre>\n"
77
+ text << message.text
78
+ text << "\n</pre>\n"
83
79
  @wiki.revise(message.subject, text, message.sender_name )
84
80
  end
85
81
 
@@ -98,11 +94,11 @@ class Mail2WikiHelper
98
94
  end
99
95
 
100
96
  def new_messages_for_wiki
101
- @imap.search('UNKEYWORD PutInWiki').each { |id| yield id }
97
+ @imap.search("UNKEYWORD #{@settings[:keyword]}").each { |id| yield id }
102
98
  end
103
99
 
104
100
  def mark_as_added( id )
105
- # @imap.store( id, '+FLAGS', ['PutInWiki'] )
101
+ @imap.store( id, '+FLAGS', [@settings[:keyword]] )
106
102
  end
107
103
 
108
104
  end
@@ -0,0 +1,149 @@
1
+ class DeleteOldPagesHelper
2
+
3
+ # Default wakes up each day at midnight and wipes all deleted pages more than 100 days old
4
+ def initialize( wiki, event_to_check_on = :day, age_to_wipe_at = 60*60*24*100 )
5
+ @wiki = wiki
6
+ @age_to_wipe_at = age_to_wipe_at
7
+ @wiki.watch_for(event_to_check_on) { check_and_delete_pages }
8
+ end
9
+
10
+ private
11
+
12
+ def check_and_delete_pages
13
+ @wiki.each(false) do |name, page|
14
+ next unless page.deleted?
15
+ next unless old_enough_to_wipe?( page )
16
+ $LOG.warn "Permanently wiping #{name} from wiki AND from disk"
17
+ @wiki.wipe_from_disk( name )
18
+ end
19
+ end
20
+
21
+ def old_enough_to_wipe?( page )
22
+ (Time.now - page.revised_on) > @age_to_wipe_at
23
+ end
24
+
25
+ end
26
+
27
+ class DeleteOldRevisionsHelper
28
+
29
+ AUTHOR = 'Automatic Revision Remover'
30
+
31
+ # Default wakes up each day at midnight and wipes all revisions more than 100 days old if there are more than 20 revisions in the page
32
+ def initialize( wiki, event_to_check_on = :day, age_to_wipe_at = 60*60*24*365, maximum_revisions = 20 )
33
+ @wiki = wiki
34
+ @age_to_wipe_at = age_to_wipe_at
35
+ @maximum_revisions = maximum_revisions
36
+ @wiki.watch_for(event_to_check_on) { check_and_delete_revisions }
37
+ end
38
+
39
+ private
40
+
41
+ def check_and_delete_revisions
42
+ @wiki.each do |name, page|
43
+ next unless page.revisions.size > @maximum_revisions
44
+ next unless old_enough_to_delete?( page.revisions.first )
45
+ delete_old_revisions_from( page )
46
+ end
47
+ end
48
+
49
+ def delete_old_revisions_from( page )
50
+ page.content_lock.synchronize do
51
+ delete_revisions_at_or_below = page.revisions.size - @maximum_revisions - 1
52
+ delete_revisions_at_or_below -= 1 while not old_enough_to_delete? page.revisions[delete_revisions_at_or_below]
53
+
54
+ new_revisions = []
55
+ new_revisions << Revision.new( page,
56
+ new_revisions.length,
57
+ page.revisions[delete_revisions_at_or_below].content.changes_from(""),
58
+ AUTHOR,
59
+ page.revisions[delete_revisions_at_or_below].created_on )
60
+
61
+ page.revisions[ (delete_revisions_at_or_below+1)..page.revisions.size].each do |revision|
62
+ new_revisions << Revision.new( page,
63
+ new_revisions.length,
64
+ revision.changes,
65
+ revision.author,
66
+ revision.created_on )
67
+ end
68
+ page.revisions = new_revisions
69
+ @wiki.save_all_revisions( page )
70
+ end
71
+ end
72
+
73
+ def old_enough_to_delete?( revision )
74
+ (Time.now - revision.created_on) > @age_to_wipe_at
75
+ end
76
+ end
77
+
78
+ class MergeOldRevisionsHelper
79
+
80
+ # Default wakes up each hour and merges all revisions more than 24 hours old, by the same author, and that are created within an hour of each other
81
+ def initialize( wiki, event_to_check_on = :day, minimum_age_to_merge = 60*60*24*365, maximum_time_between_revisions_for_merge = 60*60 )
82
+ @wiki = wiki
83
+ @minimum_age_to_merge = minimum_age_to_merge
84
+ @maximum_time_between_revisions_for_merge = maximum_time_between_revisions_for_merge
85
+ @wiki.watch_for(event_to_check_on) { check_for_pages_to_merge }
86
+ end
87
+
88
+ def check_for_pages_to_merge
89
+ @wiki.each do |name, page|
90
+ page.content_lock.synchronize do
91
+ check_revisions_to_merge_on page
92
+ end
93
+ end
94
+ end
95
+
96
+ def check_revisions_to_merge_on( page )
97
+ return if page.empty?
98
+ change_made = false
99
+ new_revisions = []
100
+ next_revision = page.revisions.first
101
+ while next_revision
102
+ ending_revision = next_revision
103
+
104
+ while can_merge?( next_revision, ending_revision.following_revision )
105
+ ending_revision = ending_revision.following_revision
106
+ end
107
+
108
+ changes = if ending_revision == next_revision
109
+ next_revision.changes
110
+ else
111
+ change_made = true
112
+ ending_revision.content.changes_from( next_revision.previous_content )
113
+ end
114
+
115
+ new_revisions << Revision.new( page,
116
+ new_revisions.length,
117
+ changes,
118
+ ending_revision.author,
119
+ ending_revision.created_on )
120
+
121
+ next_revision = ending_revision.following_revision
122
+ end
123
+ if change_made
124
+ page.revisions = new_revisions
125
+ @wiki.save_all_revisions( page )
126
+ end
127
+ end
128
+
129
+ def can_merge?( revision_a, revision_b )
130
+ return false unless revision_a && revision_b
131
+ return false unless same_author?( revision_a, revision_b )
132
+ return false unless revised_at_a_similar_time?( revision_a, revision_b )
133
+ return false unless not_to_recent?( revision_a )
134
+ return false unless not_to_recent?( revision_b )
135
+ true
136
+ end
137
+
138
+ def same_author?( a, b )
139
+ a.author == b.author
140
+ end
141
+
142
+ def revised_at_a_similar_time?( a, b )
143
+ (a.revised_on - b.revised_on).abs < @maximum_time_between_revisions_for_merge
144
+ end
145
+
146
+ def not_to_recent?( a_revision )
147
+ (Time.now - a_revision.revised_on) > @minimum_age_to_merge
148
+ end
149
+ end
@@ -7,19 +7,18 @@ class RSS2WikiHelper
7
7
  DEFAULT_SETTINGS = {
8
8
  :url => 'http://localhost:8000/rss/recent%20changes%20to%20this%20site',
9
9
  :pagename => nil, # If nil, uses channel title,
10
- :check_frequency => 1200,
10
+ :update_on_event => :hour,
11
11
  :author => 'AutomaticRSS2Wiki',
12
12
  }
13
13
 
14
14
  def initialize( wiki, settings = {} )
15
15
  @settings = DEFAULT_SETTINGS.merge( settings )
16
16
  @wiki = wiki
17
- Thread.new do
18
- loop do
19
- update_rss
20
- update_wiki
21
- sleep @settings[ :check_frequency ]
22
- end
17
+ update_rss
18
+ update_wiki
19
+ @wiki.watch_for(@settings[:update_on_event]) do
20
+ update_rss
21
+ update_wiki
23
22
  end
24
23
  end
25
24
 
@@ -36,7 +35,7 @@ class RSS2WikiHelper
36
35
  end
37
36
 
38
37
  def update_rss
39
- $stderr.puts "Updating feed"
38
+ $LOG.info "Updating feed"
40
39
  open(@settings[:url]) do |http|
41
40
  @rss = RSS::Parser.parse( http.read , false)
42
41
  end
data/lib/soks-model.rb CHANGED
@@ -2,8 +2,8 @@ require 'thread'
2
2
 
3
3
  # Revision stores changes as a diff against the more recent content version
4
4
  class Revision
5
- attr_reader :number, :author, :page
6
- attr_reader :changes, :created_on
5
+ attr_reader :number, :author, :page
6
+ attr_reader :changes, :created_on
7
7
  alias :revised_on :created_on
8
8
 
9
9
  def initialize( page, number, changes, author, created_on = Time.now )
@@ -13,7 +13,7 @@ class Revision
13
13
  # Recreates the content of the page AFTER this revision had been made.
14
14
  # Done by recursively applying diffs to more recent versions.
15
15
  def content
16
- page.revision( @number + 1 ) ? page.revision( @number + 1 ).previous_content : page.content
16
+ following_revision ? following_revision.previous_content : page.content
17
17
  end
18
18
 
19
19
  # Recreateds the content of the page BEFORE this revision had been made
@@ -21,8 +21,14 @@ class Revision
21
21
  content.split("\n").unpatch!(@changes).join("\n")
22
22
  end
23
23
 
24
- # Delegates to the Page object so that can see previous versions
25
- def textile( content = self.content ) page.textile( content ) end
24
+ def previous_revision
25
+ return nil if number == 0
26
+ page.revision( number - 1 )
27
+ end
28
+
29
+ def following_revision
30
+ page.revision( number + 1 )
31
+ end
26
32
 
27
33
  def method_missing( symbol, *args )
28
34
  raise(ArgumentError, "Revision does not respond to #{symbol}", caller) unless @page && @page.respond_to?( symbol )
@@ -41,10 +47,9 @@ class Page
41
47
 
42
48
  # Returns an empty version of itself.
43
49
  def self.empty( name )
44
- empty = self.new( name )
45
- empty.revise( $MESSAGES[:Type_what_you_want_here_and_click_save], "NoOne" )
50
+ empty = self.new( name )
51
+ empty.revise( $MESSAGES[:Type_what_you_want_here_and_click_save], "NoOne" )
46
52
  class << empty
47
- def textile; "[[#{$MESSAGES[:Create]} #{name} => /edit/#{name} ]]" end
48
53
  def empty?; true; end
49
54
  end
50
55
  empty
@@ -60,15 +65,9 @@ class Page
60
65
  # Revises the content of this page, creating a new revision class that stores the changes
61
66
  def revise( new_content, author )
62
67
  return nil if new_content == @content
63
- #I've abandoned this, because it tends to confuse users.
64
- #if the_same_author_recently_revised_this_page( author )
65
- # changes = changes_between( previous_content, new_content )
66
- # @revisions[-1] = Revision.new( self, @revisions.length - 1, changes , author )
67
- #else
68
- changes = changes_between( @content, new_content )
69
- return nil if changes.empty?
70
- @revisions << Revision.new( self, @revisions.length, changes , author )
71
- #end
68
+ changes = new_content.changes_from @content
69
+ return nil if changes.empty?
70
+ @revisions << Revision.new( self, @revisions.length, changes , author )
72
71
  @content = new_content
73
72
  @revisions.last
74
73
  end
@@ -76,13 +75,13 @@ class Page
76
75
  # Returns the content of this page to that of a previous version
77
76
  def rollback( number, author )
78
77
  revise( ( number < 0 ) ? $MESSAGES[:page_deleted] : @revisions[ number ].content, author )
79
- end
78
+ end
80
79
 
81
80
  def revision( number ) @revisions[ number ] end
82
81
 
83
82
  def deleted?
84
83
  ( content =~ /^#{$MESSAGES[:page_deleted]}/i ) ||
85
- ( content =~ /^#{$MESSAGES[:content_moved_to]} /i )
84
+ ( content =~ /^#{$MESSAGES[:content_moved_to]} /i ) ? true : false
86
85
  end
87
86
 
88
87
  def empty?; @revisions.empty? end
@@ -96,10 +95,10 @@ class Page
96
95
  def is_inserted_into( page )
97
96
  @links_lock.synchronize { @inserted_into << page unless @inserted_into.include? page }
98
97
  end
99
-
100
- def textile( content = @content ) content end
101
98
 
102
99
  def name_for_index; name.downcase end
100
+
101
+ # Refactored changes_between into the String class in soks-utils
103
102
 
104
103
  # Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc)
105
104
  def method_missing( symbol, *args )
@@ -109,20 +108,10 @@ class Page
109
108
  raise ArgumentError,"Page does not respond to #{symbol}", caller
110
109
  end
111
110
  end
112
-
113
- private
114
-
115
- def changes_between( old_content, new_content )
116
- old_content.split("\n").diff( new_content.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } }
117
- end
118
-
119
- # This is no longer used, because I've found it confuses people.
120
- def the_same_author_recently_revised_this_page( author )
121
- return false if empty?
122
- return false unless author == self.author
123
- ((Time.now - self.revised_on) < (60*5) ) # 5 Minutes
124
- end
125
-
111
+ end
112
+
113
+ class EmptyPage < Page
114
+ def empty?; true; end
126
115
  end
127
116
 
128
117
  #Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates
@@ -130,43 +119,50 @@ class UploadPage < Page
130
119
  end
131
120
 
132
121
  class ImagePage < UploadPage
133
- def textile( content = @content )
134
- deleted? ? content : "!#{$SETTINGS[:url]}#{content.strip}!:#{$SETTINGS[:url]}/view/#{@name.url_encode}"
135
- end
136
-
137
122
  def name_for_index; @name[10..-1].strip.downcase end
138
123
  end
139
124
 
140
125
  class AttachmentPage < UploadPage
141
- def textile( content = @content )
142
- deleted? ? content : %Q{[[ #{name} => #{$SETTINGS[:url]}#{content} ]]\n}
143
- end
144
126
  def name_for_index; @name[ 9..-1].strip.downcase end
145
127
  end
146
128
 
147
129
  # This class has turned into a behmoth, need to refactor.
148
130
  class Wiki
149
131
  include WikiFlatFileStore
132
+ include WikiCacheStore
150
133
  include Enumerable
151
134
  include Notify # Will notify any watchers if underlying files change
135
+
136
+ attr_accessor :check_files_every
152
137
 
153
138
  PAGE_CLASSES = [
154
139
  [ /^picture of/i, ImagePage ],
155
140
  [ /^attached/i, AttachmentPage ],
156
141
  [ /.*/, Page ]
157
142
  ]
143
+
144
+ CACHE_NAME = 'pages'
158
145
 
159
- def initialize( folder )
160
- @folder = folder
161
- @pages = {}
146
+ def initialize( content_folder, cache_folder = nil )
147
+ @cache_folder = cache_folder
148
+ @folder = content_folder
149
+ @pages = load_cache(CACHE_NAME) || {}
162
150
  @shutting_down = false
151
+ @check_files_every = nil
152
+ watch_for(:start) { start }
153
+ end
154
+
155
+ def start
163
156
  load_all_pages
164
157
  start_watching_files
158
+ setup_periodic_notifications
165
159
  end
166
160
 
167
161
  def shutdown
162
+ notify :shutdown
168
163
  sleep(1) until event_queue.empty?
169
164
  @shutting_down = true # Stop further modifications
165
+ save_cache(CACHE_NAME, @pages)
170
166
  end
171
167
 
172
168
  def page( name )
@@ -178,7 +174,7 @@ class Wiki
178
174
  end
179
175
 
180
176
  def exists?( name )
181
- @pages.include?( name.downcase ) && !page( name.downcase ).deleted?
177
+ @pages.include?( name.downcase ) && !page_named( name ).deleted?
182
178
  end
183
179
 
184
180
  def revise( pagename, content, author )
@@ -187,22 +183,43 @@ class Wiki
187
183
  mutate( pagename ) { |page| page.revise( content, author ) }
188
184
  end
189
185
 
186
+ def move( old_pagename, new_pagename, author )
187
+ old_content = page(old_pagename).content
188
+ revise( old_pagename, "#{$MESSAGES[:content_moved_to]} [[#{new_pagename}]]", author )
189
+ revise( new_pagename, "#{$MESSAGES[:content_moved_from]} [[#{old_pagename}]]", author )
190
+ revise( new_pagename, old_content, author )
191
+ end
192
+
190
193
  def rollback( pagename, number, author )
191
194
  raise "Sorry! Shutting down..." if @shutting_down
192
195
  check_disk_for_updated_page pagename
193
196
  mutate( pagename ) { |page| page.rollback( number, author ) }
194
197
  end
195
198
 
199
+ def delete( pagename, author )
200
+ revise( pagename, $MESSAGES[:page_deleted], author )
201
+ end
202
+
203
+ def wipe_from_disk( pagename )
204
+ page = page_named( pagename )
205
+ raise "Page not deleted!" unless page.deleted?
206
+ page.content_lock.synchronize do
207
+ delete_files_for_page( pagename.downcase )
208
+ @pages.delete( pagename.downcase )
209
+ end
210
+ end
211
+
196
212
  private
197
213
 
214
+ def setup_periodic_notifications
215
+ PeriodicNotification.new( :year, :month, :day, :hour, :min ) do |period|
216
+ notify period unless @shutting_down
217
+ end
218
+ end
219
+
198
220
  def start_watching_files
199
- return unless $SETTINGS[:check_files_every]
200
- Thread.new do
201
- loop do
202
- sleep $SETTINGS[:check_files_every]
203
- load_all_pages
204
- end
205
- end.priority = -5
221
+ return unless check_files_every
222
+ watch_for(check_files_every) { load_all_pages }
206
223
  end
207
224
 
208
225
  def new_page( name, initializer = :new )
@@ -216,8 +233,19 @@ class Wiki
216
233
  page = page_named( pagename ) || new_page( pagename )
217
234
  revision = nil
218
235
  page.content_lock.synchronize do
236
+ # Check if the capitalisation of the page has changed
237
+ unless page.name == pagename
238
+ move_files_for_page( page.name, pagename )
239
+ page.name = pagename
240
+ notify :page_title_recapitalized, page
241
+ end
242
+ # Yield to the mutator block
219
243
  revision, dont_save = yield page
220
- save page if revision && dont_save != :dont_save
244
+ # Save page if required
245
+ if revision && dont_save != :dont_save
246
+ save page
247
+ add_page_to_index( page )
248
+ end
221
249
  end
222
250
  if revision
223
251
  notify :page_revised, page, revision