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

benwbrum / fromthepage / 25323270135

04 May 2026 01:58PM UTC coverage: 69.762% (+0.1%) from 69.661%
25323270135

Pull #5439

github

web-flow
Merge 71a484382 into ab4bee7b3
Pull Request #5439: Fix missing CDM sync status emails for owners and staff

2416 of 3975 branches covered (60.78%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 1 file covered. (100.0%)

83 existing lines in 11 files now uncovered.

9964 of 13771 relevant lines covered (72.35%)

152.76 hits per line

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

88.89
/app/models/document_set.rb
1
# == Schema Information
2
#
3
# Table name: document_sets
4
#
5
#  id                         :integer          not null, primary key
6
#  default_orientation        :string(255)
7
#  description                :text(65535)
8
#  featured_at                :datetime
9
#  pct_completed              :integer
10
#  picture                    :string(255)
11
#  slug                       :string(255)
12
#  title                      :string(255)
13
#  visibility                 :integer          default("private"), not null
14
#  works_count                :integer          default(0)
15
#  created_at                 :datetime
16
#  updated_at                 :datetime
17
#  collection_id              :integer
18
#  next_untranscribed_page_id :integer
19
#  owner_user_id              :integer
20
#
21
# Indexes
22
#
23
#  index_document_sets_on_collection_id  (collection_id)
24
#  index_document_sets_on_owner_user_id  (owner_user_id)
25
#  index_document_sets_on_slug           (slug) UNIQUE
26
#
27
class DocumentSet < ApplicationRecord
1✔
28
  include DocumentSetStatistic
1✔
29

30
  extend FriendlyId
1✔
31
  friendly_id :slug_candidates, use: [:slugged, :history]
1✔
32

33
  before_create :fill_featured_at
1✔
34
  before_save :uniquify_slug
1✔
35
  # validate :slug_uniqueness_across_objects
36

37
  mount_uploader :picture, PictureUploader
1✔
38

39
  belongs_to :owner, class_name: 'User', foreign_key: 'owner_user_id', optional: true
1✔
40
  belongs_to :collection, optional: true
1✔
41
  belongs_to :next_untranscribed_page, foreign_key: 'next_untranscribed_page_id', class_name: 'Page', optional: true
1✔
42

43
  has_many :document_set_works
1✔
44
  has_many :works, -> { order(:title) }, through: :document_set_works
743✔
45
  has_many :pages, through: :works
1✔
46
  has_many :articles, -> { distinct }, through: :works
25✔
47
  has_many :notes, -> { order(created_at: :desc) }, through: :works
39✔
48
  has_many :deeds, ->(document_set) {
1✔
49
    where(work_id: document_set.works.select(:id))
62✔
50
      .includes(:work)
51
      .reorder('deeds.created_at DESC')
52
  }, through: :collection, source: :deeds
53

54
  has_and_belongs_to_many :collaborators, class_name: 'User', join_table: :document_set_collaborators
1✔
55

56
  has_many :bulk_exports, dependent: :delete_all
1✔
57

58
  after_save :set_next_untranscribed_page
1✔
59

60
  validates :title, presence: true, length: { minimum: 3, maximum: 255 }
1✔
61
  validates :slug, format: { with: /[[:alpha:]]/ }
1✔
62

63
  scope :unrestricted, -> { where(visibility: [:public, :read_only]) }
271✔
64
  scope :restricted, -> { where(visibility: [:private]) }
80✔
65

66
  scope :carousel, -> {
1✔
67
    where(pct_completed: [nil, 1..90])
31✔
68
      .joins(:collection)
69
      .where.not(collections: { picture: nil })
70
      .where.not(description: [nil, ''])
71
      .unrestricted
72
      .reorder(Arel.sql('RAND()'))
73
  }
74
  scope :has_intro_block, -> { where.not(description: [nil, '']) }
2✔
75
  scope :has_picture, -> { where.not(picture: nil) }
1✔
76
  scope :not_near_complete, -> { where(pct_completed: [nil, 0..90]) }
1✔
77
  scope :not_empty, -> { where.not(works_count: [0, nil]) }
2✔
78

79
  scope :featured_projects, -> {
1✔
80
    joins(works: :pages)
17✔
81
      .joins(:owner)
82
      .where(owner: { deleted: false })
83
      .unrestricted.where("LOWER(document_sets.title) NOT LIKE 'test%'")
84
      .where.not(featured_at: nil)
85
      .distinct
86
  }
87

88
  enum :visibility, {
1✔
89
    private: 0,
90
    public: 1,
91
    read_only: 2
92
  }, prefix: :visibility
93

94
  update_index('document_sets', if: -> { ELASTIC_ENABLED && !destroyed? }) { self }
337✔
95
  after_destroy :handle_index_deletion
1✔
96

97
  def self.es_search(query:, user: nil, is_public: true)
1✔
98
    blocked_collections = []
18✔
99
    collection_collabs = []
18✔
100
    docset_collabs = []
18✔
101

102
    if user.present?
18✔
103
      blocked_collections = user.blocked_collections.pluck(:id)
11✔
104
      collection_collabs  = user.collection_collaborations.pluck(:id)
11✔
105
      collection_collabs += user.owned_collections.pluck(:id)
11✔
106
      docset_collabs      = user.document_set_collaborations.pluck(:id).map { |x| "docset-#{x}" }
11✔
107
    end
108

109
    DocumentSetsIndex.query(
18✔
110
      bool: {
111
        must: {
112
          simple_query_string: {
113
            query: query,
114
            fields: [
115
              'title^2',
116
              'title.no_underscores^1.3',
117
              'intro_block',
118
              'slug'
119
            ]
120
          }
121
        },
122
        filter: [
123
          { term: { is_docset: true } },
124
          {
125
            bool: {
126
              must_not: [
127
                { terms: { collection_id: blocked_collections } }
128
              ],
129
              should: [
130
                { term: { is_public: is_public } },
131
                { term: { owner_user_id: user&.id || -1 } },
18✔
132
                { terms: { collection_id: collection_collabs } },
133
                { terms: { _id: docset_collabs } }
134
              ],
135
              minimum_should_match: 1
136
            }
137
          }
138
        ]
139
      }
140
    )
141
  end
142

143
  def show_to?(user)
1✔
144
    is_public? || visibility_read_only? || user&.like_owner?(self) || user&.collaborator?(self)
153!
145
  end
146

147
  def intro_block
1✔
148
    description
174✔
149
  end
150

151
  def messageboards_enabled
1✔
152
    false
47✔
153
  end
154

155
  def messageboards_enabled?
1✔
156
    messageboards_enabled
47✔
157
  end
158

159
  def uniquify_slug
1✔
160
    self.slug = "#{slug}-set" if Collection.where(slug: slug).exists?
228✔
161
  end
162

163
  delegate :metadata_coverages,          to: :collection
1✔
164
  delegate :enable_spellcheck,           to: :collection
1✔
165
  delegate :reviewers,                   to: :collection
1✔
166
  delegate :facet_configs,               to: :collection
1✔
167
  delegate :text_entry?,                 to: :collection
1✔
168
  delegate :metadata_entry?,             to: :collection
1✔
169
  delegate :metadata_only_entry?,        to: :collection
1✔
170
  delegate :text_and_metadata_entry?,    to: :collection
1✔
171
  delegate :hide_completed,              to: :collection
1✔
172
  delegate :hide_notes,                  to: :collection
1✔
173
  delegate :hide_notes?,                 to: :collection
1✔
174
  delegate :review_workflow,             to: :collection
1✔
175
  delegate :review_type,                 to: :collection
1✔
176
  delegate :review_type_optional?,       to: :collection
1✔
177
  delegate :review_type_required?,       to: :collection
1✔
178
  delegate :review_type_restricted?,     to: :collection
1✔
179
  delegate :user_download,               to: :collection
1✔
180
  delegate :subjects_disabled,           to: :collection
1✔
181
  delegate :editor_buttons,              to: :collection
1✔
182
  delegate :categories,                  to: :collection
1✔
183
  delegate :active?,                     to: :collection
1✔
184
  delegate :footer_block,                to: :collection
1✔
185
  delegate :help,                        to: :collection
1✔
186
  delegate :link_help,                   to: :collection
1✔
187
  delegate :voice_recognition,           to: :collection
1✔
188
  delegate :language,                    to: :collection
1✔
189
  delegate :text_language,               to: :collection
1✔
190
  delegate :field_based,                 to: :collection
1✔
191
  delegate :transcription_fields,        to: :collection
1✔
192
  delegate :metadata_fields,             to: :collection
1✔
193
  delegate :description_instructions,    to: :collection
1✔
194
  delegate :facets_enabled?,             to: :collection
1✔
195
  delegate :api_access,                  to: :collection
1✔
196
  delegate :alphabetize_works,           to: :collection
1✔
197
  delegate :institution_signature,       to: :collection
1✔
198
  delegate :most_recent_deed_created_at, to: :collection
1✔
199
  delegate :legend,                      to: :collection
1✔
200
  delegate :default_overview_orientation, to: :collection
1✔
201

202

203
  def export_subject_index_as_csv
1✔
204
    subject_link = SubjectExporter::Exporter.new(self)
×
205

UNCOV
206
    subject_link.export
×
207
  end
208

209
  def export_subject_details_as_csv
1✔
210
    subjects = SubjectDetailsExporter::Exporter.new(self)
×
211

UNCOV
212
    subjects.export
×
213
  end
214

215
  def export_subject_distribution_as_csv(subject)
1✔
216
    subjects = SubjectDistributionExporter::Exporter.new(self, subject)
×
217

UNCOV
218
    subjects.export
×
219
  end
220

221
  def supports_document_sets
1✔
222
    false
31✔
223
  end
224

225
  def restricted
1✔
226
    visibility_private?
25✔
227
  end
228

229
  def picture_url(thumb = nil)
1✔
230
    if picture.blank?
18✔
231
      collection.picture.url(thumb)
15✔
232
    else
3✔
233
      picture.url(thumb)
3✔
234
    end
235
  end
236

237
  def set_next_untranscribed_page
1✔
238
    first_work = works.unrestricted.where.not(next_untranscribed_page_id: nil).order_by_incomplete.first
271✔
239
    first_page = first_work&.next_untranscribed_page
271✔
240
    page_id = first_page&.id
271✔
241

242
    update_columns(next_untranscribed_page_id: page_id)
271✔
243
  end
244

245
  def find_next_untranscribed_page_for_user(user)
1✔
246
    return nil unless has_untranscribed_pages?
4✔
247
    return next_untranscribed_page if user.can_transcribe?(next_untranscribed_page.work, self)
2!
248

UNCOV
249
    public = works.unrestricted
×
250
                  .where.not(next_untranscribed_page_id: nil)
251
                  .order_by_incomplete
252

UNCOV
253
    public&.first&.next_untranscribed_page
×
254
  end
255

256
  def has_untranscribed_pages?
1✔
257
    next_untranscribed_page.present?
80✔
258
  end
259

260
  def fill_featured_at
1✔
261
    return if self.visibility.nil? || self.visibility.to_sym == :private
123✔
262

263
    self.featured_at = Time.current
46✔
264
  end
265

266
  def slug_candidates
1✔
267
    if self.slug
383✔
268
      [:slug]
139✔
269
    else
244✔
270
      [
271
        :title,
244✔
272
        [:title, :id]
273
      ]
274
    end
275
  end
276

277
  def should_generate_new_friendly_id?
1✔
278
    slug_changed? || super
458✔
279
  end
280

281
  def normalize_friendly_id(string)
1✔
282
    super.truncate(240, separator: '-', omission: '').gsub('_', '-')
383✔
283
  end
284

285
  def search_works(search)
1✔
UNCOV
286
    works.where('title LIKE ? OR searchable_metadata like ?', "%#{search}%", "%#{search}%")
×
287
  end
288

289
  def self.search(search)
1✔
290
    sql = "title like ? OR slug LIKE ? OR owner_user_id in (select id from \
5✔
291
           users where owner=1 and display_name like ?)"
292
    where(sql, "%#{search}%", "%#{search}%", "%#{search}%")
5✔
293
  end
294

295
  def default_orientation
1✔
296
    if !self[:default_orientation].nil?
13!
297
      self[:default_orientation]
✔
298
    elsif self[:field_based]
13!
UNCOV
299
      'ttb'
×
300
    else
13✔
301
      'ltr'
13✔
302
    end
303
  end
304

305
  def sc_collection
1✔
306
    # association does not exist for document sets
307
    nil
308
  end
309

310
  def user_help
1✔
UNCOV
311
    collection.owner.help
×
312
  end
313

314
  def is_public
1✔
315
    visibility_public?
97✔
316
  end
317

318
  def is_public?
1✔
319
    visibility_public?
194✔
320
  end
321

322
  def handle_index_deletion
1✔
323
    return unless ELASTIC_ENABLED
48✔
324

325
    Chewy.client.delete(
44✔
326
      index: DocumentSetsIndex.index_name,
327
      id: "docset-#{id}"
328
    )
329
  rescue StandardError => _e
330
    # Make sure it does not fail
331
  end
332

333
  public :user_help, :handle_index_deletion
1✔
334
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

© 2026 Coveralls, Inc