Class: Project

Inherits:
ApplicationRecord show all
Defined in:
app/models/project.rb

Constant Summary collapse

PENDING_STATUS =

Valid project status described in ADR 7 See ‘architecture-decisions/0007-valid-project-statuses.md`

"pending"
APPROVED_STATUS =
"approved"
ACTIVE_STATUS =
"active"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.approved_projectsObject



174
175
176
# File 'app/models/project.rb', line 174

def self.approved_projects
  Project.where("mediaflux_id IS NOT NULL")
end

.data_user_projects(user) ⇒ Object



178
179
180
181
182
183
184
# File 'app/models/project.rb', line 178

def self.data_user_projects(user)
  # See https://scalegrid.io/blog/using-jsonb-in-postgresql-how-to-effectively-store-index-json-data-in-postgresql/
  # for information on the @> operator
  query_ro = '{"data_user_read_only":["' + user + '"]}'
  query_rw = '{"data_user_read_write":["' + user + '"]}'
  Project.where("(metadata_json @> ? :: jsonb) OR (metadata_json @> ? :: jsonb)", query_ro, query_rw)
end

.default_storage_capacityObject



256
257
258
# File 'app/models/project.rb', line 256

def self.default_storage_capacity
  "0 GB"
end

.default_storage_unitObject



238
239
240
# File 'app/models/project.rb', line 238

def self.default_storage_unit
  "KB"
end

.default_storage_usageObject



242
243
244
# File 'app/models/project.rb', line 242

def self.default_storage_usage
  "0 #{default_storage_unit}"
end

.managed_projects(manager) ⇒ Object



166
167
168
# File 'app/models/project.rb', line 166

def self.managed_projects(manager)
  Project.where("metadata_json->>'data_manager' = ?", manager)
end

.pending_projectsObject



170
171
172
# File 'app/models/project.rb', line 170

def self.pending_projects
  Project.where("mediaflux_id IS NULL")
end

.safe_name(name) ⇒ Object

Ensure that the project directory is a valid path

Examples:

Project.safe_name("My Project") # => "My-Project"


331
332
333
334
# File 'app/models/project.rb', line 331

def self.safe_name(name)
  # only alphanumeric characters
  name.strip.gsub(/[^A-Za-z\d]/, "-")
end


162
163
164
# File 'app/models/project.rb', line 162

def self.sponsored_projects(sponsor)
  Project.where("metadata_json->>'data_sponsor' = ?", sponsor)
end

.users_projects(user) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'app/models/project.rb', line 143

def self.users_projects(user)
  # See https://scalegrid.io/blog/using-jsonb-in-postgresql-how-to-effectively-store-index-json-data-in-postgresql/
  # for information on the @> operator
  uid = user.uid
  query_ro = '{"data_user_read_only":["' + uid + '"]}'
  query_rw = '{"data_user_read_write":["' + uid + '"]}'
  query = "(metadata_json @> ? :: jsonb) OR (metadata_json @> ? :: jsonb)"
  args = [query_ro, query_rw]
  if user.eligible_sponsor?
    query += "OR (metadata_json->>'data_sponsor' = ?)"
    args << uid
  end
  if user.eligible_manager?
    query += "OR (metadata_json->>'data_manager' = ?)"
    args << uid
  end
  Project.where( query, *args)
end

Instance Method Details

#activate!(collection_id:, current_user:) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/models/project.rb', line 52

def activate!(collection_id:, current_user:)
  response = Mediaflux::AssetMetadataRequest.new(session_token: current_user.mediaflux_session, id: collection_id)
   = response. # get the metadata of the collection from mediaflux

  return unless [:collection] == true # If the collection id exists

  # check if the project doi in rails matches the project doi in mediaflux
  return unless [:project_id] == self..project_id

  # activate a project by setting the status to 'active'
  self..status = Project::ACTIVE_STATUS

  # also read in the actual project directory
  self..project_directory = [:project_directory]
  self.save!

  ProvenanceEvent.generate_active_events(project: self, user: current_user)
end

#approve!(mediaflux_id:, current_user:) ⇒ Object



35
36
37
38
39
40
41
42
43
44
# File 'app/models/project.rb', line 35

def approve!(mediaflux_id:, current_user:)
  self.mediaflux_id = mediaflux_id
  self..status = Project::APPROVED_STATUS
  self.save!

  # create two provenance events, one for approving the project and
    # another for changing the status of the project
  ProvenanceEvent.generate_approval_events(project: self, user: current_user)

end

#asset_count(session_id:) ⇒ Object



233
234
235
236
# File 'app/models/project.rb', line 233

def asset_count(session_id:)
  values = (session_id:)
  values.fetch(:total_file_count, 0)
end

#create!(initial_metadata:, user:) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'app/models/project.rb', line 18

def create!(initial_metadata:, user:)
  self. = 
  if self.valid?
    if .project_id == ProjectMetadata::DOI_NOT_MINTED
      self.draft_doi(user: user)
      self.save!
      ProvenanceEvent.generate_submission_events(project: self, user: user)
    else
      self.save!
    end
    # return doi
    self..project_id
  else
    nil
  end
end

#created_by_userObject



196
197
198
# File 'app/models/project.rb', line 196

def created_by_user
  User.find_by(uid: .created_by)
end

#departmentsObject



101
102
103
104
# File 'app/models/project.rb', line 101

def departments
  unsorted = .departments || []
  unsorted.sort
end

#draft_doi(user: nil) ⇒ Object



71
72
73
74
# File 'app/models/project.rb', line 71

def draft_doi(user: nil)
  puldatacite = PULDatacite.new
  self..project_id = puldatacite.draft_doi
end

#file_list(session_id:, size: 10) ⇒ Object

Fetches the first n files



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/project.rb', line 277

def file_list(session_id:, size: 10)
  return { files: [] } if mediaflux_id.nil?

  query_req = Mediaflux::QueryRequest.new(session_token: session_id, collection: mediaflux_id, deep_search: true, aql_query: "type!='application/arc-asset-collection'")
  iterator_id = query_req.result

  iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: size)
  results = iterator_req.result

  # Destroy _after_ fetching the first set of results from iterator_req.
  # This call is required since it possible that we have read less assets than
  # what the collection has but we are done with the iterator.
  Mediaflux::IteratorDestroyRequest.new(session_token: session_id, iterator: iterator_id).resolve

  results
end

#file_list_to_file(session_id:, filename:) ⇒ Object

Fetches the entire file list to a file



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/project.rb', line 295

def file_list_to_file(session_id:, filename:)
  return { files: [] } if mediaflux_id.nil?

  query_req = Mediaflux::QueryRequest.new(session_token: session_id, collection: mediaflux_id, deep_search: true,  aql_query: "type!='application/arc-asset-collection'")
  iterator_id = query_req.result

  start_time = Time.zone.now
  prefix = "file_list_to_file #{session_id[0..7]} #{self..project_id}"
  log_elapsed(start_time, prefix, "STARTED")

  File.open(filename, "w") do |file|
    page_number = 0
    # file header
    file.write("ID, PATH, NAME, COLLECTION?, LAST_MODIFIED, SIZE\r\n")
    loop do
      iterator_start_time = Time.zone.now
      page_number += 1
      iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: 1000)
      iterator_resp = iterator_req.result
      log_elapsed(iterator_start_time, prefix, "FETCHED page #{page_number} from iterator")
      lines = files_from_iterator(iterator_resp)
      file.write(lines.join("\r\n") + "\r\n")
      break if iterator_resp[:complete] || iterator_req.error?
    end
    log_elapsed(start_time, prefix, "ENDED")
  end

  # Destroy _after_ fetching the results from iterator_req
  # This call is technically not necessary since Mediaflux automatically deletes the iterator
  # once we have ran through it and by now we have. But it does not hurt either.
  Mediaflux::IteratorDestroyRequest.new(session_token: session_id, iterator: iterator_id).resolve
end

#in_mediaflux?Boolean

Returns:

  • (Boolean)


139
140
141
# File 'app/models/project.rb', line 139

def in_mediaflux?
  mediaflux_id.present?
end

#mediaflux_documentNokogiri::XML::Document

Returns the Mediaflux XML document for this project.

Returns:

  • (Nokogiri::XML::Document)

    the Mediaflux XML document for this project



205
206
207
# File 'app/models/project.rb', line 205

def mediaflux_document
  ProjectMediaflux.document(project: self)
end

#mediaflux_meta_elementNokogiri::XML::Element

Returns the <meta> element from the Mediaflux XML document.

Returns:

  • (Nokogiri::XML::Element)

    the <meta> element from the Mediaflux XML document



210
211
212
213
214
215
216
217
218
# File 'app/models/project.rb', line 210

def mediaflux_meta_element
  doc = mediaflux_document.clone
  # Remove the namespaces in order to simplify the XPath query
  doc.remove_namespaces!
  elements = doc.xpath("/request/service/args/meta")
  raise("Failed to extract the <meta> element found in the Mediaflux XML document for project #{self.id}") if elements.empty?

  elements.first
end

#mediaflux_meta_xmlString

Returns XML representation of the <meta> element.

Returns:

  • (String)

    XML representation of the <meta> element



221
222
223
# File 'app/models/project.rb', line 221

def mediaflux_meta_xml
  mediaflux_meta_element.to_xml
end

#mediaflux_metadata(session_id:) ⇒ Object



225
226
227
228
229
230
231
# File 'app/models/project.rb', line 225

def (session_id:)
  @mediaflux_metadata ||= begin
    accum_req = Mediaflux::AssetMetadataRequest.new(session_token: session_id, id: mediaflux_id)
    accum_req.
  end
  @mediaflux_metadata
end

#metadataObject

Ideally this method should return a ProjectMetadata object (like ‘metadata_model` does) but we’ll keep them both while we are refactoring the code so that we don’t break everything at once since ‘metadata` is used everywhere.



79
80
81
# File 'app/models/project.rb', line 79

def 
  @metadata_hash = ( || {}).with_indifferent_access
end

#metadata=(metadata_model) ⇒ Object



91
92
93
94
95
# File 'app/models/project.rb', line 91

def metadata=()
  # Convert our metadata to a hash so it can be saved on our JSONB field
   = JSON.parse(.to_json)
  self. = 
end

#metadata_modelObject



83
84
85
# File 'app/models/project.rb', line 83

def 
  @metadata_model ||= ProjectMetadata.new_from_hash(self.)
end

#metadata_model=(new_metadata_model) ⇒ Object



87
88
89
# File 'app/models/project.rb', line 87

def ()
  @metadata_model = 
end

#pending?Boolean

Returns:

  • (Boolean)


135
136
137
# File 'app/models/project.rb', line 135

def pending?
  status == PENDING_STATUS
end

#project_directoryObject



106
107
108
109
110
111
112
113
114
# File 'app/models/project.rb', line 106

def project_directory
  return nil if .project_directory.nil?
  dirname, basename = project_directory_pathname.split
  if (dirname.relative?)
    "#{Mediaflux::Connection.root_namespace}/#{safe_name(.project_directory)}"
  else
    project_directory_pathname.to_s
  end
end

#project_directory_parent_pathObject



116
117
118
119
120
121
122
123
124
# File 'app/models/project.rb', line 116

def project_directory_parent_path
  return Mediaflux::Connection.root_namespace if .project_directory.nil?
  dirname  = project_directory_pathname.dirname
  if (dirname.relative?)
    Mediaflux::Connection.root_namespace
  else
    dirname.to_s
  end
end

#project_directory_shortObject



126
127
128
129
# File 'app/models/project.rb', line 126

def project_directory_short
  return nil if .project_directory.nil?
  project_directory_pathname.basename.to_s
end

#reloadObject



46
47
48
49
50
# File 'app/models/project.rb', line 46

def reload
  super
  @metadata_model = ProjectMetadata.new_from_hash(self.)
  self
end

#save_in_mediaflux(user:) ⇒ Object



192
193
194
# File 'app/models/project.rb', line 192

def save_in_mediaflux(user:)
  ProjectMediaflux.save(project: self, user: user)
end

#statusObject



131
132
133
# File 'app/models/project.rb', line 131

def status
  .status
end

#storage_capacity(session_id:) ⇒ Object



260
261
262
263
264
265
266
267
268
# File 'app/models/project.rb', line 260

def storage_capacity(session_id:)
  values = (session_id:)
  quota_value = values.fetch(:quota_allocation, '') #if quota does not exist, set value to an empty string
  if quota_value.blank?
    return self.class.default_storage_capacity
  else
    return quota_value
  end
end

#storage_capacity_raw(session_id:) ⇒ Object



270
271
272
273
274
# File 'app/models/project.rb', line 270

def storage_capacity_raw(session_id:)
  values = (session_id:)
  quota_value = values.fetch(:quota_allocation_raw, 0) #if quota does not exist, set value to 0
  quota_value
end

#storage_usage(session_id:) ⇒ Object



246
247
248
249
# File 'app/models/project.rb', line 246

def storage_usage(session_id:)
  values = (session_id:)
  values.fetch(:quota_used, self.class.default_storage_usage) # if the storage is empty use the default
end

#storage_usage_raw(session_id:) ⇒ Object



251
252
253
254
# File 'app/models/project.rb', line 251

def storage_usage_raw(session_id:)
  values = (session_id:)
  values.fetch(:quota_used_raw, 0) # if the storage raw is empty use zero
end

#titleObject



97
98
99
# File 'app/models/project.rb', line 97

def title
  self..title
end

#to_xmlObject



200
201
202
# File 'app/models/project.rb', line 200

def to_xml
  ProjectMediaflux.xml_payload(project: self)
end

#user_has_access?(user:) ⇒ Boolean

Returns:

  • (Boolean)


186
187
188
189
190
# File 'app/models/project.rb', line 186

def user_has_access?(user:)
  return true if user.eligible_sysadmin?
  .data_sponsor == user.uid || .data_manager == user.uid ||
  .data_user_read_only.include?(user.uid) || .data_user_read_write.include?(user.uid)
end