danwrong-evil 0.1 → 0.1.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 (52) hide show
  1. data/CHANGELOG +1 -0
  2. data/LICENSE +22 -0
  3. data/Manifest +33 -7
  4. data/README.textile +20 -0
  5. data/Rakefile +21 -2
  6. data/assets/config.ru +10 -2
  7. data/assets/evil-lib.js +222 -0
  8. data/assets/evil.css +204 -10
  9. data/assets/evil.js +20 -0
  10. data/assets/logo.png +0 -0
  11. data/evil.gemspec +32 -19
  12. data/lib/evil/application.rb +85 -17
  13. data/lib/evil/extensions.rb +71 -0
  14. data/lib/evil/helpers.rb +62 -0
  15. data/lib/evil/models/config_pair.rb +7 -0
  16. data/lib/evil/models/template.rb +10 -9
  17. data/lib/evil/models.rb +0 -12
  18. data/lib/evil/open_id.rb +101 -0
  19. data/lib/evil/plugin/base.rb +56 -0
  20. data/lib/evil/plugin/configuration.rb +77 -0
  21. data/lib/evil/plugin/environment.rb +15 -0
  22. data/lib/evil/plugin/filesystem.rb +18 -0
  23. data/lib/evil/plugin/tag.rb +81 -0
  24. data/lib/evil/plugin.rb +21 -0
  25. data/lib/evil/setup/generator.rb +13 -1
  26. data/lib/evil/setup/migration.rb +4 -11
  27. data/lib/evil.rb +34 -1
  28. data/test/app/evil_test.rb +64 -0
  29. data/test/dev_env.rb +10 -0
  30. data/test/test_helper.rb +12 -0
  31. data/test/units/plugin/base_test.rb +47 -0
  32. data/test/units/plugin/tag_test.rb +108 -0
  33. data/views/_banner.haml +8 -0
  34. data/views/_errors.haml +9 -0
  35. data/views/index.haml +41 -0
  36. data/views/layout.haml +13 -0
  37. data/views/login.haml +5 -0
  38. data/views/plugins/_fields.haml +7 -0
  39. data/views/plugins/edit.haml +2 -0
  40. data/views/plugins/fields/_password.haml +3 -0
  41. data/views/plugins/fields/_text.haml +3 -0
  42. data/views/plugins/new.haml +6 -0
  43. data/views/templates/_fields.haml +23 -0
  44. data/views/templates/edit.haml +3 -0
  45. data/views/templates/new.haml +3 -0
  46. metadata +115 -13
  47. data/lib/evil/models/plugin.rb +0 -13
  48. data/test/harness/config.ru +0 -9
  49. data/test/harness/evil.db +0 -0
  50. data/test/harness/public/javascripts/evil.js +0 -0
  51. data/test/harness/public/stylesheets/evil.css +0 -24
  52. data/test/harness/tmp/restart.txt +0 -0
@@ -0,0 +1,81 @@
1
+ module Evil
2
+ module Plugin
3
+
4
+ class TagExecution
5
+
6
+ def initialize(tag, options, context, &block)
7
+ @options = evaluate(options, context)
8
+ @context = context
9
+ @tag = tag
10
+ @proc = block
11
+ end
12
+
13
+ def body(locals={})
14
+ @context.stack do
15
+ locals.each { |k, v| @context[k.to_s] = v }
16
+ return @tag.render_body(@context).to_s
17
+ end
18
+ end
19
+
20
+ def execute
21
+ self.instance_exec(@options, &@proc)
22
+ end
23
+
24
+ def to_s
25
+ execute.to_s
26
+ end
27
+
28
+ private
29
+
30
+ def evaluate(options, context)
31
+ options.inject({}) do |evaluated, pair|
32
+ opt, value = pair
33
+ evaluated[opt] = context[value]; evaluated
34
+ end
35
+ end
36
+ end
37
+
38
+ class Tag < Liquid::Block
39
+ Syntax = /((#{Liquid::TagAttributes}\s?,?\s?)*)/
40
+
41
+ class << self
42
+ attr_accessor :tag_proc
43
+ attr_accessor :plugin
44
+
45
+ def from(&block)
46
+ tag = Class.new(self)
47
+ tag.tag_proc = block; tag
48
+ end
49
+ end
50
+
51
+ def initialize(tag_name, markup, tokens)
52
+ super
53
+
54
+ if markup =~ Syntax
55
+ @options = parse_options($1)
56
+ else
57
+ raise Liquid::SyntaxError.new("Syntax Error in tag '#{tagname}' - Valid syntax: #{tagname} [ opt : 'val', opt : 'val' ]")
58
+ end
59
+ end
60
+
61
+ def render(context)
62
+ TagExecution.new(self, @options, context, &self.class.tag_proc).to_s
63
+ end
64
+
65
+ def render_body(context)
66
+ render_all(@nodelist, context)
67
+ end
68
+
69
+ private
70
+
71
+ def parse_options(opt_string)
72
+ pairs = opt_string.split(',')
73
+ pairs.inject({}) do |opts, pair|
74
+ opt, value = pair.split(':')
75
+ opts[opt.strip.to_sym] = value.strip; opts
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,21 @@
1
+ module Evil
2
+ module Plugin
3
+ autoload :Base, 'evil/plugin/base'
4
+ autoload :Tag, 'evil/plugin/tag'
5
+ autoload :Environment, 'evil/plugin/environment'
6
+ autoload :Filesystem, 'evil/plugin/filesystem'
7
+ autoload :Configuration, 'evil/plugin/configuration'
8
+
9
+ def self.evaluate(plugin_source)
10
+ Environment.module_eval plugin_source
11
+ end
12
+
13
+ def self.from_file(file)
14
+ evaluate(File.read(file))
15
+ end
16
+
17
+ def self.find_plugin(name)
18
+ Environment.plugins.find { |p| p.name == name }
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,7 @@
1
1
  require 'fileutils'
2
2
  require 'evil/models'
3
3
  require 'evil/setup/migration'
4
+ require 'digest/sha1'
4
5
 
5
6
  module Evil
6
7
  module Setup
@@ -14,11 +15,14 @@ module Evil
14
15
  public/images
15
16
  public/stylesheets
16
17
  public/javascripts
18
+ plugins
17
19
  }
18
20
 
19
21
  ASSETS = [
20
22
  ['evil.css', 'public/stylesheets/evil.css'],
21
23
  ['evil.js', 'public/javascripts/evil.js'],
24
+ ['evil-lib.js', 'public/javascripts/evil-lib.js'],
25
+ ['logo.png', 'public/images/logo.png'],
22
26
  ['config.ru', 'config.ru']
23
27
  ]
24
28
 
@@ -30,12 +34,14 @@ module Evil
30
34
  puts "Generating Evil application..."
31
35
  create_dir_tree!
32
36
  copy_assets!
37
+ set_cookie_secret!
33
38
  puts "Initializing database..."
34
39
  create_database!
35
40
  end
36
41
 
37
42
  def create_dir_tree!
38
43
  DIR_LAYOUT.each do |dir|
44
+ puts "Directory #{@path}/#{dir}"
39
45
  mkdir_p "#{@path}/#{dir}"
40
46
  end
41
47
  end
@@ -51,10 +57,16 @@ module Evil
51
57
 
52
58
  def copy_assets!
53
59
  ASSETS.each do |file, dest|
60
+ puts "File #{File.join(@path, dest)}"
54
61
  cp File.join(ASSET_PATH, file), File.join(@path, dest)
55
62
  end
56
63
  end
57
-
64
+
65
+ def set_cookie_secret!
66
+ rackup = File.read(File.join(@path, 'config.ru'))
67
+ rackup.gsub!(/__SECRET__/, Digest::SHA1.hexdigest("__EVIL__#{Time.now.to_s}__#{rand(999999)}"))
68
+ File.open(File.join(@path, 'config.ru'), 'w') { |f| f.write(rackup) }
69
+ end
58
70
  end
59
71
  end
60
72
  end
@@ -8,27 +8,20 @@ module Evil
8
8
  def create_evil_tables
9
9
  create_table :evil_templates do |t|
10
10
  t.integer :ttl, :position
11
- t.string :title, :route
11
+ t.string :title, :route, :content_type, :encoding
12
12
  t.text :source
13
13
  t.timestamps
14
- end unless Evil::Models::Template.table_exists?
15
-
16
- create_table :evil_plugins do |t|
17
- t.string :name, :description, :author
18
- t.text :source, :url
19
- t.boolean :enabled
20
- t.timestamps
21
- end unless Evil::Models::Plugin.table_exists?
14
+ end unless Evil::Models::Template.table_exists?
22
15
 
23
16
  unless Evil::Models::ConfigPair.table_exists?
24
17
  create_table :evil_config_pairs do |t|
25
- t.integer :plugin_id
18
+ t.string :plugin
26
19
  t.string :key
27
20
  t.text :value
28
21
  t.timestamps
29
22
  end
30
23
 
31
- add_index :evil_config_pairs, :plugin_id
24
+ add_index :evil_config_pairs, :plugin
32
25
  end
33
26
 
34
27
  create_table :evil_whitelists do |t|
data/lib/evil.rb CHANGED
@@ -1,3 +1,7 @@
1
+ require 'liquid'
2
+ require 'liquid_inheritance'
3
+ require 'httparty'
4
+
1
5
  module Evil
2
6
  module Setup
3
7
  autoload :DbTool, 'evil/setup/db_tool'
@@ -5,11 +9,40 @@ module Evil
5
9
  autoload :Migration, 'evil/setup/migration'
6
10
  end
7
11
 
12
+ autoload :OpenID, 'evil/open_id'
13
+ autoload :Helpers, 'evil/helpers'
14
+ autoload :Extensions, 'evil/extensions'
15
+
16
+ autoload :Plugin, 'evil/plugin'
17
+
8
18
  autoload :Application, 'evil/application'
9
19
 
10
20
  class << self
11
21
  attr_accessor :gem_root, :app_root
22
+
23
+ def heroku?
24
+ Object.const_defined?(:Heroku)
25
+ end
12
26
  end
13
27
  end
14
28
 
15
- Evil.gem_root = File.join(File.dirname(__FILE__), '..')
29
+ Evil.gem_root = File.join(File.dirname(__FILE__), '..')
30
+
31
+ class Proc
32
+ def bind(object)
33
+ block, time = self, Time.now
34
+ (class << object; self end).class_eval do
35
+ method_name = "__bind_#{time.to_i}_#{time.usec}"
36
+ define_method(method_name, &block)
37
+ method = instance_method(method_name)
38
+ remove_method(method_name)
39
+ method
40
+ end.bind(object)
41
+ end
42
+ end
43
+
44
+ class Object
45
+ def instance_exec(*arguments, &block)
46
+ block.bind(self)[*arguments]
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ require File.join(File.dirname(__FILE__), '../test_helper')
2
+ require 'evil/application'
3
+
4
+ class EvilTest < Test::Unit::TestCase
5
+ include Sinatra::Test
6
+
7
+ context 'running the evil application' do
8
+ setup do
9
+ @app = Evil::Application
10
+ end
11
+
12
+ context 'while not logged in' do
13
+
14
+ context 'GET /admin' do
15
+ setup do
16
+ get '/admin'
17
+ end
18
+
19
+ should 'not be authorized' do
20
+ assert_equal 401, response.status
21
+ assert_match /openid_url/, response.body
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ context 'while logged in' do
28
+ setup do
29
+ @env = { 'rack.session' => { :identity_url => 'www.danwebb.net' } }
30
+ end
31
+
32
+ context 'GET /admin' do
33
+ setup do
34
+ get '/admin', {}, @env
35
+ end
36
+
37
+ should 'be successful' do
38
+ assert response.ok?
39
+ end
40
+ end
41
+
42
+ context 'GET /admin/templates/1 with existing template' do
43
+ setup do
44
+ get '/admin/templates/1', {}, @env
45
+ end
46
+
47
+ should 'be successful' do
48
+ assert response.ok?
49
+ end
50
+ end
51
+
52
+ context 'GET /admin/templates/nothinghere with non-existing template' do
53
+ setup do
54
+ get '/admin/templates/nothinghere', {}, @env
55
+ end
56
+
57
+ should 'be not found' do
58
+ assert_equal 404, response.status
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ end
data/test/dev_env.rb ADDED
@@ -0,0 +1,10 @@
1
+ $:.unshift('./lib')
2
+ require 'evil'
3
+ require 'evil/models'
4
+
5
+ Evil.app_root = './test/example'
6
+
7
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3',
8
+ :database => File.join(Evil.app_root, 'evil.db')
9
+
10
+ include Evil::Models
@@ -0,0 +1,12 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '../lib'))
2
+ require 'rubygems'
3
+ require 'evil'
4
+ require 'test/unit'
5
+ require 'shoulda'
6
+ require 'sinatra/test'
7
+ require 'mocha'
8
+
9
+ require 'redgreen' rescue nil
10
+
11
+ Sinatra::Default.set :environment, 'test'
12
+ Evil.app_root = File.join(File.dirname(__FILE__), 'example')
@@ -0,0 +1,47 @@
1
+ class BaseTest < Test::Unit::TestCase
2
+ should 'initialize with a name and a block that recieves the new instance' do
3
+ plugin = Evil::Plugin::Base.new 'Test Plugin' do |p|
4
+ assert_instance_of Evil::Plugin::Base, p
5
+ end
6
+
7
+ assert_equal 'Test Plugin', plugin.name
8
+ end
9
+
10
+ context 'given a plugin instance with a description and setup set' do
11
+ setup do
12
+ @setup_proc = Proc.new {}
13
+
14
+ @plugin = Evil::Plugin::Base.new 'Test Plugin' do |p|
15
+ p.description 'A plugin for testing plugins'
16
+
17
+ p.setup &@setup_proc
18
+
19
+ end
20
+ end
21
+
22
+ should 'have a description set' do
23
+ assert_equal 'A plugin for testing plugins', @plugin.description
24
+ end
25
+
26
+ should 'call the setup proc when init called' do
27
+ @setup_proc.expects(:call)
28
+
29
+ @plugin.init
30
+ end
31
+
32
+ end
33
+
34
+ should 'create and register a tag instance when plugin initialize with a tag call' do
35
+ Evil::Plugin::Tag.expects(:from).returns(t = Class.new(Liquid::Tag))
36
+ Liquid::Template.expects(:register_tag).with(:thing, t)
37
+
38
+ Evil::Plugin::Base.new 'Test Plugin' do |p|
39
+ p.tag :thing do
40
+ 'a tag'
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+
47
+ end
@@ -0,0 +1,108 @@
1
+ require File.join(File.dirname(__FILE__), '../../test_helper')
2
+ require 'liquid'
3
+
4
+ class TagTest < Test::Unit::TestCase
5
+ context 'with a custom tag defined that returns a string' do
6
+ setup do
7
+ Liquid::Template.register_tag('test', Evil::Plugin::Tag.from { |params|
8
+ 'hello'
9
+ })
10
+ end
11
+
12
+ should 'output the string' do
13
+ template = Liquid::Template.parse('{% test %}{% endtest %}')
14
+
15
+ assert_equal 'hello', template.render
16
+ end
17
+ end
18
+
19
+ context 'with a custom tag defined that returns a parameter' do
20
+ setup do
21
+ Liquid::Template.register_tag('test', Evil::Plugin::Tag.from { |params|
22
+ params[:a]
23
+ })
24
+ end
25
+
26
+ should 'output that parameter if passed as a constant' do
27
+ template = Liquid::Template.parse('{% test a: "thing" %}{% endtest %}')
28
+
29
+ assert_equal 'thing', template.render
30
+ end
31
+
32
+ should 'output that parameter if passed as a variable' do
33
+ template = Liquid::Template.parse('{% test a: var %}{% endtest %}')
34
+
35
+ assert_equal 'thing', template.render('var' => 'thing')
36
+ end
37
+ end
38
+
39
+ context 'with a custom tag defined that uses the body method' do
40
+ setup do
41
+ Liquid::Template.register_tag('test', Evil::Plugin::Tag.from { |params|
42
+ times = params[:times] || 1
43
+ out = ''
44
+ times.times { |i| out << body(:i => i) }
45
+ out
46
+ })
47
+ end
48
+
49
+ should 'output the contents of the tag' do
50
+ template = Liquid::Template.parse('{% test %}a{% endtest %}')
51
+
52
+ assert_equal 'a', template.render
53
+ end
54
+
55
+ should 'remember the i variable passed to body' do
56
+ template = Liquid::Template.parse('{% test %}{{ i }}{% endtest %}')
57
+
58
+ assert_equal '0', template.render
59
+ end
60
+
61
+ should 'be different value of i for each repetition of body' do
62
+ template = Liquid::Template.parse('{% test times: 10 %}{{ i }}{% endtest %}')
63
+
64
+ assert_equal '0123456789', template.render
65
+ end
66
+ end
67
+
68
+ context 'with 2 custom tags defined' do
69
+ setup do
70
+ Liquid::Template.register_tag('test', Evil::Plugin::Tag.from { |params|
71
+ times = params[:times] || 1
72
+ out = ''
73
+ times.times { |i| out << body(:i => i) }
74
+ out
75
+ })
76
+
77
+ Liquid::Template.register_tag('test2', Evil::Plugin::Tag.from { |params|
78
+ params[:a]
79
+ })
80
+ end
81
+
82
+ should 'be able to nest tags' do
83
+ template = Liquid::Template.parse("{% test times: 2 %}{% test2 a: 4 %}{% endtest2 %}{% endtest %}")
84
+
85
+ assert_equal '44', template.render
86
+ end
87
+
88
+ should 'be able to use vars from outer tag in inner tag' do
89
+ template = Liquid::Template.parse("{% test times: 2 %}{% test2 a: i %}{% endtest2 %}{% endtest %}")
90
+
91
+ assert_equal '01', template.render
92
+ end
93
+ end
94
+
95
+ context 'with a custom tag with more than one parameter defined' do
96
+ setup do
97
+ Liquid::Template.register_tag('test', Evil::Plugin::Tag.from { |params|
98
+ [params[:a], params[:b]].join('|')
99
+ })
100
+ end
101
+
102
+ should 'output both parameters' do
103
+ template = Liquid::Template.parse("{% test a: 'g', b: 'a' %}{% endtest %}")
104
+
105
+ assert_equal 'g|a', template.render
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,8 @@
1
+ - if session[:identity_url]
2
+ #banner
3
+ %a{ :href => '/admin' }
4
+ %img#logo{ :src => '/images/logo.png', :alt => 'Evil' }
5
+ %form{ :method => 'post', :action => '/admin/openid/logout', :id => 'logout' }
6
+ Logged in as
7
+ %em= session[:identity_url]
8
+ %input.submit{ :type => 'submit', :value => 'Log out' }
@@ -0,0 +1,9 @@
1
+ - unless model.errors.empty?
2
+ %div.errors
3
+ %h3
4
+ There were problems saving this
5
+ = "#{name}:"
6
+ %ul
7
+ - model.errors.full_messages.each do |error|
8
+ %li= error
9
+
data/views/index.haml ADDED
@@ -0,0 +1,41 @@
1
+ #overview
2
+ %table#template-overview
3
+ %thead
4
+ %tr.title
5
+ %th{ :colspan => 3 } Templates
6
+ %tr
7
+ %th Name
8
+ %th URL Pattern
9
+ %th TTL
10
+ %th
11
+ %tbody
12
+ - @templates.each_with_index do |template, i|
13
+ %tr{ :class => ('alt' if i % 2 == 1) }
14
+ %td= template.title
15
+ %td= template.route
16
+ %td= template.route.empty? ? 'N/A' : template.ttl
17
+ %td.tools
18
+ %a{ :href => "/admin/templates/#{template.id}"} Edit
19
+ %tfoot
20
+ %tr
21
+ %td{ :colspan => 4 }
22
+ %a{ :href => '/admin/templates/new' } Add Template...
23
+
24
+ %table#plugin-overview
25
+ %thead
26
+ %tr.title
27
+ %th{ :colspan => 3 } Plugin Configuration
28
+ %tr
29
+ %th Name
30
+ %th
31
+ %tbody
32
+ - @plugins.each_with_index do |plugin, i|
33
+ %tr{ :class => ('alt' if i % 2 == 1) }
34
+ %td= plugin
35
+ %td.tools
36
+ %a{ :href => "/admin/plugins/#{urlencode(plugin)}"} Edit
37
+ %tfoot
38
+ %tr
39
+ %td{ :colspan => 3 }
40
+ %a{ :href => '/admin/plugins/new' } Add Configuration...
41
+
data/views/layout.haml ADDED
@@ -0,0 +1,13 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %title Evil Admin
5
+ %link{ :rel => 'stylesheet', :href => '/stylesheets/evil.css', :type => 'text/css' }
6
+ %body
7
+ = partial(:banner)
8
+ #content
9
+ = yield
10
+ %script{ :type => 'text/javascript', :src => 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js' }
11
+ %script{ :type => 'text/javascript', :src => '/javascripts/evil-lib.js' }
12
+ %script{ :type => 'text/javascript', :src => '/javascripts/evil.js' }
13
+
data/views/login.haml ADDED
@@ -0,0 +1,5 @@
1
+ %form{ :action => '/admin/openid/login', :id => 'login' }
2
+ %p
3
+ %img{ :src => '/images/logo.png', :alt => 'Evil' }
4
+ %input#openid_url{ :name => 'openid_url' }
5
+ %input.submit{ :type => 'submit', :value => 'Login' }
@@ -0,0 +1,7 @@
1
+ %h2= "Configure #{@plugin.name} Plugin"
2
+
3
+ - @plugin.configurator.fields.each do |field|
4
+ %input{ :type => 'hidden', :name => 'plugin', :value => @plugin.name }
5
+ = render_config_field(field)
6
+ %p
7
+ %input.submit{ :value => 'Save', :type => 'submit' }
@@ -0,0 +1,2 @@
1
+ %form{ :action => "/admin/plugins/#{urlencode(@plugin.name)}", :method => 'post' }
2
+ = partial(:"plugins/fields")
@@ -0,0 +1,3 @@
1
+ %p
2
+ %label{ :for => field.name }= field.label
3
+ %input.text{ :name => "config[#{field.name}]", :type => 'password', :id => field.name, :value => @plugin.config[field.name] }
@@ -0,0 +1,3 @@
1
+ %p
2
+ %label{ :for => field.name }= field.label
3
+ %input.text{ :name => "config[#{field.name}]", :id => field.name, :value => @plugin.config[field.name] }
@@ -0,0 +1,6 @@
1
+ %h2 Choose a plugin
2
+
3
+ %ul
4
+ - Evil::Plugin::Environment.plugins.each do |p|
5
+ %li{ :selected => (params[:plugin] == p.name ) }
6
+ %a{ :href => "/admin/plugins/#{urlencode(p.name)}" }= p.name
@@ -0,0 +1,23 @@
1
+ %p
2
+ %label{ :for => 'template_title' }
3
+ Title *
4
+ %input#template_title.text{ :name => 'template[title]', :value => @template.title }
5
+ %p
6
+ %label{ :for => 'template_route' }
7
+ Route
8
+ %input#template_route.text{ :name => 'template[route]', :value => @template.route }
9
+ %p
10
+ %label{ :for => 'template_source' }
11
+ Source *
12
+ %textarea#template_source{ :name => 'template[source]' }= @template.source
13
+ %p
14
+ %label{ :for => 'template_ttl' }
15
+ Expires every (seconds)
16
+ %input#template_ttl.text{ :name => 'template[ttl]', :value => @template.ttl || 600 }
17
+ %p
18
+ %label{ :for => 'template_content_type' }
19
+ Content Type
20
+ %input#template_content_type.short{ :name => 'template[content_type]', :value => @template.content_type || 'text/html' }
21
+ %input#template_encoding.short{ :name => 'template[encoding]', :value => @template.encoding || 'utf-8' }
22
+ %p
23
+ %input.submit{ :value => 'Save', :type => 'submit' }
@@ -0,0 +1,3 @@
1
+ %form{ :action => "/admin/templates/#{@template.id}", :method => 'post' }
2
+ = partial(:errors, :locals => { :model => @template, :name => 'template' })
3
+ = partial(:"templates/fields")
@@ -0,0 +1,3 @@
1
+ %form{ :action => '/admin/templates', :method => 'post' }
2
+ = partial(:errors, :locals => { :model => @template, :name => 'template' })
3
+ = partial(:"templates/fields")