kakimasu 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +31 -0
  6. data/.travis.yml +8 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +135 -0
  11. data/Rakefile +6 -0
  12. data/app/controllers/kakimasu/home_controller.rb +18 -0
  13. data/app/controllers/kakimasu/keys_controller.rb +89 -0
  14. data/app/controllers/kakimasu/translations_controller.rb +37 -0
  15. data/app/helpers/counting_helper.rb +11 -0
  16. data/app/helpers/formatting_helper.rb +6 -0
  17. data/app/helpers/grouping_helper.rb +11 -0
  18. data/app/helpers/pagination_helper.rb +15 -0
  19. data/app/helpers/search_key_helper.rb +66 -0
  20. data/app/helpers/search_translations_helper.rb +101 -0
  21. data/app/models/translation.rb +2 -0
  22. data/app/policies/translation_policy.rb +9 -0
  23. data/app/views/kakimasu/home/index.html.erb +35 -0
  24. data/app/views/kakimasu/keys/create.js.erb +1 -0
  25. data/app/views/kakimasu/keys/index.html.erb +183 -0
  26. data/app/views/kakimasu/shared/_navbar.html.erb +15 -0
  27. data/app/views/kakimasu/translations/create.js.erb +1 -0
  28. data/app/views/kakimasu/translations/index.html.erb +168 -0
  29. data/bin/console +14 -0
  30. data/bin/setup +8 -0
  31. data/config/initializers/i18n.rb +58 -0
  32. data/config/initializers/i18n_backend.rb +5 -0
  33. data/config/locales/en.yml +34 -0
  34. data/config/routes.rb +10 -0
  35. data/kakimasu.gemspec +49 -0
  36. data/lib/generators/kakimasu/backup_generator.rb +101 -0
  37. data/lib/generators/kakimasu/policy_generator.rb +18 -0
  38. data/lib/generators/kakimasu/restore_backup_generator.rb +89 -0
  39. data/lib/generators/kakimasu/views_generator.rb +22 -0
  40. data/lib/kakimasu.rb +11 -0
  41. data/lib/kakimasu/engine.rb +3 -0
  42. data/lib/kakimasu/version.rb +3 -0
  43. data/vendor/assets/javascripts/components/activation_button.coffee +10 -0
  44. data/vendor/assets/javascripts/components/modal.coffee +48 -0
  45. data/vendor/assets/javascripts/components/translate.coffee +93 -0
  46. data/vendor/assets/javascripts/components/translation_key_table.coffee +5 -0
  47. data/vendor/assets/javascripts/components/translation_navigation.coffee +13 -0
  48. data/vendor/assets/javascripts/components/translation_panel_table.coffee +5 -0
  49. data/vendor/assets/javascripts/components/translation_percent_chart.coffee +39 -0
  50. data/vendor/assets/javascripts/components/translation_popover.coffee +90 -0
  51. data/vendor/assets/javascripts/jquery/jquery.circliful.min.js +1 -0
  52. data/vendor/assets/javascripts/kakimasu.coffee +18 -0
  53. data/vendor/assets/stylesheets/circliful.scss +38 -0
  54. data/vendor/assets/stylesheets/components/_homepage.scss +17 -0
  55. data/vendor/assets/stylesheets/components/_key_management.scss +53 -0
  56. data/vendor/assets/stylesheets/components/_navbar.scss +52 -0
  57. data/vendor/assets/stylesheets/components/_translate_modal.scss +36 -0
  58. data/vendor/assets/stylesheets/components/_translation_panel.scss +31 -0
  59. data/vendor/assets/stylesheets/components/_translation_popover.scss +31 -0
  60. data/vendor/assets/stylesheets/font-awesome/HELP-US-OUT.txt +7 -0
  61. data/vendor/assets/stylesheets/font-awesome/css/font-awesome.css +2337 -0
  62. data/vendor/assets/stylesheets/font-awesome/css/font-awesome.min.css +4 -0
  63. data/vendor/assets/stylesheets/font-awesome/fonts/FontAwesome.otf +0 -0
  64. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.eot +0 -0
  65. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.svg +2671 -0
  66. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.ttf +0 -0
  67. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff +0 -0
  68. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff2 +0 -0
  69. data/vendor/assets/stylesheets/font-awesome/less/animated.less +34 -0
  70. data/vendor/assets/stylesheets/font-awesome/less/bordered-pulled.less +25 -0
  71. data/vendor/assets/stylesheets/font-awesome/less/core.less +12 -0
  72. data/vendor/assets/stylesheets/font-awesome/less/fixed-width.less +6 -0
  73. data/vendor/assets/stylesheets/font-awesome/less/font-awesome.less +18 -0
  74. data/vendor/assets/stylesheets/font-awesome/less/icons.less +789 -0
  75. data/vendor/assets/stylesheets/font-awesome/less/larger.less +13 -0
  76. data/vendor/assets/stylesheets/font-awesome/less/list.less +19 -0
  77. data/vendor/assets/stylesheets/font-awesome/less/mixins.less +60 -0
  78. data/vendor/assets/stylesheets/font-awesome/less/path.less +15 -0
  79. data/vendor/assets/stylesheets/font-awesome/less/rotated-flipped.less +20 -0
  80. data/vendor/assets/stylesheets/font-awesome/less/screen-reader.less +5 -0
  81. data/vendor/assets/stylesheets/font-awesome/less/stacked.less +20 -0
  82. data/vendor/assets/stylesheets/font-awesome/less/variables.less +800 -0
  83. data/vendor/assets/stylesheets/font-awesome/scss/_animated.scss +34 -0
  84. data/vendor/assets/stylesheets/font-awesome/scss/_bordered-pulled.scss +25 -0
  85. data/vendor/assets/stylesheets/font-awesome/scss/_core.scss +12 -0
  86. data/vendor/assets/stylesheets/font-awesome/scss/_fixed-width.scss +6 -0
  87. data/vendor/assets/stylesheets/font-awesome/scss/_icons.scss +789 -0
  88. data/vendor/assets/stylesheets/font-awesome/scss/_larger.scss +13 -0
  89. data/vendor/assets/stylesheets/font-awesome/scss/_list.scss +19 -0
  90. data/vendor/assets/stylesheets/font-awesome/scss/_mixins.scss +60 -0
  91. data/vendor/assets/stylesheets/font-awesome/scss/_path.scss +15 -0
  92. data/vendor/assets/stylesheets/font-awesome/scss/_rotated-flipped.scss +20 -0
  93. data/vendor/assets/stylesheets/font-awesome/scss/_screen-reader.scss +5 -0
  94. data/vendor/assets/stylesheets/font-awesome/scss/_stacked.scss +20 -0
  95. data/vendor/assets/stylesheets/font-awesome/scss/_variables.scss +800 -0
  96. data/vendor/assets/stylesheets/font-awesome/scss/font-awesome.scss +18 -0
  97. data/vendor/assets/stylesheets/kakimasu.scss +35 -0
  98. metadata +364 -0
@@ -0,0 +1,11 @@
1
+ module CountingHelper
2
+ # Counts how much keys there is in key array
3
+ def countKeys(array)
4
+ array.flatten.count - array.count
5
+ end
6
+
7
+ # Gets translation percentage
8
+ def translationPercentage(all_keys, missing_translations)
9
+ ((1 - ( missing_translations.to_f / all_keys )) * 100).round(2)
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module FormattingHelper
2
+ # Remove surrounding quotes from string
3
+ def remove_surrounding_quotes(string)
4
+ string.gsub!(/^\"|"\Z/, '') unless string.blank?
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module GroupingHelper
2
+ # Groups all translations by language in array
3
+ def group_by_language(array)
4
+ array.group_by { |el| el.partition('.').first }
5
+ end
6
+
7
+ # Gets all paths from keys array
8
+ def get_translation_key_paths(array)
9
+ array.map { |el| el.last }
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module PaginationHelper
2
+ # Set page size
3
+ def set_page_size(size)
4
+ @size = size
5
+ end
6
+
7
+ def get_page_size
8
+ @size
9
+ end
10
+
11
+ # Calculates total number of pages
12
+ def total_pages(array)
13
+ (array.length.to_f / @size).ceil
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ # Sets translation key for lazy lookup or unsets it.
2
+ module SearchKeyHelper
3
+ def lazy_lookup_key(key, path)
4
+ path = 'app/views/' + path
5
+ file = File.read(path)
6
+
7
+ matches = Array.new #All places where this key is used in given file
8
+ locations = Array.new #Indexes which determines where to make changes
9
+ file_length_differential = 0 #File length difference because substitutions will change file size.
10
+
11
+ # Finds all places where given translation key is used
12
+ file.scan(/[=|\s][t][\s]*[(]?[\s]*['|:]#{key}/) do |c|
13
+ matches.push [c, $~.offset(0)[0]]
14
+ end
15
+
16
+ matches.each do |array|
17
+ if array.first.match /[=|\s][t][\s]*[(]?[\s]*[:]\w*/ # if translation written with : not '' then its special case
18
+ # Key with ':key' is sustituted with "'.key'"
19
+ index = array.first.index(/:#{key}/)
20
+ file.slice!(array.last + index + file_length_differential, (":#{key}").length)
21
+ file.insert(array.last + index + file_length_differential, "'.#{key}'")
22
+ file_length_differential += ("'.#{key}'").length - (":#{key}").length
23
+ elsif key[0] != '.'
24
+ index = array.first.index(key)
25
+ locations.push array.last + index + file_length_differential
26
+ end
27
+ end
28
+
29
+ # Insert '.' just before key for lazy lookup
30
+ locations.each_with_index do |location, index|
31
+ file.insert(location + index, '.')
32
+ end
33
+
34
+ File.write(path, file)
35
+ end
36
+
37
+ def remove_lazy_lookup(key, path)
38
+ path = 'app/views/' + path
39
+ file = File.read(path)
40
+
41
+ # Get rid of prefix of has lazy lookup prefix
42
+ if key.include? '.'
43
+ key = key.split('.').last
44
+ key.prepend '.'
45
+ end
46
+
47
+ matches = Array.new # All places where given key is used in file
48
+ locations = Array.new #Indexes which determintes where to make changes
49
+
50
+ key.slice! 0 if key[0] == '.'
51
+ file.scan(/[=|\s][t][\s]*[(]?[\s]*['][.]#{key}/) do |c|
52
+ matches.push [c, $~.offset(0)[0]]
53
+ end
54
+
55
+ matches.each do |array|
56
+ index = array.first.index('.' + key)
57
+ locations.push array.last + index
58
+ end
59
+
60
+ locations.each_with_index do |location, index|
61
+ file.slice!(location - index) # Removes '.' from match
62
+ end
63
+
64
+ File.write(path, file)
65
+ end
66
+ end
@@ -0,0 +1,101 @@
1
+ module SearchTranslationsHelper
2
+ # Method to get prefix for lazy lookup
3
+ def prefix(path)
4
+ prefix = path.split('/')
5
+ prefix[-1] = prefix.last.split('.').first
6
+ prefix = prefix.join('.')
7
+ end
8
+
9
+ # Method returns if key has missing translation in one of locales
10
+ def missing_translations?(key)
11
+ missing_translation = false
12
+ I18n.available_locales.each do |locale|
13
+ if !I18n.exists?(key, locale)
14
+ missing_translation = true # If in one of locales key has no translation, method returns true
15
+ end
16
+ end
17
+ missing_translation
18
+ end
19
+
20
+ # Method searches used translations keys in a file
21
+ def find_keys(file, path)
22
+ array = file.scan(/[=|\s][t][\s]*[(]?[\s]*['|:]([.]*\w*)/).uniq
23
+ keys = Array.new
24
+
25
+ # If key has '.' in front of it, it should be completed as lazy lookup key.
26
+ array.each_with_index do |key, index|
27
+ if key.first[0] == '.'
28
+ array[index] = key.first.insert(0, prefix(path))
29
+ end
30
+ end
31
+ array
32
+ end
33
+
34
+ # Method returns array of keys with missing translations from given array
35
+ def find_missing_translations_keys(array)
36
+ missing_translations_keys = []
37
+
38
+ array.each do |key|
39
+ missing_translations_keys.push key if missing_translations?(key)
40
+ end
41
+
42
+ missing_translations_keys
43
+ end
44
+
45
+ # Method searches used active record human attribute name keys in given file
46
+ def find_attribute_keys(file)
47
+ array = file.scan(/(\w*).human_attribute_name[(][:](\w*)/).uniq
48
+ keys = []
49
+ # Keys are transformed into correct active record attribute keys form
50
+ array.each do |arr|
51
+ arr = 'activerecord.' + 'attributes.' + arr.first.downcase + '.' + arr.last
52
+ keys.push arr
53
+ end
54
+ keys
55
+ end
56
+
57
+ # Method searches used Model name keys in given file
58
+ def find_model_name_keys(file)
59
+ array = file.scan(/(\w*).model_name.human/).uniq
60
+ keys = []
61
+ # Keys are transformed into correct active record model name keys form
62
+ array.each do |arr|
63
+ arr = 'activerecord.models.' + arr.first.downcase
64
+ keys.push arr
65
+ end
66
+ keys
67
+ end
68
+
69
+ # Method returns array with requested translation keys
70
+ def search_for_translations(category)
71
+ keys = []
72
+
73
+ Dir.glob("app/views/**/*").each do |path|
74
+ if File.file?(path)
75
+ file = File.read(path)
76
+ case category
77
+ when 'active_record_attribute_translations'
78
+ keys.push find_attribute_keys(file)
79
+ when 'active_record_model_translations'
80
+ keys.push find_model_name_keys(file)
81
+ when 'all_translation_keys'
82
+ path.slice! "app/views/"
83
+ keys.push find_keys(file, path).push path
84
+ when 'missing_translations'
85
+ path.slice! "app/views/"
86
+ # push in array only those keys which have missing translations
87
+ keys.push find_missing_translations_keys(find_keys(file, path)).push path
88
+ end
89
+ end
90
+ end
91
+
92
+ case category
93
+ when 'active_record_attribute_translations'
94
+ keys.flatten.uniq
95
+ when 'active_record_model_translations'
96
+ keys.flatten.uniq
97
+ else
98
+ keys
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,2 @@
1
+ class Translation < ActiveRecord::Base
2
+ end
@@ -0,0 +1,9 @@
1
+ class TranslationPolicy < ApplicationPolicy
2
+ def index?
3
+ user && user.admin?
4
+ end
5
+
6
+ def create?
7
+ user && user.admin?
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ <%= render 'kakimasu/shared/navbar' %>
2
+
3
+ <div class="container">
4
+ <div class="row">
5
+ <div class="col-md-4">
6
+ <h1 class="kakimasu-greetings"> Hello, user! </h1>
7
+
8
+ <% if params[:show_translation_key] == 'true' %>
9
+ <%= link_to t('deactivate_translation_mode'), { show_translation_key: false }, class: 'btn btn-primary' %>
10
+ <% else %>
11
+ <%= link_to t('activate_translation_mode'), { show_translation_key: true }, id: 'activate_translation_mode_button', class: 'btn btn-primary' %>
12
+ <% end %>
13
+ </div>
14
+
15
+ <div class="col-md-8" data-component="TranslationPercentChart">
16
+ <div class="translation-diagram">
17
+ <div id="translation-percentage-circle"></div>
18
+ <table>
19
+ <tr>
20
+ <th><%= t('total_translation_keys') %>:</th>
21
+ <td><%= @all_keys_count %></td>
22
+ </tr>
23
+ <tr>
24
+ <th><%= t('total_keys_with_missing_translation') %>:</th>
25
+ <td><%= @missing_translation_keys_count %></td>
26
+ </tr>
27
+ <tr class="translation-percentage" data-value="<%= @percentage %>">
28
+ <th><%= t('translation_percentage') %>:</th>
29
+ <td><%= @percentage %> %</td>
30
+ </tr>
31
+ </table>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
@@ -0,0 +1 @@
1
+ window.location.reload()
@@ -0,0 +1,183 @@
1
+ <%= render 'kakimasu/shared/navbar' %>
2
+
3
+ <div data-component="TranslationModal">
4
+ <h1 class="margin-left-15"><%= t('translation_keys') %></h1>
5
+ <!-- Category list -->
6
+ <div>
7
+ <div class="col-md-3 translation-key-categories">
8
+ <h3><%=t('categories')%></h3>
9
+ <ul class="list-group">
10
+ <% @categories.each do |category| %>
11
+ <% if @category == category %>
12
+ <li class="list-group-item active"><%= link_to t(category), keys_category: category %></li>
13
+ <% else %>
14
+ <li class="list-group-item"><%= link_to t(category), keys_category: category %></li>
15
+ <% end %>
16
+ <% end %>
17
+ </ul>
18
+ </div>
19
+
20
+ <div class="col-md-9 translation-key-table">
21
+ <h3><%=t(@category)%></h3>
22
+ <!-- Translation keys table -->
23
+ <%-# Check if there are keys to show in tables -%>
24
+ <% if @category == 'missing_translations' && @keys.count == 0 %>
25
+ <h3><%= t('no_keys_with_missing_translations') %></h3>
26
+ <% elsif @category == 'all_translation_keys' && @keys.count == 0 %>
27
+ <h3><%= t('no_translation_keys_found') %></h3>
28
+ <% else %>
29
+ <table class="table" data-component="TranslationKeyTable">
30
+ <thead>
31
+ <tr>
32
+ <th><%= t('key') %></th>
33
+ <% I18n.available_locales.each do |locale| %>
34
+ <th><%= locale.upcase %></th>
35
+ <% end %>
36
+ <% if @category == 'missing_translations' || @category == 'all_translation_keys' %>
37
+ <th><%= t('path') %></th>
38
+ <% end %>
39
+ <th></th>
40
+ </tr>
41
+ </thead>
42
+
43
+ <tbody>
44
+ <%-# Iteration counter for pagination realisation -%>
45
+ <% key_nr = 0 %>
46
+ <% if @category == 'missing_translations' || @category == 'all_translation_keys' %>
47
+ <% @keys.each do |array| %>
48
+ <% path = array.pop %>
49
+ <% array = array.flatten %>
50
+ <% array.each do |key| %>
51
+ <%-# Checks if this key is in this page-%>
52
+ <% if get_page_size * (params[:page].to_i - 1) <= key_nr && key_nr < get_page_size * params[:page].to_i %>
53
+ <tr>
54
+ <td><%= key %></td>
55
+ <% translations = [] %>
56
+ <% I18n.available_locales.each do |locale| %>
57
+ <% if !I18n.exists?(key, locale) %>
58
+ <td class="missing_translation_cell"><%= key %></td>
59
+ <% else %>
60
+ <td><%= t(key, locale: locale) %></td>
61
+ <% end %>
62
+ <% translations.push t(key, locale: locale) %>
63
+ <% end %>
64
+ <td><i><%= path %></i></td>
65
+ <td class="text-center">
66
+ <%= content_tag 'button', class: 'translate-key-button', data: {toggle: 'modal', target: '#translateModal', key: "#{key}", translations: translations, locales: I18n.available_locales, path: path, unique:
67
+ (key.include? '.') } do %>
68
+ <i class="fa fa-pencil" aria-hidden="true"></i>
69
+ <% end %>
70
+ </td>
71
+ </tr>
72
+ <% end %>
73
+ <% key_nr += 1 %>
74
+ <% end %>
75
+ <% end %>
76
+
77
+ <% else %>
78
+ <% @keys.each do |key| %>
79
+ <% if get_page_size * (params[:page].to_i - 1) <= key_nr && key_nr < get_page_size * params[:page].to_i %>
80
+ <tr>
81
+ <td><%= key %></td>
82
+ <% translations = [] %>
83
+ <% I18n.available_locales.each do |locale| %>
84
+ <td><%= t(key, locale: locale) %></td>
85
+ <% translations.push t(key, locale: locale) %>
86
+ <% end %>
87
+ <td class="text-center">
88
+ <%= content_tag 'button', class: 'translate-key-button', data: {toggle: 'modal', target: '#translateModal', key: "#{key}", translations: translations, locales: I18n.available_locales} do %>
89
+ <i class="fa fa-pencil" aria-hidden="true"></i>
90
+ <% end %>
91
+ </td>
92
+ </tr>
93
+ <% end %>
94
+ <% key_nr += 1 %>
95
+ <% end %>
96
+ <% end %>
97
+ </tbody>
98
+ </table>
99
+ <% end %>
100
+
101
+ <!-- Pagination -->
102
+ <div class="text-center">
103
+ <% if total_pages(@keys.flatten) > 1 %>
104
+ <ul class="pagination">
105
+ <li>
106
+ <% if params[:page].to_i > 1 %>
107
+ <%= link_to '&laquo;'.html_safe, page: params[:page].to_i - 1, keys_category: @category %>
108
+ <% end %>
109
+ </li>
110
+ <% for i in 1..total_pages(@keys.flatten) %>
111
+ <% if params[:page].to_i == i %>
112
+ <li class="active"><%= link_to i, page: i, keys_category: @category %></li>
113
+ <% else %>
114
+ <li><%= link_to i, page: i, keys_category: @category %></li>
115
+ <% end %>
116
+ <% end %>
117
+ <li>
118
+ <% unless params[:page].to_i == total_pages(@keys.flatten) %>
119
+ <%= link_to '&raquo;'.html_safe, page: params[:page].to_i + 1, keys_category: @category %>
120
+ <% end %>
121
+ </li>
122
+ </ul>
123
+ <% end %>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Modal -->
130
+ <div class="modal fade" id="translateModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
131
+ <div class="modal-dialog" role="document">
132
+ <div class="modal-content">
133
+ <div class="modal-header">
134
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
135
+ <p class="text-left translation-key"></p>
136
+ <p class="text-left translation-key-path"></p>
137
+ </div>
138
+ <div class="modal-body">
139
+ <%= form_tag kakimasu_keys_path(keys_category: @category), id: 'translation-modal-form', remote: true do %>
140
+ <p>
141
+ <%= hidden_field_tag :key %>
142
+ </p>
143
+ <p>
144
+ <%= hidden_field_tag :path %>
145
+ </p>
146
+ <% I18n.available_locales.each_with_index do |locale, index| %>
147
+ <div class="form-group">
148
+ <div class="row">
149
+ <div class="col-xs-1">
150
+ <%= label_tag locale %>
151
+ </div>
152
+ <div class="col-xs-7 translation-input">
153
+ <%= text_area_tag locale, nil, class: "form-control" %>
154
+ </div>
155
+ <div class="col-xs-4">
156
+ <div class="suggested-translation-link">
157
+ <a href='<%="#modal-translation-#{index}"%>' data-toggle="collapse"><%=t('suggested_translations')%></a>
158
+ <div id='<%="modal-translation-#{index}"%>' class="collapse suggested-translation">
159
+ <p></p>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ <% end %>
166
+ <% if @category == 'missing_translations' || @category == 'all_translation_keys' %>
167
+ <div class="row">
168
+ <div class="col-xs-4 col-xs-offset-1">
169
+ <div id='lazy-lookup-checkbox'></div>
170
+ </div>
171
+ </div>
172
+ <% end %>
173
+ <p class="text-right">
174
+ <%= button_tag(class: "btn btn-primary") do%>
175
+ <% t('save') %>
176
+ <% end %>
177
+ </p>
178
+ <% end %>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>