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