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

pulibrary / tigerdata-app / 84a2dfd2-cbf5-4e2d-9eb7-e276471bcc35

22 Oct 2025 11:08AM UTC coverage: 91.184% (+0.1%) from 91.051%
84a2dfd2-cbf5-4e2d-9eb7-e276471bcc35

push

circleci

web-flow
Bump vite from 5.4.20 to 5.4.21 in the npm_and_yarn group across 1 directory (#2062)

Bumps the npm_and_yarn group with 1 update in the / directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 5.4.20 to 5.4.21
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v5.4.21</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.4.21 (2025-10-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix(dev): trim trailing slash before <code>server.fs.deny</code>
check (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/20968">#20968</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/20970">#20970</a>)
(<a
href="https://github.com/vitejs/vite/commit/<a class=hub.com/pulibrary/tigerdata-app/commit/cad1d31d0635dd8fd4ddfe6e5a92eb9ff13cd06c">cad1d31d0<a href="https://github.com/pulibrary/tigerdata-app/commit/c2277b4d23cf53321b272bce8dc929ddec37f4ef">&quot;&gt;cad1d31&lt;/a&gt;),
closes &lt;a
href=&quot;https://redirect.github.com/vitejs/vite/issues/20968&quot;&gt;#20968&lt;/a&gt;
&lt;a
href=&quot;https://redirect.github.com/vitejs/vite/issues/20970">#20970</a></li>
<li>chore: update CHANGELOG (<a
href="https://github.com/vitejs/vite/commit/<a class="double-link" href="https://github.com/pulibrary/tigerdata-app/commit/ca88ed7398288ce0c60176ac9a6392f10654c67c">ca88ed739</a><a href="https://github.com/pulibrary/tigerdata-app/commit/c2277b4d23cf53321b272bce8dc929ddec37f4ef">&quot;&gt;ca88ed7&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;Commits&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a
href=&quot;https://github.com/vitejs/vite/commit/</a><a class="double-link" href="https://github.com/pulibrary/tigerdata-app/commit/adce3c22c64cc9d44cc8f45cc92b543e3e4bf385">adce3c22c">adce3c2
release: v5.4.21
  • cad1d31 fix(dev): trim trailing slash before server.fs.deny check (
  • 2741 of 3006 relevant lines covered (91.18%)

    650.71 hits per line

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

    95.33
    /app/models/project.rb
    1
    # frozen_string_literal: true
    
    2
    class Project < ApplicationRecord
    
    5✔
    3
    
    
    4
      validates_with ProjectValidator
    
    5✔
    5
      has_many :provenance_events, dependent: :destroy
    
    5✔
    6
      before_save do |project|
    
    5✔
    7
        # Ensure that the metadata JSONB postgres field is persisted properly
    
    8
        project.metadata = project.metadata_model
    
    1,368✔
    9
      end
    
    10
    
    
    11
      # Valid project status described in ADR 7
    
    12
      # See `architecture-decisions/0007-valid-project-statuses.md`
    
    13
      APPROVED_STATUS = "approved"
    
    5✔
    14
      ACTIVE_STATUS = "active"
    
    5✔
    15
    
    
    16
      delegate :to_json, to: :metadata_json # field in the database
    
    5✔
    17
    
    
    18
      def create!(initial_metadata:, user:)
    
    5✔
    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:)
    
    5✔
    36
        raise StandardError.new("Only approved projects can be activated") if self.status != Project::APPROVED_STATUS
    
    210✔
    37
        metadata_request = Mediaflux::AssetMetadataRequest.new(session_token: current_user.mediaflux_session, id: self.mediaflux_id)
    
    210✔
    38
        metadata_request.resolve
    
    210✔
    39
        raise metadata_request.response_error if metadata_request.error?
    
    210✔
    40
        if self.title == metadata_request.metadata[:title]
    
    210✔
    41
          self.metadata_model.status = Project::ACTIVE_STATUS
    
    210✔
    42
          self.save!
    
    210✔
    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)
    
    5✔
    49
        puldatacite = PULDatacite.new
    
    226✔
    50
        self.metadata_model.project_id = puldatacite.draft_doi
    
    226✔
    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
    
    5✔
    57
        @metadata_hash = (metadata_json || {}).with_indifferent_access
    
    699✔
    58
      end
    
    59
    
    
    60
      def metadata_model
    
    5✔
    61
        @metadata_model ||= ProjectMetadata.new_from_hash(self.metadata)
    
    15,593✔
    62
      end
    
    63
    
    
    64
      def metadata_model=(new_metadata_model)
    
    5✔
    65
        @metadata_model = new_metadata_model
    
    505✔
    66
      end
    
    67
    
    
    68
      def metadata=(metadata_model)
    
    5✔
    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)
    
    2,858✔
    71
        self.metadata_json = metadata_hash
    
    2,858✔
    72
      end
    
    73
    
    
    74
      def title
    
    5✔
    75
        self.metadata_model.title
    
    754✔
    76
      end
    
    77
    
    
    78
      def departments
    
    5✔
    79
        unsorted = metadata_model.departments || []
    
    19✔
    80
        unsorted.sort
    
    19✔
    81
      end
    
    82
    
    
    83
      def project_directory
    
    5✔
    84
        metadata_model.project_directory || ""
    
    325✔
    85
      end
    
    86
    
    
    87
      def project_directory_short
    
    5✔
    88
        project_directory
    
    14✔
    89
      end
    
    90
    
    
    91
      def status
    
    5✔
    92
        metadata_model.status
    
    668✔
    93
      end
    
    94
    
    
    95
      def in_mediaflux?
    
    5✔
    96
        mediaflux_id.present?
    
    32✔
    97
      end
    
    98
    
    
    99
      def self.users_projects(user)
    
    5✔
    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
    
    95✔
    103
        query_ro = '{"data_user_read_only":["' + uid + '"]}'
    
    95✔
    104
        query_rw = '{"data_user_read_write":["' + uid + '"]}'
    
    95✔
    105
        query = "(metadata_json @> ? :: jsonb) OR (metadata_json @> ? :: jsonb) OR (metadata_json->>'data_sponsor' = ?) OR (metadata_json->>'data_manager' = ?)"
    
    95✔
    106
        args = [query_ro, query_rw, uid, uid]
    
    95✔
    107
        Project.where( query, *args)
    
    95✔
    108
      end
    
    109
    
    
    110
      def self.sponsored_projects(sponsor)
    
    5✔
    111
        Project.where("metadata_json->>'data_sponsor' = ?", sponsor)
    
    2✔
    112
      end
    
    113
    
    
    114
      def self.managed_projects(manager)
    
    5✔
    115
        Project.where("metadata_json->>'data_manager' = ?", manager)
    
    2✔
    116
      end
    
    117
    
    
    118
      def self.all_projects
    
    5✔
    119
        Project.all
    
    6✔
    120
      end
    
    121
    
    
    122
      def user_has_access?(user:)
    
    5✔
    123
        return true if user.eligible_sysadmin?
    
    71✔
    124
        metadata_model.data_sponsor == user.uid || metadata_model.data_manager == user.uid ||
    
    63✔
    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
    
    5✔
    129
        User.find_by(uid: metadata_model.created_by)
    
    ×
    130
      end
    
    131
    
    
    132
      def to_xml
    
    5✔
    133
        ProjectShowPresenter.new(self).to_xml
    
    2✔
    134
      end
    
    135
    
    
    136
      # @return [String] XML representation of the <meta> element
    
    137
      def mediaflux_meta_xml(user:)
    
    5✔
    138
        doc = ProjectMediaflux.document(project: self, user: user)
    
    6✔
    139
        doc.xpath("/response/reply/result/asset/meta").to_s
    
    6✔
    140
      end
    
    141
    
    
    142
      def mediaflux_metadata(session_id:)
    
    5✔
    143
        @mediaflux_metadata ||= begin
    
    797✔
    144
          accum_req = Mediaflux::AssetMetadataRequest.new(session_token: session_id, id: mediaflux_id)
    
    149✔
    145
          accum_req.metadata
    
    149✔
    146
        end
    
    147
        @mediaflux_metadata
    
    789✔
    148
      end
    
    149
    
    
    150
      def asset_count(session_id:)
    
    5✔
    151
        values = mediaflux_metadata(session_id:)
    
    35✔
    152
        values.fetch(:total_file_count, 0)
    
    35✔
    153
      end
    
    154
    
    
    155
      def self.default_storage_unit
    
    5✔
    156
        "KB"
    
    257✔
    157
      end
    
    158
    
    
    159
      def self.default_storage_usage
    
    5✔
    160
        "0 #{default_storage_unit}"
    
    255✔
    161
      end
    
    162
    
    
    163
      def storage_usage(session_id:)
    
    5✔
    164
        values = mediaflux_metadata(session_id:)
    
    261✔
    165
        values.fetch(:quota_used, self.class.default_storage_usage) # if the storage is empty use the default
    
    253✔
    166
      end
    
    167
    
    
    168
      def storage_usage_raw(session_id:)
    
    5✔
    169
        values = mediaflux_metadata(session_id:)
    
    45✔
    170
        values.fetch(:quota_used_raw, 0) # if the storage raw is empty use zero
    
    45✔
    171
      end
    
    172
    
    
    173
      def self.default_storage_capacity
    
    5✔
    174
        "0 GB"
    
    206✔
    175
      end
    
    176
    
    
    177
      def storage_capacity(session_id:)
    
    5✔
    178
        values = mediaflux_metadata(session_id:)
    
    253✔
    179
        quota_value = values.fetch(:quota_allocation, '') #if quota does not exist, set value to an empty string
    
    253✔
    180
        if quota_value.blank?
    
    253✔
    181
          return self.class.default_storage_capacity
    
    206✔
    182
        else
    
    183
          return quota_value
    
    47✔
    184
        end
    
    185
      end
    
    186
    
    
    187
      def storage_capacity_raw(session_id:)
    
    5✔
    188
        values = mediaflux_metadata(session_id:)
    
    199✔
    189
        quota_value = values.fetch(:quota_allocation_raw, 0) #if quota does not exist, set value to 0
    
    199✔
    190
        quota_value
    
    199✔
    191
      end
    
    192
    
    
    193
      # Fetches the first n files
    
    194
      def file_list(session_id:, size: 10)
    
    5✔
    195
        return { files: [] } if mediaflux_id.nil?
    
    39✔
    196
    
    
    197
        query_req = Mediaflux::QueryRequest.new(session_token: session_id, collection: mediaflux_id, deep_search: true, aql_query: "type!='application/arc-asset-collection'")
    
    29✔
    198
        iterator_id = query_req.result
    
    29✔
    199
    
    
    200
        iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: size)
    
    29✔
    201
        results = iterator_req.result
    
    29✔
    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.
    
    206
        Mediaflux::IteratorDestroyRequest.new(session_token: session_id, iterator: iterator_id).resolve
    
    29✔
    207
    
    
    208
        results
    
    29✔
    209
      end
    
    210
    
    
    211
      # Fetches the entire file list to a file
    
    212
      def file_list_to_file(session_id:, filename:)
    
    5✔
    213
        return { files: [] } if mediaflux_id.nil?
    
    22✔
    214
    
    
    215
        query_req = Mediaflux::QueryRequest.new(session_token: session_id, collection: mediaflux_id, deep_search: true,  aql_query: "type!='application/arc-asset-collection'")
    
    21✔
    216
        iterator_id = query_req.result
    
    21✔
    217
    
    
    218
        start_time = Time.zone.now
    
    21✔
    219
        prefix = "file_list_to_file #{session_id[0..7]} #{self.metadata_model.project_id}"
    
    21✔
    220
        log_elapsed(start_time, prefix, "STARTED")
    
    21✔
    221
    
    
    222
        File.open(filename, "w") do |file|
    
    21✔
    223
          page_number = 0
    
    21✔
    224
          # file header
    
    225
          file.write("ID, PATH, NAME, COLLECTION?, LAST_MODIFIED, SIZE\r\n")
    
    21✔
    226
          loop do
    
    21✔
    227
            iterator_start_time = Time.zone.now
    
    21✔
    228
            page_number += 1
    
    21✔
    229
            iterator_req = Mediaflux::IteratorRequest.new(session_token: session_id, iterator: iterator_id, size: 1000)
    
    21✔
    230
            iterator_resp = iterator_req.result
    
    21✔
    231
            log_elapsed(iterator_start_time, prefix, "FETCHED page #{page_number} from iterator")
    
    21✔
    232
            lines = files_from_iterator(iterator_resp)
    
    21✔
    233
            file.write(lines.join("\r\n") + "\r\n")
    
    21✔
    234
            break if iterator_resp[:complete] || iterator_req.error?
    
    21✔
    235
          end
    
    236
          log_elapsed(start_time, prefix, "ENDED")
    
    21✔
    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.
    
    242
        Mediaflux::IteratorDestroyRequest.new(session_token: session_id, iterator: iterator_id).resolve
    
    21✔
    243
      end
    
    244
    
    
    245
    
    
    246
      private
    
    5✔
    247
    
    
    248
        def files_from_iterator(iterator_resp)
    
    5✔
    249
          lines = []
    
    21✔
    250
          iterator_resp[:files].each do |asset|
    
    21✔
    251
            lines << "#{asset.id}, #{asset.path_only}, #{asset.name}, #{asset.collection}, #{asset.last_modified}, #{asset.size}"
    
    16✔
    252
          end
    
    253
          lines
    
    21✔
    254
        end
    
    255
    
    
    256
        def project_directory_pathname
    
    5✔
    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)
    
    5✔
    268
          Project.safe_directory(directory)
    
    ×
    269
        end
    
    270
    
    
    271
        def log_elapsed(start_time, prefix, message)
    
    5✔
    272
          elapsed_time = Time.zone.now - start_time
    
    63✔
    273
          timing_info = "#{format('%.2f', elapsed_time)} s"
    
    63✔
    274
          Rails.logger.info "#{prefix}: #{message}, #{timing_info}"
    
    63✔
    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