concen 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/app/controllers/concen/application_controller.rb +25 -0
- data/app/controllers/concen/grid_files_controller.rb +75 -0
- data/app/controllers/concen/pages_controller.rb +95 -0
- data/app/controllers/concen/performances_controller.rb +35 -0
- data/app/controllers/concen/sessions_controller.rb +30 -0
- data/app/controllers/concen/statuses_controller.rb +63 -0
- data/app/controllers/concen/traffics_controller.rb +43 -0
- data/app/controllers/concen/users_controller.rb +118 -0
- data/app/controllers/concen/visits_controller.rb +38 -0
- data/app/helpers/concen/application_helper.rb +10 -0
- data/app/helpers/concen/pages_helper.rb +5 -0
- data/app/helpers/concen/visits_helper.rb +4 -0
- data/app/models/concen/grid_file.rb +67 -0
- data/app/models/concen/page.rb +342 -0
- data/app/models/concen/response.rb +45 -0
- data/app/models/concen/user.rb +88 -0
- data/app/models/concen/visit/page.rb +100 -0
- data/app/models/concen/visit/referral.rb +45 -0
- data/app/stylesheets/application.sass +445 -0
- data/app/stylesheets/config.rb +9 -0
- data/app/stylesheets/ie.sass +4 -0
- data/app/stylesheets/non_ios.sass +16 -0
- data/app/stylesheets/partials/_base.sass +92 -0
- data/app/stylesheets/partials/_fileuploader.sass +75 -0
- data/app/stylesheets/partials/_flot.sass +8 -0
- data/app/stylesheets/partials/_form.sass +74 -0
- data/app/stylesheets/partials/_mixins.sass +8 -0
- data/app/stylesheets/partials/_variables.sass +17 -0
- data/app/stylesheets/print.sass +4 -0
- data/app/views/concen/grid_files/_form.html.haml +15 -0
- data/app/views/concen/grid_files/edit.html.haml +9 -0
- data/app/views/concen/pages/_file_list.haml +7 -0
- data/app/views/concen/pages/_files.haml +24 -0
- data/app/views/concen/pages/_form.html.haml +18 -0
- data/app/views/concen/pages/_nested_list.html.haml +15 -0
- data/app/views/concen/pages/edit.html.haml +15 -0
- data/app/views/concen/pages/index.html.haml +9 -0
- data/app/views/concen/pages/new.html.haml +9 -0
- data/app/views/concen/performances/_runtimes.html.haml +5 -0
- data/app/views/concen/performances/show.html.haml +30 -0
- data/app/views/concen/sessions/new.html.haml +12 -0
- data/app/views/concen/statuses/_server.html.haml +19 -0
- data/app/views/concen/statuses/show.html.haml +18 -0
- data/app/views/concen/traffics/_pages.html.haml +5 -0
- data/app/views/concen/traffics/_referrals.html.haml +9 -0
- data/app/views/concen/traffics/show.html.haml +30 -0
- data/app/views/concen/users/_form.html.haml +29 -0
- data/app/views/concen/users/_password_reset.html.haml +0 -0
- data/app/views/concen/users/_settings.html.haml +15 -0
- data/app/views/concen/users/edit.html.haml +9 -0
- data/app/views/concen/users/index.html.haml +32 -0
- data/app/views/concen/users/new.html.haml +4 -0
- data/app/views/concen/users/new_invite.html.haml +15 -0
- data/app/views/concen/users/new_reset_password.html.haml +15 -0
- data/app/views/concen/visits/visit_recorder_js.erb +13 -0
- data/app/views/layouts/concen/_additional_header_links.haml +0 -0
- data/app/views/layouts/concen/_header.html.haml +18 -0
- data/app/views/layouts/concen/_iphone.html.haml +6 -0
- data/app/views/layouts/concen/application.html.haml +48 -0
- data/app/views/layouts/concen/maintenance.html.haml +0 -0
- data/config/routes.rb +64 -0
- data/lib/concen.rb +11 -0
- data/lib/concen/engine.rb +14 -0
- data/lib/concen/railties/page.rake +12 -0
- data/lib/concen/railties/setup.rake +23 -0
- data/lib/concen/version.rb +3 -0
- metadata +246 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require "domainatrix"
|
2
|
+
|
3
|
+
module Concen
|
4
|
+
class VisitsController < ApplicationController
|
5
|
+
|
6
|
+
def visit_recorder_js
|
7
|
+
if cookies[:visitor_id].blank?
|
8
|
+
cookies[:visitor_id] = {:value => ActiveSupport::SecureRandom.uuid, :expires => 20.years.from_now}
|
9
|
+
end
|
10
|
+
render :layout => false, :mime_type => "text/javascript"
|
11
|
+
end
|
12
|
+
|
13
|
+
def record
|
14
|
+
current_time = Time.now.utc
|
15
|
+
current_hour = Time.utc(current_time.year, current_time.month, current_time.day, current_time.hour)
|
16
|
+
Visit::Page.collection.update(
|
17
|
+
{:url => params[:u], :hour => current_hour},
|
18
|
+
{"$inc" => {:count => 1}, "$set" => {:title => params[:t]}},
|
19
|
+
:upsert => true, :safe => false
|
20
|
+
)
|
21
|
+
begin
|
22
|
+
referral_url = params[:r]
|
23
|
+
referral = Domainatrix.parse(referral_url)
|
24
|
+
referral_domain = referral.domain + "." + referral.public_suffix
|
25
|
+
rescue
|
26
|
+
referral_url = nil
|
27
|
+
referral_domain = nil
|
28
|
+
end
|
29
|
+
Visit::Referral.collection.update(
|
30
|
+
{:url => referral_url, :hour => current_hour},
|
31
|
+
{"$inc" => {:count => 1}, "$set" => {:domain => referral_domain}},
|
32
|
+
:upsert => true, :safe => false
|
33
|
+
)
|
34
|
+
image_path = "#{Rails.root}/public/concen/images/record-visit.gif"
|
35
|
+
send_file image_path, :type => "image/gif", :disposition => "inline"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Concen
|
2
|
+
module ApplicationHelper
|
3
|
+
# Remove all the new lines from the output.
|
4
|
+
# This is very useful when used for inline-block elements, because
|
5
|
+
# white spaces will transform into extra gaps between element.
|
6
|
+
def one_line(&block)
|
7
|
+
(capture_haml(&block).gsub("\n", '')).html_safe
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Concen
|
2
|
+
class GridFile
|
3
|
+
include Mongoid::Document
|
4
|
+
include Mongoid::Timestamps
|
5
|
+
|
6
|
+
embedded_in :page, :class_name => "Concen::Page"
|
7
|
+
|
8
|
+
field :filename, :type => String
|
9
|
+
field :original_filename, :type => String
|
10
|
+
field :private, :type => Boolean
|
11
|
+
field :grid_id, :type => BSON::ObjectId
|
12
|
+
|
13
|
+
validates_presence_of :filename
|
14
|
+
validates_presence_of :original_filename
|
15
|
+
validates_presence_of :grid_id
|
16
|
+
|
17
|
+
after_destroy :destroy_gridfs
|
18
|
+
|
19
|
+
def path
|
20
|
+
"/gridfs/" + self.filename
|
21
|
+
end
|
22
|
+
|
23
|
+
def url(root_url)
|
24
|
+
root_url.gsub!("concen.", "") # Remove concen subdomain.
|
25
|
+
root_url = root_url[0..-2] # Remove trailing slash.
|
26
|
+
root_url + self.path
|
27
|
+
end
|
28
|
+
|
29
|
+
def read
|
30
|
+
grid = Mongo::Grid.new(Mongoid.database)
|
31
|
+
grid.get(self.grid_id).read
|
32
|
+
end
|
33
|
+
|
34
|
+
def size
|
35
|
+
grid = Mongo::Grid.new(Mongoid.database)
|
36
|
+
grid.get(self.grid_id).file_length
|
37
|
+
end
|
38
|
+
|
39
|
+
def text?
|
40
|
+
grid = Mongo::Grid.new(Mongoid.database)
|
41
|
+
grid.get(self.grid_id).content_type.include?("text") || grid.get(self.grid_id).content_type.include?("javascript")
|
42
|
+
end
|
43
|
+
|
44
|
+
def store(content, filename)
|
45
|
+
original_filename = filename.dup
|
46
|
+
file_extension = File.extname(filename).downcase
|
47
|
+
filename = "#{self.id.to_s}-#{File.basename(original_filename, file_extension).downcase.parameterize.gsub("_", "-")}#{file_extension}"
|
48
|
+
grid = Mongo::Grid.new(Mongoid.database)
|
49
|
+
content_type = MIME::Types.type_for(filename).first.to_s
|
50
|
+
if self.grid_id
|
51
|
+
grid.delete(self.grid_id)
|
52
|
+
end
|
53
|
+
if grid_id = grid.put(content, :content_type => content_type, :filename => filename, :safe => true)
|
54
|
+
self.update_attributes(:filename => filename, :original_filename => original_filename, :grid_id => grid_id)
|
55
|
+
else
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def destroy_gridfs
|
63
|
+
grid = Mongo::Grid.new(Mongoid.database)
|
64
|
+
grid.delete(self.grid_id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,342 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "redcarpet"
|
3
|
+
require "mustache"
|
4
|
+
require "chronic"
|
5
|
+
|
6
|
+
module Concen
|
7
|
+
class Page
|
8
|
+
include Mongoid::Document
|
9
|
+
include Mongoid::Timestamps
|
10
|
+
|
11
|
+
store_in self.name.underscore.gsub("/", ".").pluralize
|
12
|
+
|
13
|
+
references_many :children, :class_name => "Concen::Page", :foreign_key => :parent_id, :inverse_of => :parent
|
14
|
+
referenced_in :parent, :class_name => "Concen::Page", :inverse_of => :children
|
15
|
+
embeds_many :grid_files, :class_name => "Concen::GridFile"
|
16
|
+
|
17
|
+
field :parent_id, :type => BSON::ObjectId
|
18
|
+
field :level, :type => Integer
|
19
|
+
field :title, :type => String
|
20
|
+
field :description, :type => String
|
21
|
+
field :default_slug, :type => String
|
22
|
+
field :raw_text, :type => String
|
23
|
+
field :content, :type => Hash, :default => {}
|
24
|
+
field :position, :type => Integer
|
25
|
+
field :publish_time, :type => Time
|
26
|
+
field :publish_month, :type => Time
|
27
|
+
field :labels, :type => Array, :default => []
|
28
|
+
field :authors, :type => Array, :default => []
|
29
|
+
field :status, :type => String
|
30
|
+
|
31
|
+
validates_presence_of :title
|
32
|
+
validates_presence_of :default_slug
|
33
|
+
validates_uniqueness_of :title, :scope => [:parent_id, :level], :case_sensitive => false
|
34
|
+
validates_uniqueness_of :default_slug, :scope => [:parent_id, :level], :case_sensitive => false
|
35
|
+
|
36
|
+
before_validation :parse_raw_text
|
37
|
+
before_validation :set_default_slug
|
38
|
+
before_save :set_publish_month
|
39
|
+
before_create :set_position
|
40
|
+
after_save :unset_unused_dynamic_fields
|
41
|
+
after_destroy :destroy_children
|
42
|
+
after_destroy :destroy_grid_files
|
43
|
+
after_destroy :reset_position
|
44
|
+
|
45
|
+
# This scope should not be chained with other any_of criteria.
|
46
|
+
# Because the mongo driver takes a hash for a query,
|
47
|
+
# and a hash doesn't allow duplicate keys.
|
48
|
+
scope :with_slug, ->(slug) { any_of({:slug => slug}, {:default_slug => slug}) }
|
49
|
+
|
50
|
+
scope :with_position, where(:position.exists => true)
|
51
|
+
scope :published, lambda {
|
52
|
+
where(:publish_time.lte => Time.now, :status.in => [nil, /published/i])
|
53
|
+
}
|
54
|
+
scope :unpublished, lambda {
|
55
|
+
any_of({:publish_time => nil}, {:publish_time.gt => Time.now})
|
56
|
+
}
|
57
|
+
|
58
|
+
index :parent_id, :background => true
|
59
|
+
index :publish_time, :background => true
|
60
|
+
index :default_slug, :background => true
|
61
|
+
|
62
|
+
# Get the list of dynamic fields by checking againts this array.
|
63
|
+
# Values should mirror the listed fields above.
|
64
|
+
PREDEFINED_FIELDS = [:_id, :parent_id, :level, :created_at, :updated_at, :default_slug, :content, :raw_text, :position, :grid_files, :title, :description, :publish_time, :labels, :authors, :status]
|
65
|
+
|
66
|
+
# These fields can't be overwritten by user's meta data when parsing raw_text.
|
67
|
+
PROTECTED_FIELDS = [:_id, :parent_id, :level, :created_at, :updated_at, :default_slug, :content, :raw_text, :position, :grid_files]
|
68
|
+
|
69
|
+
def slug
|
70
|
+
if user_defined_slug = self.read_attribute(:slug)
|
71
|
+
user_defined_slug
|
72
|
+
else
|
73
|
+
self.default_slug
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def content_in_html(key = "main", data={})
|
78
|
+
if content = self.content.try(:[], key)
|
79
|
+
content = Mustache.render(content, data)
|
80
|
+
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, :fenced_code_blocks => true)
|
81
|
+
html = markdown.render content
|
82
|
+
content = Redcarpet::Render::SmartyPants.render html
|
83
|
+
else
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def images(filename=nil)
|
89
|
+
search_grid_files(["png", "jpg", "jpeg", "gif"], filename)
|
90
|
+
end
|
91
|
+
|
92
|
+
def stylesheets(filename=nil)
|
93
|
+
search_grid_files(["css"], filename)
|
94
|
+
end
|
95
|
+
|
96
|
+
def javascripts(filename=nil)
|
97
|
+
search_grid_files(["js"], filename)
|
98
|
+
end
|
99
|
+
|
100
|
+
def others(filename=nil)
|
101
|
+
excluded_ids = []
|
102
|
+
[:images, :stylesheets, :javascripts].each do |file_type|
|
103
|
+
excluded_ids += self.send(file_type).map(&:_id)
|
104
|
+
end
|
105
|
+
self.grid_files.where(:_id.nin => excluded_ids)
|
106
|
+
end
|
107
|
+
|
108
|
+
def search_grid_files(extensions, filename=nil)
|
109
|
+
if filename
|
110
|
+
self.grid_files.where(:original_filename => /.*#{filename}.*.*\.(#{extensions.join("|")}).*$/i).asc(:original_filename)
|
111
|
+
else
|
112
|
+
self.grid_files.where(:original_filename => /.*\.(#{extensions.join("|")}).*/i).asc(:original_filename)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def underscore_hash_keys(hash)
|
117
|
+
new_hash = {}
|
118
|
+
hash.each do |key, value|
|
119
|
+
value = underscore_hash_keys(value) if value.is_a?(Hash)
|
120
|
+
new_hash[key.gsub(" ","_").downcase.to_sym] = value
|
121
|
+
end
|
122
|
+
new_hash
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_publish_time(publish_time_string)
|
126
|
+
publish_time_string = publish_time_string.to_s
|
127
|
+
begin
|
128
|
+
Chronic.time_class = Time.zone
|
129
|
+
parsed_date = Chronic.parse(publish_time_string, :now => Time.zone.now)
|
130
|
+
rescue
|
131
|
+
parsed_date = nil
|
132
|
+
end
|
133
|
+
if parsed_date
|
134
|
+
self.publish_time = parsed_date
|
135
|
+
elsif parsed_date = Time.zone.parse(publish_time_string)
|
136
|
+
self.publish_time = parsed_date
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def published?
|
141
|
+
self.publish_time.present?
|
142
|
+
end
|
143
|
+
|
144
|
+
def previous(*args)
|
145
|
+
options = args.extract_options!
|
146
|
+
children = self.parent.children
|
147
|
+
children = children.published if options[:only_published]
|
148
|
+
if options[:chronologically]
|
149
|
+
children = children.desc(:publish_time)
|
150
|
+
children.where(:publish_time.lt => self.publish_time).first
|
151
|
+
else
|
152
|
+
children = children.desc(:position)
|
153
|
+
children.where(:position.lt => self.position).first
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def next(*args)
|
158
|
+
options = args.extract_options!
|
159
|
+
children = self.parent.children
|
160
|
+
children = children.published if options[:only_published]
|
161
|
+
if options[:chronologically]
|
162
|
+
children = children.asc(:publish_time)
|
163
|
+
children.where(:publish_time.gt => self.publish_time).first
|
164
|
+
else
|
165
|
+
children = children.asc(:position)
|
166
|
+
children.where(:position.gt => self.position).first
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def first?(*args)
|
171
|
+
options = args.extract_options!
|
172
|
+
children = self.parent.children
|
173
|
+
children = children.published if options[:only_published]
|
174
|
+
if options[:chronologically]
|
175
|
+
children = children.asc(:publish_time)
|
176
|
+
else
|
177
|
+
children = children.asc(:position)
|
178
|
+
end
|
179
|
+
if children.first
|
180
|
+
if self.id == children.first.id
|
181
|
+
return true
|
182
|
+
else
|
183
|
+
return false
|
184
|
+
end
|
185
|
+
else
|
186
|
+
return false
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def last?(*args)
|
191
|
+
options = args.extract_options!
|
192
|
+
children = self.parent.children
|
193
|
+
children = children.published if options[:only_published]
|
194
|
+
if options[:chronologically]
|
195
|
+
children = children.asc(:publish_time)
|
196
|
+
else
|
197
|
+
children = children.asc(:position)
|
198
|
+
end
|
199
|
+
if children.last
|
200
|
+
if self.id == children.last.id
|
201
|
+
return true
|
202
|
+
else
|
203
|
+
return false
|
204
|
+
end
|
205
|
+
else
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def authors_as_user
|
211
|
+
users = []
|
212
|
+
for author in self.authors
|
213
|
+
if author.is_a?(String)
|
214
|
+
if user = User.where(:username => author).first
|
215
|
+
users << user
|
216
|
+
elsif user = User.where(:email => author).first
|
217
|
+
users << user
|
218
|
+
elsif user = User.where(:full_name => author).first
|
219
|
+
users << user
|
220
|
+
end
|
221
|
+
else
|
222
|
+
users << user if User.where(:_id => author).first
|
223
|
+
end
|
224
|
+
end
|
225
|
+
return users
|
226
|
+
end
|
227
|
+
|
228
|
+
protected
|
229
|
+
|
230
|
+
def set_default_slug
|
231
|
+
self.default_slug = self.title.parameterize if self.title
|
232
|
+
end
|
233
|
+
|
234
|
+
def set_position
|
235
|
+
if Page.where(:level => self.level).count > 0
|
236
|
+
self.position = Page.with_position.where(:level => self.level).asc(:position).last.position + 1
|
237
|
+
else
|
238
|
+
self.position = 1
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def reset_position
|
243
|
+
affected_pages = Page.with_position.where(:level => self.level, :position.gt => self.position)
|
244
|
+
if affected_pages.count > 0
|
245
|
+
for page in affected_pages
|
246
|
+
page.position = page.position - 1
|
247
|
+
page.save
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def parse_raw_text
|
253
|
+
if self.raw_text && self.raw_text.length > 0 && (self.new? || self.raw_text_changed?)
|
254
|
+
self.content = {}
|
255
|
+
raw_text_array = self.raw_text.split(/(?:\r?\n-{3,}\r?\n)/)
|
256
|
+
if raw_text_array.count > 1
|
257
|
+
meta_data = raw_text_array.delete_at(0).strip
|
258
|
+
raw_text_array.each_with_index do |content, index|
|
259
|
+
content = content.strip.lines.to_a
|
260
|
+
if content.first && content.first.include?("@ ")
|
261
|
+
# Extract content key from @ syntax.
|
262
|
+
content_key = content.delete_at(0).gsub("@ ", "").downcase
|
263
|
+
content_key = content_key.gsub("content", "").strip.gsub(" ", "_")
|
264
|
+
elsif index == 0
|
265
|
+
content_key = "main"
|
266
|
+
else
|
267
|
+
content_key = (index + 1).to_s
|
268
|
+
end
|
269
|
+
self.content[content_key] = content.join
|
270
|
+
end
|
271
|
+
else
|
272
|
+
meta_data = self.raw_text.strip
|
273
|
+
self.content = {}
|
274
|
+
end
|
275
|
+
|
276
|
+
# Set each value of meta data.
|
277
|
+
meta_data = underscore_hash_keys(YAML.load(meta_data))
|
278
|
+
meta_data.each do |key, value|
|
279
|
+
unless PROTECTED_FIELDS.include?(key)
|
280
|
+
if key == :publish_time
|
281
|
+
self.parse_publish_time(value)
|
282
|
+
else
|
283
|
+
self.write_attribute(key, value)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Set the field to nil if the value isn't present in meta data.
|
289
|
+
# Except for authors.
|
290
|
+
(self.attributes.keys.map{ |k| k.to_sym } - PROTECTED_FIELDS).each do |field|
|
291
|
+
self[field] = nil if !meta_data.keys.include?(field) && field != :authors
|
292
|
+
end
|
293
|
+
|
294
|
+
self.update_raw_text
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def update_raw_text
|
299
|
+
raw_text_array = self.raw_text.split(/(?:\r?\n-{3,}\r?\n)/, 2)
|
300
|
+
meta_data = raw_text_array.delete_at(0).lines.to_a
|
301
|
+
meta_data.each_with_index do |line, index|
|
302
|
+
if line.match /publish time/i
|
303
|
+
meta_data[index] = "#{line.split(':')[0]}: #{self.publish_time}"
|
304
|
+
if line.include? "\r\n"
|
305
|
+
meta_data[index] << "\r\n"
|
306
|
+
elsif line.include? "\n"
|
307
|
+
meta_data[index] << "\n"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
self.raw_text = meta_data.join + self.raw_text.match(/(?:\r?\n-{3,}\r?\n)/).to_s + raw_text_array.join
|
312
|
+
end
|
313
|
+
|
314
|
+
def destroy_children
|
315
|
+
for child in self.children
|
316
|
+
child.destroy
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def destroy_grid_files
|
321
|
+
for grid_file in self.grid_files
|
322
|
+
grid_file.destroy
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def unset_unused_dynamic_fields
|
327
|
+
target_fields = {}
|
328
|
+
for field in self.attributes.keys
|
329
|
+
if !PREDEFINED_FIELDS.include?(field.to_sym) && self[field.to_sym].nil?
|
330
|
+
target_fields[field.to_s] = 1
|
331
|
+
end
|
332
|
+
end
|
333
|
+
Page.collection.update({"_id" => self.id}, {"$unset" => target_fields})
|
334
|
+
end
|
335
|
+
|
336
|
+
def set_publish_month
|
337
|
+
if self.publish_time
|
338
|
+
self.publish_month = Time.zone.local(self.publish_time.year, self.publish_time.month)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|