danwrong-evil 0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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")