sufia-models 6.2.0 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/actors/sufia/generic_file/actor.rb +4 -7
  3. data/app/jobs/active_fedora_id_based_job.rb +1 -1
  4. data/app/jobs/active_fedora_pid_based_job.rb +1 -2
  5. data/app/jobs/audit_job.rb +1 -1
  6. data/app/jobs/batch_update_job.rb +6 -6
  7. data/app/jobs/create_derivatives_job.rb +2 -3
  8. data/app/jobs/import_url_job.rb +8 -5
  9. data/app/jobs/ingest_local_file_job.rb +1 -1
  10. data/app/models/batch.rb +15 -21
  11. data/app/models/checksum_audit_log.rb +0 -1
  12. data/app/models/concerns/sufia/ability.rb +5 -0
  13. data/app/models/concerns/sufia/collection_behavior.rb +3 -2
  14. data/app/models/concerns/sufia/file_stat_utils.rb +19 -21
  15. data/app/models/concerns/sufia/generic_file/batches.rb +1 -3
  16. data/app/models/concerns/sufia/generic_file/characterization.rb +11 -16
  17. data/app/models/concerns/sufia/generic_file/content.rb +0 -1
  18. data/app/models/concerns/sufia/generic_file/derivatives.rb +2 -2
  19. data/app/models/concerns/sufia/generic_file/export.rb +50 -59
  20. data/app/models/concerns/sufia/generic_file/full_text_indexing.rb +15 -18
  21. data/app/models/concerns/sufia/generic_file/metadata.rb +0 -2
  22. data/app/models/concerns/sufia/generic_file/mime_types.rb +9 -9
  23. data/app/models/concerns/sufia/generic_file/permissions.rb +0 -1
  24. data/app/models/concerns/sufia/generic_file/proxy_deposit.rb +0 -1
  25. data/app/models/concerns/sufia/generic_file/querying.rb +9 -5
  26. data/app/models/concerns/sufia/generic_file/trophies.rb +1 -1
  27. data/app/models/concerns/sufia/generic_file/versions.rb +0 -4
  28. data/app/models/concerns/sufia/model_methods.rb +0 -1
  29. data/app/models/concerns/sufia/user.rb +15 -15
  30. data/app/models/concerns/sufia/user_usage_stats.rb +0 -2
  31. data/app/models/datastreams/fits_datastream.rb +25 -25
  32. data/app/models/domain_term.rb +2 -3
  33. data/app/models/featured_work.rb +3 -5
  34. data/app/models/file_download_stat.rb +3 -4
  35. data/app/models/file_usage.rb +10 -11
  36. data/app/models/file_view_stat.rb +3 -3
  37. data/app/models/follow.rb +1 -1
  38. data/app/models/geo_names_resource.rb +3 -3
  39. data/app/models/group.rb +1 -3
  40. data/app/models/local_authority.rb +26 -28
  41. data/app/models/proxy_deposit_request.rb +9 -9
  42. data/app/models/single_use_link.rb +10 -18
  43. data/app/models/sufia/download.rb +2 -2
  44. data/app/models/sufia/pageview.rb +1 -1
  45. data/app/models/trophy.rb +2 -4
  46. data/app/services/sufia/analytics.rb +10 -11
  47. data/app/services/sufia/generic_file_audit_service.rb +11 -12
  48. data/app/services/sufia/repository_audit_service.rb +1 -1
  49. data/config/locales/sufia.en.yml +2 -0
  50. data/lib/generators/sufia/models/abstract_migration_generator.rb +7 -6
  51. data/lib/generators/sufia/models/install_generator.rb +3 -3
  52. data/lib/generators/sufia/models/templates/config/arkivo_constraint.rb +1 -1
  53. data/lib/generators/sufia/models/templates/config/clamav.rb +1 -1
  54. data/lib/generators/sufia/models/templates/config/redis_config.rb +13 -5
  55. data/lib/generators/sufia/models/templates/config/resque_admin.rb +2 -2
  56. data/lib/generators/sufia/models/templates/config/resque_config.rb +1 -1
  57. data/lib/generators/sufia/models/templates/config/sufia.rb +10 -4
  58. data/lib/generators/sufia/models/templates/migrations/create_checksum_audit_logs.rb +1 -1
  59. data/lib/generators/sufia/models/templates/migrations/create_file_download_stats.rb +1 -1
  60. data/lib/generators/sufia/models/templates/migrations/create_file_view_stats.rb +1 -1
  61. data/lib/generators/sufia/models/templates/migrations/create_local_authorities.rb +1 -1
  62. data/lib/generators/sufia/models/update_content_blocks_generator.rb +0 -1
  63. data/lib/generators/sufia/models/upgrade600_generator.rb +0 -1
  64. data/lib/generators/sufia/models/user_stats_generator.rb +2 -2
  65. data/lib/sufia/messages.rb +17 -17
  66. data/lib/sufia/models.rb +1 -1
  67. data/lib/sufia/models/active_fedora/redis.rb +1 -4
  68. data/lib/sufia/models/active_record/redis.rb +2 -3
  69. data/lib/sufia/models/engine.rb +12 -7
  70. data/lib/sufia/models/file_content/versions.rb +0 -1
  71. data/lib/sufia/models/resque.rb +2 -2
  72. data/lib/sufia/models/stats/user_stat_importer.rb +65 -67
  73. data/lib/sufia/models/user_local_directory_behavior.rb +9 -13
  74. data/lib/sufia/models/utils.rb +1 -2
  75. data/lib/sufia/models/version.rb +1 -1
  76. data/lib/sufia/permissions.rb +0 -1
  77. data/lib/sufia/permissions/readable.rb +0 -1
  78. data/lib/sufia/permissions/writable.rb +20 -23
  79. data/lib/tasks/sufia-models_tasks.rake +18 -0
  80. data/sufia-models.gemspec +1 -1
  81. metadata +5 -5
@@ -14,30 +14,27 @@ module Sufia
14
14
 
15
15
  private
16
16
 
17
- def extract_content
18
- uri = URI("#{connection_url}/update/extract?extractOnly=true&wt=json&extractFormat=text")
19
- req = Net::HTTP.new(uri.host, uri.port)
20
- resp = req.post(uri.to_s, self.content.content, {
21
- 'Content-type' => "#{self.mime_type};charset=utf-8",
22
- 'Content-Length' => self.content.content.size.to_s
23
- })
24
- raise "URL '#{uri}' returned code #{resp.code}" unless resp.code == "200"
25
- self.content.content.rewind if self.content.content.respond_to?(:rewind)
26
- extracted_text = JSON.parse(resp.body)[''].rstrip
27
- full_text.content = extracted_text if extracted_text.present?
28
- rescue => e
29
- logger.error("Error extracting content from #{self.id}: #{e.inspect}")
30
- end
17
+ def extract_content
18
+ uri = URI("#{connection_url}/update/extract?extractOnly=true&wt=json&extractFormat=text")
19
+ req = Net::HTTP.new(uri.host, uri.port)
20
+ resp = req.post(uri.to_s, content.content, 'Content-type' => "#{mime_type};charset=utf-8",
21
+ 'Content-Length' => content.content.size.to_s)
22
+ raise "URL '#{uri}' returned code #{resp.code}" unless resp.code == "200"
23
+ content.content.rewind if content.content.respond_to?(:rewind)
24
+ extracted_text = JSON.parse(resp.body)[''].rstrip
25
+ full_text.content = extracted_text if extracted_text.present?
26
+ rescue => e
27
+ logger.error("Error extracting content from #{id}: #{e.inspect}")
28
+ end
31
29
 
32
- def connection_url
33
- case
30
+ def connection_url
31
+ case
34
32
  when Blacklight.connection_config[:url] then Blacklight.connection_config[:url]
35
33
  when Blacklight.connection_config["url"] then Blacklight.connection_config["url"]
36
34
  when Blacklight.connection_config[:fulltext] then Blacklight.connection_config[:fulltext]["url"]
37
35
  else Blacklight.connection_config[:default]["url"]
36
+ end
38
37
  end
39
- end
40
-
41
38
  end
42
39
  end
43
40
  end
@@ -4,7 +4,6 @@ module Sufia
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
-
8
7
  property :label, predicate: ActiveFedora::RDF::Fcrepo::Model.downloadFilename, multiple: false
9
8
 
10
9
  property :depositor, predicate: ::RDF::URI.new("http://id.loc.gov/vocabulary/relators/dpt"), multiple: false do |index|
@@ -95,7 +94,6 @@ module Sufia
95
94
  end
96
95
  type ::RDF::URI.new('http://pcdm.org/models#Object')
97
96
  end
98
-
99
97
  end
100
98
  end
101
99
  end
@@ -4,23 +4,23 @@ module Sufia
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  def pdf?
7
- self.class.pdf_mime_types.include? self.mime_type
7
+ self.class.pdf_mime_types.include? mime_type
8
8
  end
9
9
 
10
10
  def image?
11
- self.class.image_mime_types.include? self.mime_type
11
+ self.class.image_mime_types.include? mime_type
12
12
  end
13
13
 
14
14
  def video?
15
- self.class.video_mime_types.include? self.mime_type
15
+ self.class.video_mime_types.include? mime_type
16
16
  end
17
17
 
18
18
  def audio?
19
- self.class.audio_mime_types.include? self.mime_type
19
+ self.class.audio_mime_types.include? mime_type
20
20
  end
21
21
 
22
22
  def office_document?
23
- self.class.office_document_mime_types.include? self.mime_type
23
+ self.class.office_document_mime_types.include? mime_type
24
24
  end
25
25
 
26
26
  def collection?
@@ -28,10 +28,10 @@ module Sufia
28
28
  end
29
29
 
30
30
  def file_format
31
- return nil if self.mime_type.blank? and self.format_label.blank?
32
- return self.mime_type.split('/')[1]+ " ("+self.format_label.join(", ")+")" unless self.mime_type.blank? or self.format_label.blank?
33
- return self.mime_type.split('/')[1] unless self.mime_type.blank?
34
- return self.format_label
31
+ return nil if mime_type.blank? && format_label.blank?
32
+ return mime_type.split('/')[1] + " (" + format_label.join(", ") + ")" unless mime_type.blank? || format_label.blank?
33
+ return mime_type.split('/')[1] unless mime_type.blank?
34
+ format_label
35
35
  end
36
36
 
37
37
  module ClassMethods
@@ -5,7 +5,6 @@ module Sufia
5
5
 
6
6
  include Sufia::Permissions::Writable
7
7
  include Sufia::Permissions::Readable
8
-
9
8
  end
10
9
  end
11
10
  end
@@ -16,7 +16,6 @@ module Sufia
16
16
  after_create :create_transfer_request
17
17
  end
18
18
 
19
-
20
19
  def create_transfer_request
21
20
  Sufia.queue.push(ContentDepositorChangeEventJob.new(id, on_behalf_of)) if on_behalf_of.present?
22
21
  end
@@ -7,15 +7,19 @@ module Sufia
7
7
  # query to find generic files created during the time range
8
8
  # @param [DateTime] start_datetime starting date time for range query
9
9
  # @param [DateTime] end_datetime ending date time for range query
10
- def find_by_date_created(start_datetime, end_datetime=nil)
11
- return [] if start_datetime.blank? # no date just return nothing
12
- start_date_str = start_datetime.utc.strftime(self.date_format)
10
+ def find_by_date_created(start_datetime, end_datetime = nil)
11
+ return [] if start_datetime.blank? # no date just return nothing
12
+ where(build_date_query(start_datetime, end_datetime))
13
+ end
14
+
15
+ def build_date_query(start_datetime, end_datetime)
16
+ start_date_str = start_datetime.utc.strftime(date_format)
13
17
  end_date_str = if end_datetime.blank?
14
18
  "*"
15
19
  else
16
- end_datetime.utc.strftime(self.date_format)
20
+ end_datetime.utc.strftime(date_format)
17
21
  end
18
- where "system_create_dtsi:[#{start_date_str} TO #{end_date_str}]"
22
+ "system_create_dtsi:[#{start_date_str} TO #{end_date_str}]"
19
23
  end
20
24
 
21
25
  def where_private
@@ -7,7 +7,7 @@ module Sufia
7
7
  end
8
8
 
9
9
  def cleanup_trophies
10
- Trophy.destroy_all(generic_file_id: self.id)
10
+ Trophy.destroy_all(generic_file_id: id)
11
11
  end
12
12
  end
13
13
  end
@@ -1,16 +1,12 @@
1
1
  module Sufia
2
2
  module GenericFile
3
3
  module Versions
4
- @@count = 0
5
4
  def record_version_committer(user)
6
5
  version = content.latest_version
7
6
  # content datastream not (yet?) present
8
7
  return if version.nil?
9
- @@count += 1
10
- # raise "Recording #{@@count} #{version.uri} for #{user.user_key}" if @@count == 3
11
8
  VersionCommitter.create(version_id: version.uri, committer_login: user.user_key)
12
9
  end
13
-
14
10
  end
15
11
  end
16
12
  end
@@ -15,6 +15,5 @@ module Sufia
15
15
  "No Title"
16
16
  end
17
17
  end
18
-
19
18
  end
20
19
  end
@@ -43,7 +43,7 @@ module Sufia::User
43
43
  end
44
44
 
45
45
  def zotero_token
46
- self[:zotero_token].blank? ? nil : Marshal::load(self[:zotero_token])
46
+ self[:zotero_token].blank? ? nil : Marshal.load(self[:zotero_token])
47
47
  end
48
48
 
49
49
  def zotero_token=(value)
@@ -51,7 +51,7 @@ module Sufia::User
51
51
  # Resetting the token
52
52
  self[:zotero_token] = value
53
53
  else
54
- self[:zotero_token] = Marshal::dump(value)
54
+ self[:zotero_token] = Marshal.dump(value)
55
55
  end
56
56
  end
57
57
 
@@ -72,14 +72,14 @@ module Sufia::User
72
72
  # 1. validation has already flagged the ORCID as invalid
73
73
  # 2. the orcid field is blank
74
74
  # 3. the orcid is already in its normalized form
75
- return if self.errors[:orcid].first.present? || self.orcid.blank? || self.orcid.starts_with?('http://orcid.org/')
76
- bare_orcid = Sufia::OrcidValidator.match(self.orcid).string
75
+ return if errors[:orcid].first.present? || orcid.blank? || orcid.starts_with?('http://orcid.org/')
76
+ bare_orcid = Sufia::OrcidValidator.match(orcid).string
77
77
  self.orcid = "http://orcid.org/#{bare_orcid}"
78
78
  end
79
79
 
80
80
  # Format the json for select2 which requires just an id and a field called text.
81
81
  # If we need an alternate format we should probably look at a json template gem
82
- def as_json(opts = nil)
82
+ def as_json(_opts = nil)
83
83
  { id: user_key, text: display_name ? "#{display_name} (#{user_key})" : user_key }
84
84
  end
85
85
 
@@ -89,18 +89,18 @@ module Sufia::User
89
89
  end
90
90
 
91
91
  def email_address
92
- self.email
92
+ email
93
93
  end
94
94
 
95
95
  def name
96
- self.display_name.titleize || raise
96
+ display_name.titleize || raise
97
97
  rescue
98
- self.user_key
98
+ user_key
99
99
  end
100
100
 
101
101
  # Redefine this for more intuitive keys in Redis
102
102
  def to_param
103
- # hack because rails doesn't like periods in urls.
103
+ # HACK: because rails doesn't like periods in urls.
104
104
  user_key.gsub(/\./, '-dot-')
105
105
  end
106
106
 
@@ -116,20 +116,20 @@ module Sufia::User
116
116
  end
117
117
 
118
118
  # method needed for messaging
119
- def mailboxer_email(obj=nil)
119
+ def mailboxer_email(_obj = nil)
120
120
  nil
121
121
  end
122
122
 
123
123
  # The basic groups method, override or will fallback to Sufia::Ldap::User
124
124
  def groups
125
- @groups ||= self.group_list ? self.group_list.split(";?;") : []
125
+ @groups ||= group_list ? group_list.split(";?;") : []
126
126
  end
127
127
 
128
128
  def ability
129
129
  @ability ||= ::Ability.new(self)
130
130
  end
131
131
 
132
- def get_all_user_activity( since = DateTime.now.to_i - 8640)
132
+ def all_user_activity(since = DateTime.now.to_i - 8640)
133
133
  events = self.events.reverse.collect { |event| event if event[:timestamp].to_i > since }.compact
134
134
  profile_events = self.profile_events.reverse.collect { |event| event if event[:timestamp].to_i > since }.compact
135
135
  events.concat(profile_events).sort { |a, b| b[:timestamp].to_i <=> a[:timestamp].to_i }
@@ -146,7 +146,7 @@ module Sufia::User
146
146
 
147
147
  # Override this method if you aren't using email/password
148
148
  def audituser
149
- User.find_by_user_key(audituser_key) || User.create!(Devise.authentication_keys.first => audituser_key, password: Devise.friendly_token[0,20])
149
+ User.find_by_user_key(audituser_key) || User.create!(Devise.authentication_keys.first => audituser_key, password: Devise.friendly_token[0, 20])
150
150
  end
151
151
 
152
152
  # Override this method if you aren't using email as the userkey
@@ -156,7 +156,7 @@ module Sufia::User
156
156
 
157
157
  # Override this method if you aren't using email/password
158
158
  def batchuser
159
- User.find_by_user_key(batchuser_key) || User.create!(Devise.authentication_keys.first => batchuser_key, password: Devise.friendly_token[0,20])
159
+ User.find_by_user_key(batchuser_key) || User.create!(Devise.authentication_keys.first => batchuser_key, password: Devise.friendly_token[0, 20])
160
160
  end
161
161
 
162
162
  # Override this method if you aren't using email as the userkey
@@ -170,7 +170,7 @@ module Sufia::User
170
170
 
171
171
  def recent_users(start_date, end_date = nil)
172
172
  end_date ||= DateTime.now # doing or eq here so that if the user passes nil we still get now
173
- return User.where(created_at: start_date..end_date)
173
+ User.where(created_at: start_date..end_date)
174
174
  end
175
175
  end
176
176
  end
@@ -1,5 +1,4 @@
1
1
  module Sufia::UserUsageStats
2
-
3
2
  def stats
4
3
  @stats ||= UserStat.where(user_id: id).order(date: :asc)
5
4
  end
@@ -11,5 +10,4 @@ module Sufia::UserUsageStats
11
10
  def total_file_downloads
12
11
  stats.reduce(0) { |total, stat| total + stat.file_downloads }
13
12
  end
14
-
15
13
  end
@@ -5,13 +5,13 @@ class FitsDatastream < ActiveFedora::OmDatastream
5
5
  t.root(path: "fits",
6
6
  xmlns: "http://hul.harvard.edu/ois/xml/ns/fits/fits_output",
7
7
  schema: "http://hul.harvard.edu/ois/xml/xsd/fits/fits_output.xsd")
8
- t.identification {
9
- t.identity {
10
- t.format_label(path: {attribute: "format"})
11
- t.mime_type(path: {attribute: "mimetype"})
12
- }
13
- }
14
- t.fileinfo {
8
+ t.identification do
9
+ t.identity do
10
+ t.format_label(path: { attribute: "format" })
11
+ t.mime_type(path: { attribute: "mimetype" })
12
+ end
13
+ end
14
+ t.fileinfo do
15
15
  t.file_size(path: "size")
16
16
  t.last_modified(path: "lastmodified")
17
17
  t.filename(path: "filename")
@@ -19,14 +19,14 @@ class FitsDatastream < ActiveFedora::OmDatastream
19
19
  t.rights_basis(path: "rightsBasis")
20
20
  t.copyright_basis(path: "copyrightBasis")
21
21
  t.copyright_note(path: "copyrightNote")
22
- }
23
- t.filestatus {
22
+ end
23
+ t.filestatus do
24
24
  t.well_formed(path: "well-formed")
25
25
  t.valid(path: "valid")
26
26
  t.status_message(path: "message")
27
- }
28
- t.metadata {
29
- t.document {
27
+ end
28
+ t.metadata do
29
+ t.document do
30
30
  t.file_title(path: "title")
31
31
  t.file_author(path: "author")
32
32
  t.file_language(path: "language")
@@ -37,8 +37,8 @@ class FitsDatastream < ActiveFedora::OmDatastream
37
37
  t.line_count(path: "lineCount")
38
38
  t.table_count(path: "tableCount")
39
39
  t.graphics_count(path: "graphicsCount")
40
- }
41
- t.image {
40
+ end
41
+ t.image do
42
42
  t.byte_order(path: "byteOrder")
43
43
  t.compression(path: "compressionScheme")
44
44
  t.width(path: "imageWidth")
@@ -55,28 +55,28 @@ class FitsDatastream < ActiveFedora::OmDatastream
55
55
  t.gps_timestamp(path: "gpsTimeStamp")
56
56
  t.latitude(path: "gpsDestLatitude")
57
57
  t.longitude(path: "gpsDestLongitude")
58
- }
59
- t.text {
58
+ end
59
+ t.text do
60
60
  t.character_set(path: "charset")
61
61
  t.markup_basis(path: "markupBasis")
62
62
  t.markup_language(path: "markupLanguage")
63
- }
64
- t.audio {
63
+ end
64
+ t.audio do
65
65
  t.duration(path: "duration")
66
66
  t.bit_depth(path: "bitDepth")
67
67
  t.sample_rate(path: "sampleRate")
68
68
  t.channels(path: "channels")
69
69
  t.data_format(path: "dataFormatType")
70
70
  t.offset(path: "offset")
71
- }
72
- t.video {
71
+ end
72
+ t.video do
73
73
  t.width(path: "imageWidth")
74
74
  t.height(path: "imageHeight")
75
75
  t.duration(path: "duration")
76
76
  t.sample_rate(path: "sampleRate")
77
77
  t.frame_rate(path: "frameRate")
78
- }
79
- }
78
+ end
79
+ end
80
80
  t.format_label(proxy: [:identification, :identity, :format_label])
81
81
  t.mime_type(proxy: [:identification, :identity, :mime_type])
82
82
  t.file_size(proxy: [:fileinfo, :file_size])
@@ -102,7 +102,7 @@ class FitsDatastream < ActiveFedora::OmDatastream
102
102
  t.byte_order(proxy: [:metadata, :image, :byte_order])
103
103
  t.compression(proxy: [:metadata, :image, :compression])
104
104
  t.width(proxy: [:metadata, :image, :width])
105
- t.video_width( proxy: [:metadata, :video, :width])
105
+ t.video_width(proxy: [:metadata, :video, :width])
106
106
  t.height(proxy: [:metadata, :image, :height])
107
107
  t.video_height(proxy: [:metadata, :video, :height])
108
108
  t.color_space(proxy: [:metadata, :image, :color_space])
@@ -139,9 +139,9 @@ class FitsDatastream < ActiveFedora::OmDatastream
139
139
  "http://hul.harvard.edu/ois/xml/ns/fits/fits_output
140
140
  http://hul.harvard.edu/ois/xml/xsd/fits/fits_output.xsd",
141
141
  version: "0.6.0",
142
- timestamp: "1/25/12 11:04 AM") {
142
+ timestamp: "1/25/12 11:04 AM") do
143
143
  xml.identification { xml.identity(toolname: 'FITS') }
144
- }
144
+ end
145
145
  end
146
146
  builder.doc
147
147
  end
@@ -1,5 +1,4 @@
1
1
  class DomainTerm < ActiveRecord::Base
2
-
3
- # TODO we should add an index on this join table and remove the uniq query
4
- has_and_belongs_to_many :local_authorities, -> {uniq}
2
+ # TODO: we should add an index on this join table and remove the uniq query
3
+ has_and_belongs_to_many :local_authorities, -> { uniq }
5
4
  end
@@ -1,14 +1,13 @@
1
1
  class FeaturedWork < ActiveRecord::Base
2
2
  FEATURE_LIMIT = 5
3
3
  validate :count_within_limit, on: :create
4
- validates :order, inclusion: { in: Proc.new{ 0..FEATURE_LIMIT } }
4
+ validates :order, inclusion: { in: proc { 0..FEATURE_LIMIT } }
5
5
 
6
6
  default_scope { order(:order) }
7
7
 
8
8
  def count_within_limit
9
- unless FeaturedWork.can_create_another?
10
- errors.add(:base, "Limited to #{FEATURE_LIMIT} featured works.")
11
- end
9
+ return if FeaturedWork.can_create_another?
10
+ errors.add(:base, "Limited to #{FEATURE_LIMIT} featured works.")
12
11
  end
13
12
 
14
13
  attr_accessor :generic_file_solr_document
@@ -19,4 +18,3 @@ class FeaturedWork < ActiveRecord::Base
19
18
  end
20
19
  end
21
20
  end
22
-