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

pulibrary / tigerdata-app / 7291b10e-eaa3-4284-9371-5a980ceebf59

24 Nov 2025 07:18PM UTC coverage: 87.613% (-3.7%) from 91.333%
7291b10e-eaa3-4284-9371-5a980ceebf59

push

circleci

web-flow
Adds breadcrumb to Wizard (#2231)

Adds the breadcrumb to the Wizard and the functionality to allow the
user to save their changes before leaving the Wizard when clicking on
the "Dashboard" link in the breadcrumbs.

Closes #2102

5 of 12 new or added lines in 11 files covered. (41.67%)

904 existing lines in 36 files now uncovered.

2801 of 3197 relevant lines covered (87.61%)

360.23 hits per line

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

91.61
/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
859✔
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
5✔
20
    if self.valid?
5✔
21
      if initial_metadata.project_id == ProjectMetadata::DOI_NOT_MINTED
3✔
22
        self.draft_doi(user: user)
2✔
23
        self.save!
2✔
24
        ProvenanceEvent.generate_submission_events(project: self, user: user)
2✔
25
      else
26
        self.save!
1✔
27
      end
28
      # return doi
29
      self.metadata_model.project_id
3✔
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
182✔
37
    metadata_request = Mediaflux::AssetMetadataRequest.new(session_token: current_user.mediaflux_session, id: self.mediaflux_id)
182✔
38
    metadata_request.resolve
182✔
39
    raise metadata_request.response_error if metadata_request.error?
182✔
40
    if self.title == metadata_request.metadata[:title]
182✔
41
      self.metadata_model.status = Project::ACTIVE_STATUS
182✔
42
      self.save!
182✔
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
190✔
50
    self.metadata_model.project_id = puldatacite.draft_doi
190✔
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
284✔
58
  end
59

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

64
  def metadata_model=(new_metadata_model)
4✔
65
    @metadata_model = new_metadata_model
105✔
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,786✔
71
    self.metadata_json = metadata_hash
1,786✔
72
  end
73

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

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

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

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

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

95
  def in_mediaflux?
4✔
96
    mediaflux_id.present?
6✔
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)
4✔
104
    all_projects(user).select do |project|
9✔
105
      project[:data_manager] == user.uid || project[:data_sponsor] == user.uid || project[:data_users].include?(user.uid)
159✔
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, aql_query = "xpath(tigerdata:project/ProjectID) has value")
4✔
111
    request = Mediaflux::ProjectListRequest.new(session_token: user.mediaflux_session, aql_query:)
12✔
112
    request.resolve
12✔
113
    if request.error?
12✔
114
      Rails.logger.error("Error fetching project list for user #{user&.uid}: #{request.response_error[:message]}")
1✔
115
      Honeybadger.notify("Error fetching project list for user #{user&.uid}: #{request.response_error[:message]}")
1✔
116
      []
1✔
117
    else
118
      request.results
11✔
119
    end
120
  end
121

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

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

132
  def mediaflux_metadata(session_id:)
4✔
133
    @mediaflux_metadata ||= begin
76✔
134
      metadata_request = Mediaflux::AssetMetadataRequest.new(session_token: session_id, id: mediaflux_id)
54✔
135
      metadata_request.metadata
54✔
136
    end
137
    @mediaflux_metadata
72✔
138
  end
139

140
  def asset_count(session_id:)
4✔
UNCOV
141
    values = mediaflux_metadata(session_id:)
4✔
UNCOV
142
    values.fetch(:total_file_count, 0)
4✔
143
  end
144

145
  def self.default_storage_unit
4✔
146
    "KB"
7✔
147
  end
148

149
  def self.default_storage_usage
4✔
150
    "0 #{default_storage_unit}"
6✔
151
  end
152

153
  def storage_usage(session_id:)
4✔
UNCOV
154
    values = mediaflux_metadata(session_id:)
5✔
UNCOV
155
    values.fetch(:quota_used, self.class.default_storage_usage) # if the storage is empty use the default
5✔
156
  end
157

158
  def storage_usage_raw(session_id:)
4✔
UNCOV
159
    values = mediaflux_metadata(session_id:)
2✔
UNCOV
160
    values.fetch(:quota_used_raw, 0) # if the storage raw is empty use zero
2✔
161
  end
162

163
  def self.default_storage_capacity
4✔
164
    "0 GB"
×
165
  end
166

167
  def storage_capacity(session_id:)
4✔
UNCOV
168
    values = mediaflux_metadata(session_id:)
5✔
UNCOV
169
    quota_value = values.fetch(:quota_allocation, '') #if quota does not exist, set value to an empty string
5✔
UNCOV
170
    if quota_value.blank?
5✔
171
      return self.class.default_storage_capacity
×
172
    else
UNCOV
173
      return quota_value
5✔
174
    end
175
  end
176

177
  def storage_capacity_raw(session_id:)
4✔
UNCOV
178
    values = mediaflux_metadata(session_id:)
3✔
UNCOV
179
    quota_value = values.fetch(:quota_allocation_raw, 0) #if quota does not exist, set value to 0
3✔
UNCOV
180
    quota_value
3✔
181
  end
182

183
  # Fetches the first n files
184
  def file_list(session_id:, size: 10)
4✔
185
    return { files: [] } if mediaflux_id.nil?
5✔
186

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

190
    iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: size)
5✔
191
    results = iterator_req.result
5✔
192

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

198
    results
5✔
199
  end
200

201
  # Fetches the entire file list to a file
202
  def file_list_to_file(session_id:, filename:)
4✔
203
    return { files: [] } if mediaflux_id.nil?
10✔
204

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

208
    start_time = Time.zone.now
10✔
209
    prefix = "file_list_to_file #{session_id[0..7]} #{self.metadata_model.project_id}"
10✔
210
    log_elapsed(start_time, prefix, "STARTED")
10✔
211

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

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

235

236
  private
4✔
237

238
    def files_from_iterator(iterator_resp)
4✔
239
      lines = []
10✔
240
      iterator_resp[:files].each do |asset|
10✔
241
        lines << "#{asset.id}, #{asset.path_only}, #{asset.name}, #{asset.collection}, #{asset.last_modified}, #{asset.size}"
×
242
      end
243
      lines
10✔
244
    end
245

246
    def project_directory_pathname
4✔
247
      # allow the directory to be modified by changes in the metadata_json
248
      @project_directory_pathname = nil if @original_directory.present? && @original_directory != metadata_model.project_directory
×
249

250
      @project_directory_pathname ||= begin
×
251
        @original_directory = metadata_model.project_directory
×
252
        Pathname.new(@original_directory)
×
253
      end
254
    end
255

256
    # Ensure that the project directory is a valid path
257
    def safe_directory(directory)
4✔
258
      Project.safe_directory(directory)
×
259
    end
260

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