curation_concerns-models 0.8.0 → 0.9.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f40b99078f83355f035554d22016e711b0b0f379
4
- data.tar.gz: 79c5dd9cafdd3fa8da3f3407ccd3e9a1ea672a03
3
+ metadata.gz: 5660b6eec6ec27f6e8c00a1a9a001a0147a66fe6
4
+ data.tar.gz: e45350c36fd8061b4ef31871620eeeea5f450447
5
5
  SHA512:
6
- metadata.gz: f807e2358bf89d3af2ffb667fbd5740e389f202b4ff7d85e908065871f66072870b6d82c006407ad564e3b491b9b197f471c4434ed9e3d96b185bc398403110f
7
- data.tar.gz: 7cf972fb99dd8b58fb79ca6b4ccc12bcc8d60637aa0e50a5ace6178762644a7a110407ec8616b2344fab135ee90dfa9dca8a7c12f4e5f207aa095c62b8291b04
6
+ metadata.gz: 2c1ef86aa53fe09e26c1e800780c673b3499a50c01b528257683d3ee91baa2f8719e9dab83cd2e07a621b8f8b0ba90e7c407bf7188b434b4cbaf8144e2f0b950
7
+ data.tar.gz: c82db879f98a32b61da80ead6d519b3259cc4ca1ad696b2c313adb70d10854d79f1db9d9efe9643417c1a16fbe821b99392b8b7e7f3e074259dc7d8ec0f55d25
data/README.md CHANGED
@@ -16,6 +16,10 @@ To install FITS:
16
16
  * Adding the full fits.sh path to your PATH (e.g., in your .bash_profile), OR
17
17
  * Changing config/initializers/sufia.rb to point to your FITS location: config.fits_path = "/<your full path>/fits.sh"
18
18
 
19
+ ### Redis 2.6
20
+
21
+ The redlock gem requires Redis >= 2.6.
22
+
19
23
 
20
24
 
21
25
  ## Installation
@@ -0,0 +1,83 @@
1
+ module CurationConcerns
2
+ # actions for a file identified by file_set_id and relation (maps to use predicate)
3
+ class FileActor
4
+ attr_reader :file_set, :relation, :user
5
+
6
+ # @param [FileSet] file_set the parent FileSet
7
+ # @param [String] relation the type/use for the file.
8
+ # @param [User] user the user to record as the Agent acting upon the file
9
+ def initialize(file_set, relation, user)
10
+ @file_set = file_set
11
+ @relation = relation
12
+ @user = user
13
+ end
14
+
15
+ # Puts the uploaded content into a staging directory. Then kicks off a
16
+ # job to characterize and create derivatives with this on disk variant.
17
+ # Simultaneously moving a preservation copy to the repostiory.
18
+ # TODO: create a job to monitor this directory and prune old files that
19
+ # have made it to the repo
20
+ # @param [File, ActionDigest::HTTP::UploadedFile, Tempfile] file the file to save in the repository
21
+ def ingest_file(file)
22
+ working_file = copy_file_to_working_directory(file, file_set_id)
23
+ mime_type = file.respond_to?(:content_type) ? file.content_type : nil
24
+ IngestFileJob.perform_later(file_set_id, working_file, mime_type, user.user_key, relation)
25
+ make_derivative(file_set.id, working_file)
26
+ true
27
+ end
28
+
29
+ def revert_to(revision_id)
30
+ repository_file = file_set.send(relation.to_sym)
31
+ repository_file.restore_version(revision_id)
32
+
33
+ return false unless file_set.save
34
+
35
+ CurationConcerns::VersioningService.create(repository_file, user)
36
+
37
+ # Retrieve a copy of the orginal file from the repository
38
+ working_file = copy_repository_resource_to_working_directory(repository_file)
39
+ make_derivative(file_set_id, working_file)
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def file_set_id
46
+ file_set.id
47
+ end
48
+
49
+ def make_derivative(file_set_id, working_file)
50
+ CharacterizeJob.perform_later(file_set_id, working_file)
51
+ end
52
+
53
+ # @param [File, ActionDispatch::Http::UploadedFile] file
54
+ # @param [String] id the identifer of the FileSet
55
+ # @return [String] path of the working file
56
+ def copy_file_to_working_directory(file, id)
57
+ file_name = file.respond_to?(:original_filename) ? file.original_filename : ::File.basename(file)
58
+ copy_stream_to_working_directory(id, file_name, file)
59
+ end
60
+
61
+ # @param [ActiveFedora::File] file the resource in the repo
62
+ # @return [String] path of the working file
63
+ def copy_repository_resource_to_working_directory(file)
64
+ copy_stream_to_working_directory(file_set_id, file.original_name, StringIO.new(file.content))
65
+ end
66
+
67
+ # @param [String] id the identifer
68
+ # @param [String] name the file name
69
+ # @param [#read] stream the stream to copy to the working directory
70
+ # @return [String] path of the working file
71
+ def copy_stream_to_working_directory(id, name, stream)
72
+ working_path = full_filename(id, name)
73
+ FileUtils.mkdir_p(File.dirname(working_path))
74
+ IO.copy_stream(stream, working_path)
75
+ working_path
76
+ end
77
+
78
+ def full_filename(id, original_name)
79
+ pair = id.scan(/..?/).first(4)
80
+ File.join(CurationConcerns.config.working_path, *pair, original_name)
81
+ end
82
+ end
83
+ end
@@ -35,46 +35,36 @@ module CurationConcerns
35
35
  yield(file_set) if block_given?
36
36
  end
37
37
 
38
- # Puts the uploaded content into a staging directory. Then kicks off a
39
- # job to characterize and create derivatives with this on disk variant.
40
- # Simultaneously moving a preservation copy to the repostiory.
41
- # TODO: create a job to monitor this directory and prune old files that
42
- # have made it to the repo
43
38
  # @param [File, ActionDigest::HTTP::UploadedFile, Tempfile] file the file uploaded by the user.
44
- def create_content(file)
45
- # Assign label and title of File Set is necessary.
39
+ # @param [String] relation ('original_file')
40
+ def create_content(file, relation = 'original_file')
41
+ # If the file set doesn't have a title or label assigned, set a default.
46
42
  file_set.label ||= file.respond_to?(:original_filename) ? file.original_filename : ::File.basename(file)
47
43
  file_set.title = [file_set.label] if file_set.title.blank?
48
44
 
49
45
  # Need to save the file_set in order to get an id
50
46
  return false unless file_set.save
51
47
 
52
- working_file = copy_file_to_working_directory(file, file_set.id)
53
- mime_type = file.respond_to?(:content_type) ? file.content_type : nil
54
- IngestFileJob.perform_later(file_set.id, working_file, mime_type, user.user_key)
55
- make_derivative(file_set.id, working_file)
48
+ FileActor.new(file_set, relation, user).ingest_file(file)
56
49
  true
57
50
  end
58
51
 
59
- def revert_content(revision_id)
60
- file_set.original_file.restore_version(revision_id)
61
-
62
- return false unless file_set.save
63
-
64
- CurationConcerns::VersioningService.create(file_set.original_file, user)
65
-
66
- # Retrieve a copy of the orginal file from the repository
67
- working_file = copy_repository_resource_to_working_directory(file_set)
68
- make_derivative(file_set.id, working_file)
69
-
70
- CurationConcerns.config.callback.run(:after_revert_content, file_set, user, revision_id)
71
- true
52
+ # @param [String] revision_id the revision to revert to
53
+ # @param [String] relation ('original_file')
54
+ def revert_content(revision_id, relation = 'original_file')
55
+ file_actor = FileActor.new(file_set, relation, user)
56
+ if file_actor.revert_to(revision_id)
57
+ CurationConcerns.config.callback.run(:after_revert_content, file_set, user, revision_id)
58
+ true
59
+ else
60
+ false
61
+ end
72
62
  end
73
63
 
74
- def update_content(file)
75
- working_file = copy_file_to_working_directory(file, file_set.id)
76
- IngestFileJob.perform_later(file_set.id, working_file, file.content_type, user.user_key)
77
- make_derivative(file_set.id, working_file)
64
+ # @param [File, ActionDigest::HTTP::UploadedFile, Tempfile] file the file uploaded by the user.
65
+ # @param [String] relation ('original_file')
66
+ def update_content(file, relation = 'original_file')
67
+ FileActor.new(file_set, relation, user).ingest_file(file)
78
68
  CurationConcerns.config.callback.run(:after_update_content, file_set, user)
79
69
  true
80
70
  end
@@ -96,42 +86,6 @@ module CurationConcerns
96
86
 
97
87
  private
98
88
 
99
- def make_derivative(file_set_id, working_file)
100
- CharacterizeJob.perform_later(file_set_id, working_file)
101
- end
102
-
103
- # @param [File, ActionDispatch::Http::UploadedFile] file
104
- # @param [String] id the identifer
105
- # @return [String] path of the working file
106
- def copy_file_to_working_directory(file, id)
107
- # file_set.label not gaurunteed to be set at this point (e.g. if called from update_content)
108
- file_set.label ||= file.respond_to?(:original_filename) ? file.original_filename : ::File.basename(file)
109
- copy_stream_to_working_directory(id, file_set.label, file)
110
- end
111
-
112
- # @param [FileSet] file_set the resource
113
- # @return [String] path of the working file
114
- def copy_repository_resource_to_working_directory(file_set)
115
- file = file_set.original_file
116
- copy_stream_to_working_directory(file_set.id, file.original_name, StringIO.new(file.content))
117
- end
118
-
119
- # @param [String] id the identifer
120
- # @param [String] name the file name
121
- # @param [#read] stream the stream to copy to the working directory
122
- # @return [String] path of the working file
123
- def copy_stream_to_working_directory(id, name, stream)
124
- working_path = full_filename(id, name)
125
- FileUtils.mkdir_p(File.dirname(working_path))
126
- IO.copy_stream(stream, working_path)
127
- working_path
128
- end
129
-
130
- def full_filename(id, original_name)
131
- pair = id.scan(/..?/).first(4)
132
- File.join(CurationConcerns.config.working_path, *pair, original_name)
133
- end
134
-
135
89
  # Takes an optional block and executes the block if the save was successful.
136
90
  # returns false if the save was unsuccessful
137
91
  def save
@@ -1,7 +1,12 @@
1
1
  class IngestFileJob < ActiveJob::Base
2
2
  queue_as :ingest
3
3
 
4
- def perform(file_set_id, filename, mime_type, user_key)
4
+ # @param [String] file_set_id
5
+ # @param [String] filename
6
+ # @param [String,NilClass] mime_type
7
+ # @param [String] user_key
8
+ # @param [String] relation ('original_file')
9
+ def perform(file_set_id, filename, mime_type, user_key, relation = 'original_file')
5
10
  file_set = FileSet.find(file_set_id)
6
11
 
7
12
  file = File.open(filename, "rb")
@@ -14,14 +19,14 @@ class IngestFileJob < ActiveJob::Base
14
19
  end
15
20
 
16
21
  # Tell AddFileToFileSet service to skip versioning because versions will be minted by VersionCommitter (called by save_characterize_and_record_committer) when necessary
17
- Hydra::Works::AddFileToFileSet.call(file_set, file, :original_file, versioning: false)
22
+ Hydra::Works::AddFileToFileSet.call(file_set, file, relation.to_sym, versioning: false)
18
23
 
19
24
  # Persist changes to the file_set
20
25
  file_set.save!
21
26
 
22
27
  # Do post file ingest actions
23
28
  user = User.find_by_user_key(user_key)
24
- CurationConcerns::VersioningService.create(file_set.original_file, user)
29
+ CurationConcerns::VersioningService.create(file_set.send(relation.to_sym), user)
25
30
  CurationConcerns.config.callback.run(:after_create_content, file_set, user)
26
31
  end
27
32
  end
@@ -38,20 +38,6 @@ module CurationConcerns
38
38
  index.as :stored_searchable
39
39
  end
40
40
 
41
- # We reserve date_uploaded for the original creation date of the record.
42
- # For example, when migrating data from a fedora3 repo to fedora4,
43
- # fedora's system created date will reflect the date when the record
44
- # was created in fedora4, but the date_uploaded will preserve the
45
- # original creation date from the old repository.
46
- property :date_uploaded, predicate: ::RDF::Vocab::DC.dateSubmitted, multiple: false do |index|
47
- index.type :date
48
- index.as :stored_sortable
49
- end
50
-
51
- property :date_modified, predicate: ::RDF::Vocab::DC.modified, multiple: false do |index|
52
- index.type :date
53
- index.as :stored_sortable
54
- end
55
41
  property :subject, predicate: ::RDF::Vocab::DC.subject do |index|
56
42
  index.as :stored_searchable, :facetable
57
43
  end
@@ -10,7 +10,7 @@ module CurationConcerns
10
10
  include CurationConcerns::Permissions
11
11
 
12
12
  included do
13
- validates :title, presence: true
13
+ validates_with HasOneTitleValidator
14
14
  end
15
15
 
16
16
  def to_s
@@ -6,9 +6,25 @@ module CurationConcerns
6
6
  property :depositor, predicate: ::RDF::URI.new('http://id.loc.gov/vocabulary/relators/dpt'), multiple: false do |index|
7
7
  index.as :symbol, :stored_searchable
8
8
  end
9
+
9
10
  property :title, predicate: ::RDF::Vocab::DC.title do |index|
10
11
  index.as :stored_searchable, :facetable
11
12
  end
13
+
14
+ # We reserve date_uploaded for the original creation date of the record.
15
+ # For example, when migrating data from a fedora3 repo to fedora4,
16
+ # fedora's system created date will reflect the date when the record
17
+ # was created in fedora4, but the date_uploaded will preserve the
18
+ # original creation date from the old repository.
19
+ property :date_uploaded, predicate: ::RDF::Vocab::DC.dateSubmitted, multiple: false do |index|
20
+ index.type :date
21
+ index.as :stored_sortable
22
+ end
23
+
24
+ property :date_modified, predicate: ::RDF::Vocab::DC.modified, multiple: false do |index|
25
+ index.type :date
26
+ index.as :stored_sortable
27
+ end
12
28
  end
13
29
  end
14
30
  end
@@ -0,0 +1,8 @@
1
+ # validates that the title has at least one title
2
+ class HasOneTitleValidator < ActiveModel::Validator
3
+ def validate(record)
4
+ if record.title.reject(&:empty?).empty?
5
+ record.errors[:title] << "You must provide a title"
6
+ end
7
+ end
8
+ end
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_dependency 'active_attr'
23
- spec.add_dependency 'hydra-collections', '~> 7.0'
23
+ spec.add_dependency 'hydra-collections', '~> 8.0'
24
24
  spec.add_dependency 'hydra-head', '~> 9.3'
25
25
  spec.add_dependency 'hydra-works', '~> 0.7'
26
26
  spec.add_dependency 'active_fedora-noid', '~> 1.0'
@@ -56,11 +56,24 @@ module CurationConcerns
56
56
  @fits_message_length ||= 5
57
57
  end
58
58
 
59
- attr_accessor :temp_file_base, :enable_local_ingest, :analytic_start_date,
60
- :fits_to_desc_mapping, :max_days_between_audits,
61
- :resource_types, :resource_types_to_schema,
62
- :permission_levels, :owner_permission_levels, :analytics,
63
- :display_microdata, :microdata_default_type
59
+ attr_accessor :temp_file_base, :enable_local_ingest,
60
+ :analytics, :analytic_start_date
61
+
62
+ attr_writer :display_microdata
63
+ def display_microdata
64
+ return @display_microdata unless @display_microdata.nil?
65
+ @display_microdata = true
66
+ end
67
+
68
+ attr_writer :microdata_default_type
69
+ def microdata_default_type
70
+ @microdata_default_type ||= 'http://schema.org/CreativeWork'
71
+ end
72
+
73
+ attr_writer :max_days_between_audits
74
+ def max_days_between_audits
75
+ @max_days_between_audits ||= 7
76
+ end
64
77
 
65
78
  attr_writer :enable_noids
66
79
  def enable_noids
@@ -4,7 +4,6 @@ module CurationConcerns
4
4
  config.autoload_paths += %W(
5
5
  #{config.root}/app/actors/concerns
6
6
  #{config.root}/lib/curation_concerns
7
- #{config.root}/app/models/datastreams
8
7
  )
9
8
 
10
9
  initializer 'requires' do
@@ -1,5 +1,5 @@
1
1
  module CurationConcerns
2
2
  module Models
3
- VERSION = "0.8.0".freeze
3
+ VERSION = "0.9.0".freeze
4
4
  end
5
5
  end
@@ -1,55 +1,18 @@
1
1
  CurationConcerns.configure do |config|
2
- config.fits_to_desc_mapping = {
3
- file_title: :title,
4
- file_author: :creator
5
- }
6
-
7
- config.max_days_between_audits = 7
8
-
9
- config.resource_types = {
10
- 'Article' => 'Article',
11
- 'Audio' => 'Audio',
12
- 'Book' => 'Book',
13
- 'Capstone Project' => 'Capstone Project',
14
- 'Conference Proceeding' => 'Conference Proceeding',
15
- 'Dataset' => 'Dataset',
16
- 'Dissertation' => 'Dissertation',
17
- 'Image' => 'Image',
18
- 'Journal' => 'Journal',
19
- 'Map or Cartographic Material' => 'Map or Cartographic Material',
20
- 'Masters Thesis' => 'Masters Thesis',
21
- 'Part of Book' => 'Part of Book',
22
- 'Poster' => 'Poster',
23
- 'Presentation' => 'Presentation',
24
- 'Project' => 'Project',
25
- 'Report' => 'Report',
26
- 'Research Paper' => 'Research Paper',
27
- 'Software or Program Code' => 'Software or Program Code',
28
- 'Video' => 'Video',
29
- 'Other' => 'Other'
30
- }
31
-
32
- config.display_microdata = true
33
- config.microdata_default_type = 'http://schema.org/CreativeWork'
34
-
35
- config.resource_types_to_schema = config.resource_types.map do |k, v|
36
- [k, I18n.t("curation_concerns.schema_org.resource_type.#{v}", default: config.microdata_default_type)]
37
- end.to_h
38
-
39
- config.permission_levels = {
40
- 'Choose Access' => 'none',
41
- 'View/Download' => 'read',
42
- 'Edit' => 'edit'
43
- }
44
-
45
- config.owner_permission_levels = {
46
- 'Edit' => 'edit'
47
- }
2
+ # Should schema.org microdata be displayed?
3
+ # config.display_microdata = true
4
+
5
+ # What default microdata type should be used if a more appropriate
6
+ # type can not be found in the locale file?
7
+ # config.microdata_default_type = 'http://schema.org/CreativeWork'
8
+
9
+ # How frequently should a file be audited.
10
+ # Note: In CurationConcerns you must trigger the FileSetAuditService manually.
11
+ # config.max_days_between_audits = 7
48
12
 
49
13
  # Enable displaying usage statistics in the UI
50
- # Defaults to FALSE
51
14
  # Requires a Google Analytics id and OAuth2 keyfile. See README for more info
52
- config.analytics = false
15
+ # config.analytics = false
53
16
 
54
17
  # Specify a Google Analytics tracking ID to gather usage statistics
55
18
  # config.google_analytics_id = 'UA-99999999-1'
@@ -60,12 +23,15 @@ CurationConcerns.configure do |config|
60
23
  # Where to store tempfiles, leave blank for the system temp directory (e.g. /tmp)
61
24
  # config.temp_file_base = '/home/developer1'
62
25
 
63
- # Specify the form of hostpath to be used in Endnote exports
64
- # config.persistent_hostpath = 'http://localhost/files/'
65
-
66
26
  # Location on local file system where derivatives will be stored.
27
+ # If you use a multi-server architecture, this MUST be a shared volume.
67
28
  # config.derivatives_path = File.join(Rails.root, 'tmp', 'derivatives')
68
29
 
30
+ # Location on local file system where uploaded files will be staged
31
+ # prior to being ingested into the repository or having derivatives generated.
32
+ # If you use a multi-server architecture, this MUST be a shared volume.
33
+ # config.working_path = File.join(Rails.root, 'tmp', 'uploads')
34
+
69
35
  # If you have ffmpeg installed and want to transcode audio and video uncomment this line
70
36
  # config.enable_ffmpeg = true
71
37
 
@@ -77,6 +43,7 @@ CurationConcerns.configure do |config|
77
43
  # config.noid_template = ".reeddeeddk"
78
44
 
79
45
  # Store identifier minter's state in a file for later replayability
46
+ # If you use a multi-server architecture, this MUST be on a shared volume.
80
47
  # config.minter_statefile = '/tmp/minter-state'
81
48
 
82
49
  # Specify the prefix for Redis keys:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curation_concerns-models
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-16 00:00:00.000000000 Z
11
+ date: 2016-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_attr
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '7.0'
33
+ version: '8.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '7.0'
40
+ version: '8.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: hydra-head
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -180,6 +180,7 @@ files:
180
180
  - app/actors/concerns/curation_concerns/manages_embargoes_actor.rb
181
181
  - app/actors/curation_concerns/base_actor.rb
182
182
  - app/actors/curation_concerns/embargo_actor.rb
183
+ - app/actors/curation_concerns/file_actor.rb
183
184
  - app/actors/curation_concerns/file_set_actor.rb
184
185
  - app/actors/curation_concerns/lease_actor.rb
185
186
  - app/actors/curation_concerns/work_actor_behavior.rb
@@ -203,7 +204,6 @@ files:
203
204
  - app/models/concerns/curation_concerns/file_set/full_text_indexing.rb
204
205
  - app/models/concerns/curation_concerns/file_set/indexing.rb
205
206
  - app/models/concerns/curation_concerns/file_set/querying.rb
206
- - app/models/concerns/curation_concerns/file_set/versions.rb
207
207
  - app/models/concerns/curation_concerns/file_set_behavior.rb
208
208
  - app/models/concerns/curation_concerns/has_representative.rb
209
209
  - app/models/concerns/curation_concerns/human_readable_type.rb
@@ -234,6 +234,7 @@ files:
234
234
  - app/services/curation_concerns/thumbnail_path_service.rb
235
235
  - app/services/curation_concerns/time_service.rb
236
236
  - app/services/curation_concerns/versioning_service.rb
237
+ - app/validators/has_one_title_validator.rb
237
238
  - config/locales/curation_concerns.en.yml
238
239
  - curation_concerns-models.gemspec
239
240
  - lib/curation_concerns/configuration.rb
@@ -282,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
282
283
  version: '0'
283
284
  requirements: []
284
285
  rubyforge_project:
285
- rubygems_version: 2.4.5.1
286
+ rubygems_version: 2.5.1
286
287
  signing_key:
287
288
  specification_version: 4
288
289
  summary: Simple institutional repository models for Hydra
@@ -1,15 +0,0 @@
1
- module CurationConcerns
2
- module FileSet
3
- module Versions
4
- @@count = 0
5
- def record_version_committer(user)
6
- version = latest_version
7
- # content datastream not (yet?) present
8
- return if version.nil?
9
- @@count += 1
10
- # raise "Recording #{@@count} #{version.uri} for #{user.user_key}" if @@count == 3
11
- VersionCommitter.create(version_id: version.uri, committer_login: user.user_key)
12
- end
13
- end
14
- end
15
- end