aerial 0.0.0

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