form_input 0.9.0.pre1

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +19 -0
  4. data/README.md +3160 -0
  5. data/Rakefile +19 -0
  6. data/example/controllers/ramaze/press_release.rb +104 -0
  7. data/example/controllers/ramaze/profile.rb +38 -0
  8. data/example/controllers/sinatra/press_release.rb +114 -0
  9. data/example/controllers/sinatra/profile.rb +39 -0
  10. data/example/forms/change_password_form.rb +17 -0
  11. data/example/forms/login_form.rb +14 -0
  12. data/example/forms/lost_password_form.rb +14 -0
  13. data/example/forms/new_password_form.rb +15 -0
  14. data/example/forms/password_form.rb +18 -0
  15. data/example/forms/press_release_form.rb +153 -0
  16. data/example/forms/profile_form.rb +21 -0
  17. data/example/forms/signup_form.rb +25 -0
  18. data/example/views/press_release.slim +65 -0
  19. data/example/views/profile.slim +28 -0
  20. data/example/views/snippets/form_block.slim +27 -0
  21. data/example/views/snippets/form_chunked.slim +25 -0
  22. data/example/views/snippets/form_hidden.slim +21 -0
  23. data/example/views/snippets/form_panel.slim +89 -0
  24. data/form_input.gemspec +32 -0
  25. data/lib/form_input/core.rb +1165 -0
  26. data/lib/form_input/localize.rb +49 -0
  27. data/lib/form_input/r18n/cs.yml +97 -0
  28. data/lib/form_input/r18n/en.yml +70 -0
  29. data/lib/form_input/r18n/pl.yml +122 -0
  30. data/lib/form_input/r18n/sk.yml +120 -0
  31. data/lib/form_input/r18n.rb +163 -0
  32. data/lib/form_input/steps.rb +365 -0
  33. data/lib/form_input/types.rb +176 -0
  34. data/lib/form_input/version.rb +12 -0
  35. data/lib/form_input.rb +5 -0
  36. data/test/helper.rb +21 -0
  37. data/test/localize/en.yml +63 -0
  38. data/test/r18n/cs.yml +60 -0
  39. data/test/r18n/xx.yml +51 -0
  40. data/test/reference/cs.txt +352 -0
  41. data/test/reference/cs.yml +14 -0
  42. data/test/reference/en.txt +76 -0
  43. data/test/reference/en.yml +8 -0
  44. data/test/reference/pl.txt +440 -0
  45. data/test/reference/pl.yml +16 -0
  46. data/test/reference/sk.txt +352 -0
  47. data/test/reference/sk.yml +14 -0
  48. data/test/test_core.rb +1272 -0
  49. data/test/test_localize.rb +27 -0
  50. data/test/test_r18n.rb +373 -0
  51. data/test/test_steps.rb +482 -0
  52. data/test/test_types.rb +307 -0
  53. metadata +145 -0
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # Rake makefile.
2
+
3
+ require 'rake/testtask'
4
+
5
+ task :default => :test
6
+
7
+ desc 'Run tests'
8
+ task :test do
9
+ sh "bacon --automatic --quiet"
10
+ end
11
+
12
+ desc 'Run tests with coverage'
13
+ task :cov do
14
+ sh "rm -rf coverage"
15
+ ENV['COVERAGE'] = 'on'
16
+ Rake::Task[:test].execute
17
+ end
18
+
19
+ # EOF #
@@ -0,0 +1,104 @@
1
+ # The press release controller - example of a complex multistep form handling in Ramaze.
2
+
3
+ class PRController < Controller
4
+
5
+ # Create press release.
6
+ def create
7
+
8
+ # Start with fresh form.
9
+
10
+ unless request.post?
11
+ @form = PressReleaseForm.new(
12
+ user.active_plan,
13
+ date: Time.now,
14
+ city: user.profile.city,
15
+ state: user.profile.state,
16
+ country: user.profile.country,
17
+ contact_name: user.full_name,
18
+ contact_organization: user.profile.company,
19
+ contact_phone: user.profile.phone,
20
+ contact_email: user.email,
21
+ )
22
+ return
23
+ end
24
+
25
+ # Process the data from the post.
26
+
27
+ @form = PressReleaseForm.new( user.active_plan, request )
28
+ return unless process_form( @form )
29
+
30
+ # Save the press release.
31
+
32
+ unless user.create_press_release( @form )
33
+ @form.step = :summary
34
+ @state = :create_failed
35
+ return
36
+ end
37
+
38
+ redirect r
39
+ end
40
+
41
+ # Edit press release.
42
+ def edit( hid )
43
+
44
+ # Fetch the existing press release.
45
+ # Note that it can be edited by admins in addition to its owner.
46
+
47
+ not_found unless pr = PressRelease.from_hid( hid )
48
+
49
+ owner = ( pr.user_id == user.id )
50
+ forbidden unless owner or user.admin?
51
+
52
+ # Start by filling the form from the press release.
53
+
54
+ unless request.post?
55
+ @form = PressReleaseForm.new( pr.user.active_plan, pr.form_hash ).unlock_steps
56
+ return
57
+ end
58
+
59
+ # Process the data from the post.
60
+
61
+ pr = get_press_release( hid )
62
+ @form = PressReleaseForm.new( pr.user.active_plan, request )
63
+ return unless process_form( @form )
64
+
65
+ # Update the press release.
66
+
67
+ unless pr.user.update_press_release( pr, @form, user )
68
+ @form.step = :summary
69
+ @state = :update_failed
70
+ return
71
+ end
72
+
73
+ redirect owner ? r : AdminController.r( :press_releases )
74
+ end
75
+
76
+ alias_view :edit, :create
77
+
78
+ # Common logic for processing the form steps.
79
+ def process_form( form )
80
+
81
+ # Report errors whenever there are some in the currently finished step.
82
+
83
+ @state = :report if form.incomplete_step?
84
+
85
+ # That's all until the last steps are reached.
86
+
87
+ return if form.step_before?( :summary )
88
+
89
+ # In case the form is still invalid, return to the appropriate step.
90
+
91
+ unless form.valid?
92
+ form.step = form.incorrect_step
93
+ @state = :report
94
+ return
95
+ end
96
+
97
+ # That's all until the last step is reached.
98
+
99
+ return form.last_step?
100
+ end
101
+
102
+ end
103
+
104
+ # EOF #
@@ -0,0 +1,38 @@
1
+ # Example of simple form handling in Ramaze.
2
+
3
+ class HomeController < Controller
4
+
5
+ # Profile.
6
+ def profile
7
+
8
+ # Prefill the form with current user profile.
9
+
10
+ unless request.post?
11
+ @form = ProfileForm.new( user.profile_hash )
12
+ return
13
+ end
14
+
15
+ # Check new data when the form is posted.
16
+
17
+ @form = ProfileForm.new( request )
18
+ unless @form.valid?
19
+ @state = :report
20
+ return
21
+ end
22
+
23
+ # Attempt to update the profile.
24
+
25
+ unless user.update_profile( @form )
26
+ @state = :update_failed
27
+ return
28
+ end
29
+
30
+ # Refetch the profile just in case the model changed something, and report success.
31
+
32
+ @form = ProfileForm.new( user.profile_hash )
33
+ @state = :done
34
+ end
35
+
36
+ end
37
+
38
+ # EOF #
@@ -0,0 +1,114 @@
1
+ # The press release controller - example of a complex multistep form handling in Sinatra.
2
+
3
+ class App < Sinatra::Base
4
+
5
+ # Create press release - start.
6
+ get '/pr/create' do
7
+
8
+ # Start with fresh form, prefilled with some user info.
9
+
10
+ @form = PressReleaseForm.new(
11
+ user.active_plan,
12
+ date: Time.now,
13
+ city: user.profile.city,
14
+ state: user.profile.state,
15
+ country: user.profile.country,
16
+ contact_name: user.full_name,
17
+ contact_organization: user.profile.company,
18
+ contact_phone: user.profile.phone,
19
+ contact_email: user.email,
20
+ )
21
+ slim :press_release
22
+ end
23
+
24
+ # Create press release - creation.
25
+ post '/pr/create' do
26
+
27
+ # Process the data from the post.
28
+
29
+ @form = PressReleaseForm.new( user.active_plan, request )
30
+ unless process_form( @form )
31
+ return slim :press_release
32
+ end
33
+
34
+ # Save the press release.
35
+
36
+ unless user.create_press_release( @form )
37
+ @form.step = :summary
38
+ @state = :create_failed
39
+ return slim :press_release
40
+ end
41
+
42
+ redirect '/'
43
+ end
44
+
45
+ # Edit press release - start.
46
+ get '/pr/edit/:hid' do |hid|
47
+
48
+ # Fetch the existing press release.
49
+ # Note that it can be edited by admins in addition to its owner.
50
+
51
+ pr = get_press_release( hid )
52
+ @form = PressReleaseForm.new( pr.user.active_plan, pr.form_hash ).unlock_steps
53
+ slim :press_release
54
+ end
55
+
56
+ # Edit press release - update.
57
+ post '/pr/edit/:hid' do |hid|
58
+
59
+ # Process the data from the post.
60
+
61
+ pr = get_press_release( hid )
62
+ @form = PressReleaseForm.new( pr.user.active_plan, request )
63
+ unless process_form( @form )
64
+ return slim :press_release
65
+ end
66
+
67
+ # Update the press release.
68
+
69
+ unless pr.user.update_press_release( pr, @form, user )
70
+ @form.step = :summary
71
+ @state = :update_failed
72
+ return slim :press_release
73
+ end
74
+
75
+ redirect '/'
76
+ end
77
+
78
+ # Get the press release for given hashed id.
79
+ def get_press_release( hid )
80
+ not_found unless pr = PressRelease.from_hid( hid )
81
+
82
+ owner = ( pr.user_id == user.id )
83
+ forbidden unless owner or user.admin?
84
+
85
+ pr
86
+ end
87
+
88
+ # Common logic for processing the form steps.
89
+ def process_form( form )
90
+
91
+ # Report errors whenever there are some in the currently finished step.
92
+
93
+ @state = :report if form.incomplete_step?
94
+
95
+ # That's all until the last steps are reached.
96
+
97
+ return if form.step_before?( :summary )
98
+
99
+ # In case the form is still invalid, return to the appropriate step.
100
+
101
+ unless form.valid?
102
+ form.step = form.incorrect_step
103
+ @state = :report
104
+ return
105
+ end
106
+
107
+ # That's all until the last step is reached.
108
+
109
+ return form.last_step?
110
+ end
111
+
112
+ end
113
+
114
+ # EOF #
@@ -0,0 +1,39 @@
1
+ # Example of simple form handling in Sinatra.
2
+
3
+ class App < Sinatra::Application
4
+
5
+ get '/profile' do
6
+
7
+ # Prefill the form with current user profile.
8
+
9
+ @form = ProfileForm.new( user.profile_hash )
10
+ slim :profile
11
+ end
12
+
13
+ post '/profile' do
14
+
15
+ # Validate the posted data.
16
+
17
+ @form = ProfileForm.new( request )
18
+ unless @form.valid?
19
+ @state = :report
20
+ return slim :profile
21
+ end
22
+
23
+ # Attempt to update the profile.
24
+
25
+ unless user.update_profile( @form )
26
+ @state = :update_failed
27
+ return slim :profile
28
+ end
29
+
30
+ # Refetch the profile just in case the model changed something, and report success.
31
+
32
+ @form = ProfileForm.new( user.profile_hash )
33
+ @state = :done
34
+ slim :profile
35
+ end
36
+
37
+ end
38
+
39
+ # EOF #
@@ -0,0 +1,17 @@
1
+ # Change password form.
2
+
3
+ require_relative 'new_password_form'
4
+
5
+ class ChangePasswordForm < FormInput
6
+
7
+ # Note that we intentionally don't impose any format on this,
8
+ # as we don't want to assume what they have registered with.
9
+ # Only strip the password newline in case they cut&paste it from somewhere.
10
+
11
+ param! :old_password, "Old password", type: :password, filter: ->{ chomp }
12
+
13
+ copy NewPasswordForm
14
+
15
+ end
16
+
17
+ # EOF #
@@ -0,0 +1,14 @@
1
+ # Login form.
2
+
3
+ class LoginForm < FormInput
4
+
5
+ # Note that we intentionally don't impose any format of these,
6
+ # as we don't want to assume what they have registered with.
7
+ # Only strip the password newline in case they cut&paste it from somewhere.
8
+
9
+ param! :login, :email, "Email"
10
+ param! :password, "Password", type: :password, filter: ->{ chomp }
11
+
12
+ end
13
+
14
+ # EOF #
@@ -0,0 +1,14 @@
1
+ # Lost password form.
2
+
3
+ class LostPasswordForm < FormInput
4
+
5
+ # Note that we intentionally don't impose any format of the email,
6
+ # as we don't want to assume what they have registered with.
7
+ # We will let them know in case we don't find it anyway.
8
+ # We merely check if it looks like an email at all.
9
+
10
+ param! :email, "Email", match: /@/
11
+
12
+ end
13
+
14
+ # EOF #
@@ -0,0 +1,15 @@
1
+ # New password form.
2
+
3
+ require_relative 'password_form'
4
+
5
+ class NewPasswordForm < FormInput
6
+
7
+ copy PasswordForm, title: "New password"
8
+
9
+ param! :password_check, "Repeated password", type: :password,
10
+ filter: self[ :password ].filter,
11
+ check: ->{ report( "%p must match password" ) unless value == form.password }
12
+
13
+ end
14
+
15
+ # EOF #
@@ -0,0 +1,18 @@
1
+ # Password form.
2
+
3
+ # Separate form with only the password parameter to make it easy to include it wherever necessary.
4
+ class PasswordForm < FormInput
5
+
6
+ # Enforce size and format of the password.
7
+ # Note that we strip the password newline in case they cut&paste it from somewhere.
8
+
9
+ param! :password, "Password", type: :password, min_size: 6,
10
+ reject: /\P{ASCII}|[\t\r\n]/u,
11
+ reject_msg: "%p may contain only ASCII characters and spaces",
12
+ match: [ /[a-z]/i, /\d/ ],
13
+ msg: "%p must contain at least one digit and one letter",
14
+ filter: ->{ chomp }
15
+
16
+ end
17
+
18
+ # EOF #
@@ -0,0 +1,153 @@
1
+ # Press Release Form - example of a complex multistep form.
2
+
3
+ class PressReleaseForm < FormInput
4
+
5
+ ### Parameters ###
6
+
7
+ # Pricing plan we use for evaluating things.
8
+
9
+ attr_reader :plan
10
+
11
+ # Creation process steps.
12
+
13
+ define_steps(
14
+ content: "Content",
15
+ date: "Date",
16
+ location: "Location",
17
+ contact: "Contact",
18
+ keywords: "Keywords",
19
+ quote: "Quote",
20
+ images: "Images",
21
+ videos: "Videos",
22
+ website: "Web Site",
23
+ release: "Release Date",
24
+ channels: "Channels",
25
+ summary: "Summary",
26
+ post: nil,
27
+ )
28
+
29
+ # Press release itself.
30
+
31
+ param! :title, "Title", 120, tag: :content,
32
+ subtitle: "(Maximum 120 characters, recommended max 80 characters)",
33
+ help: "The main title of your press release. It should be brief, clear and to the point."
34
+ param :subtitle, "Subtitle", 160, tag: :content,
35
+ subtitle: "(Maximum 160 characters)",
36
+ help: "Optional but recommended. If used, make sure it's descriptive and builds on the headline."
37
+ param! :text, "Text", 65000, max_bytesize: 65535, type: :textarea, size: 16, tag: :content,
38
+ subtitle: ->{ "(Maximum #{plural( form.limit( :words ), 'word' )})" },
39
+ filter: ->{ strip },
40
+ check: ->{
41
+ count = form.feature_count( :words, value )
42
+ limit = form.limit( :words )
43
+ report "Your text has #{plural( count, 'word' )}, but the limit is #{delimited( limit )}." if count > limit
44
+ }
45
+
46
+ # Date.
47
+
48
+ param! :date, "Date", DATE_ARGS, tag: :date
49
+
50
+ # Location.
51
+
52
+ param! :city, "City", 100, tag: :location
53
+ param :state, "State", STATE_ARGS, tag: :location
54
+ param! :country, "Country", COUNTRY_ARGS, tag: :location
55
+
56
+ # Contact info.
57
+
58
+ param! :contact_name, "Full Name", 60, tag: :contact
59
+ param! :contact_organization, "Organization Name", 60, tag: :contact
60
+ param! :contact_phone, "Phone", 30, PHONE_ARGS, tag: :contact
61
+ param! :contact_email, "Email Address", 60, EMAIL_ARGS, tag: :contact
62
+
63
+ # Keywords.
64
+
65
+ array :keywords, "Keywords", 35, PRUNED_ARGS, tag: :keywords,
66
+ row: :keywords, cols: 5,
67
+ max_count: ->{ form.limit( :keywords ) }
68
+ array :keywords_urls, "Keywords URL", PRUNED_ARGS, WEB_URL_ARGS, max_count: 30, tag: :keywords,
69
+ row: :keywords, cols: 7,
70
+ max_count: ->{ form.limit( :keywords ) }
71
+
72
+ def balance_keywords
73
+ self.keywords ||= []
74
+ self.keywords_urls ||= []
75
+
76
+ report( :keywords, "Keywords must match the keyword URLs" ) if keywords.count < keywords_urls.count
77
+ report( :keywords_urls, "Keyword URLs must match the keywords" ) if keywords.count > keywords_urls.count
78
+
79
+ self.keywords << "" while keywords.count < keywords_urls.count
80
+ self.keywords_urls << "" while keywords.count > keywords_urls.count
81
+ end
82
+
83
+ # Quote.
84
+
85
+ param :quote, "Quote", 200, tag: :quote,
86
+ subtitle: "(Maximum 200 characters)",
87
+ disabled: ->{ form.disabled?( :quote ) }
88
+ param :quote_author, "Quote Author", 100, tag: :quote,
89
+ subtitle: "(Maximum 100 characters)",
90
+ disabled: ->{ form.disabled?( :quote ) }
91
+
92
+ # Images.
93
+
94
+ array :images, "Images", PRUNED_ARGS, tag: :images,
95
+ max_count: ->{ form.limit( :images ) }
96
+
97
+ # Video.
98
+
99
+ array :videos, "Videos", PRUNED_ARGS, tag: :videos,
100
+ max_count: ->{ form.limit( :videos ) }
101
+
102
+ # Website.
103
+
104
+ param :website_url, "URL", WEB_URL_ARGS, tag: :website,
105
+ disabled: ->{ form.disabled?( :website ) }
106
+
107
+ # Release date.
108
+
109
+ param :release_date, "Release Date", US_DATE_ARGS, tag: :release,
110
+ disabled: ->{ form.disabled?( :scheduling ) }
111
+ param :release_time, "Release Time", HOURS_ARGS, tag: :release,
112
+ disabled: ->{ form.disabled?( :scheduling ) }
113
+
114
+ # Distribution.
115
+
116
+ array! :channels, "Channels", type: :select, tag: :channels,
117
+ size: 25,
118
+ max_count: ->{ form.limit( :channels ) },
119
+ data: ->{ Channel.all.map{ |x| [ x.code, x.name ] } },
120
+ filter: ->{ self if Channel[ self ] }
121
+
122
+ ### Methods ###
123
+
124
+ # Initialize new instance.
125
+ def initialize( plan, *args )
126
+ @plan = plan
127
+ super( *args )
128
+ balance_keywords
129
+ end
130
+
131
+ # Get the items to show in the sidebar.
132
+ def sidebar_items
133
+ { base: 'Base Price' }.merge( step_names )
134
+ end
135
+
136
+ # Get limit for given feature.
137
+ def limit( feature )
138
+ plan.extra_limit( feature )
139
+ end
140
+
141
+ # Get count for given feature and value.
142
+ def feature_count( feature, value )
143
+ plan.feature_count( feature, value )
144
+ end
145
+
146
+ # Test if given feature is disabled for current plan.
147
+ def disabled?( feature )
148
+ not plan.has_feature?( feature )
149
+ end
150
+
151
+ end
152
+
153
+ # EOF #
@@ -0,0 +1,21 @@
1
+ # Profile form.
2
+
3
+ require_relative 'signup_form'
4
+
5
+ class ProfileForm < FormInput
6
+
7
+ copy SignupForm[ :first_name, :last_name ]
8
+
9
+ param :company, "Company"
10
+ param :street, "Street"
11
+ param :city, "City"
12
+ param :state, "State", STATE_ARGS
13
+ param :country, "Country", COUNTRY_ARGS
14
+ param :zip, "ZIP code", ZIP_ARGS
15
+ param :phone, "Phone number", PHONE_ARGS
16
+ param :fax, "Fax number", PHONE_ARGS
17
+ param :url, "URL", WEB_URL_ARGS
18
+
19
+ end
20
+
21
+ # EOF #
@@ -0,0 +1,25 @@
1
+ # Signup form.
2
+
3
+ require_relative 'password_form'
4
+
5
+ class SignupForm < FormInput
6
+
7
+ # Currently we restrict names to latin based alphabets.
8
+
9
+ param! :first_name, "First name", match: LATIN_NAMES_RE,
10
+ msg: "Sorry, only names using latin alphabet are allowed"
11
+ param! :last_name, "Last name", match: LATIN_NAMES_RE,
12
+ msg: "Sorry, only surnames using latin alphabet are allowed"
13
+
14
+ # Note that we only enforce relaxed format of the emails.
15
+ # Real problems will be found out by verification anyway.
16
+
17
+ param! :login, :email, "Email", EMAIL_ARGS
18
+
19
+ # Enforce size and format of the password the same way the standard password form does.
20
+
21
+ copy PasswordForm
22
+
23
+ end
24
+
25
+ # EOF #
@@ -0,0 +1,65 @@
1
+ // Press Release editing form - example of a complex multi-step form.
2
+
3
+ - @title = "Press Release: #{@form.step_name}"
4
+
5
+ - edit_mode = ( page_action == 'edit' )
6
+
7
+ form *form_attrs
8
+
9
+ // Invisible first submit button to send users to the next page
10
+ // when they press enter in the text field.
11
+ button.invisible type='submit' name='next' tabindex='-1' value=@form.next_step
12
+
13
+ .row
14
+ .col-md-3 role='navigation'
15
+
16
+ == snippet :sidebar,
17
+ title: "#{edit_mode ? 'Edit' : 'Create'} Press Release",
18
+ items: @form.sidebar_items,
19
+ accessible: @form.accessible_steps,
20
+ valid: @form.good_steps,
21
+ invalid: @form.bad_steps,
22
+ current: @form.step
23
+
24
+ .col-md-9 role='main'
25
+
26
+ .panel.panel-default
27
+ .panel-heading
28
+ = @title
29
+ .panel-body
30
+
31
+ // Additional info derived from @state or @form.step will usually go here.
32
+
33
+ fieldset
34
+ == snippet :form_hidden,
35
+ params: @form.other_params
36
+ == snippet :form_chunked,
37
+ params: @form.chunked_params( @form.current_params ),
38
+ report: @state == :report,
39
+ focus: true
40
+
41
+ - unless @form.extra_step?
42
+ .btn-toolbar.pull-left
43
+ button.btn.btn-default type='submit'
44
+ Update
45
+
46
+ .btn-toolbar.pull-right
47
+ a.btn.btn-default href=r
48
+ Cancel
49
+ - if @form.step == :summary
50
+ button.btn.btn-primary type='submit' name='next' value='post'
51
+ Save
52
+ - else
53
+ button.btn.btn-default type='submit' name='next' value='summary'
54
+ Finish
55
+ - unless edit_mode
56
+ button.btn.btn-primary.pull-right type='submit' name='next' value=@form.next_step
57
+ Next
58
+
59
+ // These make it easy to debug the form if needed:
60
+ //
61
+ // pre = @form.params.map{|x| [ x.name, x.value ] }.inspect
62
+ // pre = @form.errors.inspect
63
+ // pre = @state
64
+
65
+ // EOF //
@@ -0,0 +1,28 @@
1
+ // Update user profile - example of rendering simple form page.
2
+
3
+ - @title = "Profile"
4
+
5
+ .panel.panel-default
6
+ .panel-heading
7
+ = @title
8
+ .panel-body
9
+ form *form_attrs
10
+
11
+ - case @state
12
+ - when :done
13
+ .alert.alert-success.alert-block
14
+ Your profile was successfully updated.
15
+ - when :update_failed
16
+ .alert.alert-danger.alert-block
17
+ strong Profile update failed!
18
+ br
19
+ We apologize for such inconvenience. Please try again later.
20
+
21
+ fieldset
22
+ == snippet :form_panel,
23
+ params: @form.params,
24
+ report: @state == :report
25
+ focus: @state == :report
26
+ button.btn.btn-default type='submit' Update Profile
27
+
28
+ // EOF //