renalware-core 2.0.0.pre.rc13 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/renalware/base/_variables.scss +3 -3
  3. data/app/assets/stylesheets/renalware/modules/_clinical.scss +107 -4
  4. data/app/assets/stylesheets/renalware/modules/_patients.scss +0 -77
  5. data/app/assets/stylesheets/renalware/partials/_layout.scss +67 -7
  6. data/app/assets/stylesheets/renalware/partials/_navigation.scss +2 -7
  7. data/app/assets/stylesheets/renalware/partials/_tables.scss +1 -1
  8. data/app/controllers/renalware/base_controller.rb +5 -0
  9. data/app/controllers/renalware/clinics/visits_controller.rb +1 -1
  10. data/app/controllers/renalware/concerns/pdf_renderable.rb +2 -1
  11. data/app/controllers/renalware/devise/sessions_controller.rb +5 -0
  12. data/app/controllers/renalware/letters/pdf_letter_cache_controller.rb +13 -0
  13. data/app/controllers/renalware/pd/regimes_controller.rb +1 -1
  14. data/app/controllers/renalware/renal/aki_alerts_controller.rb +20 -3
  15. data/app/controllers/renalware/research/studies_controller.rb +3 -1
  16. data/app/controllers/renalware/session_timeout_controller.rb +1 -1
  17. data/app/models/renalware/letters/html_renderer.rb +2 -1
  18. data/app/models/renalware/letters/pdf_letter_cache.rb +4 -1
  19. data/app/models/renalware/letters/pdf_renderer.rb +2 -1
  20. data/app/models/renalware/pathology/create_observations_grouped_by_date_table.rb +0 -3
  21. data/app/models/renalware/pathology/observations_grouped_by_date_query.rb +7 -1
  22. data/app/models/renalware/pd/apd_regime.rb +6 -0
  23. data/app/models/renalware/renal/aki_alert.rb +1 -0
  24. data/app/models/renalware/renal/aki_alert_query.rb +32 -0
  25. data/app/models/renalware/renal/aki_alert_search_form.rb +31 -0
  26. data/app/models/renalware/system/event.rb +12 -0
  27. data/app/models/renalware/system/user_feedback.rb +3 -1
  28. data/app/models/renalware/system/visit.rb +11 -0
  29. data/app/presenters/renalware/events/summary_part.rb +3 -1
  30. data/app/presenters/renalware/letters/summary_part.rb +1 -1
  31. data/app/presenters/renalware/mdm_presenter.rb +4 -1
  32. data/app/presenters/renalware/medications/summary_part.rb +2 -0
  33. data/app/presenters/renalware/problems/summary_part.rb +4 -2
  34. data/app/presenters/renalware/summary_part.rb +4 -0
  35. data/app/views/renalware/accesses/procedures/_list.html.slim +7 -5
  36. data/app/views/renalware/accesses/profiles/_list.html.slim +5 -5
  37. data/app/views/renalware/admin/cache/show.html.slim +26 -2
  38. data/app/views/renalware/devise/sessions/_warning.html.slim +7 -0
  39. data/app/views/renalware/devise/sessions/new.html.slim +15 -21
  40. data/app/views/renalware/events/events/_summary_part.html.slim +1 -1
  41. data/app/views/renalware/events/events/cell/_investigation.html.slim +0 -2
  42. data/app/views/renalware/events/events/toggled_cell/_biopsy.html.slim +1 -1
  43. data/app/views/renalware/events/events/toggled_cell/_investigation.html.slim +1 -1
  44. data/app/views/renalware/events/events/toggled_cell/_simple.html.slim +1 -1
  45. data/app/views/renalware/events/events/toggled_cell/_swab.html.slim +1 -1
  46. data/app/views/renalware/hd/cannulation_types/_form.html.slim +7 -10
  47. data/app/views/renalware/hd/cannulation_types/edit.html.slim +2 -1
  48. data/app/views/renalware/hd/cannulation_types/index.html.slim +4 -3
  49. data/app/views/renalware/hd/cannulation_types/new.html.slim +2 -1
  50. data/app/views/renalware/hd/dialysers/_form.html.slim +8 -11
  51. data/app/views/renalware/hd/dialysers/edit.html.slim +2 -1
  52. data/app/views/renalware/hd/dialysers/index.html.slim +4 -3
  53. data/app/views/renalware/hd/dialysers/new.html.slim +2 -1
  54. data/app/views/renalware/layouts/_non_patient.html.slim +14 -13
  55. data/app/views/renalware/layouts/_patient.html.slim +5 -5
  56. data/app/views/renalware/letters/_summary_part.html.slim +1 -1
  57. data/app/views/renalware/letters/letters/show.html.slim +16 -15
  58. data/app/views/renalware/medications/_summary_part.html.slim +18 -17
  59. data/app/views/renalware/modalities/reasons/index.html.slim +1 -3
  60. data/app/views/renalware/navigation/_footer.html.slim +2 -2
  61. data/app/views/renalware/navigation/_help_items.html.slim +1 -0
  62. data/app/views/renalware/navigation/_more_help_items.html.slim +1 -0
  63. data/app/views/renalware/navigation/_user.html.slim +4 -0
  64. data/app/views/renalware/pathology/requests/requests/index.html.slim +40 -42
  65. data/app/views/renalware/pathology/requests/rules/index.html.slim +15 -18
  66. data/app/views/renalware/patients/_side_menu.html.slim +0 -1
  67. data/app/views/renalware/patients/alerts/_alert.html.slim +7 -7
  68. data/app/views/renalware/patients/alerts/_list.html.slim +1 -3
  69. data/app/views/renalware/patients/alerts/create.js.erb +1 -1
  70. data/app/views/renalware/patients/alerts/destroy.js.erb +1 -1
  71. data/app/views/renalware/patients/primary_care_physicians/_form.html.slim +22 -25
  72. data/app/views/renalware/patients/primary_care_physicians/edit.html.slim +2 -1
  73. data/app/views/renalware/patients/primary_care_physicians/index.html.slim +4 -3
  74. data/app/views/renalware/patients/primary_care_physicians/new.html.slim +2 -1
  75. data/app/views/renalware/patients/side_menu/_general.html.slim +1 -1
  76. data/app/views/renalware/pd/_apd_regimes.html.slim +4 -38
  77. data/app/views/renalware/pd/_capd_regimes.html.slim +4 -45
  78. data/app/views/renalware/pd/_regimes.html.slim +32 -0
  79. data/app/views/renalware/pd/assessments/_form.html.slim +8 -5
  80. data/app/views/renalware/pd/regimes/_apd_fields.html.slim +1 -0
  81. data/app/views/renalware/pd/regimes/_apd_regime_show.html.slim +2 -0
  82. data/app/views/renalware/pd/regimes/_current_apd_regime.html.slim +3 -0
  83. data/app/views/renalware/problems/problems/_summary_part.html.slim +1 -1
  84. data/app/views/renalware/renal/aki_alerts/_filters.html.slim +25 -0
  85. data/app/views/renalware/renal/aki_alerts/index.html.slim +18 -14
  86. data/app/views/renalware/research/_alert.html.slim +6 -0
  87. data/app/views/renalware/research/_alerts.html.slim +6 -0
  88. data/app/views/renalware/research/studies/index.html.slim +2 -0
  89. data/app/views/renalware/system/email_templates/index.html.slim +7 -8
  90. data/app/views/renalware/system/user_feedback/new.html.slim +3 -7
  91. data/app/views/renalware/transplants/mdm/_pathology_cmvdna.html.slim +0 -1
  92. data/app/views/renalware/virology/vaccinations/_toggled_cell.html.slim +1 -1
  93. data/config/initializers/ahoy.rb +14 -0
  94. data/config/locales/renalware/events/investigation.en.yml +8 -0
  95. data/config/locales/renalware/patients/side_menu.en.yml +1 -1
  96. data/config/routes.rb +1 -0
  97. data/db/migrate/20180307191650_add_dwell_time_to_pd_regime.rb +5 -0
  98. data/db/migrate/20180307223111_create_system_visits_and_events.rb +44 -0
  99. data/db/migrate/20180309140316_add_unique_constraint_to_obr_requestor.rb +5 -0
  100. data/db/migrate/20180311071146_update_patient_summaries_to_version6.rb +5 -0
  101. data/db/views/patient_summaries_v06.sql +16 -0
  102. data/lib/renalware/configuration.rb +3 -0
  103. data/lib/renalware/engine.rb +1 -1
  104. data/lib/renalware/version.rb +1 -1
  105. data/spec/factories/pathology/observation_requests.rb +3 -1
  106. metadata +34 -8
  107. data/app/assets/stylesheets/renalware/patient_pages.scss +0 -43
  108. data/app/views/renalware/hd/cannulation_types/_header.html.slim +0 -5
  109. data/app/views/renalware/hd/dialysers/_header.html.slim +0 -5
  110. data/app/views/renalware/patients/primary_care_physicians/_header.html.slim +0 -5
@@ -6,10 +6,14 @@ module Renalware
6
6
  include Renalware::Concerns::Pageable
7
7
 
8
8
  def index
9
- alerts = AKIAlert.includes(:updated_by, :action, :hospital_ward, :patient)
10
- .ordered.page(page).per(per_page)
9
+ query = search_form.query
10
+ alerts = query.call.page(page).per(per_page)
11
11
  authorize alerts
12
- render locals: { alerts: alerts }
12
+ render locals: {
13
+ alerts: alerts,
14
+ form: search_form,
15
+ search: query.search
16
+ }
13
17
  end
14
18
 
15
19
  def edit
@@ -28,6 +32,13 @@ module Renalware
28
32
 
29
33
  private
30
34
 
35
+ def search_form
36
+ @search_form ||= begin
37
+ options = params.key?(:q) ? search_params : {}
38
+ AKIAlertSearchForm.new(options)
39
+ end
40
+ end
41
+
31
42
  def render_edit(alert)
32
43
  render :edit, locals: { alert: alert }
33
44
  end
@@ -44,6 +55,12 @@ module Renalware
44
55
  :max_cre, :cre_date, :max_aki, :aki_date
45
56
  )
46
57
  end
58
+
59
+ def search_params
60
+ params
61
+ .require(:q) {}
62
+ .permit(:term, :on_hotlist, :action, :hospital_unit_id, :hospital_ward_id, :s)
63
+ end
47
64
  end
48
65
  end
49
66
  end
@@ -3,8 +3,10 @@ require_dependency "renalware/research"
3
3
  module Renalware
4
4
  module Research
5
5
  class StudiesController < BaseController
6
+ include Renalware::Concerns::Pageable
7
+
6
8
  def index
7
- studies = Study.ordered
9
+ studies = Study.ordered.page(page).per(per_page)
8
10
  authorize studies
9
11
  render locals: { studies: studies }
10
12
  end
@@ -53,7 +53,7 @@ module Renalware
53
53
  # Returns a truthy value if we came from a devise URL like users/sign_in
54
54
  def referrer_is_a_devise_url?
55
55
  referrer = request.referer
56
- return if request.blank?
56
+ return if request.blank? || referrer.blank?
57
57
  regex_defining_devise_paths = %r{(#{new_user_session_path}|users\/password|users\/sign_up)}
58
58
  URI.parse(referrer).path =~ regex_defining_devise_paths
59
59
  end
@@ -5,7 +5,8 @@ module Renalware
5
5
  context = LettersController.new
6
6
  context.render_to_string(
7
7
  partial: "/renalware/letters/formatted_letters/letter",
8
- locals: { letter: letter }
8
+ locals: { letter: letter },
9
+ encoding: "UTF-8"
9
10
  )
10
11
  end
11
12
  end
@@ -35,6 +35,8 @@ module Renalware
35
35
  module Letters
36
36
  class PdfLetterCache
37
37
  class << self
38
+ delegate :clear, to: :store
39
+
38
40
  def fetch(letter)
39
41
  store.fetch(cache_key_for(letter)) { yield }
40
42
  end
@@ -46,7 +48,8 @@ module Renalware
46
48
  # valid and a new key and cache entry will be created.
47
49
  def cache_key_for(letter)
48
50
  timestamp = letter&.updated_at&.strftime("%Y%m%d%H%M%S")
49
- "letter-pdf-#{letter.id}-#{timestamp}-#{Digest::MD5.hexdigest(letter.to_html)}"
51
+ pat_id = letter.patient.id
52
+ "letter-pdf-#{letter.id}-#{pat_id}-#{timestamp}-#{Digest::MD5.hexdigest(letter.to_html)}"
50
53
  end
51
54
 
52
55
  def cache_path
@@ -11,7 +11,8 @@ module Renalware
11
11
  footer: {
12
12
  font_size: 8,
13
13
  right: "Page [page] of [topage]"
14
- }
14
+ },
15
+ encoding: "UTF-8"
15
16
  }.freeze
16
17
 
17
18
  def self.call(letter)
@@ -12,9 +12,6 @@ module Renalware
12
12
  end
13
13
 
14
14
  def call
15
- if observation_descriptions.blank?
16
- raise(ArgumentError, "No observation_descriptions supplied")
17
- end
18
15
  create_observations_table
19
16
  end
20
17
 
@@ -36,7 +36,8 @@ module Renalware
36
36
 
37
37
  def initialize(patient:, observation_descriptions:, page: 1, per_page: 50)
38
38
  @patient = patient
39
- @observation_descriptions = observation_descriptions
39
+ @observation_descriptions =
40
+ observation_descriptions.presence || observation_descriptions_null_object
40
41
  @page = Integer(page)
41
42
  @limit = Integer(per_page)
42
43
  end
@@ -52,11 +53,16 @@ module Renalware
52
53
  end
53
54
 
54
55
  def all
56
+ return Pathology::Observation.none if observation_descriptions.empty?
55
57
  conn.execute(to_paginated_sql)
56
58
  end
57
59
 
58
60
  private
59
61
 
62
+ def observation_descriptions_null_object
63
+ Pathology::Observation.none
64
+ end
65
+
60
66
  def to_sql
61
67
  <<-SQL.squish
62
68
  select obs_req.patient_id, cast(observed_at as date) as observed_on,
@@ -20,6 +20,7 @@ module Renalware
20
20
  additional_manual_exchange_volumes: 500..5_000,
21
21
  cycles_per_apd: 2..20,
22
22
  overnight_volumes: 3_000..25_000,
23
+ dwell_times: 10..120,
23
24
  tidal_percentages: (60..100).step(5).to_a
24
25
  ).freeze
25
26
 
@@ -63,6 +64,11 @@ module Renalware
63
64
  numericality: { only_integer: true },
64
65
  numeric_inclusion: { in: VALID_RANGES.therapy_times }
65
66
 
67
+ validates :dwell_time,
68
+ allow_nil: true,
69
+ numericality: { only_integer: true },
70
+ numeric_inclusion: { in: VALID_RANGES.dwell_times }
71
+
66
72
  validate :all_active_days_have_the_same_available_volume
67
73
 
68
74
  before_save -> { APD::CalculateVolumes.new(self).call }
@@ -4,6 +4,7 @@ module Renalware
4
4
  module Renal
5
5
  class AKIAlert < ApplicationRecord
6
6
  include Accountable
7
+ include PatientsRansackHelper
7
8
  scope :ordered, ->{ order(created_at: :desc) }
8
9
  belongs_to :patient, class_name: "Renal::Patient", touch: true
9
10
  belongs_to :action, class_name: "Renal::AKIAlertAction"
@@ -0,0 +1,32 @@
1
+ require_dependency "renalware/clinics"
2
+
3
+ module Renalware
4
+ module Renal
5
+ class AKIAlertQuery
6
+ DEFAULT_SORT = "aki_date :desc".freeze
7
+ attr_reader :query
8
+
9
+ def initialize(query = nil)
10
+ @query = query || {}
11
+ @query[:s] = DEFAULT_SORT if @query[:s].blank?
12
+ end
13
+
14
+ def self.call(query)
15
+ new(query).call
16
+ end
17
+
18
+ def call
19
+ search.result
20
+ end
21
+
22
+ def search
23
+ @search ||= begin
24
+ AKIAlert
25
+ .joins(:patient) # required for PatientsRansackHelper - see Admission
26
+ .includes(:patient, :updated_by, :action, hospital_ward: :hospital_unit)
27
+ .ransack(query)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ require_dependency "renalware/renal"
2
+
3
+ module Renalware
4
+ module Renal
5
+ class AKIAlertSearchForm
6
+ include ActiveModel::Model
7
+ include Virtus::Model
8
+
9
+ attribute :hospital_unit_id, Integer
10
+ attribute :hospital_ward_id, Integer
11
+ attribute :action, String
12
+ attribute :term, String
13
+ attribute :on_hotlist, String
14
+ attribute :s, String
15
+
16
+ def query
17
+ @query ||= begin
18
+ options = {
19
+ identity_match: term,
20
+ hospital_ward_id_eq: hospital_ward_id,
21
+ hospital_ward_hospital_unit_id_eq: hospital_unit_id,
22
+ action_id_eq: action,
23
+ hotlist_eq: on_hotlist,
24
+ s: s
25
+ }
26
+ AKIAlertQuery.new(options)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ require_dependency "renalware/system"
2
+
3
+ module Renalware
4
+ module System
5
+ class Event < ApplicationRecord
6
+ include Ahoy::QueryMethods # See https://github.com/ankane/ahoy
7
+
8
+ belongs_to :visit, class_name: "System::Visit"
9
+ belongs_to :user, optional: true
10
+ end
11
+ end
12
+ end
@@ -10,7 +10,9 @@ module Renalware
10
10
 
11
11
  belongs_to :author, class_name: "User"
12
12
 
13
- enumerize :category, in: %i(general bug missing_feature), default: :general
13
+ enumerize :category,
14
+ in: %i(urgent_bug non_urgent_bug missing_feature general_comment),
15
+ default: :general_comment
14
16
  end
15
17
  end
16
18
  end
@@ -0,0 +1,11 @@
1
+ require_dependency "renalware/system"
2
+
3
+ module Renalware
4
+ module System
5
+ class Visit < ApplicationRecord
6
+ # See https://github.com/ankane/ahoy
7
+ has_many :events, class_name: "System::Event"
8
+ belongs_to :user, optional: true
9
+ end
10
+ end
11
+ end
@@ -28,8 +28,10 @@ module Renalware
28
28
  # We purposefully don't use the recent_events relation here as it has includes and a limit
29
29
  # and apart from being slower, using LIMIT in cache_key sql has been known to produce
30
30
  # inconsistent results.
31
+ # We need to include the patient.cache_key otherwise if there are no events, the key will
32
+ # be the same for other patients with no events.
31
33
  def cache_key
32
- Events::Event.for_patient(patient).cache_key
34
+ [patient.cache_key, Events::Event.for_patient(patient).cache_key].join("~")
33
35
  end
34
36
 
35
37
  def to_partial_path
@@ -22,7 +22,7 @@ module Renalware
22
22
  end
23
23
 
24
24
  def cache_key
25
- letters_patient.letters.cache_key
25
+ [letters_patient.cache_key, letters_patient.letters.cache_key].join("~")
26
26
  end
27
27
 
28
28
  private
@@ -120,7 +120,10 @@ module Renalware
120
120
  if codes.nil?
121
121
  Pathology::RelevantObservationDescription.all
122
122
  else
123
- Pathology::ObservationDescription.for(Array(codes))
123
+ codes = Array(codes)
124
+ descriptions = Pathology::ObservationDescription.for(Array(codes))
125
+ warn("No OBX(es) found for codes #{codes}") if descriptions.empty?
126
+ descriptions
124
127
  end
125
128
  end
126
129
 
@@ -39,12 +39,14 @@ module Renalware
39
39
  def cache_key
40
40
  # Based on AR::Relation.cache_key, this key incorporates the following, scoped to the
41
41
  # current patient:
42
+ # - patient cachekey eg renalware/patients/166-20180306184938146827
42
43
  # - max(prescriptions.updated)
43
44
  # - count(prescriptions)
44
45
  # - max(drug.updated_at) in drugs across all prescriptions
45
46
  # - count(drugs) across all prescriptions (same as prescriptions.count so not really
46
47
  # required, but comes for free with AR::Relation.cache_key)
47
48
  [
49
+ patient.cache_key,
48
50
  prescriptions.cache_key,
49
51
  Drugs::Drug.where(id: prescriptions.pluck(:drug_id)).cache_key
50
52
  ].join("$")
@@ -5,12 +5,14 @@ require_dependency "renalware/problems"
5
5
  module Renalware
6
6
  module Problems
7
7
  class SummaryPart < Renalware::SummaryPart
8
- delegate :cache_key, to: :problems
9
-
10
8
  def problems
11
9
  @problems ||= patient.problems.ordered
12
10
  end
13
11
 
12
+ def cache_key
13
+ [patient.cache_key, patient.problems.cache_key].join("~")
14
+ end
15
+
14
16
  def to_partial_path
15
17
  "renalware/problems/problems/summary_part"
16
18
  end
@@ -20,6 +20,10 @@ module Renalware
20
20
  true
21
21
  end
22
22
 
23
+ def cache?
24
+ cache_key.present?
25
+ end
26
+
23
27
  protected
24
28
 
25
29
  def date_formatted_for_cache(date)
@@ -7,12 +7,13 @@ article.access-procedures
7
7
 
8
8
  table.auto-layout
9
9
  thead
10
- th
11
- th Performed
10
+ th.col-width-small
11
+ th.col-width-date Performed
12
12
  th Procedure
13
- th Side
13
+ th.col-width-tiny Side
14
14
  th Performed By
15
- th First Use
15
+ th.col-width-date First Use
16
+ th Notes
16
17
  th Outcome
17
18
 
18
19
  tbody
@@ -27,4 +28,5 @@ article.access-procedures
27
28
  td= procedure.side
28
29
  td= procedure.performed_by
29
30
  td= procedure.first_used_on
30
- td= procedure.outcome
31
+ td.col-width-mediumish-with-ellipsis(title=procedure.notes)=procedure.notes
32
+ td.col-width-mediumish-with-ellipsis(title=procedure.outcome)=procedure.outcome
@@ -7,12 +7,12 @@ article.access-profiles
7
7
 
8
8
  table.auto-layout
9
9
  thead
10
- th
11
- th Formed On
12
- th Start Date
13
- th Term. Date
10
+ th.col-width-small
11
+ th.col-width-date Formed On
12
+ th.col-width-date Start Date
13
+ th.col-width-date Term. Date
14
14
  th Type
15
- th Side
15
+ th.col-width-small Side
16
16
 
17
17
  tbody
18
18
  - @profiles.each do |profile|
@@ -1,5 +1,6 @@
1
- = within_admin_layout(title: "Cache") do
1
+ = within_admin_layout(title: "Clearing the Cache") do
2
2
  .panel
3
+ h2 Application Cache
3
4
  p
4
5
  | The cache (backed by Redis) stores some queries and html fragments in order to make
5
6
  | the rendering of pages faster and less resource intensive. Cached elements are invalidated
@@ -7,7 +8,9 @@
7
8
  | count of records, in order to catch edits, inserts and deletions) or the html template
8
9
  | changes (changes are only reflected after am app restart in this latter case).
9
10
  p
10
- | There are certain cases where you might need to clear the cache:
11
+ | You can check the size and number of keys in the Redis cache by running&nbsp;
12
+ code redis-cli --stat
13
+ p There are certain cases where you might need to clear the cache:
11
14
  ol
12
15
  li
13
16
  | You have changed the underlying database data directly in SQL without also updating the
@@ -18,3 +21,24 @@
18
21
  method: :delete,
19
22
  data: { confirm: "Are you sure you want to clear the application cache?\n" },
20
23
  class: "button alert"
24
+
25
+ .panel
26
+ h2 PDF Letter Cache
27
+ p
28
+ | The PDF letter cache stores generated PDFs so they do not need to be regenerated if
29
+ | unless their content has changed. It is a FileStore cache and the files are stored
30
+ | in&nbsp;
31
+ code shared/tmp/pdf_letter_cache
32
+ | - though in a folder layout that a little tricky to navigate
33
+ | and search if you are looking for something; however it is NOT meant to be the canonical
34
+ | source for PDF letters, rather a volatile cache to aid performance. EPR should be the place
35
+ | to look for letter content - or just view the PDF through the UI.
36
+ p
37
+ | You may wish to clear the PDF Letter Cache if you notice any anomalies in the PDFs. Please
38
+ | raise a GitHub issue after clearing the cache so we can look into it.
39
+
40
+ = link_to "Clear the PDF Letter Cache",
41
+ letters_pdf_letter_cache_path,
42
+ method: :delete,
43
+ data: { confirm: "Are you sure you want to clear the PDF letter cache?\n" },
44
+ class: "button alert"