• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / tigerdata-app / 07f4d952-7d18-4551-a848-48bb16200c76

24 Oct 2025 03:07PM UTC coverage: 88.171% (-3.0%) from 91.202%
07f4d952-7d18-4551-a848-48bb16200c76

Pull #2096

circleci

carolyncole
Adding more details to the request index page
Pull Request #2096: Adding more details to the request index page

2 of 6 new or added lines in 1 file covered. (33.33%)

651 existing lines in 39 files now uncovered.

2661 of 3018 relevant lines covered (88.17%)

285.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.33
/app/models/project.rb
1
# frozen_string_literal: true
2
class Project < ApplicationRecord
4✔
3

4
  validates_with ProjectValidator
4✔
5
  has_many :provenance_events, dependent: :destroy
4✔
6
  before_save do |project|
4✔
7
    # Ensure that the metadata JSONB postgres field is persisted properly
8
    project.metadata = project.metadata_model
565✔
9
  end
10

11
  # Valid project status described in ADR 7
12
  # See `architecture-decisions/0007-valid-project-statuses.md`
13
  APPROVED_STATUS = "approved"
4✔
14
  ACTIVE_STATUS = "active"
4✔
15

16
  delegate :to_json, to: :metadata_json # field in the database
4✔
17

18
  def create!(initial_metadata:, user:)
4✔
19
    self.metadata_model = initial_metadata
1✔
20
    if self.valid?
1✔
21
      if initial_metadata.project_id == ProjectMetadata::DOI_NOT_MINTED
×
22
        self.draft_doi(user: user)
×
23
        self.save!
×
24
        ProvenanceEvent.generate_submission_events(project: self, user: user)
×
25
      else
26
        self.save!
×
27
      end
28
      # return doi
29
      self.metadata_model.project_id
×
30
    else
31
      nil
32
    end
33
  end
34

35
  def activate(current_user:)
4✔
36
    raise StandardError.new("Only approved projects can be activated") if self.status != Project::APPROVED_STATUS
83✔
37
    metadata_request = Mediaflux::AssetMetadataRequest.new(session_token: current_user.mediaflux_session, id: self.mediaflux_id)
83✔
38
    metadata_request.resolve
83✔
39
    raise metadata_request.response_error if metadata_request.error?
83✔
40
    if self.title == metadata_request.metadata[:title]
83✔
41
      self.metadata_model.status = Project::ACTIVE_STATUS
83✔
42
      self.save!
83✔
43
    else
44
      raise StandardError.new("Title mismatch: #{self.title} != #{metadata_request.metadata[:title]}")
×
45
    end
46
  end
47

48
  def draft_doi(user: nil)
4✔
49
    puldatacite = PULDatacite.new
85✔
50
    self.metadata_model.project_id = puldatacite.draft_doi
85✔
51
  end
52

53
  # Ideally this method should return a ProjectMetadata object (like `metadata_model` does)
54
  # but we'll keep them both while we are refactoring the code so that we don't break
55
  # everything at once since `metadata` is used everywhere.
56
  def metadata
4✔
57
    @metadata_hash = (metadata_json || {}).with_indifferent_access
351✔
58
  end
59

60
  def metadata_model
4✔
61
    @metadata_model ||= ProjectMetadata.new_from_hash(self.metadata)
6,699✔
62
  end
63

64
  def metadata_model=(new_metadata_model)
4✔
65
    @metadata_model = new_metadata_model
219✔
66
  end
67

68
  def metadata=(metadata_model)
4✔
69
    # Convert our metadata to a hash so it can be saved on our JSONB field
70
    metadata_hash = JSON.parse(metadata_model.to_json)
1,181✔
71
    self.metadata_json = metadata_hash
1,181✔
72
  end
73

74
  def title
4✔
75
    self.metadata_model.title
451✔
76
  end
77

78
  def departments
4✔
UNCOV
79
    unsorted = metadata_model.departments || []
11✔
UNCOV
80
    unsorted.sort
11✔
81
  end
82

83
  def project_directory
4✔
84
    metadata_model.project_directory || ""
162✔
85
  end
86

87
  def project_directory_short
4✔
88
    project_directory
10✔
89
  end
90

91
  def status
4✔
92
    metadata_model.status
467✔
93
  end
94

95
  def in_mediaflux?
4✔
96
    mediaflux_id.present?
22✔
97
  end
98

99
  def self.users_projects(user)
4✔
100
    # See https://scalegrid.io/blog/using-jsonb-in-postgresql-how-to-effectively-store-index-json-data-in-postgresql/
101
    # for information on the @> operator
102
    uid = user.uid
66✔
103
    query_ro = '{"data_user_read_only":["' + uid + '"]}'
66✔
104
    query_rw = '{"data_user_read_write":["' + uid + '"]}'
66✔
105
    query = "(metadata_json @> ? :: jsonb) OR (metadata_json @> ? :: jsonb) OR (metadata_json->>'data_sponsor' = ?) OR (metadata_json->>'data_manager' = ?)"
66✔
106
    args = [query_ro, query_rw, uid, uid]
66✔
107
    Project.where( query, *args)
66✔
108
  end
109

110
  def self.sponsored_projects(sponsor)
4✔
111
    Project.where("metadata_json->>'data_sponsor' = ?", sponsor)
×
112
  end
113

114
  def self.managed_projects(manager)
4✔
115
    Project.where("metadata_json->>'data_manager' = ?", manager)
×
116
  end
117

118
  def self.all_projects
4✔
119
    Project.all
2✔
120
  end
121

122
  def user_has_access?(user:)
4✔
123
    return true if user.eligible_sysadmin?
43✔
124
    metadata_model.data_sponsor == user.uid || metadata_model.data_manager == user.uid ||
35✔
125
    metadata_model.data_user_read_only.include?(user.uid) || metadata_model.data_user_read_write.include?(user.uid)
126
  end
127

128
  def created_by_user
4✔
129
    User.find_by(uid: metadata_model.created_by)
×
130
  end
131

132
  def to_xml
4✔
UNCOV
133
    ProjectShowPresenter.new(self).to_xml
×
134
  end
135

136
  # @return [String] XML representation of the <meta> element
137
  def mediaflux_meta_xml(user:)
4✔
138
    doc = ProjectMediaflux.document(project: self, user: user)
1✔
139
    doc.xpath("/response/reply/result/asset/meta").to_s
1✔
140
  end
141

142
  def mediaflux_metadata(session_id:)
4✔
143
    @mediaflux_metadata ||= begin
729✔
144
      accum_req = Mediaflux::AssetMetadataRequest.new(session_token: session_id, id: mediaflux_id)
126✔
145
      accum_req.metadata
126✔
146
    end
147
    @mediaflux_metadata
729✔
148
  end
149

150
  def asset_count(session_id:)
4✔
151
    values = mediaflux_metadata(session_id:)
27✔
152
    values.fetch(:total_file_count, 0)
27✔
153
  end
154

155
  def self.default_storage_unit
4✔
156
    "KB"
236✔
157
  end
158

159
  def self.default_storage_usage
4✔
160
    "0 #{default_storage_unit}"
236✔
161
  end
162

163
  def storage_usage(session_id:)
4✔
164
    values = mediaflux_metadata(session_id:)
236✔
165
    values.fetch(:quota_used, self.class.default_storage_usage) # if the storage is empty use the default
236✔
166
  end
167

168
  def storage_usage_raw(session_id:)
4✔
169
    values = mediaflux_metadata(session_id:)
43✔
170
    values.fetch(:quota_used_raw, 0) # if the storage raw is empty use zero
43✔
171
  end
172

173
  def self.default_storage_capacity
4✔
174
    "0 GB"
193✔
175
  end
176

177
  def storage_capacity(session_id:)
4✔
178
    values = mediaflux_metadata(session_id:)
236✔
179
    quota_value = values.fetch(:quota_allocation, '') #if quota does not exist, set value to an empty string
236✔
180
    if quota_value.blank?
236✔
181
      return self.class.default_storage_capacity
193✔
182
    else
UNCOV
183
      return quota_value
43✔
184
    end
185
  end
186

187
  def storage_capacity_raw(session_id:)
4✔
UNCOV
188
    values = mediaflux_metadata(session_id:)
187✔
UNCOV
189
    quota_value = values.fetch(:quota_allocation_raw, 0) #if quota does not exist, set value to 0
187✔
UNCOV
190
    quota_value
187✔
191
  end
192

193
  # Fetches the first n files
194
  def file_list(session_id:, size: 10)
4✔
195
    return { files: [] } if mediaflux_id.nil?
29✔
196

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

UNCOV
200
    iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: size)
24✔
UNCOV
201
    results = iterator_req.result
24✔
202

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

UNCOV
208
    results
24✔
209
  end
210

211
  # Fetches the entire file list to a file
212
  def file_list_to_file(session_id:, filename:)
4✔
UNCOV
213
    return { files: [] } if mediaflux_id.nil?
5✔
214

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

UNCOV
218
    start_time = Time.zone.now
4✔
UNCOV
219
    prefix = "file_list_to_file #{session_id[0..7]} #{self.metadata_model.project_id}"
4✔
UNCOV
220
    log_elapsed(start_time, prefix, "STARTED")
4✔
221

UNCOV
222
    File.open(filename, "w") do |file|
4✔
UNCOV
223
      page_number = 0
4✔
224
      # file header
UNCOV
225
      file.write("ID, PATH, NAME, COLLECTION?, LAST_MODIFIED, SIZE\r\n")
4✔
UNCOV
226
      loop do
4✔
UNCOV
227
        iterator_start_time = Time.zone.now
4✔
UNCOV
228
        page_number += 1
4✔
UNCOV
229
        iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: 1000)
4✔
UNCOV
230
        iterator_resp = iterator_req.result
4✔
UNCOV
231
        log_elapsed(iterator_start_time, prefix, "FETCHED page #{page_number} from iterator")
4✔
UNCOV
232
        lines = files_from_iterator(iterator_resp)
4✔
UNCOV
233
        file.write(lines.join("\r\n") + "\r\n")
4✔
UNCOV
234
        break if iterator_resp[:complete] || iterator_req.error?
4✔
235
      end
UNCOV
236
      log_elapsed(start_time, prefix, "ENDED")
4✔
237
    end
238

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

245

246
  private
4✔
247

248
    def files_from_iterator(iterator_resp)
4✔
UNCOV
249
      lines = []
4✔
UNCOV
250
      iterator_resp[:files].each do |asset|
4✔
251
        lines << "#{asset.id}, #{asset.path_only}, #{asset.name}, #{asset.collection}, #{asset.last_modified}, #{asset.size}"
16✔
252
      end
UNCOV
253
      lines
4✔
254
    end
255

256
    def project_directory_pathname
4✔
257
      # allow the directory to be modified by changes in the metadata_json
258
      @project_directory_pathname = nil if @original_directory.present? && @original_directory != metadata_model.project_directory
×
259

260
      @project_directory_pathname ||= begin
×
261
        @original_directory = metadata_model.project_directory
×
262
        Pathname.new(@original_directory)
×
263
      end
264
    end
265

266
    # Ensure that the project directory is a valid path
267
    def safe_directory(directory)
4✔
268
      Project.safe_directory(directory)
×
269
    end
270

271
    def log_elapsed(start_time, prefix, message)
4✔
UNCOV
272
      elapsed_time = Time.zone.now - start_time
12✔
UNCOV
273
      timing_info = "#{format('%.2f', elapsed_time)} s"
12✔
UNCOV
274
      Rails.logger.info "#{prefix}: #{message}, #{timing_info}"
12✔
275
    end
276
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc