rethoth 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/bin/thoth +233 -0
  4. data/lib/proto/config.ru +45 -0
  5. data/lib/proto/thoth.conf.sample +206 -0
  6. data/lib/thoth/cache.rb +53 -0
  7. data/lib/thoth/config.rb +158 -0
  8. data/lib/thoth/controller/admin.rb +75 -0
  9. data/lib/thoth/controller/api/comment.rb +73 -0
  10. data/lib/thoth/controller/api/page.rb +134 -0
  11. data/lib/thoth/controller/api/post.rb +122 -0
  12. data/lib/thoth/controller/api/tag.rb +59 -0
  13. data/lib/thoth/controller/archive.rb +50 -0
  14. data/lib/thoth/controller/comment.rb +173 -0
  15. data/lib/thoth/controller/main.rb +193 -0
  16. data/lib/thoth/controller/media.rb +172 -0
  17. data/lib/thoth/controller/page.rb +167 -0
  18. data/lib/thoth/controller/post.rb +310 -0
  19. data/lib/thoth/controller/search.rb +86 -0
  20. data/lib/thoth/controller/tag.rb +107 -0
  21. data/lib/thoth/controller.rb +48 -0
  22. data/lib/thoth/errors.rb +35 -0
  23. data/lib/thoth/helper/admin.rb +86 -0
  24. data/lib/thoth/helper/cookie.rb +45 -0
  25. data/lib/thoth/helper/error.rb +122 -0
  26. data/lib/thoth/helper/pagination.rb +131 -0
  27. data/lib/thoth/helper/wiki.rb +77 -0
  28. data/lib/thoth/helper/ysearch.rb +89 -0
  29. data/lib/thoth/importer/pants.rb +81 -0
  30. data/lib/thoth/importer/poseidon.rb +54 -0
  31. data/lib/thoth/importer/thoth.rb +103 -0
  32. data/lib/thoth/importer.rb +131 -0
  33. data/lib/thoth/layout/default.rhtml +47 -0
  34. data/lib/thoth/middleware/minify.rb +82 -0
  35. data/lib/thoth/migrate/001_create_schema.rb +108 -0
  36. data/lib/thoth/migrate/002_add_media_size.rb +37 -0
  37. data/lib/thoth/migrate/003_add_post_draft.rb +38 -0
  38. data/lib/thoth/migrate/004_add_comment_email.rb +37 -0
  39. data/lib/thoth/migrate/005_add_page_position.rb +37 -0
  40. data/lib/thoth/migrate/006_add_comment_close_delete.rb +43 -0
  41. data/lib/thoth/migrate/007_add_comment_summary.rb +37 -0
  42. data/lib/thoth/model/comment.rb +216 -0
  43. data/lib/thoth/model/media.rb +87 -0
  44. data/lib/thoth/model/page.rb +204 -0
  45. data/lib/thoth/model/post.rb +262 -0
  46. data/lib/thoth/model/tag.rb +80 -0
  47. data/lib/thoth/model/tags_posts_map.rb +34 -0
  48. data/lib/thoth/monkeypatch/sequel/model/errors.rb +37 -0
  49. data/lib/thoth/plugin/thoth_delicious.rb +105 -0
  50. data/lib/thoth/plugin/thoth_flickr.rb +86 -0
  51. data/lib/thoth/plugin/thoth_pinboard.rb +98 -0
  52. data/lib/thoth/plugin/thoth_tags.rb +68 -0
  53. data/lib/thoth/plugin/thoth_twitter.rb +175 -0
  54. data/lib/thoth/plugin.rb +59 -0
  55. data/lib/thoth/public/css/admin.css +223 -0
  56. data/lib/thoth/public/css/thoth.css +592 -0
  57. data/lib/thoth/public/images/admin-sprite.png +0 -0
  58. data/lib/thoth/public/images/thoth-sprite.png +0 -0
  59. data/lib/thoth/public/js/admin/comments.js +116 -0
  60. data/lib/thoth/public/js/admin/name.js +244 -0
  61. data/lib/thoth/public/js/admin/tagcomplete.js +332 -0
  62. data/lib/thoth/public/js/lazyload-min.js +4 -0
  63. data/lib/thoth/public/js/thoth.js +203 -0
  64. data/lib/thoth/public/robots.txt +5 -0
  65. data/lib/thoth/version.rb +37 -0
  66. data/lib/thoth/view/admin/index.rhtml +1 -0
  67. data/lib/thoth/view/admin/login.rhtml +23 -0
  68. data/lib/thoth/view/admin/toolbar.rhtml +117 -0
  69. data/lib/thoth/view/admin/welcome.rhtml +58 -0
  70. data/lib/thoth/view/archive/index.rhtml +24 -0
  71. data/lib/thoth/view/comment/comment.rhtml +47 -0
  72. data/lib/thoth/view/comment/delete.rhtml +15 -0
  73. data/lib/thoth/view/comment/form.rhtml +81 -0
  74. data/lib/thoth/view/comment/index.rhtml +68 -0
  75. data/lib/thoth/view/comment/list.rhtml +48 -0
  76. data/lib/thoth/view/media/delete.rhtml +15 -0
  77. data/lib/thoth/view/media/edit.rhtml +12 -0
  78. data/lib/thoth/view/media/form.rhtml +7 -0
  79. data/lib/thoth/view/media/list.rhtml +35 -0
  80. data/lib/thoth/view/media/media.rhtml +44 -0
  81. data/lib/thoth/view/media/new.rhtml +7 -0
  82. data/lib/thoth/view/page/delete.rhtml +15 -0
  83. data/lib/thoth/view/page/edit.rhtml +15 -0
  84. data/lib/thoth/view/page/form.rhtml +57 -0
  85. data/lib/thoth/view/page/index.rhtml +9 -0
  86. data/lib/thoth/view/page/list.rhtml +49 -0
  87. data/lib/thoth/view/page/new.rhtml +15 -0
  88. data/lib/thoth/view/post/comments.rhtml +12 -0
  89. data/lib/thoth/view/post/compact.rhtml +48 -0
  90. data/lib/thoth/view/post/delete.rhtml +15 -0
  91. data/lib/thoth/view/post/edit.rhtml +15 -0
  92. data/lib/thoth/view/post/form.rhtml +83 -0
  93. data/lib/thoth/view/post/index.rhtml +48 -0
  94. data/lib/thoth/view/post/list.rhtml +61 -0
  95. data/lib/thoth/view/post/new.rhtml +15 -0
  96. data/lib/thoth/view/post/tiny.rhtml +4 -0
  97. data/lib/thoth/view/search/index.rhtml +45 -0
  98. data/lib/thoth/view/tag/index.rhtml +34 -0
  99. data/lib/thoth/view/thoth/css.rhtml +9 -0
  100. data/lib/thoth/view/thoth/footer.rhtml +15 -0
  101. data/lib/thoth/view/thoth/header.rhtml +23 -0
  102. data/lib/thoth/view/thoth/index.rhtml +11 -0
  103. data/lib/thoth/view/thoth/js.rhtml +6 -0
  104. data/lib/thoth/view/thoth/sidebar.rhtml +38 -0
  105. data/lib/thoth/view/thoth/util/pager.rhtml +23 -0
  106. data/lib/thoth/view/thoth/util/simple_pager.rhtml +15 -0
  107. data/lib/thoth/view/thoth/util/table_sortheader.rhtml +20 -0
  108. data/lib/thoth.rb +394 -0
  109. metadata +409 -0
@@ -0,0 +1,37 @@
1
+ #--
2
+ # Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of this project nor the names of its contributors may be
14
+ # used to endorse or promote products derived from this software without
15
+ # specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ #++
28
+
29
+ class AddCommentSummary < Sequel::Migration
30
+ def down
31
+ drop_column(:comments, :summary)
32
+ end
33
+
34
+ def up
35
+ add_column(:comments, :summary, :varchar, :default => '', :null => false)
36
+ end
37
+ end
@@ -0,0 +1,216 @@
1
+ #--
2
+ # Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of this project nor the names of its contributors may be
14
+ # used to endorse or promote products derived from this software without
15
+ # specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ #++
28
+
29
+ require 'digest/md5'
30
+ require 'strscan'
31
+
32
+ module Thoth
33
+ class Comment < Sequel::Model
34
+ plugin :hook_class_methods
35
+ plugin :validation_helpers
36
+
37
+ CONFIG_SANITIZE = {
38
+ :elements => [
39
+ 'a', 'b', 'blockquote', 'br', 'code', 'dd', 'dl', 'dt', 'em', 'i',
40
+ 'li', 'ol', 'p', 'pre', 'small', 'strike', 'strong', 'sub', 'sup',
41
+ 'u', 'ul'
42
+ ],
43
+
44
+ :attributes => {
45
+ 'a' => ['href', 'title'],
46
+ 'blockquote' => ['cite'],
47
+ 'pre' => ['class']
48
+ },
49
+
50
+ :add_attributes => {'a' => {'rel' => 'nofollow'}},
51
+ :protocols => {'a' => {'href' => ['ftp', 'http', 'https', 'mailto']}}
52
+ }
53
+
54
+ before_create do
55
+ self.created_at = Time.now
56
+ end
57
+
58
+ before_save do
59
+ self.updated_at = Time.now
60
+ end
61
+
62
+ #--
63
+ # Class Methods
64
+ #++
65
+
66
+ # Recently-posted comments (up to _limit_) sorted in reverse order by
67
+ # creation time.
68
+ def self.recent(page = 1, limit = 10)
69
+ filter(:deleted => false).reverse_order(:created_at).paginate(page, limit)
70
+ end
71
+
72
+ #--
73
+ # Instance Methods
74
+ #++
75
+
76
+ def author=(author)
77
+ self[:author] = author.strip unless author.nil?
78
+ end
79
+
80
+ def author_email=(email)
81
+ @gravatar_url = nil
82
+ self[:author_email] = email.strip unless email.nil?
83
+ end
84
+
85
+ def author_url=(url)
86
+ self[:author_url] = url.strip unless url.nil?
87
+ end
88
+
89
+ def body=(body)
90
+ body = body.force_encoding('utf-8')
91
+ redcloth = RedCloth.new(body, [:filter_styles])
92
+
93
+ self[:body] = body
94
+ self[:body_rendered] = insert_breaks(Sanitize.clean(redcloth.to_html(
95
+ :refs_textile,
96
+ :block_textile_lists,
97
+ :inline_textile_link,
98
+ :inline_textile_code,
99
+ :glyphs_textile,
100
+ :inline_textile_span
101
+ ), CONFIG_SANITIZE))
102
+
103
+ summary = Sanitize.clean(body[0..128].gsub(/[\r\n]/, ' '))
104
+
105
+ if summary.length >= 64
106
+ summary = summary[0..64] + '...'
107
+ end
108
+
109
+ self[:summary] = summary
110
+ end
111
+
112
+ # Gets the creation time of this comment. If _format_ is provided, the time
113
+ # will be returned as a formatted String. See Time.strftime for details.
114
+ def created_at(format = nil)
115
+ if new?
116
+ format ? Time.now.strftime(format) : Time.now
117
+ else
118
+ format ? self[:created_at].strftime(format) : self[:created_at]
119
+ end
120
+ end
121
+
122
+ # Gets the Gravatar URL for this comment.
123
+ def gravatar_url
124
+ return @gravatar_url if @gravatar_url
125
+
126
+ md5 = Digest::MD5.hexdigest((author_email || author).downcase)
127
+ default = CGI.escape(Config.site['gravatar']['default'])
128
+ rating = Config.site['gravatar']['rating']
129
+ size = Config.site['gravatar']['size']
130
+
131
+ @gravatar_url = "http://www.gravatar.com/avatar/#{md5}.jpg?d=#{default}&r=#{rating}&s=#{size}"
132
+ end
133
+
134
+ # Gets the post to which this comment is attached.
135
+ def post
136
+ @post ||= Post[post_id]
137
+ end
138
+
139
+ def relative_url
140
+ new? ? '#' : "#comment-#{id}"
141
+ end
142
+
143
+ def title=(title)
144
+ self[:title] = title.strip unless title.nil?
145
+ end
146
+
147
+ # Gets the time this comment was last updated. If _format_ is provided, the
148
+ # time will be returned as a formatted String. See Time.strftime for details.
149
+ def updated_at(format = nil)
150
+ if new?
151
+ format ? Time.now.strftime(format) : Time.now
152
+ else
153
+ format ? self[:updated_at].strftime(format) : self[:updated_at]
154
+ end
155
+ end
156
+
157
+ # URL for this comment.
158
+ def url
159
+ new? ? '#' : post.url + "#comment-#{id}"
160
+ end
161
+
162
+ def validate
163
+ validates_presence(:author, :message => 'Please enter your name.')
164
+
165
+ validates_max_length(64, :author, :message => 'Please enter a name under 64 characters.')
166
+ validates_max_length(255, :author_email, :message => 'Please enter a shorter email address.')
167
+ validates_max_length(255, :author_url, :message => 'Please enter a shorter URL.')
168
+ validates_max_length(65536, :body, :message => 'You appear to be writing a novel. Please try to keep it under 64K.')
169
+
170
+ validates_format(/[^\s@]+@[^\s@]+\.[^\s@]+/, :author_email, :message => 'Please enter a valid email address.')
171
+ validates_format(/^(?:$|https?:\/\/\S+\.\S+)/i, :author_url, :message => 'Please enter a valid URL or leave the URL field blank.')
172
+ end
173
+
174
+ protected
175
+
176
+ # Inserts <wbr /> tags in long strings without spaces, while being careful
177
+ # not to break HTML tags.
178
+ def insert_breaks(str, length = 30)
179
+ scanner = StringScanner.new(str)
180
+
181
+ char = ''
182
+ count = 0
183
+ in_tag = 0
184
+ new_str = ''
185
+
186
+ while char = scanner.getch do
187
+ case char
188
+ when '<'
189
+ in_tag += 1
190
+
191
+ when '>'
192
+ in_tag -= 1
193
+ in_tag = 0 if in_tag < 0
194
+
195
+ when /\s/
196
+ count = 0 if in_tag == 0
197
+
198
+ else
199
+ if in_tag == 0
200
+ if count == length
201
+ new_str << '<wbr />'
202
+ count = 0
203
+ end
204
+
205
+ count += 1
206
+ end
207
+ end
208
+
209
+ new_str << char
210
+ end
211
+
212
+ return new_str
213
+ end
214
+
215
+ end
216
+ end
@@ -0,0 +1,87 @@
1
+ #--
2
+ # Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of this project nor the names of its contributors may be
14
+ # used to endorse or promote products derived from this software without
15
+ # specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ #++
28
+
29
+ module Thoth
30
+ class Media < Sequel::Model(:media)
31
+ plugin :hook_class_methods
32
+
33
+ before_create do
34
+ self.created_at = Time.now
35
+ end
36
+
37
+ before_destroy do
38
+ FileUtils.rm(path)
39
+ end
40
+
41
+ before_save do
42
+ self.updated_at = Time.now
43
+ end
44
+
45
+ # Gets the creation time of this file. If _format_ is provided, the time
46
+ # will be returned as a formatted String. See Time.strftime for details.
47
+ def created_at(format = nil)
48
+ if new?
49
+ format ? Time.now.strftime(format) : Time.now
50
+ else
51
+ format ? self[:created_at].strftime(format) : self[:created_at]
52
+ end
53
+ end
54
+
55
+ def filename=(filename)
56
+ self[:filename] = filename.strip unless filename.nil?
57
+ end
58
+
59
+ # Gets the absolute path to this file.
60
+ def path
61
+ File.join(Config.media, filename[0].chr.downcase, filename)
62
+ end
63
+
64
+ def size
65
+ return self[:size] unless self[:size] == 0 && File.exist?(path)
66
+
67
+ self[:size] = File.size(path)
68
+ save
69
+ self[:size]
70
+ end
71
+
72
+ # Gets the time this file was last updated. If _format_ is provided, the time
73
+ # will be returned as a formatted String. See Time.strftime for details.
74
+ def updated_at(format = nil)
75
+ if new?
76
+ format ? Time.now.strftime(format) : Time.now
77
+ else
78
+ format ? self[:updated_at].strftime(format) : self[:updated_at]
79
+ end
80
+ end
81
+
82
+ # URL for this file.
83
+ def url
84
+ Config.site['url'].chomp('/') + MediaController.r(:/, filename).to_s
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,204 @@
1
+ #--
2
+ # Copyright (c) 2017 John Pagonis <john@pagonis.org>
3
+ # Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright notice,
10
+ # this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ # * Neither the name of this project nor the names of its contributors may be
15
+ # used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
22
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ #++
29
+
30
+ module Thoth
31
+ class Page < Sequel::Model
32
+ include Thoth::Helper::Wiki
33
+
34
+ plugin :hook_class_methods
35
+ plugin :validation_helpers
36
+ plugin :blacklist_security
37
+
38
+ after_destroy do
39
+ Page.normalize_positions
40
+ end
41
+
42
+ before_create do
43
+ self.created_at = Time.now
44
+ end
45
+
46
+ before_save do
47
+ self.updated_at = Time.now
48
+ end
49
+
50
+ set_restricted_columns :position
51
+
52
+ #--
53
+ # Class Methods
54
+ #++
55
+
56
+ # Returns true if the specified page name is already taken or is a reserved
57
+ # name.
58
+ def self.name_unique?(name)
59
+ !PageController.methods.include?(name) &&
60
+ !PageController.instance_methods.include?(name) &&
61
+ !Page[:name => name.to_s.downcase]
62
+ end
63
+
64
+ # Returns true if the specified page name consists of valid characters and
65
+ # is not too long or too short.
66
+ def self.name_valid?(name)
67
+ !!(name =~ /^[0-9a-z_-]{1,64}$/i)
68
+ end
69
+
70
+ # Adjusts the position values of all pages, resolving duplicate positions
71
+ # and eliminating gaps.
72
+ def self.normalize_positions
73
+ db.transaction do
74
+ i = 1
75
+
76
+ order(:position).all do |page|
77
+ unless page.position == i
78
+ filter(:id => page.id).update(:position => i)
79
+ end
80
+
81
+ i += 1
82
+ end
83
+ end
84
+ end
85
+
86
+ # Sets the display position of the specified page, adjusting the position of
87
+ # other pages as necessary.
88
+ def self.set_position(page, pos)
89
+ unless page.is_a?(Page) || page = Page[page.to_i]
90
+ raise ArgumentError, "Invalid page id: #{page}"
91
+ end
92
+
93
+ pos = pos.to_i
94
+ cur_pos = page.position
95
+
96
+ unless pos > 0
97
+ raise ArgumentError, "Invalid position: #{pos}"
98
+ end
99
+
100
+ db.transaction do
101
+ if pos < cur_pos
102
+ filter{:position >= pos && :position < cur_pos}.
103
+ update(:position => 'position + 1'.lit)
104
+ elsif pos > cur_pos
105
+ filter{:position > cur_pos && :position <= pos}.
106
+ update(:position => 'position - 1'.lit)
107
+ end
108
+
109
+ filter(:id => page.id).update(:position => pos)
110
+ end
111
+ end
112
+
113
+ # Returns a valid, unique page name based on the specified title. If the
114
+ # title is empty or cannot be converted into a valid name, an empty string
115
+ # will be returned.
116
+ def self.suggest_name(title)
117
+ index = 1
118
+
119
+ # Remove HTML entities and non-alphanumeric characters, replace spaces
120
+ # with hyphens, and truncate the name at 64 characters.
121
+ name = title.to_s.strip.downcase.gsub(/&[^\s;]+;/, '_').
122
+ gsub(/[^\s0-9a-z-]/, '').gsub(/\s+/, '-')[0..63]
123
+
124
+ # Strip off any trailing non-alphanumeric characters.
125
+ name.gsub!(/[_-]+$/, '')
126
+
127
+ return '' if name.empty?
128
+
129
+ # Ensure that the name doesn't conflict with any methods on the Page
130
+ # controller and that no two pages have the same name.
131
+ until self.name_unique?(name)
132
+ if name[-1] == index
133
+ name[-1] = (index += 1).to_s
134
+ else
135
+ name = name[0..62] if name.size >= 64
136
+ name += (index += 1).to_s
137
+ end
138
+ end
139
+
140
+ return name
141
+ end
142
+
143
+ #--
144
+ # Instance Methods
145
+ #++
146
+
147
+ def body=(body)
148
+ self[:body] = body.strip
149
+ self[:body_rendered] = RedCloth.new(wiki_to_html(body.dup.strip)).to_html
150
+ end
151
+
152
+ # Gets the creation time of this page. If _format_ is provided, the time
153
+ # will be returned as a formatted String. See Time.strftime for details.
154
+ def created_at(format = nil)
155
+ if new?
156
+ format ? Time.now.strftime(format) : Time.now
157
+ else
158
+ format ? self[:created_at].strftime(format) : self[:created_at]
159
+ end
160
+ end
161
+
162
+ def name=(name)
163
+ self[:name] = name.strip.downcase unless name.nil?
164
+ end
165
+
166
+ def title=(title)
167
+ title.strip!
168
+
169
+ # Set the page name if it isn't already set.
170
+ if self[:name].nil? || self[:name].empty?
171
+ self[:name] = Page.suggest_name(title)
172
+ end
173
+
174
+ self[:title] = title
175
+ end
176
+
177
+ # Gets the time this page was last updated. If _format_ is provided, the time
178
+ # will be returned as a formatted String. See Time.strftime for details.
179
+ def updated_at(format = nil)
180
+ if new?
181
+ format ? Time.now.strftime(format) : Time.now
182
+ else
183
+ format ? self[:updated_at].strftime(format) : self[:updated_at]
184
+ end
185
+ end
186
+
187
+ # URL for this page.
188
+ def url
189
+ Config.site['url'].chomp('/') + PageController.r(:/, name).to_s
190
+ end
191
+
192
+ def validate
193
+ validates_presence(:name, :message => 'Please enter a name for this page.')
194
+ validates_presence(:title, :message => 'Please enter a title for this page.')
195
+ validates_presence(:body, :message => "Come on, I'm sure you can think of something to write.")
196
+
197
+ validates_max_length(255, :title, :message => 'Please enter a title under 255 characters.')
198
+ validates_max_length(64, :name, :message => 'Please enter a name under 64 characters.')
199
+
200
+ validates_format(/^[0-9a-z_-]+$/i, :name, :message => 'Page names may only contain letters, numbers, underscores, and dashes.')
201
+ end
202
+
203
+ end
204
+ end