godmin 1.4.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +0 -2
  3. data/.gitignore +1 -0
  4. data/.travis.yml +9 -10
  5. data/Appraisals +4 -4
  6. data/CHANGELOG.md +77 -2
  7. data/Gemfile +1 -2
  8. data/README.md +110 -9
  9. data/Rakefile +2 -2
  10. data/app/views/godmin/resource/_breadcrumb.html.erb +12 -24
  11. data/app/views/godmin/resource/_breadcrumb_actions.html.erb +41 -0
  12. data/app/views/godmin/resource/_button_actions.html.erb +1 -1
  13. data/app/views/godmin/resource/_pagination.html.erb +4 -4
  14. data/app/views/godmin/resource/columns/_actions.html.erb +3 -3
  15. data/app/views/godmin/resource/show.html.erb +1 -1
  16. data/config/locales/en.yml +2 -0
  17. data/config/locales/pt-BR.yml +2 -0
  18. data/config/locales/sv.yml +2 -0
  19. data/gemfiles/rails_5.gemfile +3 -4
  20. data/gemfiles/rails_6.gemfile +8 -0
  21. data/godmin.gemspec +18 -16
  22. data/lib/generators/godmin/resource/resource_generator.rb +7 -1
  23. data/lib/generators/godmin/resource/templates/resource_model.rb +4 -0
  24. data/lib/godmin/application_controller.rb +10 -11
  25. data/lib/godmin/authentication.rb +11 -11
  26. data/lib/godmin/authentication/sessions_controller.rb +1 -0
  27. data/lib/godmin/authorization.rb +34 -14
  28. data/lib/godmin/engine_wrapper.rb +10 -1
  29. data/lib/godmin/generators/base.rb +4 -4
  30. data/lib/godmin/generators/named_base.rb +4 -4
  31. data/lib/godmin/helpers/application.rb +3 -3
  32. data/lib/godmin/helpers/batch_actions.rb +1 -1
  33. data/lib/godmin/helpers/forms.rb +5 -1
  34. data/lib/godmin/resolver.rb +20 -5
  35. data/lib/godmin/resources/resource_controller.rb +24 -5
  36. data/lib/godmin/resources/resource_controller/batch_actions.rb +1 -1
  37. data/lib/godmin/resources/resource_service.rb +8 -7
  38. data/lib/godmin/resources/resource_service/associations.rb +23 -0
  39. data/lib/godmin/version.rb +1 -1
  40. data/template.rb +17 -3
  41. data/test/dummy/admin/app/controllers/admin/authorized_articles_controller.rb +29 -0
  42. data/test/dummy/admin/app/models/admin/article.rb +4 -0
  43. data/test/dummy/admin/app/models/admin/magazine.rb +4 -0
  44. data/test/dummy/admin/app/policies/admin/article_policy.rb +11 -0
  45. data/test/dummy/admin/app/policies/admin/magazine_policy.rb +4 -0
  46. data/test/dummy/admin/app/views/admin/shared/_navigation.html.erb +5 -0
  47. data/test/dummy/admin/config/routes.rb +1 -0
  48. data/test/dummy/app/assets/config/manifest.js +4 -0
  49. data/test/dummy/app/controllers/another_admin_sessions_controller.rb +0 -2
  50. data/test/dummy/app/controllers/comments_controller.rb +3 -0
  51. data/test/dummy/app/models/article.rb +1 -0
  52. data/test/dummy/app/models/comment.rb +7 -0
  53. data/test/dummy/app/models/magazine.rb +2 -0
  54. data/test/dummy/app/services/article_service.rb +2 -0
  55. data/test/dummy/app/services/comment_service.rb +7 -0
  56. data/test/dummy/bin/rails +1 -1
  57. data/test/dummy/config/application.rb +2 -14
  58. data/test/dummy/config/locales/en.yml +9 -0
  59. data/test/dummy/config/routes.rb +3 -1
  60. data/test/dummy/db/migrate/20150717121532_create_articles.rb +1 -1
  61. data/test/dummy/db/migrate/20150907133753_create_admin_users.rb +1 -1
  62. data/test/dummy/db/migrate/20160713134238_create_comment.rb +9 -0
  63. data/test/dummy/db/migrate/20210519215502_create_magazines.rb +9 -0
  64. data/test/dummy/db/schema.rb +35 -20
  65. data/test/fakes/article.rb +1 -1
  66. data/test/fakes/article_service.rb +3 -7
  67. data/test/generators/resource_generator_test.rb +78 -0
  68. data/test/integration/authentication_test.rb +3 -2
  69. data/test/integration/authorization_test.rb +13 -0
  70. data/test/integration/nested_resources_test.rb +47 -0
  71. data/test/test_helper.rb +1 -7
  72. data/test/{lib/godmin → unit}/engine_wrapper_test.rb +0 -0
  73. data/test/{lib/godmin → unit}/helpers/filters_test.rb +0 -0
  74. data/test/{lib/godmin → unit}/paginator_test.rb +0 -0
  75. data/test/{lib/godmin → unit}/resolver_test.rb +0 -0
  76. data/test/{lib/godmin → unit}/resources/resource_service/batch_actions_test.rb +0 -0
  77. data/test/{lib/godmin → unit}/resources/resource_service/filters_test.rb +0 -0
  78. data/test/{lib/godmin → unit}/resources/resource_service/ordering_test.rb +0 -0
  79. data/test/{lib/godmin → unit}/resources/resource_service/pagination_test.rb +0 -0
  80. data/test/{lib/godmin → unit}/resources/resource_service/scopes_test.rb +0 -0
  81. data/test/{lib/godmin → unit}/resources/resource_service_test.rb +0 -0
  82. metadata +169 -107
  83. data/gemfiles/rails_4.gemfile +0 -9
  84. data/lib/godmin/authorization/policy_finder.rb +0 -31
  85. data/lib/tasks/godmin_tasks.rake +0 -4
  86. data/test/lib/godmin/policy_finder_test.rb +0 -55
data/Rakefile CHANGED
@@ -36,14 +36,14 @@ namespace :sandbox do
36
36
  task :deploy do
37
37
  message = "Generated from: https://github.com/varvet/godmin/commit/#{`git rev-parse HEAD`.strip}"
38
38
  template_path = File.expand_path("../template.rb", __FILE__)
39
- Bundler.with_clean_env do
39
+ Bundler.with_unbundled_env do
40
40
  Dir.mktmpdir do |dir|
41
41
  Dir.chdir(dir)
42
42
  system("git clone git@github.com:varvet/godmin-sandbox.git")
43
43
  if $CHILD_STATUS.success?
44
44
  Dir.chdir("godmin-sandbox")
45
45
  system("rm -rf *")
46
- system("rails new . -d postgresql -m #{template_path} --without-engine")
46
+ system("rails _5.2.5_ new . -d postgresql -m #{template_path} --without-engine --skip-spring")
47
47
  if $CHILD_STATUS.success?
48
48
  system("git add --all")
49
49
  system("git commit -m '#{message}'")
@@ -1,12 +1,22 @@
1
1
  <div id="breadcrumb">
2
2
  <ol class="breadcrumb">
3
+ <% if @resource_parents %>
4
+ <% @resource_parents.each do |parent| %>
5
+ <li>
6
+ <%= link_to parent.class.model_name.human(count: 2), parent.class %>
7
+ </li>
8
+ <li>
9
+ <%= link_to parent.to_s, parent %>
10
+ </li>
11
+ <% end %>
12
+ <% end %>
3
13
  <% if action_name == "index" %>
4
14
  <li class="active">
5
15
  <%= @resource_class.model_name.human(count: 2) %>
6
16
  </li>
7
17
  <% else %>
8
18
  <li>
9
- <%= link_to @resource_class.model_name.human(count: 2), @resource_class %>
19
+ <%= link_to @resource_class.model_name.human(count: 2), [*@resource_parents, @resource_class] %>
10
20
  </li>
11
21
  <li class="active">
12
22
  <% if @resource.new_record? %>
@@ -16,29 +26,7 @@
16
26
  <% end %>
17
27
  </li>
18
28
  <% if @resource.persisted? %>
19
- <li class="dropdown pull-right">
20
- <a href="#" data-toggle="dropdown" role="button">
21
- <%= translate_scoped("actions.label") %> <span class="caret"></span>
22
- </a>
23
- <ul class="dropdown-menu">
24
- <% if policy(@resource).show? && action_name != "show" %>
25
- <li>
26
- <%= link_to translate_scoped("actions.show"), @resource %>
27
- </li>
28
- <% end %>
29
- <% if policy(@resource).edit? && action_name != "edit" %>
30
- <li>
31
- <%= link_to translate_scoped("actions.edit"), [:edit, @resource] %>
32
- </li>
33
- <% end %>
34
- <% if policy(@resource).destroy? %>
35
- <li>
36
- <%= link_to translate_scoped("actions.destroy"), @resource, method: :delete,
37
- data: { confirm: translate_scoped("actions.confirm_message") } %>
38
- </li>
39
- <% end %>
40
- </ul>
41
- </li>
29
+ <%= render partial: "breadcrumb_actions" %>
42
30
  <% end %>
43
31
  <% end %>
44
32
  </ol>
@@ -0,0 +1,41 @@
1
+ <li class="dropdown pull-right">
2
+ <a href="#" data-toggle="dropdown">
3
+ <%= translate_scoped("actions.label") %> <span class="caret"></span>
4
+ </a>
5
+ <ul class="dropdown-menu">
6
+ <% if policy(@resource).show? && action_name != "show" %>
7
+ <li>
8
+ <%= link_to translate_scoped("actions.show"), [*@resource_parents, @resource] %>
9
+ </li>
10
+ <% end %>
11
+ <% if policy(@resource).edit? && action_name != "edit" %>
12
+ <li>
13
+ <%= link_to translate_scoped("actions.edit"), [:edit, *@resource_parents, @resource] %>
14
+ </li>
15
+ <% end %>
16
+ <% if policy(@resource).destroy? %>
17
+ <li>
18
+ <%= link_to translate_scoped("actions.destroy"), [*@resource_parents, @resource], method: :delete,
19
+ data: { confirm: translate_scoped("actions.confirm_message") } %>
20
+ </li>
21
+ <% end %>
22
+ </ul>
23
+ </li>
24
+
25
+ <% if @resource_service.has_many_map.present? %>
26
+ <li class="dropdown pull-right">
27
+ <a href="#" data-toggle="dropdown">
28
+ <%= translate_scoped("associations.label") %> <span class="caret"></span>
29
+ </a>
30
+ <ul class="dropdown-menu">
31
+ <% @resource_service.has_many_map.each do |name, options| %>
32
+ <% if policy(options[:class_name].constantize).index? %>
33
+ <li>
34
+ <%= link_to(options[:class_name].constantize.model_name.human(count: 2),
35
+ send("#{@resource_class.name.underscore}_#{name}_path", @resource)) %>
36
+ </li>
37
+ <% end %>
38
+ <% end %>
39
+ </ul>
40
+ </li>
41
+ <% end %>
@@ -1,3 +1,3 @@
1
1
  <% if policy(@resource_service.build_resource({})).new? %>
2
- <%= link_to t("helpers.submit.create", model: @resource_class.model_name.human), [:new, @resource_class.model_name.singular_route_key], class: "btn btn-default" %>
2
+ <%= link_to t("helpers.submit.create", model: @resource_class.model_name.human), [:new, *@resource_parents, @resource_class.model_name.singular_route_key.to_sym], class: "btn btn-default" %>
3
3
  <% end %>
@@ -27,14 +27,14 @@
27
27
  </div>
28
28
  <div class="pagination-entries pull-right hidden-xs">
29
29
  <% if @resources.length == 0 %>
30
- <%= translate_scoped("pagination.entries.zero", {
30
+ <%= translate_scoped("pagination.entries.zero",
31
31
  resource: @resource_class.model_name.human(count: @resources.length).downcase
32
- }) %>
32
+ ) %>
33
33
  <% else %>
34
- <%= translate_scoped("pagination.entries.other", {
34
+ <%= translate_scoped("pagination.entries.other",
35
35
  resource: @resource_class.model_name.human(count: @resources.length).downcase,
36
36
  count: @resources.length,
37
37
  total: @resource_service.paginator.total_resources
38
- }) %>
38
+ ) %>
39
39
  <% end %>
40
40
  </div>
@@ -2,7 +2,7 @@
2
2
  <% if policy(resource).show? %>
3
3
  <%= link_to(
4
4
  translate_scoped("actions.show"),
5
- resource,
5
+ [*@resource_parents, resource],
6
6
  class: "btn btn-default",
7
7
  title: translate_scoped("actions.show_title", resource: resource)
8
8
  ) %>
@@ -10,7 +10,7 @@
10
10
  <% if policy(resource).edit? %>
11
11
  <%= link_to(
12
12
  translate_scoped("actions.edit"),
13
- [:edit, resource],
13
+ [:edit, *@resource_parents, resource],
14
14
  class: "btn btn-default",
15
15
  title: translate_scoped("actions.edit_title", resource: resource)
16
16
  ) %>
@@ -18,7 +18,7 @@
18
18
  <% if policy(resource).destroy? %>
19
19
  <%= link_to(
20
20
  translate_scoped("actions.destroy"),
21
- resource,
21
+ [*@resource_parents, resource],
22
22
  method: :delete,
23
23
  class: "btn btn-default",
24
24
  title: translate_scoped("actions.destroy_title", resource: resource),
@@ -1,6 +1,6 @@
1
1
  <%= render partial: "breadcrumb" %>
2
2
 
3
- <table class="table table-striped">
3
+ <table class="table">
4
4
  <% @resource_service.attrs_for_show.each do |attr| %>
5
5
  <tr>
6
6
  <th><%= @resource_class.human_attribute_name(attr) %></th>
@@ -25,6 +25,8 @@ en:
25
25
  confirm_message: Are you sure?
26
26
  export: Export
27
27
  export_as: As
28
+ associations:
29
+ label: Nested resources
28
30
  sessions:
29
31
  sign_in: Sign in
30
32
  sign_out: Sign out
@@ -25,6 +25,8 @@ pt-BR:
25
25
  confirm_message: Você tem certeza?
26
26
  export: Exportar
27
27
  export_as: Como
28
+ associations:
29
+ label: Nested resources
28
30
  sessions:
29
31
  sign_in: Entrar
30
32
  sign_out: Sair
@@ -25,6 +25,8 @@ sv:
25
25
  confirm_message: Är du säker?
26
26
  export: Exportera
27
27
  export_as: Som
28
+ associations:
29
+ label: Nästlade resurser
28
30
  sessions:
29
31
  sign_in: Logga in
30
32
  sign_out: Logga ut
@@ -2,8 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "admin", :path => "../test/dummy/admin", :group => [:test, :development]
6
- gem "codeclimate-test-reporter", :group => :test, :require => nil
7
- gem "rails", "~> 5.0"
5
+ gem "admin", path: "../test/dummy/admin", group: [:test, :development]
6
+ gem "rails", "~> 5.2"
8
7
 
9
- gemspec :path => "../"
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "admin", path: "../test/dummy/admin", group: [:test, :development]
6
+ gem "rails", "~> 6.1"
7
+
8
+ gemspec path: "../"
data/godmin.gemspec CHANGED
@@ -10,8 +10,8 @@ Gem::Specification.new do |gem|
10
10
  gem.authors = ["Jens Ljungblad", "Linus Pettersson", "Varvet"]
11
11
  gem.email = ["info@varvet.se"]
12
12
  gem.homepage = "https://github.com/varvet/godmin"
13
- gem.summary = "Godmin is an admin framework for Rails 4+"
14
- gem.description = "Godmin is an admin framework for Rails 4+. Use it to build dedicated admin sections for your apps, or stand alone admin apps such as internal tools."
13
+ gem.summary = "Godmin is an admin framework for Rails 5+"
14
+ gem.description = "Godmin is an admin framework for Rails 5+. Use it to build dedicated admin sections for your apps, or stand alone admin apps such as internal tools."
15
15
  gem.license = "MIT"
16
16
 
17
17
  gem.files = `git ls-files`.split($/)
@@ -19,23 +19,25 @@ Gem::Specification.new do |gem|
19
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
20
  gem.require_paths = ["lib"]
21
21
 
22
- gem.add_dependency "bcrypt", "~> 3.1"
23
- gem.add_dependency "bootstrap_form", "~> 2.4"
22
+ gem.add_dependency "bcrypt", [">= 3.0", "< 4.0"]
24
23
  gem.add_dependency "bootstrap-sass", "~> 3.3"
25
- gem.add_dependency "coffee-rails", [">= 4.0", "< 6.0"]
24
+ gem.add_dependency "bootstrap_form", "~> 2.4"
26
25
  gem.add_dependency "csv_builder", "~> 2.1"
27
- gem.add_dependency "jquery-rails", [">= 3.0", "< 5.0"]
26
+ gem.add_dependency "jquery-rails", [">= 4.0", "< 5.0"]
28
27
  gem.add_dependency "momentjs-rails", "~> 2.8"
29
- gem.add_dependency "rails", [">= 4.0", "< 6.0"]
30
- gem.add_dependency "sass-rails", [">= 4.0", "< 6.0"]
28
+ gem.add_dependency "pundit", [">= 2.0.0", "< 3.0"]
29
+ gem.add_dependency "rails", [">= 5.0", "< 7.0"]
30
+ gem.add_dependency "sass-rails", [">= 5.0", "< 7.0"]
31
31
  gem.add_dependency "selectize-rails", "~> 0.12"
32
32
 
33
- gem.add_development_dependency "appraisal", "~> 2.1"
34
- gem.add_development_dependency "capybara", "~> 2.4"
35
- gem.add_development_dependency "m", "~> 1.3"
36
- gem.add_development_dependency "minitest-reporters", "~> 1.0"
37
- gem.add_development_dependency "minitest", "~> 5.5"
38
- gem.add_development_dependency "poltergeist", "~> 1.7"
39
- gem.add_development_dependency "pry", "~> 0.10"
40
- gem.add_development_dependency "sqlite3", "~> 1.3"
33
+ gem.add_development_dependency "appraisal"
34
+ gem.add_development_dependency "bootsnap"
35
+ gem.add_development_dependency "byebug"
36
+ gem.add_development_dependency "capybara"
37
+ gem.add_development_dependency "minitest"
38
+ gem.add_development_dependency "minitest-reporters"
39
+ gem.add_development_dependency "poltergeist"
40
+ gem.add_development_dependency "pry"
41
+ gem.add_development_dependency "puma"
42
+ gem.add_development_dependency "sqlite3"
41
43
  end
@@ -4,7 +4,7 @@ class Godmin::ResourceGenerator < Godmin::Generators::NamedBase
4
4
  argument :attributes, type: :array, default: [], banner: "attribute attribute"
5
5
 
6
6
  def add_route
7
- route "resources :#{file_name.pluralize}"
7
+ invoke "resource_route"
8
8
  end
9
9
 
10
10
  def add_navigation
@@ -15,6 +15,12 @@ class Godmin::ResourceGenerator < Godmin::Generators::NamedBase
15
15
  end
16
16
  end
17
17
 
18
+ def create_model
19
+ if namespaced?
20
+ template "resource_model.rb", File.join("app/models", class_path, "#{file_name}.rb")
21
+ end
22
+ end
23
+
18
24
  def create_controller
19
25
  template "resource_controller.rb", File.join("app/controllers", class_path, "#{file_name.pluralize}_controller.rb")
20
26
  end
@@ -0,0 +1,4 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %> < ::<%= class_name %>
3
+ end
4
+ <% end -%>
@@ -28,15 +28,6 @@ module Godmin
28
28
 
29
29
  protected
30
30
 
31
- def redirect_back
32
- case Rails::VERSION::MAJOR
33
- when 4
34
- redirect_to :back
35
- when 5
36
- super(fallback_location: root_path)
37
- end
38
- end
39
-
40
31
  private
41
32
 
42
33
  def engine_wrapper
@@ -47,12 +38,20 @@ module Godmin
47
38
  append_view_path Godmin::Resolver.resolvers(controller_path, engine_wrapper)
48
39
  end
49
40
 
41
+ def disable_authentication
42
+ @_disable_authentication = true
43
+ end
44
+
45
+ def disable_authorization
46
+ @_disable_authorization = true
47
+ end
48
+
50
49
  def authentication_enabled?
51
- singleton_class.include?(Godmin::Authentication)
50
+ !@_disable_authentication && singleton_class.include?(Godmin::Authentication)
52
51
  end
53
52
 
54
53
  def authorization_enabled?
55
- singleton_class.include?(Godmin::Authorization)
54
+ !@_disable_authorization && singleton_class.include?(Godmin::Authorization)
56
55
  end
57
56
  end
58
57
  end
@@ -6,26 +6,26 @@ module Godmin
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- before_action :authenticate_admin_user
9
+ before_action :authenticate
10
10
 
11
11
  helper_method :admin_user
12
12
  helper_method :admin_user_signed_in?
13
13
  end
14
14
 
15
- def authenticate_admin_user
16
- unless admin_user_signed_in? || controller_name == "sessions"
17
- redirect_to new_session_path
18
- end
19
- end
15
+ def authenticate
16
+ return unless authentication_enabled?
17
+ return if admin_user_signed_in?
20
18
 
21
- def admin_user_class
22
- raise NotImplementedError, "Must define the admin user class"
19
+ redirect_to new_session_path
23
20
  end
24
21
 
22
+ def admin_user_class; end
23
+
25
24
  def admin_user
26
- if session[:admin_user_id]
27
- @admin_user ||= admin_user_class.find_by(id: session[:admin_user_id])
28
- end
25
+ return unless admin_user_class
26
+ return unless session[:admin_user_id]
27
+
28
+ @_admin_user ||= admin_user_class.find_by(id: session[:admin_user_id])
29
29
  end
30
30
 
31
31
  def admin_user_signed_in?
@@ -5,6 +5,7 @@ module Godmin
5
5
 
6
6
  included do
7
7
  layout "godmin/login"
8
+ prepend_before_action :disable_authentication
8
9
  end
9
10
 
10
11
  def new
@@ -1,34 +1,54 @@
1
+ require "pundit"
1
2
  require "godmin/authorization/policy"
2
- require "godmin/authorization/policy_finder"
3
3
 
4
4
  module Godmin
5
5
  module Authorization
6
6
  extend ActiveSupport::Concern
7
7
 
8
- included do
9
- helper_method :policy
8
+ include Pundit
10
9
 
11
- rescue_from NotAuthorizedError do
10
+ included do
11
+ rescue_from Pundit::NotAuthorizedError do
12
12
  render plain: "You are not authorized to do this", status: 403, layout: "godmin/login"
13
13
  end
14
14
  end
15
15
 
16
- def authorize(record, query = nil)
17
- policy = policy(record)
16
+ def policy(record)
17
+ policies[record] ||= Pundit.policy!(pundit_user, namespaced_record(record))
18
+ end
18
19
 
19
- unless policy.public_send(query || action_name + "?")
20
- fail NotAuthorizedError
21
- end
20
+ def pundit_user
21
+ admin_user
22
22
  end
23
23
 
24
- def policy(record)
25
- policies[record] ||= PolicyFinder.find(record, engine_wrapper.namespace).new(admin_user, record)
24
+ def namespaced_record(record)
25
+ return record unless engine_wrapper.namespaced?
26
+
27
+ class_name = find_class_name(record)
28
+ if already_namespaced?(class_name)
29
+ record
30
+ else
31
+ engine_wrapper.namespaced_path.map(&:to_sym) << record
32
+ end
26
33
  end
27
34
 
28
- def policies
29
- @_policies ||= {}
35
+ # Borrowed from Pundit::PolicyFinder
36
+ def find_class_name(subject)
37
+ if subject.respond_to?(:model_name)
38
+ subject.model_name
39
+ elsif subject.class.respond_to?(:model_name)
40
+ subject.class.model_name
41
+ elsif subject.is_a?(Class)
42
+ subject
43
+ elsif subject.is_a?(Symbol)
44
+ subject.to_s.camelize
45
+ else
46
+ subject.class
47
+ end
30
48
  end
31
49
 
32
- class NotAuthorizedError < StandardError; end
50
+ def already_namespaced?(subject)
51
+ subject.to_s.start_with?("#{engine_wrapper.namespace.name}::")
52
+ end
33
53
  end
34
54
  end