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
@@ -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