rethoth 0.4.1

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