/
posts_helper.rb
307 lines (266 loc) · 10.3 KB
/
posts_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
require 'render_anywhere'
module PostsHelper
CATEGORY_DESCRIPTIONS = {project: "cool things I've made",
thought: "miscellaneous topics",
review: "neat (or not so neat) works that I write about",
testing: "debugging this website"}
SORT_DEFAULTS = { order: :ascending,
sort: :date }
POST_CAT_CACHE_PATH = 'posts/category'
POST_SOURCE_PATH = Rails.root.join "app", "views", Post::FILE_PATH
POST_SOURCE_FILE_EXTS = ['.haml']
POST_SOURCE_METADATA = ['title', 'subtitle', 'splash_img', 'splash_img_credit', 'tags']
# Enforce sequential access to the post modification routines
POST_CHANGE_LOCK = Mutex.new
listener = Listen.to(Rails.root.join(PostsHelper::POST_SOURCE_PATH)) do |modified, added, removed|
PostsHelper.dir_watcher modified, added, removed
end
listener.start
class StubComment
def temp_comments
@temp_comments ||= []
end
def temp_comments=(list)
@temp_comments = list
end
end
def self.tree_comments(post)
PostsHelper.tree_comments_2_pass post
end
def self.tree_comments_2_pass(post_or_comments, opts = {})
found_comments = {}
comment_list = []
# support treeing comments from either a post or a comment list
comments = post_or_comments
comments = comments.comments if post_or_comments.is_a? Post
comments.each do |c|
# add this element to the scanned list
found_comments[c.id] = c
# add this comment to the top-level list if it is a top level comment
if c.nesting_level == 0
comment_list << c
# PostsHelper.add_to_comment_list comment_list, c, opts
end
end
# parent all comments
found_comments.each do |k, v|
# Get our parent
if v.parent_comment_id
parent = found_comments[v.parent_comment_id]
parent.temp_comments << v
# PostsHelper.add_to_comment_list parent.temp_comments, v, opts
end
end
# puts "found_comments.length: #{found_comments.length}"
comment_list
end
def self.tree_comments_array_placeholder(post_or_comments)
found_comments = {}
comment_list = []
# support treeing comments from either a post or a comment list
comments = post_or_comments
comments = comments.comments if post_or_comments.is_a? Post
comments.each do |c|
# parent this element
unless c.parent_comment_id.nil?
parent = found_comments[c.parent_comment_id]
if parent.is_a? Array
parent << c
# If it's a comment, then add it to the list directly
elsif parent.is_a? Comment
parent.temp_comments << c
# nil: first time this parent has been encountered
else
list = [c]
found_comments[c.parent_comment_id] = list
end
end
# add this comment to the list
if found_comments.key? c.id
# if we already exist in this list, then it's in array form
c.temp_comments = found_comments[c.id]
else
found_comments[c.id] = c
end
comment_list << c if c.nesting_level == 0
end
# puts "found_comments.length: #{found_comments.length}"
comment_list
end
def self.tree_comments_stub_placeholder(post_or_comments)
found_comments = {}
comment_list = []
# support treeing comments from either a post or a comment list
comments = post_or_comments
comments = comments.comments if post_or_comments.is_a? Post
comments.each do |c|
# parent this element
unless c.parent_comment_id.nil?
parent = found_comments[c.parent_comment_id]
if parent.nil?
# stub the parent
parent = StubComment.new
found_comments[c.parent_comment_id] = parent
end
parent.temp_comments << c
end
# add this comment to the list
if found_comments.key? c.id
# if we already exist in this list, then it's a stub
stub = found_comments[c.id]
c.temp_comments = stub.temp_comments
else
found_comments[c.id] = c
end
comment_list << c if c.nesting_level == 0
end
# puts "found_comments.length: #{found_comments.length}"
comment_list
end
def get_description(category)
CATEGORY_DESCRIPTIONS[category.to_sym]
end
def post_cat_path_fallback(post)
"/posts/#{post.category}/#{post[:file_path]}"
end
def banned_category(cat)
PostsHelper.banned_category cat
end
def self.banned_category(cat)
cat == :testing && Rails.env == "production"
end
##
# Given a file system path (absolute or relative) to a post, returns the category of the post as well as the file_path in the format the Post model expects
def self.path_to_cat_and_file_path(path, real_path = false)
category = nil
file = nil
# which costs more, string construction or an if statement?
path = path.to_s unless path.is_a? String
# get the path to the post source folder
root = PostsHelper::POST_SOURCE_PATH.to_s
# inspect the partition of this path
phead, _, ptail = path.partition root
# If the head is non-empty, then let's assume we were given a relative path and try again
begin
return PostsHelper.path_to_cat_and_file_path PostsHelper::POST_SOURCE_PATH.join(path).realpath, true if !phead.empty? && !real_path
rescue Errno::ENOENT
# the file doesn't exist anyway, so don't bother parsing
return [nil, nil]
end
# if we're still here then, let's try parsing the category and path from the tail
category, file = ptail.split '/', 2
# convert the category to a symbol
category = category.try :singularize
category = category.try :to_sym
# attempt the strip the file of its leading underscore (posts are partials) and extensions
actual_file_index = file.try :index, /\/?[^\/]*$/ # /(^_|[^\/]*\/(_))/
unless actual_file_index.nil? || file.nil?
# find the leading underscore and remove it
u_index = file.index '_', actual_file_index
file.slice! u_index
# to support hidden files (we don't need to, but we can at no cost), we'll look for the extension after the first character of the actual file name, which is two off from actual_file_index, since it points to the '/' directory separator
ext_index = file.index '.', actual_file_index + 2
file.slice! ext_index, file.length
end
[category, file]
end
def self.dir_watcher(modified, added, removed)
[modified, added, removed].each_with_index do |files, index|
# Rather than writing the code to loop through these arrays three times, we'll loop through them generically and use
modding = files.equal? modified
adding = files.equal? added
removing = files.equal? removed
files.each do |file|
# skip hidden files and emacs garbage
leading_char = File.basename(file)[0]
next if leading_char == '.' || leading_char == '#'
# Delegate the actions
PostsHelper.update_post_by_path file if modding
PostsHelper.create_post_by_path file if adding
PostsHelper.delete_post_by_path file if removing
end
end
end
def self.get_post_info_from_path(path)
category, file_path = PostsHelper.path_to_cat_and_file_path path
# get the post info
post_fields = PostMetadataExtractor.extract_from_path path
# construct the post hash for creating the object
post_obj_hash = { title: post_fields.delete(:title),
tags: post_fields.delete(:tags),
file_path: file_path,
category: category }
# anything left in post_fields is additional_info
# keys are now already symbols
## convert string keys to symbols
## additional_info = post_fields.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
[post_obj_hash, post_fields]
end
def self.update_post_by_path(path, logger = Rails.logger)
POST_CHANGE_LOCK.synchronize do
logger.info "updating: #{path}"
# Retrieve the post
category, file_path = PostsHelper.path_to_cat_and_file_path path
post = Post.where category: category, file_path: file_path
# Guarding against modified files that haven't been created yet
return self.create_post_by_path path, logger unless post.exists?
post = post.first
# Update the time if necessary
time = File.mtime post.abs_file_path
post.set u_at: time if post.u_at != time
# Update the other post info if necessary
# get the post info
post_obj_hash, additional_info = get_post_info_from_path path
# update those that need to be updated
post_obj_hash.each do |k, v|
eval %Q(
post.set #{k}: v if post.#{k} != v
)
end
# update the additional_info
post.set additional_info: post.additional_info.merge(additional_info) if post.additional_info != additional_info
end
end
def self.create_post_by_path(path, logger = Rails.logger)
# we can be called by another, that holds this lock, so let's make sure not to deadlock
locked = POST_CHANGE_LOCK.try_lock
logger.info "creating: #{path}"
# get the post info
post_obj_hash, additional_info = get_post_info_from_path path
post = Post.new post_obj_hash
# anything left in post_fields is additional_info
post.additional_info = additional_info
# update the u_at time
time = Time.now
begin
time = File.mtime post.abs_file_path
rescue
logger.error "#{$!.message} Backtrace:\n#{$!.backtrace.join "\n"}"
end
post.u_at = time
# create the post
logger.info "Inserting: #{post.title}"
# Guard against a double create
begin
logger.error "Post with title exists: #{Post.where(title: post.title).exists?}"
post.save! unless Post.where(title: post.title).exists?
rescue
logger.error "#{$!.message} Backtrace:\n#{$!.backtrace.join "\n"}"
end
POST_CHANGE_LOCK.unlock if locked
end
def self.delete_post_by_path(path, logger = Rails.logger)
POST_CHANGE_LOCK.synchronize do
logger.info "deleting: #{path}"
# Retrieve the post
category, path = PostsHelper.path_to_cat_and_file_path path
post = Post.where category: category, file_path: path
# Guarding against modified files that haven't been created yet
return unless post.exists?
post = post.first
post.destroy
end
end
# load all posts that we may not have in database
DirectoryWorker.perform_async
end