kms 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +30 -0
- data/app/assets/fonts/casper-webfont.eot +0 -0
- data/app/assets/fonts/casper-webfont.svg +278 -0
- data/app/assets/fonts/casper-webfont.ttf +0 -0
- data/app/assets/fonts/casper-webfont.woff +0 -0
- data/app/assets/fonts/casper-webfont.woff2 +0 -0
- data/app/assets/fonts/glyphicons-halflings-regular.eot +0 -0
- data/app/assets/fonts/glyphicons-halflings-regular.svg +229 -0
- data/app/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/app/assets/fonts/glyphicons-halflings-regular.woff +0 -0
- data/app/assets/javascripts/kms/application.js +49 -0
- data/app/assets/javascripts/kms/application/controllers/assets_controller.coffee +70 -0
- data/app/assets/javascripts/kms/application/controllers/help_controller.coffee +12 -0
- data/app/assets/javascripts/kms/application/controllers/pages_controller.coffee +95 -0
- data/app/assets/javascripts/kms/application/controllers/settings_controller.coffee +21 -0
- data/app/assets/javascripts/kms/application/controllers/sidebar_controller.coffee +26 -0
- data/app/assets/javascripts/kms/application/controllers/snippets_controller.coffee +44 -0
- data/app/assets/javascripts/kms/application/controllers/templates_controller.coffee +46 -0
- data/app/assets/javascripts/kms/application/controllers/users_controller.coffee +31 -0
- data/app/assets/javascripts/kms/application/module.coffee +24 -0
- data/app/assets/javascripts/kms/application/routes.coffee.erb +171 -0
- data/app/assets/javascripts/kms/application/services/errors_service.coffee +8 -0
- data/app/assets/javascripts/templates/assets/edit.html.slim +10 -0
- data/app/assets/javascripts/templates/assets/index.html.slim +31 -0
- data/app/assets/javascripts/templates/assets/new.html.slim +16 -0
- data/app/assets/javascripts/templates/help.html.slim +172 -0
- data/app/assets/javascripts/templates/help/filters.html.slim +51 -0
- data/app/assets/javascripts/templates/help/variables.html.slim +96 -0
- data/app/assets/javascripts/templates/pages/edit.html.slim +7 -0
- data/app/assets/javascripts/templates/pages/form.html.slim +32 -0
- data/app/assets/javascripts/templates/pages/index.html.slim +33 -0
- data/app/assets/javascripts/templates/pages/new.html.slim +5 -0
- data/app/assets/javascripts/templates/settings.html.slim +20 -0
- data/app/assets/javascripts/templates/snippets/edit.html.slim +5 -0
- data/app/assets/javascripts/templates/snippets/form.html.slim +10 -0
- data/app/assets/javascripts/templates/snippets/index.html.slim +22 -0
- data/app/assets/javascripts/templates/snippets/new.html.slim +5 -0
- data/app/assets/javascripts/templates/templates/edit.html.slim +7 -0
- data/app/assets/javascripts/templates/templates/form.html.slim +7 -0
- data/app/assets/javascripts/templates/templates/index.html.slim +22 -0
- data/app/assets/javascripts/templates/templates/new.html.slim +5 -0
- data/app/assets/javascripts/templates/users/form.html.slim +14 -0
- data/app/assets/javascripts/templates/users/index.html.slim +21 -0
- data/app/assets/javascripts/templates/users/new.html.slim +5 -0
- data/app/assets/stylesheets/kms/application.css +57 -0
- data/app/assets/stylesheets/kms/custom.css.scss +67 -0
- data/app/assets/stylesheets/kms/responsive_dashboard.css.less +9 -0
- data/app/controllers/kms/application_controller.rb +24 -0
- data/app/controllers/kms/assets_controller.rb +58 -0
- data/app/controllers/kms/kms_controller.rb +8 -0
- data/app/controllers/kms/pages_controller.rb +67 -0
- data/app/controllers/kms/public/pages_controller.rb +27 -0
- data/app/controllers/kms/public/search_controller.rb +18 -0
- data/app/controllers/kms/resources_controller.rb +9 -0
- data/app/controllers/kms/settings_controller.rb +24 -0
- data/app/controllers/kms/snippets_controller.rb +37 -0
- data/app/controllers/kms/templates_controller.rb +43 -0
- data/app/controllers/kms/users_controller.rb +36 -0
- data/app/controllers/users/confirmations_controller.rb +28 -0
- data/app/controllers/users/omniauth_callbacks_controller.rb +28 -0
- data/app/controllers/users/passwords_controller.rb +32 -0
- data/app/controllers/users/registrations_controller.rb +70 -0
- data/app/controllers/users/sessions_controller.rb +25 -0
- data/app/controllers/users/unlocks_controller.rb +28 -0
- data/app/helpers/kms/application_helper.rb +4 -0
- data/app/models/ability.rb +38 -0
- data/app/models/concerns/kms/compile_templates.rb +39 -0
- data/app/models/concerns/kms/permalinkable.rb +13 -0
- data/app/models/concerns/kms/update_stylesheets_text.rb +20 -0
- data/app/models/kms/asset.rb +83 -0
- data/app/models/kms/page.rb +61 -0
- data/app/models/kms/settings.rb +7 -0
- data/app/models/kms/snippet.rb +11 -0
- data/app/models/kms/template.rb +11 -0
- data/app/models/kms/user.rb +18 -0
- data/app/serializers/kms/settings_serializer.rb +5 -0
- data/app/serializers/kms/snippet_serializer.rb +5 -0
- data/app/services/kms/ability_service.rb +11 -0
- data/app/services/kms/externals_registry.rb +11 -0
- data/app/services/kms/form_customization_service.rb +14 -0
- data/app/services/kms/help_service.rb +13 -0
- data/app/services/kms/resource_service.rb +22 -0
- data/app/services/kms/search_service.rb +11 -0
- data/app/services/kms/settings_service.rb +13 -0
- data/app/uploaders/kms/asset_uploader.rb +61 -0
- data/app/views/devise/registrations/new.html.erb +21 -0
- data/app/views/devise/sessions/new.html.erb +17 -0
- data/app/views/devise/shared/_links.html.erb +25 -0
- data/app/views/layouts/kms/devise.html.erb +35 -0
- data/app/views/layouts/kms/kms.html.erb +139 -0
- data/config/initializers/bower_rails.rb +16 -0
- data/config/initializers/devise.rb +15 -0
- data/config/initializers/externals.rb +42 -0
- data/config/initializers/help.rb +1 -0
- data/config/initializers/liquor.rb +49 -0
- data/config/initializers/resources.rb +5 -0
- data/config/locales/devise.en.yml +59 -0
- data/config/locales/devise.ru.yml +63 -0
- data/config/locales/en.yml +172 -0
- data/config/locales/ru.yml +172 -0
- data/config/routes.rb +31 -0
- data/db/migrate/20141027065341_create_pages.rb +12 -0
- data/db/migrate/20141027083603_create_templates.rb +10 -0
- data/db/migrate/20141027083633_add_template_id_to_pages.rb +6 -0
- data/db/migrate/20141029145253_create_assets.rb +10 -0
- data/db/migrate/20141031125758_add_ancestry_to_pages.rb +6 -0
- data/db/migrate/20141031140308_devise_create_users.rb +42 -0
- data/db/migrate/20141119084306_add_fullpath_to_pages.rb +5 -0
- data/db/migrate/20141121112652_add_role_to_users.rb +5 -0
- data/db/migrate/20141127073902_add_templatable_fields_to_pages.rb +6 -0
- data/db/migrate/20141209132901_add_hidden_to_page.rb +5 -0
- data/db/migrate/20150209120632_add_position_to_kms_pages.rb +5 -0
- data/db/migrate/20160129100437_create_kms_snippets.rb +11 -0
- data/db/migrate/20160712094512_create_kms_settings.rb +9 -0
- data/lib/generators/kms/install/install_generator.rb +34 -0
- data/lib/generators/kms/install/templates/devise.rb +257 -0
- data/lib/generators/kms/install/templates/dragonfly.rb +35 -0
- data/lib/kms.rb +6 -0
- data/lib/kms/dependencies.rb +15 -0
- data/lib/kms/drops/page_drop.rb +24 -0
- data/lib/kms/drops/search_item_drop.rb +5 -0
- data/lib/kms/engine.rb +65 -0
- data/lib/kms/externals/bigdecimal.rb +5 -0
- data/lib/kms/externals/request.rb +4 -0
- data/lib/kms/functions/assets.rb +40 -0
- data/lib/kms/functions/currency.rb +21 -0
- data/lib/kms/search_item.rb +28 -0
- data/lib/kms/version.rb +3 -0
- data/lib/tasks/kms_tasks.rake +4 -0
- data/lib/tasks/precompile_hook.rake +42 -0
- data/spec/controllers/kms/snippets_controller_spec.rb +74 -0
- data/spec/factories/kms_settings.rb +6 -0
- data/spec/factories/snippets.rb +10 -0
- data/spec/factories/users.rb +7 -0
- data/spec/internal/Rakefile +2 -0
- data/spec/internal/config/database.yml +7 -0
- data/spec/internal/config/initializers/devise.rb +257 -0
- data/spec/internal/config/routes.rb +7 -0
- data/spec/internal/db/schema.rb +69 -0
- data/spec/internal/log/test.log +14998 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/internal/tmp/cache/assets/test/sprockets/v3.0/1XyAFYlYI0pK7WAgjR4PgXV6BgU6huJSviWmHetdCRs.cache +1 -0
- data/spec/models/kms/setting_spec.rb +7 -0
- data/spec/models/kms/snippet_spec.rb +14 -0
- data/spec/services/kms/help_service_spec.rb +8 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/controller_macros.rb +11 -0
- data/spec/support/request_helpers.rb +7 -0
- metadata +521 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module Kms
|
2
|
+
module CompileTemplates
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
validate :compiled_templates
|
7
|
+
# on validation step object has no id,
|
8
|
+
# that's why we need additional compile if validation pass
|
9
|
+
after_commit :compile_templates, on: [:create, :update]
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def compiled_templates
|
15
|
+
Kms.template_manager.diagnostics.clear
|
16
|
+
register_liquor_template
|
17
|
+
return if Kms.template_manager.compile
|
18
|
+
Kms.template_manager.errors.each do |error|
|
19
|
+
splitted_error = error.message.split(':')
|
20
|
+
errors.add(:content, [Kms.template_manager.decorate(error), splitted_error].flatten.join('<br>').gsub(/\s/, ' '))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def register_liquor_template
|
25
|
+
if is_a?(Kms::Template)
|
26
|
+
Kms.template_manager.register_layout(register_id, content, Kms::ExternalsRegistry.externals.keys)
|
27
|
+
elsif is_a?(Kms::Page)
|
28
|
+
Kms.template_manager.register_template(register_id, content, Kms::ExternalsRegistry.externals.keys)
|
29
|
+
elsif is_a?(Kms::Snippet)
|
30
|
+
Kms.template_manager.register_partial(register_id, content)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def compile_templates
|
35
|
+
register_liquor_template
|
36
|
+
Kms.template_manager.compile
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kms
|
2
|
+
module Permalinkable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Entity should respond to "slug"
|
6
|
+
def permalink
|
7
|
+
templatable_page = Kms::Page.where(templatable_type: self.class.name).first
|
8
|
+
if templatable_page
|
9
|
+
Pathname.new(templatable_page.parent.fullpath).join(self.slug.to_s).to_s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Kms
|
2
|
+
module UpdateStylesheetsText
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
after_commit :restore_text, on: :create
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def restore_text
|
12
|
+
unless self.stylesheet_or_javascript?
|
13
|
+
Asset.where("content_type like '%css%'").each do |asset|
|
14
|
+
asset.store_text
|
15
|
+
asset.save
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Kms
|
2
|
+
class Asset < ActiveRecord::Base
|
3
|
+
include UpdateStylesheetsText
|
4
|
+
|
5
|
+
mount_uploader :file, AssetUploader
|
6
|
+
|
7
|
+
# заимствовано из Locomotive
|
8
|
+
before_validation :store_text
|
9
|
+
|
10
|
+
validate :unique_filename, on: :create
|
11
|
+
|
12
|
+
attr_accessor :text, :performing_plain_text
|
13
|
+
|
14
|
+
def filename
|
15
|
+
read_attribute(:file)
|
16
|
+
end
|
17
|
+
|
18
|
+
def url
|
19
|
+
file.url
|
20
|
+
end
|
21
|
+
|
22
|
+
def text
|
23
|
+
@text ||= (file.read.force_encoding('UTF-8') rescue nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias :content :text
|
27
|
+
|
28
|
+
def performing_plain_text?
|
29
|
+
performing_plain_text
|
30
|
+
end
|
31
|
+
|
32
|
+
def stylesheet_or_javascript?
|
33
|
+
stylesheet? || javascript?
|
34
|
+
end
|
35
|
+
|
36
|
+
def stylesheet?
|
37
|
+
content_type.include?("css")
|
38
|
+
end
|
39
|
+
|
40
|
+
def javascript?
|
41
|
+
content_type.include?("javascript")
|
42
|
+
end
|
43
|
+
|
44
|
+
def store_text
|
45
|
+
return if persisted? && !stylesheet_or_javascript?
|
46
|
+
|
47
|
+
data = performing_plain_text? ? text : (file.present? ? file.read : nil)
|
48
|
+
|
49
|
+
return if !stylesheet_or_javascript? || data.blank?
|
50
|
+
|
51
|
+
sanitized_source = replace_urls(data)
|
52
|
+
|
53
|
+
self.file = ::CarrierWave::SanitizedFile.new({
|
54
|
+
tempfile: StringIO.new(sanitized_source),
|
55
|
+
filename: filename || file.filename
|
56
|
+
})
|
57
|
+
|
58
|
+
@text = sanitized_source # no need to reset the plain_text instance variable to have the last version
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def unique_filename
|
64
|
+
if Asset.where(file: file.filename).exists?
|
65
|
+
self.errors.add(:file, "is duplicate")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def replace_urls(text)
|
70
|
+
return if text.blank?
|
71
|
+
|
72
|
+
text.gsub(/url\((.+)\)/) do |path|
|
73
|
+
asset_name = $1.split("/")[-1]
|
74
|
+
|
75
|
+
if asset = Asset.where(file: asset_name.gsub(/['"]/, '')).first
|
76
|
+
"url(#{asset.file.url})"
|
77
|
+
else
|
78
|
+
path
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Kms
|
2
|
+
class Page < ActiveRecord::Base
|
3
|
+
include Liquor::Dropable
|
4
|
+
include CompileTemplates
|
5
|
+
|
6
|
+
INDEX_SLUG = "index"
|
7
|
+
INDEX_FULLPATH = ""
|
8
|
+
|
9
|
+
scope :published, lambda { where(published: true) }
|
10
|
+
scope :listed, lambda { where(hidden: false) }
|
11
|
+
scope :not_templatable, lambda { where(templatable: false) }
|
12
|
+
|
13
|
+
belongs_to :template
|
14
|
+
|
15
|
+
validates :slug, uniqueness: true, presence: true
|
16
|
+
|
17
|
+
before_save :build_fullpath
|
18
|
+
|
19
|
+
before_create :set_position
|
20
|
+
|
21
|
+
has_ancestry
|
22
|
+
|
23
|
+
def index?
|
24
|
+
slug == INDEX_SLUG
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_fullpath
|
28
|
+
if index?
|
29
|
+
self.fullpath = INDEX_FULLPATH # self.slug
|
30
|
+
else
|
31
|
+
slugs = (ancestors + [self]).map(&:slug)
|
32
|
+
slugs.shift unless slugs.size == 1
|
33
|
+
self.fullpath = File.join slugs.compact
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_position
|
38
|
+
if parent.present?
|
39
|
+
max = parent.children.maximum(:position)
|
40
|
+
else
|
41
|
+
max = Page.where('ancestry IS ?', nil).maximum(:position)
|
42
|
+
end
|
43
|
+
self.position = max ? max + 1 : 0
|
44
|
+
end
|
45
|
+
|
46
|
+
# fetch items for templatable page
|
47
|
+
def fetch_items
|
48
|
+
templatable_type.constantize.all
|
49
|
+
end
|
50
|
+
|
51
|
+
# fetch item by slug
|
52
|
+
def fetch_item!(slug)
|
53
|
+
return nil unless templatable?
|
54
|
+
templatable_type.constantize.find_by_slug!(slug)
|
55
|
+
end
|
56
|
+
|
57
|
+
def register_id
|
58
|
+
"#{self.class.name.parameterize("_")}_#{id}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Kms
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
# Include default devise modules. Others available are:
|
4
|
+
# :confirmable, :lockable, :timeoutable and :omniauthable
|
5
|
+
devise :database_authenticatable, :registerable,
|
6
|
+
:recoverable, :rememberable, :validatable
|
7
|
+
|
8
|
+
ROLES = %i[admin content_manager]
|
9
|
+
|
10
|
+
def admin?
|
11
|
+
role == "admin"
|
12
|
+
end
|
13
|
+
|
14
|
+
def localized_role
|
15
|
+
I18n.t("roles.#{role}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Kms
|
2
|
+
class FormCustomizationService
|
3
|
+
cattr_accessor :resources do
|
4
|
+
Hash.new {|h, key| h[key] = []}
|
5
|
+
end
|
6
|
+
|
7
|
+
# resource - resource class
|
8
|
+
# fields - additional templates for resource form
|
9
|
+
def self.register_templates(resource, *templates)
|
10
|
+
resources[resource].concat templates
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kms
|
2
|
+
class HelpService
|
3
|
+
cattr_accessor :templates do
|
4
|
+
Hash.new {|h, key| h[key] = []}
|
5
|
+
end
|
6
|
+
|
7
|
+
# engine - engine class
|
8
|
+
# help_templates - engine help templates
|
9
|
+
def self.register_templates(engine, *help_templates)
|
10
|
+
templates[engine].concat help_templates
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Kms
|
2
|
+
class ResourceService
|
3
|
+
cattr_accessor :resources do
|
4
|
+
{}
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.register(group, resource, tab_icon_class)
|
8
|
+
self.resources[group] ||= {}
|
9
|
+
self.resources[group][resource] = tab_icon_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.unregister(group, resource)
|
13
|
+
self.resources[group].delete(resource)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.external_resources_hash
|
17
|
+
self.resources.values.map(&:keys).flatten.reject {|r| [Page, Template, Asset, User].include?(r)}.map do |resource_class|
|
18
|
+
{type: resource_class.name, title: resource_class.model_name.human}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kms
|
2
|
+
class SettingsService
|
3
|
+
cattr_accessor :templates do
|
4
|
+
Hash.new {|h, key| h[key] = []}
|
5
|
+
end
|
6
|
+
|
7
|
+
# engine - engine class
|
8
|
+
# settings_templates - engine settings templates
|
9
|
+
def self.register_templates(engine, *settings_templates)
|
10
|
+
templates[engine].concat settings_templates
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'carrierwave/processing/mime_types'
|
4
|
+
module Kms
|
5
|
+
class AssetUploader < CarrierWave::Uploader::Base
|
6
|
+
|
7
|
+
# Include RMagick or MiniMagick support:
|
8
|
+
# include CarrierWave::RMagick
|
9
|
+
# include CarrierWave::MiniMagick
|
10
|
+
|
11
|
+
# Choose what kind of storage to use for this uploader:
|
12
|
+
storage :file
|
13
|
+
# storage :fog
|
14
|
+
|
15
|
+
# Override the directory where uploaded files will be stored.
|
16
|
+
# This is a sensible default for uploaders that are meant to be mounted:
|
17
|
+
def store_dir
|
18
|
+
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Provide a default URL as a default if there hasn't been a file uploaded:
|
22
|
+
# def default_url
|
23
|
+
# # For Rails 3.1+ asset pipeline compatibility:
|
24
|
+
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
|
25
|
+
#
|
26
|
+
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
|
27
|
+
# end
|
28
|
+
|
29
|
+
# Process files as they are uploaded:
|
30
|
+
# process :scale => [200, 300]
|
31
|
+
#
|
32
|
+
# def scale(width, height)
|
33
|
+
# # do something
|
34
|
+
# end
|
35
|
+
|
36
|
+
# Create different versions of your uploaded files:
|
37
|
+
# version :thumb do
|
38
|
+
# process :scale => [50, 50]
|
39
|
+
# end
|
40
|
+
|
41
|
+
#process :set_content_type
|
42
|
+
process :save_content_type_in_model
|
43
|
+
|
44
|
+
def save_content_type_in_model
|
45
|
+
model.content_type = file.content_type if file.content_type
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add a white list of extensions which are allowed to be uploaded.
|
49
|
+
# For images you might use something like this:
|
50
|
+
# def extension_white_list
|
51
|
+
# %w(jpg jpeg gif png)
|
52
|
+
# end
|
53
|
+
|
54
|
+
# Override the filename of the uploaded files:
|
55
|
+
# Avoid using model.id or version_name here, see uploader/store.rb for details.
|
56
|
+
# def filename
|
57
|
+
# "something.jpg" if original_filename
|
58
|
+
# end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|