aerial 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +1 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.md +74 -0
  4. data/Rakefile +96 -0
  5. data/VERSION +1 -0
  6. data/config/config.yml +19 -0
  7. data/config/deploy.rb +50 -0
  8. data/lib/aerial.rb +100 -0
  9. data/lib/aerial/article.rb +241 -0
  10. data/lib/aerial/base.rb +172 -0
  11. data/lib/aerial/comment.rb +160 -0
  12. data/lib/aerial/config.rb +41 -0
  13. data/lib/aerial/content.rb +74 -0
  14. data/lib/aerial/vendor/akismetor.rb +52 -0
  15. data/lib/aerial/vendor/cache.rb +139 -0
  16. data/lib/features/article.feature +10 -0
  17. data/lib/features/home.feature +16 -0
  18. data/lib/features/step_definitions/article_steps.rb +4 -0
  19. data/lib/features/step_definitions/home_steps.rb +8 -0
  20. data/lib/features/support/env.rb +38 -0
  21. data/lib/features/support/pages/article.rb +9 -0
  22. data/lib/features/support/pages/homepage.rb +9 -0
  23. data/lib/spec/aerial_spec.rb +203 -0
  24. data/lib/spec/article_spec.rb +338 -0
  25. data/lib/spec/base_spec.rb +65 -0
  26. data/lib/spec/comment_spec.rb +216 -0
  27. data/lib/spec/config_spec.rb +25 -0
  28. data/lib/spec/fixtures/articles/sample-article/sample-article.article +6 -0
  29. data/lib/spec/fixtures/articles/test-article-one/test-article.article +7 -0
  30. data/lib/spec/fixtures/articles/test-article-three/test-article.article +7 -0
  31. data/lib/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +8 -0
  32. data/lib/spec/fixtures/articles/test-article-two/test-article.article +7 -0
  33. data/lib/spec/fixtures/articles/test-article-two/test-comment.comment +10 -0
  34. data/lib/spec/fixtures/config.yml +35 -0
  35. data/lib/spec/fixtures/public/javascripts/application.js +109 -0
  36. data/lib/spec/fixtures/public/javascripts/jquery-1.3.1.min.js +19 -0
  37. data/lib/spec/fixtures/public/javascripts/jquery.template.js +255 -0
  38. data/lib/spec/fixtures/views/article.haml +19 -0
  39. data/lib/spec/fixtures/views/articles.haml +2 -0
  40. data/lib/spec/fixtures/views/comment.haml +8 -0
  41. data/lib/spec/fixtures/views/home.haml +2 -0
  42. data/lib/spec/fixtures/views/layout.haml +22 -0
  43. data/lib/spec/fixtures/views/post.haml +27 -0
  44. data/lib/spec/fixtures/views/rss.haml +15 -0
  45. data/lib/spec/fixtures/views/sidebar.haml +21 -0
  46. data/lib/spec/fixtures/views/style.sass +163 -0
  47. data/lib/spec/spec_helper.rb +117 -0
  48. metadata +101 -0
@@ -0,0 +1,172 @@
1
+ CONFIG = YAML.load_file( File.join(AERIAL_ROOT, 'config', 'config.yml') ) unless defined?(CONFIG)
2
+
3
+ require 'aerial/content'
4
+ require 'aerial/article'
5
+ require 'aerial/comment'
6
+ require 'aerial/vendor/cache'
7
+ require 'aerial/vendor/akismetor'
8
+ require 'aerial/config'
9
+
10
+ module Aerial
11
+
12
+ VERSION = '0.1.0'
13
+
14
+ class << self
15
+ attr_accessor :debug, :logger, :repo, :config
16
+
17
+ def log(str)
18
+ logger.debug { str } if debug
19
+ end
20
+ end
21
+
22
+ # Make sure git is added to the env path
23
+ ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin"
24
+ @logger ||= ::Logger.new(STDOUT)
25
+ @config ||= Aerial::Config.new(CONFIG)
26
+ @repo ||= Grit::Repo.new(File.join(AERIAL_ROOT, '.'))
27
+ @debug ||= false
28
+
29
+ module Helper
30
+
31
+ # Defines the url for our application
32
+ def hostname
33
+ if request.env['HTTP_X_FORWARDED_SERVER'] =~ /[a-z]*/
34
+ request.env['HTTP_X_FORWARDED_SERVER']
35
+ else
36
+ request.env['HTTP_HOST']
37
+ end
38
+ end
39
+
40
+ # Returns the
41
+ def path
42
+ base = "#{request.env['REQUEST_URI']}".scan(/\w+/).first
43
+ return base.blank? ? "index" : base
44
+ end
45
+
46
+ # Creates a complete link including the hostname
47
+ def full_hostname(link = "")
48
+ "http://#{hostname}#{link}"
49
+ end
50
+
51
+ # Display the page titles in proper format
52
+ def page_title
53
+ title = @page_title ? "| #{@page_title}" : ""
54
+ return "#{Aerial.config.title} #{title}"
55
+ end
56
+
57
+ # Format just the DATE in a nice easy to read format
58
+ def humanized_date(date)
59
+ if date && date.respond_to?(:strftime)
60
+ date.strftime('%A %B, %d %Y').strip
61
+ else
62
+ 'Never'
63
+ end
64
+ end
65
+
66
+ # Format just the DATE in a short way
67
+ def short_date(date)
68
+ if date && date.respond_to?(:strftime)
69
+ date.strftime('%b %d').strip
70
+ else
71
+ 'Never'
72
+ end
73
+ end
74
+
75
+ # Format for the rss 2.0 feed
76
+ def rss_date(date)
77
+ date.strftime("%a, %d %b %Y %H:%M:%S %Z") #Tue, 03 Jun 2003 09:39:21 GMT
78
+ end
79
+
80
+ # Truncate a string
81
+ def blurb(text, options ={})
82
+ options.merge!(:length => 160, :omission => "...")
83
+ if text
84
+ l = options[:length] - options[:omission].length
85
+ chars = text
86
+ (chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s
87
+ end
88
+ end
89
+
90
+ # Handy method to render partials including collections
91
+ def partial(template, options = {})
92
+ options.merge!(:layout => false)
93
+ return if options.has_key?(:collection) && options[:collection].nil?
94
+
95
+ if collection = options.delete(:collection) then
96
+ collection.inject([]) do |buffer, member|
97
+ buffer << haml(template, options.merge(:layout => false,
98
+ :locals => {template.to_sym => member}))
99
+ end.join("\n")
100
+ else
101
+ haml(template, options)
102
+ end
103
+ end
104
+
105
+ # Author link
106
+ def link_to_author(comment)
107
+ unless comment.homepage.blank?
108
+ return "<a href='#{comment.homepage}' rel='external'>#{comment.author}</a>"
109
+ end
110
+ comment.author
111
+ end
112
+
113
+ # Create a list of hyperlinks with a set of tags
114
+ def link_to_tags(tags)
115
+ return unless tags
116
+ links = []
117
+ tags.each do |tag|
118
+ links << "<a href='/tags/#{tag}' rel='#{tag}'>#{tag}</a>"
119
+ end
120
+ links.join(", ")
121
+ end
122
+
123
+ end
124
+
125
+ # Provides a few methods for interacting with that Aerial repository
126
+ class Git
127
+
128
+ # Commit the new file and push it to the remote repository
129
+ def self.commit_and_push(path, message)
130
+ self.commit(path, message)
131
+ self.push
132
+ end
133
+
134
+ # Added the file in the path and commit the changs to the repo
135
+ # +path+ to the new file to commit
136
+ # +message+ description of the commit
137
+ def self.commit(path, message)
138
+ Dir.chdir(File.expand_path(Aerial.repo.working_dir)) do
139
+ Aerial.repo.add(path)
140
+ end
141
+ Aerial.repo.commit_index(message)
142
+ end
143
+
144
+ # Adds all untracked files and commits them to the repo
145
+ def self.commit_all(path = ".", message = "Commited all changes at: #{DateTime.now}")
146
+ unless Aerial.repo.status.untracked.empty?
147
+ self.commit(path, message)
148
+ end
149
+ true
150
+ end
151
+
152
+ # Upload all new commits to the remote repo (if exists)
153
+ def self.push
154
+ return unless Aerial.config.git.name && Aerial.config.git.branch
155
+
156
+ begin
157
+ cmd = "push #{Aerial.config.git.name} #{Aerial.config.git.branch} "
158
+ Aerial.repo.git.run('', cmd, '', {}, "")
159
+ rescue Exception => e
160
+ Aerial.log(e.message)
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ end
167
+
168
+ class Object
169
+ def blank?
170
+ respond_to?(:empty?) ? empty? : !self
171
+ end
172
+ end
@@ -0,0 +1,160 @@
1
+ module Aerial
2
+
3
+ # Anonymous feedback
4
+ class Comment < Content
5
+
6
+ attr_reader :id, :permalink, :article, :spam, :file_path
7
+ attr_accessor :archive_name, :spam, :published_at, :name, :referrer,
8
+ :email, :homepage, :user_ip, :user_agent, :file_name
9
+
10
+ def initialize(atts = {})
11
+ super
12
+ sanitize_url
13
+ end
14
+
15
+ # =============================================================================================
16
+ # PUBLIC CLASS METHODS
17
+ # =============================================================================================
18
+
19
+ # Create a new instance and write comment to disk
20
+ # +path+ the file location of the comment
21
+ def self.create(archive_name, attributes ={})
22
+ comment = Comment.new(attributes.merge(:archive_name => archive_name))
23
+ if comment.valid?
24
+ return self.save_new(comment)
25
+ end
26
+ false
27
+ end
28
+
29
+ # Open and existing comment
30
+ # +data+ contains info about the comment
31
+ def self.open(data, options={})
32
+ self.new( self.extract_comment_from(data, options) )
33
+ end
34
+
35
+ # =============================================================================================
36
+ # PUBLIC INSTANCE METHODS
37
+ # =============================================================================================
38
+
39
+ # Save the instance to disk
40
+ # +archive_name+ the parent directory of the article, we're forcing the parameter to ensure
41
+ # the archive_name is established before attemping to write it to disk
42
+ def save(archive_name)
43
+ self.archive_name = archive_name
44
+ if File.directory? self.archive_path
45
+ Comment.save_new(self)
46
+ end
47
+ end
48
+
49
+ # Absolute path of the comment file
50
+ def expand_file
51
+ return unless self.archive_name
52
+ File.join(self.archive_path, self.name)
53
+ end
54
+
55
+ # The absolute file path of the archive
56
+ def archive_path
57
+ File.join(Aerial.repo.working_dir, Aerial.config.articles.dir, self.archive_name)
58
+ end
59
+
60
+ # Make sure comment has the required data
61
+ def valid?
62
+ return false if self.email.blank? || self.author.blank? || self.body.blank?
63
+ true
64
+ end
65
+
66
+ # Ask Akismetor if comment is spam
67
+ def suspicious?
68
+ return self.spam if self.spam
69
+ self.spam = Akismetor.spam?(akismet_attributes)
70
+ end
71
+
72
+ # Create a unique file name for this comment
73
+ def generate_name!
74
+ return self.name unless self.name.nil?
75
+
76
+ extenstion = self.suspicious? ? "spam" : "comment"
77
+ self.name = "#{DateTime.now.strftime("%Y%m%d%H%d%S")}_#{self.email}.#{extenstion}"
78
+ end
79
+
80
+ # String representation
81
+ def to_s
82
+ me = ""
83
+ me << "Author: #{self.author} \n" if self.author
84
+ me << "Published: #{self.published_at} \n" if self.published_at.to_s
85
+ me << "Email: #{self.email} \n" if self.email
86
+ me << "Homepage: #{self.homepage} \n" if self.homepage
87
+ me << "User IP: #{self.user_ip} \n" if self.user_ip
88
+ me << "User Agent: #{self.user_agent} \n" if self.user_agent
89
+ me << "Spam?: #{self.spam} \n" if self.user_agent
90
+ me << "\n#{self.body}" if self.body
91
+ return me
92
+ end
93
+
94
+ private
95
+
96
+ # =============================================================================================
97
+ # PRIVATE CLASS METHODS
98
+ # =============================================================================================
99
+
100
+ # Create a new Comment instance with data from the given file
101
+ def self.extract_comment_from(data, options = {})
102
+ comment_file = data.to_s
103
+ comment = options
104
+ comment[:id] = self.extract_header("id", comment_file)
105
+ comment[:user_ip] = self.extract_header("ip", comment_file)
106
+ comment[:user_agent] = self.extract_header("user-agent", comment_file)
107
+ comment[:referrer] = self.extract_header("referrer", comment_file)
108
+ comment[:permalink] = self.extract_header("permalink", comment_file)
109
+ comment[:author] = self.extract_header("author", comment_file)
110
+ comment[:email] = self.extract_header("email", comment_file)
111
+ comment[:homepage] = self.extract_header("homepage", comment_file)
112
+ comment[:published_at]= DateTime.parse(self.extract_header("published", comment_file))
113
+ comment[:body] = self.scan_for_field(comment_file, self.body_field)
114
+ return comment
115
+ end
116
+
117
+ # Write the contents of the comment to the same directory as the article
118
+ def self.save_new(comment)
119
+ return false unless comment && comment.archive_name
120
+ comment.generate_name!
121
+ comment.published_at = DateTime.now
122
+ path = File.join(Aerial.config.articles.dir, comment.archive_name, comment.name)
123
+ Dir.chdir(Aerial.repo.working_dir) do
124
+ File.open(path, 'w') do |file|
125
+ file << comment.to_s
126
+ end
127
+ end
128
+ Aerial::Git.commit_and_push(path, "New comment: #{comment.name}")
129
+ return comment
130
+ end
131
+
132
+ # =============================================================================================
133
+ # PRIVATE INSTANCE METHODS
134
+ # =============================================================================================
135
+
136
+ # Make sure the url is cleaned
137
+ def sanitize_url
138
+ return unless self.homepage
139
+ homepage.gsub!(/^(.*)/, 'http://\1') unless homepage =~ %r{^http://} or homepage.empty?
140
+ end
141
+
142
+ # Try to prevent spam with akismet
143
+ def akismet_attributes
144
+ {
145
+ :key => Aerial.config.akismet.key,
146
+ :blog => Aerial.config.akismet.url,
147
+ :user_ip => self.user_ip,
148
+ :user_agent => self.user_agent,
149
+ :referrer => self.referrer,
150
+ :permalink => self.permalink,
151
+ :comment_type => 'comment',
152
+ :comment_author => self.author,
153
+ :comment_author_email => self.email,
154
+ :comment_author_url => self.homepage,
155
+ :comment_content => self.body
156
+ }
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,41 @@
1
+ module Aerial
2
+
3
+ class Config
4
+
5
+ class << self
6
+ attr_accessor :config
7
+ end
8
+
9
+ def initialize(yaml)
10
+ @config = nested_hash_to_openstruct(yaml)
11
+ end
12
+
13
+ # Complete path to the directory theme
14
+ def theme_directory
15
+ directory = File.join(AERIAL_ROOT, self.views.dir)
16
+ File.join(directory)
17
+ end
18
+
19
+ def method_missing(method_name, *attributes)
20
+ if @config.respond_to?(method_name.to_sym)
21
+ return @config.send(method_name.to_sym)
22
+ else
23
+ false
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Recursively convert nested Hashes into Openstructs
30
+ def nested_hash_to_openstruct(obj)
31
+ if obj.is_a? Hash
32
+ obj.each { |key, value| obj[key] = nested_hash_to_openstruct(value) }
33
+ OpenStruct.new(obj)
34
+ else
35
+ return obj
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,74 @@
1
+ require 'coderay'
2
+ require 'date'
3
+
4
+ module Aerial
5
+
6
+ # Base class for all the site's content
7
+ class Content
8
+
9
+ attr_reader :id, :author, :title, :body, :published_at, :archive_name, :file_name
10
+
11
+ def initialize(atts = {})
12
+ atts.each_pair { |key, value| instance_variable_set("@#{key}", value) if self.respond_to? key}
13
+ end
14
+
15
+ protected
16
+
17
+ # =============================================================================================
18
+ # PROTECTED CLASS METHODS
19
+ # =============================================================================================
20
+
21
+ # With the comment string, attempt to find the given field
22
+ # +field+ the label before the ":"
23
+ # +comment+ is the contents of the comment
24
+ def self.extract_header(field, content)
25
+ return self.scan_for_field(content, self.header_field_for(field))
26
+ end
27
+
28
+ # Returns the string that matches the given pattern
29
+ def self.scan_for_field(contents, pattern)
30
+ content = contents.scan(pattern).first.to_s.strip
31
+ end
32
+
33
+ # Returns the regular expression pattern for the header fields
34
+ def self.header_field_for(header)
35
+ exp = Regexp.new('^'+header+'\s*:(.*)$', Regexp::IGNORECASE)
36
+ end
37
+
38
+ # Returns the regular expression pattern for the body field
39
+ def self.body_field
40
+ exp = Regexp.new('^\n(.*)$', Regexp::MULTILINE)
41
+ end
42
+
43
+ # Look for <code> blocks and convert it for syntax highlighting
44
+ def self.parse_coderay(text)
45
+ text.scan(/(\<code>(.+?)\<\/code>)/m).each do |match|
46
+ match[1] = match[1].gsub("<br>", "\n").
47
+ gsub("&amp;nbsp;", " ").
48
+ gsub("&amp;lt;", "<").
49
+ gsub("&amp;gt;", ">").
50
+ gsub("&amp;quot;", '"')
51
+ text.gsub!(match[0], CodeRay.scan(match[1].strip, :ruby).div(:line_numbers => :table, :css => :class))
52
+ end
53
+ return text
54
+ end
55
+
56
+ # =============================================================================================
57
+ # PROTECTED INSTANCE METHODS
58
+ # =============================================================================================
59
+
60
+ # Ensure string contains valid ASCII characters
61
+ def escape(string)
62
+ return unless string
63
+ result = String.new(string)
64
+ result.gsub!(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics).
65
+ result.gsub!(/[^\w_ \-]+/i, '') # Remove unwanted chars.
66
+ result.gsub!(/[ \-]+/i, '-') # No more than one of the separator in a row.
67
+ result.gsub!(/^\-|\-$/i, '') # Remove leading/trailing separator.
68
+ result.downcase!
69
+ return result
70
+ end
71
+
72
+ end
73
+
74
+ end