concen 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/app/controllers/concen/application_controller.rb +25 -0
  2. data/app/controllers/concen/grid_files_controller.rb +75 -0
  3. data/app/controllers/concen/pages_controller.rb +95 -0
  4. data/app/controllers/concen/performances_controller.rb +35 -0
  5. data/app/controllers/concen/sessions_controller.rb +30 -0
  6. data/app/controllers/concen/statuses_controller.rb +63 -0
  7. data/app/controllers/concen/traffics_controller.rb +43 -0
  8. data/app/controllers/concen/users_controller.rb +118 -0
  9. data/app/controllers/concen/visits_controller.rb +38 -0
  10. data/app/helpers/concen/application_helper.rb +10 -0
  11. data/app/helpers/concen/pages_helper.rb +5 -0
  12. data/app/helpers/concen/visits_helper.rb +4 -0
  13. data/app/models/concen/grid_file.rb +67 -0
  14. data/app/models/concen/page.rb +342 -0
  15. data/app/models/concen/response.rb +45 -0
  16. data/app/models/concen/user.rb +88 -0
  17. data/app/models/concen/visit/page.rb +100 -0
  18. data/app/models/concen/visit/referral.rb +45 -0
  19. data/app/stylesheets/application.sass +445 -0
  20. data/app/stylesheets/config.rb +9 -0
  21. data/app/stylesheets/ie.sass +4 -0
  22. data/app/stylesheets/non_ios.sass +16 -0
  23. data/app/stylesheets/partials/_base.sass +92 -0
  24. data/app/stylesheets/partials/_fileuploader.sass +75 -0
  25. data/app/stylesheets/partials/_flot.sass +8 -0
  26. data/app/stylesheets/partials/_form.sass +74 -0
  27. data/app/stylesheets/partials/_mixins.sass +8 -0
  28. data/app/stylesheets/partials/_variables.sass +17 -0
  29. data/app/stylesheets/print.sass +4 -0
  30. data/app/views/concen/grid_files/_form.html.haml +15 -0
  31. data/app/views/concen/grid_files/edit.html.haml +9 -0
  32. data/app/views/concen/pages/_file_list.haml +7 -0
  33. data/app/views/concen/pages/_files.haml +24 -0
  34. data/app/views/concen/pages/_form.html.haml +18 -0
  35. data/app/views/concen/pages/_nested_list.html.haml +15 -0
  36. data/app/views/concen/pages/edit.html.haml +15 -0
  37. data/app/views/concen/pages/index.html.haml +9 -0
  38. data/app/views/concen/pages/new.html.haml +9 -0
  39. data/app/views/concen/performances/_runtimes.html.haml +5 -0
  40. data/app/views/concen/performances/show.html.haml +30 -0
  41. data/app/views/concen/sessions/new.html.haml +12 -0
  42. data/app/views/concen/statuses/_server.html.haml +19 -0
  43. data/app/views/concen/statuses/show.html.haml +18 -0
  44. data/app/views/concen/traffics/_pages.html.haml +5 -0
  45. data/app/views/concen/traffics/_referrals.html.haml +9 -0
  46. data/app/views/concen/traffics/show.html.haml +30 -0
  47. data/app/views/concen/users/_form.html.haml +29 -0
  48. data/app/views/concen/users/_password_reset.html.haml +0 -0
  49. data/app/views/concen/users/_settings.html.haml +15 -0
  50. data/app/views/concen/users/edit.html.haml +9 -0
  51. data/app/views/concen/users/index.html.haml +32 -0
  52. data/app/views/concen/users/new.html.haml +4 -0
  53. data/app/views/concen/users/new_invite.html.haml +15 -0
  54. data/app/views/concen/users/new_reset_password.html.haml +15 -0
  55. data/app/views/concen/visits/visit_recorder_js.erb +13 -0
  56. data/app/views/layouts/concen/_additional_header_links.haml +0 -0
  57. data/app/views/layouts/concen/_header.html.haml +18 -0
  58. data/app/views/layouts/concen/_iphone.html.haml +6 -0
  59. data/app/views/layouts/concen/application.html.haml +48 -0
  60. data/app/views/layouts/concen/maintenance.html.haml +0 -0
  61. data/config/routes.rb +64 -0
  62. data/lib/concen.rb +11 -0
  63. data/lib/concen/engine.rb +14 -0
  64. data/lib/concen/railties/page.rake +12 -0
  65. data/lib/concen/railties/setup.rake +23 -0
  66. data/lib/concen/version.rb +3 -0
  67. 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,5 @@
1
+ module Concen
2
+ module PagesHelper
3
+
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Concen
2
+ module VisitsHelper
3
+ end
4
+ 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