Class: Thoth::Post

Inherits:
Sequel::Model show all
Includes:
Helper::Wiki
Defined in:
lib/thoth/model/post.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.get(name) ⇒ Object

Gets the published post with the specified name, where name can be either a name or an id. Does not return drafts.



59
60
61
62
63
64
65
# File 'lib/thoth/model/post.rb', line 59

def self.get(name)
  return Post[:id => name, :is_draft => false] if name.is_a?(Numeric)

  name = name.to_s.downcase
  name =~ /^\d+$/ ? Post[:id => name, :is_draft => false] :
      Post[:name => name, :is_draft => false]
end

.name_unique?(name) ⇒ Boolean

Returns true if the specified post name is already taken or is a reserved name.

Returns:

  • (Boolean)


69
70
71
72
73
# File 'lib/thoth/model/post.rb', line 69

def self.name_unique?(name)
  !PostController.methods.include?(name) &&
      !PostController.instance_methods.include?(name) &&
      !Post[:name => name.to_s.downcase]
end

.name_valid?(name) ⇒ Boolean

Returns true if the specified post name consists of valid characters and is not too long or too short.

Returns:

  • (Boolean)


77
78
79
# File 'lib/thoth/model/post.rb', line 77

def self.name_valid?(name)
  !!(name =~ /^[0-9a-z_-]{1,64}$/i) && !(name =~ /^[0-9]+$/)
end

.recent(page = 1, limit = 10) ⇒ Object

Gets a paginated dataset of recent published posts sorted in reverse order by creation time. Does not return drafts.



83
84
85
86
# File 'lib/thoth/model/post.rb', line 83

def self.recent(page = 1, limit = 10)
  filter(:is_draft => false).reverse_order(:created_at).paginate(page,
      limit)
end

.recent_drafts(page = 1, limit = 10) ⇒ Object

Gets a paginated dataset of recent draft posts sorted in reverse order by creation time. Does not return published posts.



90
91
92
93
# File 'lib/thoth/model/post.rb', line 90

def self.recent_drafts(page = 1, limit = 10)
  filter(:is_draft => true).reverse_order(:created_at).paginate(page,
      limit)
end

.suggest_name(title) ⇒ Object

Returns a valid, unique post name based on the specified title. If the title is empty or cannot be converted into a valid name, an empty string will be returned.



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
# File 'lib/thoth/model/post.rb', line 98

def self.suggest_name(title)
  index = 1

  # Remove HTML entities and non-alphanumeric characters, replace spaces
  # with hyphens, and truncate the name at 64 characters.
  name = title.to_s.strip.downcase.gsub(/&[^\s;]+;/, '_').
      gsub(/[^\s0-9a-z-]/, '').gsub(/\s+/, '-')[0..63]

  # Strip off any trailing non-alphanumeric characters.
  name.gsub!(/[_-]+$/, '')

  return '' if name.empty?

  # If the name consists solely of numeric characters, add an alpha
  # character to prevent name/id ambiguity.
  name += '_' unless name =~ /[a-z_-]/

  # Ensure that the name doesn't conflict with any methods on the Post
  # controller and that no two posts have the same name.
  until self.name_unique?(name)
    if name[-1] == index
      name[-1] = (index += 1).to_s
    else
      name = name[0..62] if name.size >= 64
      name += (index += 1).to_s
    end
  end

  return name
end

Instance Method Details

#atom_urlObject

Gets the Atom feed URL for this post.



134
135
136
# File 'lib/thoth/model/post.rb', line 134

def atom_url
  Config.site['url'].chomp('/') + PostController.r(:atom, name).to_s
end

#body=(body) ⇒ Object



138
139
140
141
# File 'lib/thoth/model/post.rb', line 138

def body=(body)
  self[:body]          = body.strip
  self[:body_rendered] = RedCloth.new(wiki_to_html(body.dup.strip)).to_html
end

#commentsObject

Gets a dataset of visible comments attached to this post, ordered by creation time.



145
146
147
# File 'lib/thoth/model/post.rb', line 145

def comments
  @comments ||= Comment.filter(:post_id => id, :deleted => false).order(:created_at)
end

#created_at(format = nil) ⇒ Object

Gets the creation time of this post. If format is provided, the time will be returned as a formatted String. See Time.strftime for details.



151
152
153
154
155
156
157
# File 'lib/thoth/model/post.rb', line 151

def created_at(format = nil)
  if new?
    format ? Time.now.strftime(format) : Time.now
  else
    format ? self[:created_at].strftime(format) : self[:created_at]
  end
end

#name=(name) ⇒ Object



159
160
161
# File 'lib/thoth/model/post.rb', line 159

def name=(name)
  self[:name] = name.to_s.strip.downcase unless name.nil?
end

#tagsObject

Gets an Array of tags attached to this post, ordered by name.



164
165
166
167
168
169
170
# File 'lib/thoth/model/post.rb', line 164

def tags
  if new?
    @fake_tags || []
  else
    @tags ||= tags_dataset.all
  end
end

#tags=(tag_names) ⇒ Object



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
# File 'lib/thoth/model/post.rb', line 172

def tags=(tag_names)
  if tag_names.is_a?(String)
    tag_names = tag_names.split(',', 64)
  elsif !tag_names.is_a?(Array)
    raise ArgumentError, "Expected String or Array, got #{tag_names.class}"
  end

  tag_names = tag_names.map{|n| n.strip.downcase}.uniq.delete_if{|n|
      n.empty?}

  if new?
    # This Post hasn't been saved yet, so instead of attaching actual tags
    # to it, we'll create a bunch of fake tags just for the preview. We
    # won't create the real ones until the Post is saved.
    @fake_tags = []

    tag_names.each {|name| @fake_tags << Tag.new(:name => name)}
    @fake_tags.sort! {|a, b| a.name <=> b.name }

    return @fake_tags
  else
    real_tags = []

    # First delete any existing tag mappings for this post.
    TagsPostsMap.filter(:post_id => id).delete

    # Create new tags and new mappings.
    tag_names.each do |name|
      tag = Tag.find_or_create(:name => name)
      real_tags << tag
      TagsPostsMap.create(:post_id => id, :tag_id => tag.id)
    end

    return real_tags
  end
end

#title=(title) ⇒ Object



209
210
211
212
213
214
215
216
217
218
# File 'lib/thoth/model/post.rb', line 209

def title=(title)
  title.strip!

  # Set the post's name if it isn't already set.
  if self[:name].nil? || self[:name].empty?
    self[:name] = Post.suggest_name(title)
  end

  self[:title] = title
end

#updated_at(format = nil) ⇒ Object

Gets the time this post was last updated. If format is provided, the time will be returned as a formatted String. See Time.strftime for details.



223
224
225
226
227
228
229
# File 'lib/thoth/model/post.rb', line 223

def updated_at(format = nil)
  if new?
    format ? Time.now.strftime(format) : Time.now
  else
    format ? self[:updated_at].strftime(format) : self[:updated_at]
  end
end

#urlObject

Gets the URL for this post.



232
233
234
# File 'lib/thoth/model/post.rb', line 232

def url
  Config.site['url'].chomp('/') + PostController.r(:/, name).to_s
end

#validateObject



236
237
238
239
240
241
242
243
244
245
246
# File 'lib/thoth/model/post.rb', line 236

def validate
  validates_presence(:name,  :message => 'Please enter a name for this post.')
  validates_presence(:title, :message => 'Please enter a title for this post.')
  validates_presence(:body,  :message => "What's the matter? Cat got your tongue?")

  validates_max_length(255, :title, :message => 'Please enter a title under 255 characters.')
  validates_max_length(64,  :name,  :message => 'Please enter a name under 64 characters.')

  validates_format(/^[0-9a-z_-]+$/i, :name, :message => 'Post names may only contain letters, numbers, underscores, and dashes.')
  validates_format(/[a-z_-]/i,       :name, :message => 'Post names must contain at least one non-numeric character.')
end