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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/bin/thoth +233 -0
- data/lib/proto/config.ru +45 -0
- data/lib/proto/thoth.conf.sample +206 -0
- data/lib/thoth/cache.rb +53 -0
- data/lib/thoth/config.rb +158 -0
- data/lib/thoth/controller/admin.rb +75 -0
- data/lib/thoth/controller/api/comment.rb +73 -0
- data/lib/thoth/controller/api/page.rb +134 -0
- data/lib/thoth/controller/api/post.rb +122 -0
- data/lib/thoth/controller/api/tag.rb +59 -0
- data/lib/thoth/controller/archive.rb +50 -0
- data/lib/thoth/controller/comment.rb +173 -0
- data/lib/thoth/controller/main.rb +193 -0
- data/lib/thoth/controller/media.rb +172 -0
- data/lib/thoth/controller/page.rb +167 -0
- data/lib/thoth/controller/post.rb +310 -0
- data/lib/thoth/controller/search.rb +86 -0
- data/lib/thoth/controller/tag.rb +107 -0
- data/lib/thoth/controller.rb +48 -0
- data/lib/thoth/errors.rb +35 -0
- data/lib/thoth/helper/admin.rb +86 -0
- data/lib/thoth/helper/cookie.rb +45 -0
- data/lib/thoth/helper/error.rb +122 -0
- data/lib/thoth/helper/pagination.rb +131 -0
- data/lib/thoth/helper/wiki.rb +77 -0
- data/lib/thoth/helper/ysearch.rb +89 -0
- data/lib/thoth/importer/pants.rb +81 -0
- data/lib/thoth/importer/poseidon.rb +54 -0
- data/lib/thoth/importer/thoth.rb +103 -0
- data/lib/thoth/importer.rb +131 -0
- data/lib/thoth/layout/default.rhtml +47 -0
- data/lib/thoth/middleware/minify.rb +82 -0
- data/lib/thoth/migrate/001_create_schema.rb +108 -0
- data/lib/thoth/migrate/002_add_media_size.rb +37 -0
- data/lib/thoth/migrate/003_add_post_draft.rb +38 -0
- data/lib/thoth/migrate/004_add_comment_email.rb +37 -0
- data/lib/thoth/migrate/005_add_page_position.rb +37 -0
- data/lib/thoth/migrate/006_add_comment_close_delete.rb +43 -0
- data/lib/thoth/migrate/007_add_comment_summary.rb +37 -0
- data/lib/thoth/model/comment.rb +216 -0
- data/lib/thoth/model/media.rb +87 -0
- data/lib/thoth/model/page.rb +204 -0
- data/lib/thoth/model/post.rb +262 -0
- data/lib/thoth/model/tag.rb +80 -0
- data/lib/thoth/model/tags_posts_map.rb +34 -0
- data/lib/thoth/monkeypatch/sequel/model/errors.rb +37 -0
- data/lib/thoth/plugin/thoth_delicious.rb +105 -0
- data/lib/thoth/plugin/thoth_flickr.rb +86 -0
- data/lib/thoth/plugin/thoth_pinboard.rb +98 -0
- data/lib/thoth/plugin/thoth_tags.rb +68 -0
- data/lib/thoth/plugin/thoth_twitter.rb +175 -0
- data/lib/thoth/plugin.rb +59 -0
- data/lib/thoth/public/css/admin.css +223 -0
- data/lib/thoth/public/css/thoth.css +592 -0
- data/lib/thoth/public/images/admin-sprite.png +0 -0
- data/lib/thoth/public/images/thoth-sprite.png +0 -0
- data/lib/thoth/public/js/admin/comments.js +116 -0
- data/lib/thoth/public/js/admin/name.js +244 -0
- data/lib/thoth/public/js/admin/tagcomplete.js +332 -0
- data/lib/thoth/public/js/lazyload-min.js +4 -0
- data/lib/thoth/public/js/thoth.js +203 -0
- data/lib/thoth/public/robots.txt +5 -0
- data/lib/thoth/version.rb +37 -0
- data/lib/thoth/view/admin/index.rhtml +1 -0
- data/lib/thoth/view/admin/login.rhtml +23 -0
- data/lib/thoth/view/admin/toolbar.rhtml +117 -0
- data/lib/thoth/view/admin/welcome.rhtml +58 -0
- data/lib/thoth/view/archive/index.rhtml +24 -0
- data/lib/thoth/view/comment/comment.rhtml +47 -0
- data/lib/thoth/view/comment/delete.rhtml +15 -0
- data/lib/thoth/view/comment/form.rhtml +81 -0
- data/lib/thoth/view/comment/index.rhtml +68 -0
- data/lib/thoth/view/comment/list.rhtml +48 -0
- data/lib/thoth/view/media/delete.rhtml +15 -0
- data/lib/thoth/view/media/edit.rhtml +12 -0
- data/lib/thoth/view/media/form.rhtml +7 -0
- data/lib/thoth/view/media/list.rhtml +35 -0
- data/lib/thoth/view/media/media.rhtml +44 -0
- data/lib/thoth/view/media/new.rhtml +7 -0
- data/lib/thoth/view/page/delete.rhtml +15 -0
- data/lib/thoth/view/page/edit.rhtml +15 -0
- data/lib/thoth/view/page/form.rhtml +57 -0
- data/lib/thoth/view/page/index.rhtml +9 -0
- data/lib/thoth/view/page/list.rhtml +49 -0
- data/lib/thoth/view/page/new.rhtml +15 -0
- data/lib/thoth/view/post/comments.rhtml +12 -0
- data/lib/thoth/view/post/compact.rhtml +48 -0
- data/lib/thoth/view/post/delete.rhtml +15 -0
- data/lib/thoth/view/post/edit.rhtml +15 -0
- data/lib/thoth/view/post/form.rhtml +83 -0
- data/lib/thoth/view/post/index.rhtml +48 -0
- data/lib/thoth/view/post/list.rhtml +61 -0
- data/lib/thoth/view/post/new.rhtml +15 -0
- data/lib/thoth/view/post/tiny.rhtml +4 -0
- data/lib/thoth/view/search/index.rhtml +45 -0
- data/lib/thoth/view/tag/index.rhtml +34 -0
- data/lib/thoth/view/thoth/css.rhtml +9 -0
- data/lib/thoth/view/thoth/footer.rhtml +15 -0
- data/lib/thoth/view/thoth/header.rhtml +23 -0
- data/lib/thoth/view/thoth/index.rhtml +11 -0
- data/lib/thoth/view/thoth/js.rhtml +6 -0
- data/lib/thoth/view/thoth/sidebar.rhtml +38 -0
- data/lib/thoth/view/thoth/util/pager.rhtml +23 -0
- data/lib/thoth/view/thoth/util/simple_pager.rhtml +15 -0
- data/lib/thoth/view/thoth/util/table_sortheader.rhtml +20 -0
- data/lib/thoth.rb +394 -0
- 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
|