interpret 0.2.0 → 0.2.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.
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/README.md +2 -0
- data/app/controllers/interpret/base_controller.rb +10 -14
- data/app/controllers/interpret/missing_translations_controller.rb +41 -10
- data/app/controllers/interpret/search_controller.rb +15 -3
- data/app/controllers/interpret/tools_controller.rb +2 -2
- data/app/controllers/interpret/translations_controller.rb +15 -12
- data/app/models/interpret/ability.rb +9 -0
- data/app/models/interpret/translation.rb +38 -3
- data/app/views/interpret/missing_translations/blank.html.erb +35 -0
- data/app/views/interpret/missing_translations/index.html.erb +25 -14
- data/app/views/interpret/missing_translations/stale.html.erb +43 -0
- data/app/views/interpret/missing_translations/unused.html.erb +27 -0
- data/app/views/interpret/tools/index.html.erb +15 -25
- data/app/views/interpret/translations/_listing.html.erb +3 -15
- data/app/views/interpret/translations/index.html.erb +3 -0
- data/app/views/layouts/interpret.html.erb +26 -6
- data/config/environment.rb +0 -0
- data/config/routes.rb +4 -1
- data/interpret.gemspec +4 -1
- data/lib/generators/interpret/templates/migration.rb +1 -1
- data/lib/interpret.rb +23 -2
- data/lib/interpret/controller_filter.rb +0 -10
- data/lib/interpret/helpers.rb +2 -2
- data/lib/interpret/version.rb +1 -1
- data/public/javascripts/interpret.js +0 -3
- data/public/stylesheets/interpret_style.css +0 -6
- data/spec/integration/missing_translations_spec.rb +61 -0
- data/spec/integration/search_spec.rb +88 -0
- data/spec/integration/stale_translations_spec.rb +28 -0
- data/spec/integration/tools_spec.rb +86 -0
- data/spec/integration/translations_spec.rb +26 -0
- data/spec/models/translation_spec.rb +58 -25
- data/spec/observers/expiration_observer_spec.rb +2 -0
- data/spec/spec_helper.rb +3 -15
- data/spec/support/selenium_db_hack.rb +19 -0
- data/spec/support/utils.rb +89 -0
- data/test_app/Gemfile +1 -1
- data/test_app/app/controllers/application_controller.rb +2 -4
- data/test_app/app/models/interpret_ability.rb +7 -0
- data/test_app/app/models/user.rb +0 -3
- data/test_app/app/views/layouts/application.html.erb +4 -7
- data/test_app/config/initializers/interpret.rb +2 -2
- data/test_app/config/initializers/rack_patch.rb +13 -0
- data/test_app/config/locales/es.yml +1 -1
- data/test_app/db/migrate/20110219173536_create_users.rb +0 -2
- data/test_app/db/migrate/{20110219143622_interpret_create_translations.rb → 20111021100344_interpret_create_translations.rb} +1 -1
- data/test_app/db/schema.rb +2 -3
- data/test_app/db/seeds.rb +1 -1
- data/test_app/public/javascripts/interpret.js +0 -3
- data/test_app/public/stylesheets/interpret_style.css +0 -6
- metadata +122 -119
- data/spec/database_helpers.rb +0 -15
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
Interpret
|
2
2
|
=========
|
3
|
+
[](http://travis-ci.org/rogercampos/interpret)
|
4
|
+
|
3
5
|
|
4
6
|
Interpret is a rails 3 engine to help you manage your application
|
5
7
|
translations, also allowing you to have editable contents in live. In order to
|
@@ -1,14 +1,16 @@
|
|
1
1
|
class Interpret::BaseController < eval(Interpret.parent_controller.classify)
|
2
2
|
before_filter :set_locale
|
3
|
-
before_filter :
|
3
|
+
before_filter { authorize! :use, :interpret }
|
4
|
+
before_filter :check_authorized_language
|
4
5
|
layout 'interpret'
|
5
6
|
|
6
7
|
protected
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
def current_interpret_user
|
9
|
+
@current_interpret_user ||= eval(Interpret.current_user)
|
10
|
+
end
|
11
|
+
|
12
|
+
def current_ability
|
13
|
+
@current_ability ||= Interpret.ability.new(current_interpret_user)
|
12
14
|
end
|
13
15
|
|
14
16
|
private
|
@@ -16,14 +18,8 @@ private
|
|
16
18
|
I18n.locale = params[:locale] if params[:locale]
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
20
|
-
|
21
|
-
@interpret_user = eval(Interpret.current_user)
|
22
|
-
@interpret_admin = true
|
23
|
-
if Interpret.admin && @interpret_user.respond_to?(Interpret.admin)
|
24
|
-
@interpret_admin = @interpret_user.send(Interpret.admin)
|
25
|
-
end
|
26
|
-
end
|
21
|
+
def check_authorized_language
|
22
|
+
authorize! :use, :"interpret_in_#{I18n.locale}"
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
@@ -1,15 +1,46 @@
|
|
1
1
|
class Interpret::MissingTranslationsController < Interpret::BaseController
|
2
2
|
def index
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
authorize! :read, :missing_translations
|
4
|
+
return if I18n.locale == I18n.default_locale
|
5
|
+
|
6
|
+
case ActiveRecord::Base.connection.adapter_name
|
7
|
+
when "Mysql2"
|
8
|
+
res = ActiveRecord::Base.connection.execute("select t.id from translations t where t.locale ='#{I18n.default_locale}' and (select count(*) from translations t2 where t2.key = t.key and t2.locale ='#{I18n.locale}') = 0")
|
9
|
+
|
10
|
+
when "SQLite"
|
11
|
+
res = ActiveRecord::Base.connection.execute("select t.id from translations t where t.locale ='#{I18n.default_locale}' and (select count(*) from translations t2 where t2.key = t.key and t2.locale ='#{I18n.locale}') = 0")
|
12
|
+
|
13
|
+
else
|
14
|
+
raise NotImplementedError, "database adapter not supported"
|
13
15
|
end
|
16
|
+
|
17
|
+
ids = res.map{|x| x.first}
|
18
|
+
translations = Interpret::Translation.allowed.where(:id => ids).order("translations.key ASC").all
|
19
|
+
translations = translations.select{|x| x.value.present?}
|
20
|
+
@missing_translations = translations.map{|x| {:ref_value => x.value, :key => x.key, :source => x}}
|
21
|
+
end
|
22
|
+
|
23
|
+
def blank
|
24
|
+
authorize! :read, :blank_translations
|
25
|
+
@blank_translations = Interpret::Translation.allowed.locale(I18n.locale).where(:value => "--- \"\"\n")
|
26
|
+
@ref_translations = Interpret::Translation.allowed.locale(I18n.default_locale).where(:key => @blank_translations.map{|x| x.key})
|
27
|
+
|
28
|
+
@blank_translations.map do |x|
|
29
|
+
foo = @ref_translations.detect{|y| x.key == y.key}
|
30
|
+
[x, foo ? foo.value : nil]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def unused
|
35
|
+
authorize! :read, :unused_translations
|
36
|
+
used_keys = Interpret::Translation.allowed.locale(I18n.default_locale).all.map{|x| x.key}
|
37
|
+
@unused_translations = Interpret::Translation.allowed.locale(I18n.locale).where("translations.key NOT IN (?)", used_keys)
|
38
|
+
end
|
39
|
+
|
40
|
+
def stale
|
41
|
+
authorize! :read, :stale_translations
|
42
|
+
@stale_translations = Interpret::Translation.allowed.stale.locale(I18n.locale).order("translations.key ASC")
|
43
|
+
refs = Interpret::Translation.locale(I18n.default_locale).where(:key => @stale_translations.map{|x| x.key})
|
44
|
+
@stale_translations.map!{|x| [x, refs.detect{|y| y.key == x.key}]}
|
14
45
|
end
|
15
46
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class Interpret::SearchController < Interpret::BaseController
|
2
|
+
before_filter { authorize! :use, :search }
|
3
|
+
|
2
4
|
def index
|
3
5
|
if request.post?
|
4
6
|
opts = {}
|
@@ -7,10 +9,20 @@ class Interpret::SearchController < Interpret::BaseController
|
|
7
9
|
redirect_to interpret_search_url(opts)
|
8
10
|
else
|
9
11
|
if params[:key].present? || params[:value].present?
|
12
|
+
sanitizer = case ActiveRecord::Base.connection.adapter_name
|
13
|
+
when "SQLite"
|
14
|
+
if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new("1.9")
|
15
|
+
lambda {|x| "%#{x}%"}
|
16
|
+
else
|
17
|
+
lambda {|x| "%#{CGI.escape(x)}%"}
|
18
|
+
end
|
19
|
+
else
|
20
|
+
lambda {|x| "%#{CGI.escape(x)}%"}
|
21
|
+
end
|
10
22
|
t = Interpret::Translation.arel_table
|
11
|
-
search_key = params[:key].present? ? params[:key].split(" ").map{|x|
|
12
|
-
search_value = params[:value].present? ? params[:value].split(" ").map{|x|
|
13
|
-
@translations = Interpret::Translation.locale(I18n.locale).where(t[:key].matches_all(search_key).or(t[:value].matches_all(search_value))
|
23
|
+
search_key = params[:key].present? ? params[:key].split(" ").map{|x| sanitizer.call(x)} : []
|
24
|
+
search_value = params[:value].present? ? params[:value].split(" ").map{|x| sanitizer.call(x)} : []
|
25
|
+
@translations = Interpret::Translation.allowed.locale(I18n.locale).where(t[:key].matches_all(search_key).or(t[:value].matches_all(search_value))).order("translations.key ASC")
|
14
26
|
end
|
15
27
|
end
|
16
28
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
class Interpret::ToolsController < Interpret::BaseController
|
2
|
-
before_filter :
|
2
|
+
before_filter { authorize! :use, :tools }
|
3
3
|
|
4
4
|
def dump
|
5
5
|
Interpret::Translation.dump
|
@@ -16,7 +16,7 @@ class Interpret::ToolsController < Interpret::BaseController
|
|
16
16
|
hash = Interpret::Translation.export(translations)
|
17
17
|
text = hash.ya2yaml
|
18
18
|
|
19
|
-
send_data text[5..text.length], :filename => "#{I18n.locale}.yml"
|
19
|
+
send_data text[5..text.length], :filename => "#{I18n.locale}.yml", :type => "text/plain", :disposition => "attachment"
|
20
20
|
end
|
21
21
|
|
22
22
|
def run_update
|
@@ -1,28 +1,27 @@
|
|
1
1
|
class Interpret::TranslationsController < Interpret::BaseController
|
2
2
|
before_filter :get_tree, :only => :index
|
3
|
+
authorize_resource :class => "Interpret::Translation"
|
3
4
|
|
4
5
|
def index
|
5
6
|
key = params[:key]
|
6
7
|
t = Interpret::Translation.arel_table
|
7
8
|
if key
|
8
|
-
@translations = Interpret::Translation.locale(I18n.locale).where(t[:key].matches("#{CGI.escape(key)}.%"))
|
9
|
+
@translations = Interpret::Translation.allowed.locale(I18n.locale).where(t[:key].matches("#{CGI.escape(key)}.%")).order("translations.key ASC")
|
9
10
|
if I18n.locale != I18n.default_locale
|
10
|
-
@references = Interpret::Translation.locale(I18n.default_locale).where(t[:key].matches("#{CGI.escape(key)}.%"))
|
11
|
+
@references = Interpret::Translation.allowed.locale(I18n.default_locale).where(t[:key].matches("#{CGI.escape(key)}.%")).order("translations.key ASC")
|
11
12
|
end
|
12
13
|
else
|
13
|
-
@translations = Interpret::Translation.locale(I18n.locale).where(t[:key].does_not_match("%.%"))
|
14
|
+
@translations = Interpret::Translation.allowed.locale(I18n.locale).where(t[:key].does_not_match("%.%")).order("translations.key ASC")
|
14
15
|
if I18n.locale != I18n.default_locale
|
15
|
-
@references = Interpret::Translation.locale(I18n.default_locale).where(t[:key].does_not_match("%.%"))
|
16
|
+
@references = Interpret::Translation.allowed.locale(I18n.default_locale).where(t[:key].does_not_match("%.%")).order("translations.key ASC")
|
16
17
|
end
|
17
18
|
end
|
18
|
-
if @interpret_user
|
19
|
-
@translations = @translations.where(:protected => false) if !@interpret_admin
|
20
|
-
@references = @references.where(:protected => false) if @references && !@interpret_admin
|
21
|
-
end
|
22
19
|
|
23
20
|
# not show translations inside nested folders, \w avoids dots
|
24
21
|
@translations = @translations.select{|x| x.key =~ /#{key}\.\w+$/} if key
|
25
22
|
@references = @references.select{|x| x.key =~ /#{key}\.\w+$/} if key && @references
|
23
|
+
|
24
|
+
@total_keys_number = Interpret::Translation.locale(I18n.locale).count
|
26
25
|
end
|
27
26
|
|
28
27
|
def edit
|
@@ -30,10 +29,6 @@ class Interpret::TranslationsController < Interpret::BaseController
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def update
|
33
|
-
if @interpret_user && !@interpret_admin && params[:interpret_translation].has_key?(:protected)
|
34
|
-
head :error
|
35
|
-
return
|
36
|
-
end
|
37
32
|
@translation = Interpret::Translation.find(params[:id])
|
38
33
|
old_value = @translation.value
|
39
34
|
|
@@ -67,6 +62,14 @@ class Interpret::TranslationsController < Interpret::BaseController
|
|
67
62
|
end
|
68
63
|
end
|
69
64
|
|
65
|
+
def destroy
|
66
|
+
@translation = Interpret::Translation.find(params[:id])
|
67
|
+
|
68
|
+
@translation.destroy
|
69
|
+
flash[:notice] = "Translation #{@translation.key} destroyed."
|
70
|
+
redirect_to request.env["HTTP_REFERER"]
|
71
|
+
end
|
72
|
+
|
70
73
|
def live_edit
|
71
74
|
blobs = params[:key].split(".")
|
72
75
|
locale = blobs.first
|
@@ -4,7 +4,37 @@ module Interpret
|
|
4
4
|
default_scope order('locale ASC')
|
5
5
|
validates_uniqueness_of :key, :scope => :locale
|
6
6
|
|
7
|
+
after_update :set_stale
|
8
|
+
|
9
|
+
scope :stale, where(:stale => true)
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# If this translations is in the main language, mark this translation in
|
14
|
+
# other languages as stale, so the translators know that they must change
|
15
|
+
# it.
|
16
|
+
def set_stale
|
17
|
+
return unless locale == I18n.default_locale.to_s
|
18
|
+
|
19
|
+
Translation.where(:key => key).where(Translation.arel_table[:locale].not_eq(locale)).update_all({:stale => true})
|
20
|
+
end
|
21
|
+
|
7
22
|
class << self
|
23
|
+
|
24
|
+
def allowed
|
25
|
+
s = order("")
|
26
|
+
if Interpret.wild_blacklist.any?
|
27
|
+
black_keys = Interpret.wild_blacklist.map{|x| "#{CGI.escape(x)}%"}
|
28
|
+
s = s.where(arel_table[:key].does_not_match_all(black_keys))
|
29
|
+
end
|
30
|
+
if Interpret.fixed_blacklist.any?
|
31
|
+
black_keys = Interpret.fixed_blacklist.map{|x| "#{CGI.escape(x)}"}
|
32
|
+
s = s.where(arel_table[:key].does_not_match_all(black_keys))
|
33
|
+
end
|
34
|
+
s
|
35
|
+
end
|
36
|
+
|
37
|
+
|
8
38
|
# Generates a hash representing the tree structure of the translations
|
9
39
|
# for the given locale. It includes only "folders" in the sense of
|
10
40
|
# locale keys that includes some real translations, or other keys.
|
@@ -40,10 +70,9 @@ module Interpret
|
|
40
70
|
|
41
71
|
# Import the contents of the given .yml locale file into the database
|
42
72
|
# backend. If a given key already exists in database, it will be
|
43
|
-
# overwritten, otherwise it won't be touched. This means that
|
73
|
+
# overwritten, otherwise it won't be touched. This means that it won't
|
44
74
|
# delete any existing translation, it only overwrites the ones you give
|
45
75
|
# in the file.
|
46
|
-
# If the given file has new translations, these will be ignored.
|
47
76
|
#
|
48
77
|
# The language will be obtained from the first unique key of the yml
|
49
78
|
# file.
|
@@ -53,12 +82,18 @@ module Interpret
|
|
53
82
|
|
54
83
|
lang = hash.keys.first
|
55
84
|
|
85
|
+
unless lang.to_s == I18n.locale.to_s
|
86
|
+
raise ArgumentError, "the language doesn't match"
|
87
|
+
end
|
88
|
+
|
56
89
|
records = parse_hash(hash.first[1], lang)
|
57
90
|
transaction do
|
58
91
|
records.each do |x|
|
59
92
|
if tr = locale(lang).find_by_key(x.key)
|
60
93
|
tr.value = x.value
|
61
|
-
tr.save
|
94
|
+
tr.save!
|
95
|
+
else
|
96
|
+
x.save!
|
62
97
|
end
|
63
98
|
end
|
64
99
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<% if @blank_translations.empty? %>
|
2
|
+
<p>There are no blank translations</p>
|
3
|
+
<% else %>
|
4
|
+
<p>There are <%= @blank_translations.size %> blank translations in this
|
5
|
+
language [ <%= I18n.locale %> ]. These are translations that exists but are
|
6
|
+
empty strings, so I show you them in case you want to take some action.</p>
|
7
|
+
<table>
|
8
|
+
<thead>
|
9
|
+
<tr class="header">
|
10
|
+
<th>Key</th>
|
11
|
+
<th>Value in [ <%= I18n.default_locale %> ]</th>
|
12
|
+
<th>Your translation to [ <%= I18n.locale %> ]</th>
|
13
|
+
<th>Delete</th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<% @blank_translations.each do |trans, ref| %>
|
17
|
+
<tr>
|
18
|
+
<td><%= trans.key %></td>
|
19
|
+
<td><%= ref || "It's empty" %></td>
|
20
|
+
<td>
|
21
|
+
<%= form_for Interpret::Translation.new, :url => interpret_translations_path do |f| %>
|
22
|
+
<%= f.hidden_field "locale", :value => I18n.locale %>
|
23
|
+
<%= f.hidden_field "key", :value => trans.key %>
|
24
|
+
<%= f.text_area :value, :rows => 4, :cols => 60 %>
|
25
|
+
<%= submit_tag "Create" %>
|
26
|
+
<% end %>
|
27
|
+
</td>
|
28
|
+
<td>
|
29
|
+
<%= link_to "Destroy it", trans, :method => :delete, :confirm => "Are you sure?" %>
|
30
|
+
</td>
|
31
|
+
</tr>
|
32
|
+
<% end %>
|
33
|
+
</table>
|
34
|
+
<% end %>
|
35
|
+
|
@@ -7,27 +7,38 @@
|
|
7
7
|
create them below and you can see the translation key and also the original
|
8
8
|
content in the reference language [ <%= I18n.default_locale %> ]
|
9
9
|
</p>
|
10
|
-
<table>
|
10
|
+
<table id='missing_translations'>
|
11
11
|
<thead>
|
12
12
|
<tr class="header">
|
13
13
|
<th>Key</th>
|
14
14
|
<th>Value in [ <%= I18n.default_locale %> ]</th>
|
15
15
|
<th>Your translation to [ <%= I18n.locale %> ]</th>
|
16
|
+
<% if can? :destroy, Interpret::Translation %>
|
17
|
+
<th>Delete</th>
|
18
|
+
<% end %>
|
16
19
|
</tr>
|
17
20
|
</thead>
|
18
|
-
|
19
|
-
|
20
|
-
<
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
<%=
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
<tbody>
|
22
|
+
<% @missing_translations.each do |trans| %>
|
23
|
+
<tr>
|
24
|
+
<td><%= trans[:key] %></td>
|
25
|
+
<td><%= trans[:ref_value] %></td>
|
26
|
+
<td>
|
27
|
+
<%= form_for Interpret::Translation.new, :url => interpret_translations_path do |f| %>
|
28
|
+
<%= f.hidden_field "locale", :value => I18n.locale %>
|
29
|
+
<%= f.hidden_field "key", :value => trans[:key] %>
|
30
|
+
<%= f.text_area :value, :rows => 4, :cols => 60 %>
|
31
|
+
<%= submit_tag "Create" %>
|
32
|
+
<% end %>
|
33
|
+
</td>
|
34
|
+
<% if can? :destroy, trans[:source] %>
|
35
|
+
<td>
|
36
|
+
<%= link_to "Destroy", trans[:source], :method => :delete, :confirm => "Are you sure?" %>
|
37
|
+
the original [ <%= I18n.default_locale %> ] translation.
|
38
|
+
</td>
|
28
39
|
<% end %>
|
29
|
-
</
|
30
|
-
|
31
|
-
|
40
|
+
</tr>
|
41
|
+
<% end %>
|
42
|
+
</tbody>
|
32
43
|
</table>
|
33
44
|
<% end %>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<% if I18n.locale == I18n.default_locale %>
|
2
|
+
<p>There can't be stale translations for the main language</p>
|
3
|
+
|
4
|
+
<% elsif @stale_translations.empty? %>
|
5
|
+
<p>There are no stale translations</p>
|
6
|
+
|
7
|
+
<% else %>
|
8
|
+
<p>There are <%= @stale_translations.size %> stale translations in [<%= I18n.locale %>].</p>
|
9
|
+
<p>
|
10
|
+
They have been modified in the master language (<%= I18n.default_locale %>)
|
11
|
+
so probably you want to change them in the current languge too.
|
12
|
+
Once you think your new adaptation is correct click on "Mark as OK" button and that translation will no longer appear as stale.
|
13
|
+
</p>
|
14
|
+
<table id='stale_translations'>
|
15
|
+
<thead>
|
16
|
+
<tr class="header">
|
17
|
+
<th>Key</th>
|
18
|
+
<th>Value in [ <%= I18n.default_locale %> ]</th>
|
19
|
+
<th>Your translation to [ <%= I18n.locale %> ]</th>
|
20
|
+
<th>Actions</th>
|
21
|
+
</tr>
|
22
|
+
</thead>
|
23
|
+
<% @stale_translations.each do |trans, ref| %>
|
24
|
+
<tr>
|
25
|
+
<td class='key'><%= trans.key %></td>
|
26
|
+
<td><%= ref.value %></td>
|
27
|
+
<td class='content' id='translation_<%= trans.id %>'>
|
28
|
+
<%= best_in_place trans, :value,
|
29
|
+
:type => :textarea,
|
30
|
+
:path => interpret_translation_path(trans),
|
31
|
+
:activator => "#translation_#{trans.id}",
|
32
|
+
:sanitize => false %>
|
33
|
+
</td>
|
34
|
+
<td>
|
35
|
+
<%= form_for trans, :url => interpret_translation_path(trans) do |f| %>
|
36
|
+
<%= f.hidden_field :stale, :value => false %>
|
37
|
+
<%= submit_tag "Mark as OK" %>
|
38
|
+
<% end %>
|
39
|
+
</td>
|
40
|
+
</tr>
|
41
|
+
<% end %>
|
42
|
+
</table>
|
43
|
+
<% end %>
|