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