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

pulibrary / tigerdata-app / 62d2653f-d28c-4b7b-bbf1-1e4879e08c0c

05 Nov 2025 06:00PM UTC coverage: 91.5% (+0.03%) from 91.467%
62d2653f-d28c-4b7b-bbf1-1e4879e08c0c

push

circleci

web-flow
upgrade mflux_dev to v0.18.0 (#2144)

2831 of 3094 relevant lines covered (91.5%)

988.81 hits per line

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

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

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

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

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

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

35
  def activate(current_user:)
6✔
36
    raise StandardError.new("Only approved projects can be activated") if self.status != Project::APPROVED_STATUS
458✔
37
    metadata_request = Mediaflux::AssetMetadataRequest.new(session_token: current_user.mediaflux_session, id: self.mediaflux_id)
458✔
38
    metadata_request.resolve
458✔
39
    raise metadata_request.response_error if metadata_request.error?
458✔
40
    if self.title == metadata_request.metadata[:title]
458✔
41
      self.metadata_model.status = Project::ACTIVE_STATUS
458✔
42
      self.save!
458✔
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)
6✔
49
    puldatacite = PULDatacite.new
474✔
50
    self.metadata_model.project_id = puldatacite.draft_doi
474✔
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
6✔
57
    @metadata_hash = (metadata_json || {}).with_indifferent_access
1,094✔
58
  end
59

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

64
  def metadata_model=(new_metadata_model)
6✔
65
    @metadata_model = new_metadata_model
320✔
66
  end
67

68
  def metadata=(metadata_model)
6✔
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)
4,620✔
71
    self.metadata_json = metadata_hash
4,620✔
72
  end
73

74
  def title
6✔
75
    self.metadata_model.title
946✔
76
  end
77

78
  def departments
6✔
79
    unsorted = metadata_model.departments || []
30✔
80
    unsorted.sort
30✔
81
  end
82

83
  def project_directory
6✔
84
    metadata_model.project_directory || ""
358✔
85
  end
86

87
  def project_directory_short
6✔
88
    project_directory
20✔
89
  end
90

91
  def status
6✔
92
    metadata_model.status
1,178✔
93
  end
94

95
  def in_mediaflux?
6✔
96
    mediaflux_id.present?
52✔
97
  end
98

99
  # This method narrows the list down returned by `all_projects` to only those projects where the user has
100
  # been given a role (e.g. sponsor, manager, or data user.) For most users `all_projects` and `user_projects`
101
  # are identical, but for administrators the lists can be very different since they are not part of most
102
  # projects even though they have access to them in Mediaflux.
103
  def self.users_projects(user)
6✔
104
    all_projects(user).select do |project|
135✔
105
      project[:data_manager] == user.uid || project[:data_sponsor] == user.uid || project[:data_users].include?(user.uid)
8,452✔
106
    end
107
  end
108

109
  # Returns the projects that the current user has access in Mediaflux given their credentials
110
  def self.all_projects(user)
6✔
111
    request = Mediaflux::ProjectListRequest.new(session_token: user.mediaflux_session, aql_query: "xpath(tigerdata:project/ProjectID) has value")
143✔
112
    request.results
143✔
113
  end
114

115
  def user_has_access?(user:)
6✔
116
    return true if user.eligible_sysadmin?
112✔
117
    metadata_model.data_sponsor == user.uid || metadata_model.data_manager == user.uid ||
96✔
118
    metadata_model.data_user_read_only.include?(user.uid) || metadata_model.data_user_read_write.include?(user.uid)
119
  end
120

121
  def created_by_user
6✔
122
    User.find_by(uid: metadata_model.created_by)
×
123
  end
124

125
  def to_xml
6✔
126
    ProjectShowPresenter.new(self).to_xml
2✔
127
  end
128

129
  # @return [String] XML representation of the <meta> element
130
  def mediaflux_meta_xml(user:)
6✔
131
    doc = ProjectMediaflux.document(project: self, user: user)
6✔
132
    doc.xpath("/response/reply/result/asset/meta").to_s
6✔
133
  end
134

135
  def mediaflux_metadata(session_id:)
6✔
136
    @mediaflux_metadata ||= begin
1,548✔
137
      accum_req = Mediaflux::AssetMetadataRequest.new(session_token: session_id, id: mediaflux_id)
246✔
138
      accum_req.metadata
246✔
139
    end
140
    @mediaflux_metadata
1,540✔
141
  end
142

143
  def asset_count(session_id:)
6✔
144
    values = mediaflux_metadata(session_id:)
60✔
145
    values.fetch(:total_file_count, 0)
60✔
146
  end
147

148
  def self.default_storage_unit
6✔
149
    "KB"
436✔
150
  end
151

152
  def self.default_storage_usage
6✔
153
    "0 #{default_storage_unit}"
434✔
154
  end
155

156
  def storage_usage(session_id:)
6✔
157
    values = mediaflux_metadata(session_id:)
440✔
158
    values.fetch(:quota_used, self.class.default_storage_usage) # if the storage is empty use the default
432✔
159
  end
160

161
  def storage_usage_raw(session_id:)
6✔
162
    values = mediaflux_metadata(session_id:)
256✔
163
    values.fetch(:quota_used_raw, 0) # if the storage raw is empty use zero
256✔
164
  end
165

166
  def self.default_storage_capacity
6✔
167
    "0 GB"
30✔
168
  end
169

170
  def storage_capacity(session_id:)
6✔
171
    values = mediaflux_metadata(session_id:)
432✔
172
    quota_value = values.fetch(:quota_allocation, '') #if quota does not exist, set value to an empty string
432✔
173
    if quota_value.blank?
432✔
174
      return self.class.default_storage_capacity
30✔
175
    else
176
      return quota_value
402✔
177
    end
178
  end
179

180
  def storage_capacity_raw(session_id:)
6✔
181
    values = mediaflux_metadata(session_id:)
356✔
182
    quota_value = values.fetch(:quota_allocation_raw, 0) #if quota does not exist, set value to 0
356✔
183
    quota_value
356✔
184
  end
185

186
  # Fetches the first n files
187
  def file_list(session_id:, size: 10)
6✔
188
    return { files: [] } if mediaflux_id.nil?
66✔
189

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

193
    iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: size)
54✔
194
    results = iterator_req.result
54✔
195

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

201
    results
54✔
202
  end
203

204
  # Fetches the entire file list to a file
205
  def file_list_to_file(session_id:, filename:)
6✔
206
    return { files: [] } if mediaflux_id.nil?
24✔
207

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

211
    start_time = Time.zone.now
24✔
212
    prefix = "file_list_to_file #{session_id[0..7]} #{self.metadata_model.project_id}"
24✔
213
    log_elapsed(start_time, prefix, "STARTED")
24✔
214

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

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

238

239
  private
6✔
240

241
    def files_from_iterator(iterator_resp)
6✔
242
      lines = []
24✔
243
      iterator_resp[:files].each do |asset|
24✔
244
        lines << "#{asset.id}, #{asset.path_only}, #{asset.name}, #{asset.collection}, #{asset.last_modified}, #{asset.size}"
32✔
245
      end
246
      lines
24✔
247
    end
248

249
    def project_directory_pathname
6✔
250
      # allow the directory to be modified by changes in the metadata_json
251
      @project_directory_pathname = nil if @original_directory.present? && @original_directory != metadata_model.project_directory
×
252

253
      @project_directory_pathname ||= begin
×
254
        @original_directory = metadata_model.project_directory
×
255
        Pathname.new(@original_directory)
×
256
      end
257
    end
258

259
    # Ensure that the project directory is a valid path
260
    def safe_directory(directory)
6✔
261
      Project.safe_directory(directory)
×
262
    end
263

264
    def log_elapsed(start_time, prefix, message)
6✔
265
      elapsed_time = Time.zone.now - start_time
72✔
266
      timing_info = "#{format('%.2f', elapsed_time)} s"
72✔
267
      Rails.logger.info "#{prefix}: #{message}, #{timing_info}"
72✔
268
    end
269
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