cloudfactory 0.1.12 → 0.1.13

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.
@@ -1,2 +1,2 @@
1
- Company,Website,meta_data_company
2
- Apple,apple.com,Apple
1
+ Company,Website
2
+ Apple,apple.com
data/lib/cf.rb CHANGED
@@ -90,4 +90,5 @@ require "#{directory}/cf/custom_task_form"
90
90
  require "#{directory}/cf/run"
91
91
  require "#{directory}/cf/department"
92
92
  require "#{directory}/cf/final_output"
93
- require "#{directory}/cf/robot_worker"
93
+ require "#{directory}/cf/robot_worker"
94
+ require "#{directory}/cf/version"
@@ -18,6 +18,7 @@ require "#{cli_directory}/config"
18
18
  require "#{cli_directory}/line"
19
19
  require "#{cli_directory}/form"
20
20
  require "#{cli_directory}/production"
21
+ require "#{cli_directory}/line_yaml_validator"
21
22
 
22
23
  if ENV['TEST']
23
24
  require 'ruby-debug'
@@ -28,6 +29,8 @@ module Cf # :nodoc: all
28
29
  include Thor::Actions
29
30
  include Cf::Config
30
31
 
32
+ map "-v" => :version
33
+
31
34
  desc "login", "Setup the cloudfactory credentials"
32
35
  def login
33
36
  email = ask("Enter your email:")
@@ -128,5 +131,9 @@ module Cf # :nodoc: all
128
131
  end
129
132
  end
130
133
 
134
+ desc "version", "Shows the current version of cloudfactory gem"
135
+ def version
136
+ say("Version: #{CF::VERSION}")
137
+ end
131
138
  end
132
139
  end
@@ -100,7 +100,15 @@ module Cf # :nodoc: all
100
100
  say "The line.yml file does not exist in this directory", :red
101
101
  return
102
102
  end
103
-
103
+
104
+ errors = LineYamlValidator.validate(yaml_source)
105
+
106
+ if errors.present?
107
+ say("Invalid line.yml file. Correct its structure as per the errors shown below.", :red)
108
+ errors.each {|error| say(" #{error}", :cyan)}
109
+ exit(1)
110
+ end
111
+
104
112
  set_target_uri(false)
105
113
  set_api_key(yaml_source)
106
114
 
@@ -0,0 +1,183 @@
1
+ module Cf
2
+ class LineYamlValidator
3
+
4
+ def self.validate(yaml_path)
5
+ line_dump = YAML::load(File.read(yaml_path))
6
+ errors = []
7
+ # Checking Department
8
+ if line_dump['department'].nil?
9
+ errors << "The line Department is missing!"
10
+ end
11
+
12
+ # Checking Input Formats
13
+ input_formats = line_dump['input_formats']
14
+ if input_formats.nil?
15
+ errors << "The Input Format is missing!"
16
+ else
17
+ if input_formats.class == Array
18
+ input_formats.each_with_index do |input_format, index|
19
+ name = input_format['name']
20
+ required = input_format['required']
21
+ valid_type = input_format['valid_type']
22
+ if name.nil?
23
+ errors << "Input Format name is missing in Block #{index+1}!"
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # Checking Stations
30
+ stations = line_dump['stations']
31
+ if stations.nil?
32
+ errors << "Station is missing!"
33
+ else
34
+ if stations.class == Array
35
+ if stations.first['station'].nil?
36
+ errors << "Station Settings missing!"
37
+ else
38
+ stations.each_with_index do |station, i|
39
+ station_index = station['station']['station_index']
40
+ errors << "Station Index is missing in Block station #{i+1}!" if station_index.nil?
41
+
42
+ station_type = station['station']['station_type']
43
+ errors << "Station type is missing in Block station #{i+1}!" if station_type.nil?
44
+
45
+ if station_type == "tournament"
46
+ jury_worker = station['station']['jury_worker']
47
+ if jury_worker.nil?
48
+ errors << "Jury worker setting is missing in Block station #{i+1}!"
49
+ elsif !jury_worker.nil?
50
+ reward = jury_worker['reward']
51
+ errors << "Reward for worker is missing in Block station #{i+1}!" if reward.nil?
52
+ errors << "Reward Must be greater than 0 in Block station #{i+1}!" if !reward.nil? && reward < 1
53
+ end
54
+ end
55
+ # Checking Worker
56
+ worker = station['station']['worker']
57
+ if worker.class != Hash
58
+ errors << "Worker is missing in Block station #{i+1}!"
59
+ elsif worker.class == Hash
60
+ # Checking Worker type
61
+ worker.each_pair do |k, v|
62
+ errors << "Should not be an array" if v.class == Array && k != "skill_badges"
63
+ end
64
+ worker_type = worker['worker_type']
65
+ if worker_type.nil?
66
+ errors << "Worker Type is missing!"
67
+ else
68
+ if worker_type != "human"
69
+ errors << "Worker type is invalid in Block station #{i+1}!" if worker_type.split("_").last != "robot"
70
+ if worker_type.split("_").last == "robot"
71
+ settings = worker['settings']
72
+ errors << "Settings for the robot worker is missingin Block station #{i+1}!" if settings.nil?
73
+ end
74
+ elsif worker_type == "human"
75
+ # Checking number of workers if worker_type == "human"
76
+ num_workers = worker['num_workers']
77
+ if num_workers.nil?
78
+ errors << "Number of workers not specified in Block station #{i+1}!"
79
+ else
80
+ errors << "Number of workers must be greater than 0 in Block station #{i+1}!" if num_workers < 1
81
+ end
82
+
83
+ # Checking reward of workers if worker_type == "human"
84
+ reward = worker['reward']
85
+ if reward.nil?
86
+ errors << "Reward of workers not specified in Block station #{i+1}!"
87
+ else
88
+ errors << "Reward of workers must be greater than 0 in Block station #{i+1}!" if reward < 1
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # Checking Stat_badge
95
+ stat_badge = station['station']['worker']['stat_badge']
96
+ if !stat_badge.nil?
97
+ errors << "Stat badge setting is invalid in Block station #{i+1}!" if stat_badge.class != Hash
98
+ end
99
+
100
+ # Checking skill_badge
101
+ skill_badges = station['station']['worker']['skill_badges']
102
+ if !skill_badges.nil?
103
+ errors << "Skill badge settings is invalid in Block station #{i+1}!" if skill_badges.class != Array
104
+ skill_badges.each_with_index do |badge, badge_index|
105
+ badge_title = badge['title']
106
+ badge_description = badge['description']
107
+ max_badges = badge['max_badges']
108
+ badge_test = badge['test']
109
+ test_input = badge_test['input'] if badge_test.class == Hash
110
+ expected_output = badge_test['expected_output'] if badge_test.class == Hash
111
+ errors << "Skill badge title is Missing in Block #{badge_index+1}!" if badge_title.nil?
112
+ errors << "Skill badge Description is Missing in Block #{badge_index+1}!" if badge_description.nil?
113
+ errors << "Skill badge max_badges must be greater than 1000 in Block #{badge_index+1}!" if max_badges < 1000 && !max_badges.nil?
114
+ errors << "Skill badge Test is Missing in Block #{badge_index+1}!" if badge_test.nil?
115
+ errors << "Skill badge Test is Invalid (must be Hash) in Block #{badge_index+1}!" if badge_test.class != Hash && !badge_test.nil?
116
+ errors << "Skill badge Test input is Missing in Block #{badge_index+1}!" if test_input.nil? && !badge_test.nil?
117
+ errors << "Skill badge Test input is Invalid (must be Hash) in Block #{badge_index+1}!" if test_input.class != Hash && !test_input.nil? && !badge_test.nil?
118
+ errors << "Skill badge Test expected_output is Missing in Block #{badge_index+1}!" if expected_output.nil? && !badge_test.nil?
119
+ errors << "Skill badge Test expected_output is Invalid (must be an array) in Block #{badge_index+1}!" if expected_output.class != Array && !expected_output.nil? && !badge_test.nil?
120
+ end
121
+ end
122
+
123
+ # Checking TaskForm
124
+ if worker_type == "human"
125
+ task_form = station['station']['task_form']
126
+ if task_form.nil?
127
+ custom_task_form = station['station']['custom_task_form']
128
+ if custom_task_form.nil?
129
+ errors << "Form is missing in Block station #{i+1}!"
130
+ elsif custom_task_form.class == Hash
131
+ form_title = custom_task_form['form_title']
132
+ errors << "Form Title is missing in Block station #{i+1}!" if form_title.nil?
133
+
134
+ instruction = custom_task_form['instruction']
135
+ errors << "Form Instruction is missing in Block station #{i+1}!" if instruction.nil?
136
+ end
137
+ elsif task_form.class == Hash
138
+ form_title = task_form['form_title']
139
+ errors << "Form Title is missing in Block station #{i+1}!" if form_title.nil?
140
+
141
+ instruction = task_form['instruction']
142
+ errors << "Form Instruction is missing in Block station #{i+1}!" if instruction.nil?
143
+
144
+ # Checking Form Fields
145
+ form_fields = task_form['form_fields']
146
+ errors << "Form Field is missing in Block station #{i+1}!" if form_fields.nil?
147
+ if form_fields.class == Array
148
+ form_fields.each_with_index do |form_field, index|
149
+ field_label = form_field['label']
150
+ errors << "Label is missing in block #{index+1} of Form Field within Station #{i+1}!" if field_label.nil?
151
+ required = form_field['required']
152
+ field_type = form_field['field_type']
153
+ if !field_type.nil?
154
+ unless %w(short_answer long_answer radio_button check_box select_box).include?(field_type)
155
+ errors << "Field Type of Form Field is invalid in Block #{index+1} of station Block #{i+1}!"
156
+ end
157
+ if field_type == "radio_button" || field_type == "select_box"
158
+ option_values = form_field['option_values']
159
+ if option_values.nil?
160
+ errors << "Option values is required for field_type => #{field_type} in block #{index+1} of Form Field within Station #{i+1} !"
161
+ elsif !option_values.nil?
162
+ if option_values.class != Array
163
+ errors << "Option values must be an array for field_type => #{field_type} in block #{index+1} of Form Field within Station #{i+1}!"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ else
170
+ errors << "Form fields must be an array for Station #{i+1}!"
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ return errors
180
+ end
181
+
182
+ end
183
+ end
@@ -125,5 +125,22 @@ module Cf # :nodoc: all
125
125
  say("\n")
126
126
  say(runs_table)
127
127
  end
128
+
129
+ desc "production resume", "resume a paused production run"
130
+ method_option :run_title, :type => :string, :required => true, :aliases => "-r", :desc => "the title of the run to resume"
131
+ def resume
132
+ set_target_uri(false)
133
+ set_api_key
134
+ CF.account_name = CF::Account.info.name
135
+ result = CF::Run.resume(options['run_title'].parameterize)
136
+
137
+ if result.error.present?
138
+ say("Error: #{result.error.message}", :red) and exit(1)
139
+ end
140
+
141
+ # if result.status == "resumed"
142
+ say("Run with title \"#{result.title}\" is resumed!", :green)
143
+ # end
144
+ end
128
145
  end
129
146
  end
@@ -60,14 +60,6 @@ module CF
60
60
  if type == "Improve" && self.stations.size < 1
61
61
  raise ImproveStationNotAllowed.new("You cannot add Improve Station as a first station of a line")
62
62
  else
63
- request_general =
64
- {
65
- :body =>
66
- {
67
- :api_key => CF.api_key,
68
- :station => {:type => type, :input_formats => @station_input_formats}
69
- }
70
- }
71
63
  if type == "Tournament"
72
64
  @jury_worker = stations.jury_worker
73
65
  @auto_judge = stations.auto_judge
@@ -81,6 +73,14 @@ module CF
81
73
  }
82
74
  resp = HTTParty.post("#{CF.api_url}#{CF.api_version}/lines/#{CF.account_name}/#{self.title.downcase}/stations.json",request_tournament)
83
75
  else
76
+ request_general =
77
+ {
78
+ :body =>
79
+ {
80
+ :api_key => CF.api_key,
81
+ :station => {:type => type, :input_formats => @station_input_formats}
82
+ }
83
+ }
84
84
  resp = HTTParty.post("#{CF.api_url}#{CF.api_version}/lines/#{CF.account_name}/#{self.title.downcase}/stations.json",request_general)
85
85
  end
86
86
  station = CF::Station.new()
@@ -157,6 +157,7 @@ module CF
157
157
  end
158
158
 
159
159
  end
160
+
160
161
  def input_formats=(input_formats_value) # :nodoc:
161
162
  @input_formats << input_formats_value
162
163
  end
@@ -27,10 +27,10 @@ module CF
27
27
  # You can pass line object instead of passing line title:
28
28
  # run = CF::Run.new(line_object, "run name", file_path)
29
29
  def initialize(line, title, input)
30
- if line.class == CF::Line || Hashie::Mash
30
+ if line.class == CF::Line || line.class == Hashie::Mash
31
31
  @line = line
32
- @line_title = @line.title
33
- else
32
+ @line_title = line.title
33
+ elsif line.class == String
34
34
  @line_title = line
35
35
  end
36
36
  @title = title
@@ -1,3 +1,3 @@
1
1
  module CF # :nodoc: all
2
- VERSION = "0.1.12"
2
+ VERSION = "0.1.13"
3
3
  end
@@ -40,8 +40,8 @@ module CF
40
40
  line.stations.first.type.should eql("WorkStation")
41
41
  line.stations.first.worker.number.should eql(2)
42
42
  line.stations.first.worker.reward.should eql(20)
43
- line.stations.first.worker.skill_badges.first.should eql([{"title"=>"Football Fanatic", "description"=>"This qualification allows you to perform work at stations which have this badge.", "score"=>nil, "speed"=>nil, "quality_rating"=>nil, "max_badges"=>3, "skill_test"=>{"score_after"=>"submit", "manual_scoring"=>false, "display_answers"=>false, "edit_answers"=>true, "retries"=>0, "pass_percentage"=>100, "test_units"=>[{"input"=>{"name"=>"Lionel Andres Messi", "country"=>"Argentina"}, "expected_output"=>[{"birthplace"=>"Rosario, Santa Fe, Argentina", "match_options"=>{"tolerance"=>"1", "ignore_case"=>"false"}, "position"=>"CF", "current-club"=>"Barcelona"}], "match_options"=>{"tolerance"=>0, "ignore_case"=>false}}]}}])
44
- line.stations.first.worker.skill_badges.last.should eql([{"title"=>"Football Fanatic", "description"=>"This qualification allows you to perform work at stations which have this badge.", "score"=>nil, "speed"=>nil, "quality_rating"=>nil, "max_badges"=>3, "skill_test"=>{"score_after"=>"submit", "manual_scoring"=>false, "display_answers"=>false, "edit_answers"=>true, "retries"=>0, "pass_percentage"=>100, "test_units"=>[{"input"=>{"name"=>"Cristiano Ronaldo", "country"=>"Portugal"}, "expected_output"=>[{"birthplace"=>"Rosario, Santa Fe, Portugal", "match_options"=>{"tolerance"=>"1", "ignore_case"=>"false"}, "position"=>"CF", "current-club"=>"Real Madrid"}], "match_options"=>{"tolerance"=>0, "ignore_case"=>false}}]}}])
43
+ line.stations.first.worker.skill_badges.first.should eql([{"title"=>"Football Fanatic", "description"=>"This qualification allows you to perform work at stations which have this badge.", "score"=>nil, "quality_rating"=>nil, "max_badges"=>3, "skill_test"=>{"score_after"=>"submit", "manual_scoring"=>false, "display_answers"=>false, "edit_answers"=>true, "retries"=>0, "pass_percentage"=>100, "test_units"=>[{"input"=>{"name"=>"Lionel Andres Messi", "country"=>"Argentina"}, "expected_output"=>[{"birthplace"=>"Rosario, Santa Fe, Argentina", "match_options"=>{"tolerance"=>"1", "ignore_case"=>"false"}, "position"=>"CF", "current-club"=>"Barcelona"}], "match_options"=>{"tolerance"=>0, "ignore_case"=>false}}]}}])
44
+ line.stations.first.worker.skill_badges.last.should eql([{"title"=>"Football Fanatic", "description"=>"This qualification allows you to perform work at stations which have this badge.", "score"=>nil, "quality_rating"=>nil, "max_badges"=>3, "skill_test"=>{"score_after"=>"submit", "manual_scoring"=>false, "display_answers"=>false, "edit_answers"=>true, "retries"=>0, "pass_percentage"=>100, "test_units"=>[{"input"=>{"name"=>"Cristiano Ronaldo", "country"=>"Portugal"}, "expected_output"=>[{"birthplace"=>"Rosario, Santa Fe, Portugal", "match_options"=>{"tolerance"=>"1", "ignore_case"=>"false"}, "position"=>"CF", "current-club"=>"Real Madrid"}], "match_options"=>{"tolerance"=>0, "ignore_case"=>false}}]}}])
45
45
  line.stations.first.worker.stat_badge.should eql({"approval_rating"=>80, "assignment_duration"=>3600, "abandonment_rate"=>30, "country"=>nil})
46
46
  end
47
47
  end
@@ -10,7 +10,7 @@ module CF
10
10
  line = CF::Line.create("concept_tagging_robot","Digitization") do |l|
11
11
  CF::InputFormat.new({:line => l, :name => "url", :valid_type => "url", :required => "true"})
12
12
  CF::Station.create({:line => l, :type => "work"}) do |s|
13
- CF::RobotWorker.create({:station => s, :type => "concept_tagging_robot", :settings => {:url => ["{url}"]}})
13
+ CF::RobotWorker.create({:station => s, :type => "concept_tagging_robot", :settings => {:url => ["{{url}}"]}})
14
14
  end
15
15
  end
16
16
  run = CF::Run.create(line, "concept_tagging_robot_run", [{"url"=>"www.mosexindex.com"}])
@@ -18,9 +18,9 @@ module CF
18
18
  output.first.final_output.first.concept_tagging_of_url.should eql(["Canada", "English language"])
19
19
  output.first.final_output.first.concept_tagging_relevance_of_url.should eql([89.5153, 79.0912])
20
20
  line.stations.first.worker.class.should eql(CF::RobotWorker)
21
- line.stations.first.worker.reward.should eql(1)
21
+ line.stations.first.worker.reward.should eql(0.5)
22
22
  line.stations.first.worker.number.should eql(1)
23
- line.stations.first.worker.settings.should eql({:url => ["{url}"]})
23
+ line.stations.first.worker.settings.should eql({:url => ["{{url}}"]})
24
24
  line.stations.first.worker.type.should eql("ConceptTaggingRobot")
25
25
  end
26
26
  end
@@ -35,7 +35,7 @@ module CF
35
35
  station = CF::Station.new({:type => "work"})
36
36
  line.stations station
37
37
 
38
- worker = CF::RobotWorker.create({:type => "concept_tagging_robot", :settings => {:url => ["{url}"]}})
38
+ worker = CF::RobotWorker.create({:type => "concept_tagging_robot", :settings => {:url => ["{{url}}"]}})
39
39
  line.stations.first.worker = worker
40
40
 
41
41
  run = CF::Run.create(line, "concept_tagging_robot_run_1", [{"url"=>"www.mosexindex.com"}])
@@ -43,9 +43,9 @@ module CF
43
43
  output.first.final_output.first.concept_tagging_of_url.should eql(["Canada", "English language"])
44
44
  output.first.final_output.first.concept_tagging_relevance_of_url.should eql([89.5153, 79.0912])
45
45
  line.stations.first.worker.class.should eql(CF::RobotWorker)
46
- line.stations.first.worker.reward.should eql(1)
46
+ line.stations.first.worker.reward.should eql(0.5)
47
47
  line.stations.first.worker.number.should eql(1)
48
- line.stations.first.worker.settings.should eql({:url => ["{url}"]})
48
+ line.stations.first.worker.settings.should eql({:url => ["{{url}}"]})
49
49
  line.stations.first.worker.type.should eql("ConceptTaggingRobot")
50
50
  end
51
51
  end
@@ -17,10 +17,9 @@ module CF
17
17
 
18
18
  output = run.final_output
19
19
  output.first.final_output.first.scraped_link_from_document.should eql([["http://www.cloudfactory.com", "http://www.bizcardarmy.com"]])
20
- output.first.final_output.first.query.should eql("1st 2 links after Sprout products")
21
20
 
22
21
  line.stations.first.worker.class.should eql(CF::RobotWorker)
23
- line.stations.first.worker.reward.should eql(10)
22
+ line.stations.first.worker.reward.should eql(0.5)
24
23
  line.stations.first.worker.number.should eql(1)
25
24
  line.stations.first.worker.settings.should eql({:document => ["http://www.sprout-technology.com"], :query => "1st 2 links after Sprout products"})
26
25
  line.stations.first.worker.type.should eql("ContentScrapingRobot")
@@ -44,10 +43,9 @@ module CF
44
43
 
45
44
  output = run.final_output
46
45
  output.first.final_output.first.scraped_link_from_document.should eql([["http://www.cloudfactory.com", "http://www.bizcardarmy.com"]])
47
- output.first.final_output.first.query.should eql("1st 2 links after Sprout products")
48
46
 
49
47
  line.stations.first.worker.class.should eql(CF::RobotWorker)
50
- line.stations.first.worker.reward.should eql(10)
48
+ line.stations.first.worker.reward.should eql(0.5)
51
49
  line.stations.first.worker.number.should eql(1)
52
50
  line.stations.first.worker.settings.should eql({:document => ["http://www.sprout-technology.com"], :query => "1st 2 links after Sprout products"})
53
51
  line.stations.first.worker.type.should eql("ContentScrapingRobot")
@@ -78,14 +78,14 @@ describe CF::CustomTaskForm do
78
78
  line = CF::Line.create("Digitizecustomform11", "Digitization") do
79
79
  CF::InputFormat.new({:line => self, :name => "Name", :required => true, :valid_format => "general"})
80
80
  CF::InputFormat.new({:line => self, :name => "Contact", :required => true, :valid_type => "url"})
81
- CF::Station.create({:line => self, :type => "tournament", :max_judges => 10, :auto_judge => true}) do |s|
81
+ CF::Station.create({:line => self, :type => "work"}) do |s|
82
82
  CF::HumanWorker.new({:station => s, :number => 3, :reward => 20})
83
83
  CF::CustomTaskForm.create({:station => s, :title => "Enter text from a business card image", :instruction => "Describe", :raw_html => html, :raw_css => css, :raw_javascript => javascript})
84
84
  end
85
85
  end
86
86
  line.title.should eql("Digitizecustomform11")
87
87
  line.department_name.should eql("Digitization")
88
- line.stations.first.type.should eql("TournamentStation")
88
+ line.stations.first.type.should eql("WorkStation")
89
89
  CGI.unescape_html(line.stations.first.form.raw_html).should eql(html)
90
90
  CGI.unescape_html(line.stations.first.form.raw_css).should eql(css)
91
91
  CGI.unescape_html(line.stations.first.form.raw_javascript).should eql(javascript)
@@ -94,7 +94,7 @@ describe CF::CustomTaskForm do
94
94
 
95
95
  it "in plain ruby way" do
96
96
  VCR.use_cassette "custom-task-form/plain/create", :record => :new_episodes do
97
- html = '<div id="form-content">
97
+ html = '<form><div id="form-content">
98
98
  <div id="instructions">
99
99
  <ul>
100
100
  <li>Look at the business card properly and fill in asked data.</li>
@@ -108,18 +108,18 @@ describe CF::CustomTaskForm do
108
108
  </div>
109
109
  <div id = "field-panel">
110
110
  Name<br />
111
- <input class="input-field first_name" type="text" name="final_output[first_name]" />
112
- <input class="input-field middle_name" type="text" name="final_output[middle_name]" />
113
- <input class="input-field last_name" type="text" name="final_output[last_name]" /><br />
111
+ <input class="input-field first_name" type="text" name="output[first_name]" />
112
+ <input class="input-field middle_name" type="text" name="output[middle_name]" />
113
+ <input class="input-field last_name" type="text" name="output[last_name]" /><br />
114
114
 
115
115
  <br />Contact<br />
116
- <input class="input-field email" type="text" name="final_output[email]" placeholder="Email"/>
117
- <input class="input-field phone" type="text" name="final_output[phone]" placeholder="Phone"/>
118
- <input class="input-field mobile" type="text" name="final_output[mobile]" placeholder="Mobile"/><br />
116
+ <input class="input-field email" type="text" name="output[email]" placeholder="Email"/>
117
+ <input class="input-field phone" type="text" name="output[phone]" placeholder="Phone"/>
118
+ <input class="input-field mobile" type="text" name="output[mobile]" placeholder="Mobile"/><br />
119
119
 
120
120
  </div>
121
121
  </div>
122
- </div>'
122
+ </div></form>'
123
123
 
124
124
  css = '<style>body {background:#fbfbfb;}
125
125
  #instructions{