copy 0.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.
@@ -0,0 +1,118 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+ require 'redcarpet'
4
+
5
+ module Copy
6
+ class Server < Sinatra::Base
7
+ set :views, './views'
8
+ set :public, './public'
9
+ set :root, File.dirname(File.expand_path(__FILE__))
10
+
11
+ helpers do
12
+ def protected!
13
+ unless authorized?
14
+ response['WWW-Authenticate'] = %(Basic realm="Copy Admin Area")
15
+ throw(:halt, [401, "Not authorized\n"])
16
+ end
17
+ end
18
+
19
+ def authorized?
20
+ return false unless settings.respond_to?(:admin_user) && settings.respond_to?(:admin_password)
21
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
22
+ @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [settings.admin_user, settings.admin_password]
23
+ end
24
+
25
+ def set_cache_control_header
26
+ if settings.respond_to?(:cache_time) && settings.cache_time.is_a?(Numeric) && settings.cache_time > 0
27
+ expires settings.cache_time, :public
28
+ else
29
+ cache_control :no_cache
30
+ end
31
+ end
32
+
33
+ def format_text(name, content, options = {})
34
+ original = content.dup
35
+ # Apply markdown formatting.
36
+ content = Redcarpet.new(content, :smart).to_html.chomp
37
+
38
+ html_attrs = %Q(class="_copy_editable" data-name="#{name}")
39
+
40
+ if original =~ /\n/ # content with newlines renders in a div
41
+ tag = options[:wrap_tag] || :div
42
+ %Q(<#{tag} #{html_attrs}>#{content}</#{tag}>)
43
+ else # single line content renders in a span without <p> tags
44
+ tag = options[:wrap_tag] || :span
45
+ content.gsub!(/<\/*p>/, '')
46
+ %Q(<#{tag} #{html_attrs}>#{content}</#{tag}>)
47
+ end
48
+ end
49
+
50
+ def copy(name, options = {}, &block)
51
+ if !Copy::Storage.connected? || !(content = Copy::Storage.get(name))
52
+ # Side-step the output buffer so we can capture the block, but not output it.
53
+ @_out_buf, old_buffer = '', @_out_buf
54
+ content = yield
55
+ @_out_buf = old_buffer
56
+
57
+ # Get the first line from captured text.
58
+ first_line = content.split("\n").first
59
+ # Determine how much white space it has in front.
60
+ white_space = first_line.match(/^(\s)*/)[0]
61
+ # Remove that same amount of white space from the beginning of every line.
62
+ content.gsub!(Regexp.new("^#{white_space}"), '')
63
+
64
+ # Save the content so it can be edited.
65
+ Copy::Storage.set(name, content) if Copy::Storage.connected?
66
+ end
67
+
68
+ # Append the output buffer.
69
+ @_out_buf << format_text(name, content, options)
70
+ end
71
+ end
72
+
73
+ def self.config(&block)
74
+ class_eval(&block)
75
+ end
76
+
77
+ before do
78
+ if settings.respond_to?(:storage) && !Copy::Storage.connected?
79
+ Copy::Storage.connect!(settings.storage)
80
+ end
81
+ end
82
+
83
+ get '/_copy/?' do
84
+ protected!
85
+ ERB.new(File.read(settings.root + '/admin/index.html.erb')).result(self.send(:binding))
86
+ end
87
+
88
+ get '/_copy.js' do
89
+ protected!
90
+ content_type(:js)
91
+ ERB.new(File.read(settings.root + '/admin/index.js.erb')).result(self.send(:binding))
92
+ end
93
+
94
+ get '/_copy/:name' do
95
+ protected!
96
+ @name = params[:name]
97
+ @doc = Copy::Storage.get(params[:name])
98
+ ERB.new(File.read(settings.root + '/admin/edit.html.erb')).result(self.send(:binding))
99
+ end
100
+
101
+ put '/_copy/:name' do
102
+ protected!
103
+ Copy::Storage.set(params[:name], params[:content])
104
+ format_text(params[:name], Copy::Storage.get(params[:name]), :wrap_tag => params[:wrap_tag])
105
+ end
106
+
107
+ get '*' do
108
+ route = Copy::Router.new(params[:splat].first, settings.views)
109
+ if route.success?
110
+ set_cache_control_header
111
+ content_type(route.format)
112
+ send(route.renderer, route.template, :layout => route.layout)
113
+ else
114
+ not_found
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,39 @@
1
+ require 'mongo'
2
+ require 'uri'
3
+
4
+ module Copy
5
+ module Storage
6
+ class Mongodb
7
+ def initialize(connection_url)
8
+ uri = URI.parse(connection_url)
9
+ connection = ::Mongo::Connection.from_uri(connection_url)
10
+ database = connection.db(uri.path.gsub(/^\//, ''))
11
+
12
+ @collection = database['copy-content']
13
+ @collection.ensure_index([['name', Mongo::ASCENDING]], :unique => true)
14
+ @collection
15
+ end
16
+
17
+ def get(name)
18
+ doc = find(name)
19
+ doc['content'] unless doc.nil?
20
+ end
21
+
22
+ def set(name, content)
23
+ doc = find(name)
24
+ if doc
25
+ doc['content'] = content
26
+ @collection.update({ '_id' => doc['_id'] }, doc)
27
+ else
28
+ @collection.insert('name' => name, 'content' => content)
29
+ end
30
+ end
31
+
32
+ private
33
+ def find(name)
34
+ docs = @collection.find('name' => name)
35
+ docs.first if docs.respond_to?(:first)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ require 'redis'
2
+
3
+ module Copy
4
+ module Storage
5
+ class Redis
6
+ def initialize(connection_url)
7
+ @redis = ::Redis.new(connection_url)
8
+ end
9
+
10
+ def get(name)
11
+ @redis.hget("copy:content", name)
12
+ end
13
+
14
+ def set(name, content)
15
+ @redis.hset("copy:content", name, content)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ require 'data_mapper'
2
+
3
+ module Copy
4
+ module Storage
5
+ class Relational
6
+ class Document
7
+ include DataMapper::Resource
8
+
9
+ storage_names[:default] = 'copy_documents'
10
+
11
+ property :id, Serial
12
+ property :name, String, :unique_index => true
13
+ property :content, Text
14
+ end
15
+
16
+ def initialize(connection_url)
17
+ DataMapper.setup(:default, connection_url)
18
+ DataMapper.finalize
19
+ DataMapper.auto_upgrade!
20
+ end
21
+
22
+ def get(name)
23
+ doc = Document.first(:name => name)
24
+ doc.content unless doc.nil?
25
+ end
26
+
27
+ def set(name, content)
28
+ doc = Document.first(:name => name)
29
+ if doc
30
+ doc.update(:content => content)
31
+ else
32
+ Document.create(:name => name, :content => content)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ require 'uri'
2
+
3
+ module Copy
4
+ module Storage
5
+ autoload :Mongodb, 'copy/storage/mongodb'
6
+ autoload :Redis, 'copy/storage/redis'
7
+ autoload :Relational, 'copy/storage/relational'
8
+
9
+ def self.connect!(connection_url)
10
+ scheme = URI.parse(connection_url).scheme
11
+ klass = scheme.capitalize
12
+ if %w(sqlite mysql postgres).include?(scheme)
13
+ klass = 'Relational'
14
+ end
15
+ @@storage = Copy::Storage.const_get(klass).new(connection_url)
16
+ end
17
+
18
+ def self.connected?
19
+ !defined?(@@storage).nil?
20
+ end
21
+
22
+ def self.get(name)
23
+ @@storage.get(name.to_s)
24
+ end
25
+
26
+ def self.set(name, content)
27
+ @@storage.set(name.to_s, content)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Copy
2
+ VERSION = '0.0.1'
3
+ end
data/lib/copy.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Copy
2
+ autoload :Router, 'copy/router'
3
+ autoload :Server, 'copy/server'
4
+ autoload :Storage, 'copy/storage'
5
+ autoload :Content, 'copy/content'
6
+ end
@@ -0,0 +1,108 @@
1
+ require 'test_helper'
2
+
3
+ class RouterTest < Test::Unit::TestCase
4
+ test "format defaults to html" do
5
+ assert_equal :html, router('/something').format
6
+ assert_equal :html, router('/a/b/c/').format
7
+ end
8
+
9
+ test "recognize format in path" do
10
+ assert_equal :html, router('/something.html').format
11
+ assert_equal :html, router('/about/something.html').format
12
+ assert_equal :xml, router('/index.xml').format
13
+ assert_equal :csv, router('/employees.csv').format
14
+ assert_equal :rss, router('/news/feed.rss').format
15
+ end
16
+
17
+ test "find template file" do
18
+ Dir.expects(:glob).with('./views/about.html*').twice.returns(['./views/about.html.erb'])
19
+
20
+ assert_equal './views/about.html.erb', router('/about', './views').template_file
21
+ assert_equal './views/about.html.erb', router('/about.html', './views').template_file
22
+ end
23
+
24
+ test "find template file as index in dir" do
25
+ Dir.expects(:glob).with('./views/about.html*').returns([])
26
+ Dir.expects(:glob).with('./views/about/index.html*').returns(['./views/about/index.html.erb'])
27
+
28
+ assert_equal './views/about/index.html.erb', router('/about', './views').template_file
29
+ end
30
+
31
+ test "find template file as index in dir when path has trailing slash" do
32
+ Dir.expects(:glob).with('./views/about/us.html*').returns([])
33
+ Dir.expects(:glob).with('./views/about/us/index.html*').returns(['./views/about/us/index.html.erb'])
34
+
35
+ assert_equal './views/about/us/index.html.erb', router('/about/us/', './views').template_file
36
+ end
37
+
38
+ test "find template file in dir when path has trailing slash" do
39
+ Dir.expects(:glob).with('./views/about/us.html*').returns(['./views/about/us.html.erb'])
40
+
41
+ assert_equal './views/about/us.html.erb', router('/about/us/', './views').template_file
42
+ end
43
+
44
+ test "find index template file when empty path given" do
45
+ Dir.expects(:glob).with('./views/index.html*').returns(['./views/index.html.erb'])
46
+
47
+ assert_equal './views/index.html.erb', router('/', './views').template_file
48
+ end
49
+
50
+ test "renderer determined from template file extension" do
51
+ r1 = router('/about')
52
+ r1.expects(:template_file).returns('about.html.erb')
53
+ assert_equal :erb, r1.renderer
54
+
55
+ r2 = router('/about')
56
+ r2.expects(:template_file).returns('about.html.haml')
57
+ assert_equal :haml, r2.renderer
58
+ end
59
+
60
+ test "template determined from template file" do
61
+ r1 = router('/about', './views')
62
+ r1.expects(:template_file).at_least_once.returns('./views/about.html.erb')
63
+ assert_equal :'about.html', r1.template
64
+
65
+ r2 = router('/about/nothing.html', './views')
66
+ r2.expects(:template_file).at_least_once.returns('./views/about/nothing.html.erb')
67
+ assert_equal :'about/nothing.html', r2.template
68
+ end
69
+
70
+ test "layout for html format and presense of layout file" do
71
+ r = router('/about', './views')
72
+ r.expects(:template_file).returns('./views/about.html.erb')
73
+ File.expects(:exists?).with('./views/layout.html.erb').returns(true)
74
+
75
+ assert_equal :'layout.html', r.layout
76
+ end
77
+
78
+ test "layout is false when no layout file is found" do
79
+ r = router('/about', './views')
80
+ r.expects(:template_file).returns('./views/about.html.erb')
81
+ File.expects(:exists?).with('./views/layout.html.erb').returns(false)
82
+
83
+ assert_equal false, r.layout
84
+ end
85
+
86
+ test "layout is false with non-html format" do
87
+ r = router('/people.csv', './views')
88
+ assert_equal false, r.layout
89
+ end
90
+
91
+ test "success when template file found" do
92
+ Dir.expects(:glob).with('./views/about.html*').returns(['./views/about.html.erb'])
93
+
94
+ assert router('/about', './views').success?
95
+ end
96
+
97
+ test "no success when template file not found" do
98
+ Dir.expects(:glob).with('./views/about.html*').returns([])
99
+ Dir.expects(:glob).with('./views/about/index.html*').returns([])
100
+
101
+ assert !router('/about', './views').success?
102
+ end
103
+
104
+ private
105
+ def router(path, views = '')
106
+ Copy::Router.new(path, views)
107
+ end
108
+ end
@@ -0,0 +1,167 @@
1
+ require 'test_helper'
2
+ require 'rack/test'
3
+
4
+ class ServerTest < Test::Unit::TestCase
5
+ include CopyAppSetup
6
+ include Rack::Test::Methods
7
+
8
+ test "GET index" do
9
+ get '/'
10
+
11
+ assert last_response.ok?
12
+ assert_equal 'text/html;charset=utf-8', last_response.headers['Content-Type']
13
+ assert_match "<title>I'm the layout!</title>", last_response.body
14
+ assert_match "<p>I'm the index!</p>", last_response.body
15
+ end
16
+
17
+ test "GET path with index in folder" do
18
+ %w(/about /about/).each do |path|
19
+ get path
20
+
21
+ assert last_response.ok?
22
+ assert_match "<p>About!</p>", last_response.body
23
+ end
24
+ end
25
+
26
+ test "GET path to template in folder" do
27
+ %w(/about/us /about/us/).each do |path|
28
+ get path
29
+
30
+ assert last_response.ok?
31
+ assert_match "<p>About us!</p>", last_response.body
32
+ end
33
+ end
34
+
35
+ test "GET non-existent path" do
36
+ get '/nope'
37
+ assert last_response.status == 404
38
+ end
39
+
40
+ test "cache_time setting sets a Cache-Control header" do
41
+ app.config { set :cache_time, 456 }
42
+ get '/'
43
+ assert_equal 'public, max-age=456', last_response.headers['Cache-Control']
44
+
45
+ [0, nil, false].each do |time|
46
+ app.config { set :cache_time, time }
47
+ get '/'
48
+ assert_equal 'no-cache', last_response.headers['Cache-Control']
49
+ end
50
+ end
51
+
52
+ test "GET csv" do
53
+ get 'data/people.csv'
54
+
55
+ assert last_response.ok?
56
+ assert_equal 'text/csv;charset=utf-8', last_response.headers['Content-Type']
57
+ assert_equal File.read(app.settings.views + '/data/people.csv.erb'), last_response.body
58
+ end
59
+
60
+ test "GET xml" do
61
+ get 'data/people.xml'
62
+
63
+ assert last_response.ok?
64
+ assert_equal 'application/xml;charset=utf-8', last_response.headers['Content-Type']
65
+ assert_equal File.read(app.settings.views + '/data/people.xml.erb'), last_response.body
66
+ end
67
+
68
+ test "connects to storage when setting present" do
69
+ connection_url = 'redis://localhost:1234'
70
+ app.config { set :storage, connection_url }
71
+ Copy::Storage.expects(:connect!).with(connection_url).once.returns(true)
72
+ get '/'
73
+ assert last_response.ok?
74
+ end
75
+ end
76
+
77
+ class ServerCopyHelperTest < Test::Unit::TestCase
78
+ include CopyAppSetup
79
+ include Rack::Test::Methods
80
+
81
+ test "copy helper displays content from storage" do
82
+ Copy::Storage.stubs(:connected?).returns(true)
83
+ Copy::Storage.expects(:get).with(:facts).returns("truth")
84
+
85
+ get 'with_copy_helper'
86
+ assert last_response.ok?
87
+ assert_match "truth", last_response.body
88
+ end
89
+
90
+ test "copy helper saves defaults text when content is not in storage and renders it" do
91
+ Copy::Storage.stubs(:connected?).returns(true)
92
+ Copy::Storage.expects(:get).with(:facts).returns(nil)
93
+ Copy::Storage.expects(:set).with(:facts, "_Default Text_\n").returns(true)
94
+
95
+ get 'with_copy_helper'
96
+ assert last_response.ok?, last_response.errors
97
+ assert_match %Q(<div class="_copy_editable" data-name="facts"><p><em>Default Text</em></p></div>), last_response.body
98
+ end
99
+
100
+ test "copy helper shows default text when not connected" do
101
+ Copy::Storage.expects(:connected?).twice.returns(false)
102
+
103
+ get 'with_copy_helper'
104
+ assert last_response.ok?
105
+ assert_match %Q(<div class="_copy_editable" data-name="facts"><p><em>Default Text</em></p></div>), last_response.body
106
+ end
107
+
108
+ test "copy helper renders single line content correctly" do
109
+ Copy::Storage.expects(:connected?).twice.returns(false)
110
+
111
+ get 'with_copy_helper_one_line'
112
+ assert last_response.ok?
113
+ assert_match %Q(<span class="_copy_editable" data-name="headline">Important!</span>), last_response.body
114
+ end
115
+ end
116
+
117
+ class ServerAdminTest < Test::Unit::TestCase
118
+ include CopyAppSetup
119
+ include Rack::Test::Methods
120
+
121
+ test "GET /_copy is protected when no user/pass are set" do
122
+ get '/_copy'
123
+ assert_equal 401, last_response.status
124
+ end
125
+
126
+ test "GET /_copy protected when user/pass are set, but supplied incorrectly" do
127
+ setup_auth 'good', 'girl'
128
+ authorize 'bad', 'boy'
129
+ get '/_copy'
130
+ assert_equal 401, last_response.status
131
+ end
132
+
133
+ test "GET /_copy with valid credentials" do
134
+ authorize!
135
+ get '/_copy'
136
+ assert last_response.ok?
137
+ assert_match 'Edit Copy', last_response.body
138
+ end
139
+
140
+ test "GET /_copy.js" do
141
+ authorize!
142
+ get '/_copy.js'
143
+ assert last_response.ok?, last_response.errors
144
+ assert_match 'jQuery JavaScript Library', last_response.body
145
+ end
146
+
147
+ test "GET /_copy/:name" do
148
+ Copy::Storage.stubs(:connected?).returns(true)
149
+ Copy::Storage.expects(:get).with('fun').returns('party')
150
+
151
+ authorize!
152
+ get '/_copy/fun'
153
+ assert last_response.ok?, last_response.errors
154
+ assert_match "party</textarea>", last_response.body
155
+ end
156
+
157
+ test "PUT /_copy/:name" do
158
+ Copy::Storage.stubs(:connected?).returns(true)
159
+ Copy::Storage.expects(:set).with('fun', '_party_').returns(true)
160
+ Copy::Storage.expects(:get).with('fun').returns('_party_')
161
+
162
+ authorize!
163
+ put '/_copy/fun', :content => '_party_', :wrap_tag => 'article'
164
+ assert last_response.ok?, last_response.errors
165
+ assert_match %Q(<article class="_copy_editable" data-name="fun"><em>party</em></article>), last_response.body
166
+ end
167
+ end
@@ -0,0 +1,50 @@
1
+ require 'test_helper'
2
+
3
+ # Maybe TODO: Should the tests require a running redis, mongo, mysql
4
+ # instance and actually test getting, setting data?
5
+
6
+ class StorageTest < Test::Unit::TestCase
7
+ test "mongodb connect!" do
8
+ connection_url ='mongodb://copy:secret@localhost/copy-content'
9
+ Copy::Storage::Mongodb.expects(:new).with(connection_url).returns(true)
10
+
11
+ assert Copy::Storage.connect!(connection_url)
12
+ end
13
+
14
+ test "redis connect!" do
15
+ connection_url ='redis://localhost:6379'
16
+ Copy::Storage::Redis.expects(:new).with(connection_url).returns(true)
17
+
18
+ assert Copy::Storage.connect!(connection_url)
19
+ end
20
+
21
+ test "mysql connect!" do
22
+ connection_url = 'mysql://localhost/copy_content'
23
+ Copy::Storage::Relational.expects(:new).with(connection_url).returns(true)
24
+
25
+ assert Copy::Storage.connect!(connection_url)
26
+ end
27
+
28
+ test "postgres connect!" do
29
+ connection_url = 'postgres://localhost/copy_content'
30
+ Copy::Storage::Relational.expects(:new).with(connection_url).returns(true)
31
+
32
+ assert Copy::Storage.connect!(connection_url)
33
+ end
34
+
35
+ test "sqlite connect!" do
36
+ connection_url = 'sqlite:///path/to/copy_content.db'
37
+ Copy::Storage::Relational.expects(:new).with(connection_url).returns(true)
38
+
39
+ assert Copy::Storage.connect!(connection_url)
40
+ end
41
+
42
+ test "get and set" do
43
+ connection_url ='redis://localhost:6379'
44
+ Copy::Storage::Redis.expects(:new).with(connection_url).returns(stub(:get => :result1, :set => :result2))
45
+
46
+ Copy::Storage.connect!(connection_url)
47
+ assert_equal :result1, Copy::Storage.get('name')
48
+ assert_equal :result2, Copy::Storage.set('name', 'content')
49
+ end
50
+ end
@@ -0,0 +1 @@
1
+ <p>About!</p>
@@ -0,0 +1 @@
1
+ <p>About us!</p>
@@ -0,0 +1 @@
1
+ Javan,Zooey,Nali
@@ -0,0 +1 @@
1
+ <person>Javan</person>
@@ -0,0 +1 @@
1
+ <p>I'm the index!</p>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title>I'm the layout!</title>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
@@ -0,0 +1,3 @@
1
+ <% copy :facts do %>
2
+ _Default Text_
3
+ <% end %>
@@ -0,0 +1 @@
1
+ <h1><% copy :headline do %>Important!<% end %></h1>
@@ -0,0 +1,49 @@
1
+ dir = File.dirname(File.expand_path(__FILE__))
2
+ $LOAD_PATH.unshift dir + '/../lib'
3
+
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+ begin
7
+ require 'turn'
8
+ rescue LoadError
9
+ end
10
+ require 'mocha'
11
+ require 'copy'
12
+
13
+ class Test::Unit::TestCase
14
+ def self.test(name, &block)
15
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
16
+ end
17
+
18
+ def self.setup(&block)
19
+ define_method(:setup, &block)
20
+ end
21
+
22
+ def self.teardown(&block)
23
+ define_method(:teardown, &block)
24
+ end
25
+ end
26
+
27
+ module CopyAppSetup
28
+ def app
29
+ Copy::Server
30
+ end
31
+
32
+ def setup
33
+ app.config do
34
+ set :views, File.dirname(File.expand_path(__FILE__)) + '/test_app/views'
35
+ end
36
+ end
37
+
38
+ def setup_auth(user, pass)
39
+ app.config do
40
+ set :admin_user, user
41
+ set :admin_password, pass
42
+ end
43
+ end
44
+
45
+ def authorize!
46
+ setup_auth 'super', 'secret'
47
+ authorize 'super', 'secret'
48
+ end
49
+ end