renalware-core 2.0.109 → 2.0.110

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/renalware/loading-gears-animation.gif +0 -0
  3. data/app/assets/javascripts/renalware/drugs.js +3 -4
  4. data/app/assets/javascripts/renalware/letters.js +47 -2
  5. data/app/assets/pdf/blank_page.pdf +56 -0
  6. data/app/assets/stylesheets/renalware/modules/_letters.scss +46 -0
  7. data/app/assets/stylesheets/renalware/partials/_tables.scss +2 -2
  8. data/app/controllers/renalware/base_controller.rb +9 -2
  9. data/app/controllers/renalware/clinics/appointments_controller.rb +8 -3
  10. data/app/controllers/renalware/letters/batches_controller.rb +106 -0
  11. data/app/controllers/renalware/letters/completed_batches_controller.rb +32 -0
  12. data/app/controllers/renalware/letters/lists_controller.rb +34 -5
  13. data/app/controllers/renalware/pathology/requests/requests_controller.rb +1 -1
  14. data/app/jobs/renalware/hd/update_rolling_patient_statistics_dj_job.rb +38 -0
  15. data/app/jobs/renalware/hd/update_rolling_patient_statistics_job.rb +4 -2
  16. data/app/jobs/renalware/letters/printing/batch_print_job.rb +27 -0
  17. data/app/models/concerns/renalware/patients_ransack_helper.rb +1 -1
  18. data/app/models/concerns/renalware/using_temp_folder.rb +14 -0
  19. data/app/models/renalware/clinics/appointment.rb +2 -3
  20. data/app/models/renalware/clinics/appointment_query.rb +5 -1
  21. data/app/models/renalware/hd/sessions/save_session.rb +5 -1
  22. data/app/models/renalware/hd/update_rolling_patient_statistics.rb +1 -1
  23. data/app/models/renalware/letters/batch.rb +37 -0
  24. data/app/models/renalware/letters/batch_item.rb +17 -0
  25. data/app/models/renalware/letters/lists/form.rb +101 -0
  26. data/app/models/renalware/letters/pdf_letter_cache.rb +1 -1
  27. data/app/models/renalware/letters/printing/batch_compile_pdfs.rb +121 -0
  28. data/app/models/renalware/letters/printing/complete_batch.rb +24 -0
  29. data/app/models/renalware/letters/printing/create_pdf_by_interleaving_address_sheet_and_letter_for_each_recipient.rb.dead +90 -0
  30. data/app/models/renalware/letters/printing/duplex_interleaved_pdf_renderer.rb +1 -1
  31. data/app/models/renalware/letters/printing/pdf_combining.rb +24 -21
  32. data/app/models/renalware/pathology/{consultant.rb → consultant.rb.dead} +0 -0
  33. data/app/models/renalware/pathology/observations_grouped_by_date_query.rb +1 -1
  34. data/app/models/renalware/pathology/relevant_observation_description.rb +1 -1
  35. data/app/models/renalware/pathology/requests/global_rule/transplant_date_within_weeks.rb +1 -1
  36. data/app/models/renalware/pathology/requests/global_rule/transplant_registration_status.rb +3 -1
  37. data/app/models/renalware/pathology/requests/request.rb +1 -1
  38. data/app/models/renalware/pathology/requests/request_params_factory.rb +11 -12
  39. data/app/models/renalware/patient.rb +3 -2
  40. data/app/models/renalware/pd/apd_regime.rb +6 -6
  41. data/app/models/renalware/renal/consultant.rb +16 -0
  42. data/app/models/renalware/transplants/registrations/wait_list_query.rb +4 -10
  43. data/app/models/renalware/ukrdc/treatment_timeline/generator_factory.rb +1 -1
  44. data/app/pdfs/renalware/letters/printing/recipient_address_page_pdf.rb +50 -0
  45. data/app/policies/renalware/letters/batch_policy.rb +13 -0
  46. data/app/presenters/renalware/letters/recipient_presenter.rb +1 -1
  47. data/app/presenters/renalware/pathology/requests/request_presenter.rb +1 -1
  48. data/app/presenters/renalware/transplants/mdm_presenter.rb +1 -1
  49. data/app/presenters/renalware/virology/dashboard_presenter.rb +2 -1
  50. data/app/views/renalware/api/ukrdc/patients/_hd_session_observations.xml.builder +2 -2
  51. data/app/views/renalware/clinics/appointments/index.html.slim +5 -4
  52. data/app/views/renalware/clinics/appointments/new.html.slim +3 -0
  53. data/app/views/renalware/letters/batches/_create.html.slim +27 -0
  54. data/app/views/renalware/letters/batches/create.js.erb +3 -0
  55. data/app/views/renalware/letters/batches/index.html.slim +20 -0
  56. data/app/views/renalware/letters/batches/show.html.slim +7 -0
  57. data/app/views/renalware/letters/completed_batches/create.js.erb +9 -0
  58. data/app/views/renalware/letters/completed_batches/new.html.slim +26 -0
  59. data/app/views/renalware/letters/lists/_table.html.slim +1 -1
  60. data/app/views/renalware/letters/lists/_tabs.html.slim +9 -0
  61. data/app/views/renalware/letters/lists/index.html.slim.dead +101 -0
  62. data/app/views/renalware/letters/lists/index.html.slim.dead1 +90 -0
  63. data/app/views/renalware/letters/lists/show.html.slim +57 -11
  64. data/app/views/renalware/medications/prescriptions/_form.html.slim +1 -0
  65. data/app/views/renalware/navigation/_super_admin.html.slim +1 -0
  66. data/app/views/renalware/pathology/requests/requests/_pdf_header.html.slim +2 -3
  67. data/app/views/renalware/pathology/requests/requests/index.html.slim +1 -1
  68. data/app/views/renalware/pathology/requests/requests/new.html.slim +0 -1
  69. data/config/locales/renalware/letters/letter.en.yml +5 -0
  70. data/config/routes/letters.rb +18 -1
  71. data/db/migrate/20190830153416_create_letter_batches.rb +14 -0
  72. data/db/migrate/20190902085216_create_letter_batch_items.rb +13 -0
  73. data/db/migrate/20190919073410_add_filepath_to_letter_batches.rb +7 -0
  74. data/db/migrate/20190920063447_add_status_to_letter_batch_items.rb +15 -0
  75. data/db/migrate/20190925104902_remove_user_from_appointments.rb +19 -0
  76. data/db/migrate/20190925130052_add_telephone_to_renal_consultants.rb +7 -0
  77. data/db/migrate/20190925161724_change_path_request_consultant_id.rb +19 -0
  78. data/db/migrate/20190925173849_renal_consultants_changes.rb +9 -0
  79. data/db/migrate/20190928131032_add_some_more_missing_indexes.rb +18 -0
  80. data/db/views/pathology_observation_digests_v01.sql +1 -1
  81. data/lib/renalware/configuration.rb +2 -0
  82. data/lib/renalware/engine.rb +1 -0
  83. data/lib/renalware/version.rb +1 -1
  84. data/lib/tasks/demo_data.rake +66 -0
  85. data/lib/tasks/pd.rake +32 -0
  86. data/spec/factories/clinics/appointments.rb +1 -1
  87. data/spec/factories/medications/medication_route.rb +5 -0
  88. data/spec/factories/renal/consultants.rb +9 -0
  89. data/spec/support/pages/medications/prescription_fom.rb +54 -0
  90. metadata +43 -7
  91. data/app/models/renalware/letters/printing/create_pdf_by_interleaving_address_sheet_and_letter_for_each_recipient.rb +0 -140
  92. data/spec/factories/pathology/consultant.rb +0 -5
@@ -38,7 +38,7 @@ module Renalware
38
38
  layout: false,
39
39
  locals: local_vars.merge(
40
40
  all_clinics: Renalware::Pathology::Clinic.for_algorithm,
41
- all_consultants: Renalware::Pathology::Consultant.ordered,
41
+ all_consultants: Renalware::Renal::Consultant.ordered,
42
42
  all_templates: Renalware::Pathology::Requests::Request::TEMPLATES
43
43
  )
44
44
  )
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When executed this job updates rolling statistics for a patient's
4
+ # last past 12 HD sessions.
5
+ # Because this job will be triggered again the next time an HD Sessions is
6
+ # created, it is not crucial to keep each event around - ie they have a short
7
+ # shelf-life. For this reason we only retry 3 times then delete failed jobs.
8
+ module Renalware
9
+ module HD
10
+ UpdateRollingPatientStatisticsDjJob = Struct.new(:patient_id) do
11
+ def perform
12
+ patient = Renalware::HD::Patient.find(patient_id)
13
+ UpdateRollingPatientStatistics.new(patient: patient).call
14
+ end
15
+
16
+ def max_attempts
17
+ 3
18
+ end
19
+
20
+ def queue_name
21
+ "hd_patient_statistics"
22
+ end
23
+
24
+ def priority
25
+ 4
26
+ end
27
+
28
+ def destroy_failed_jobs?
29
+ true
30
+ end
31
+
32
+ # Retry at intervals of 1, 2, 3 hours
33
+ def reschedule_at(current_time, attempts)
34
+ current_time + attempts.hours
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,12 +2,14 @@
2
2
 
3
3
  # When executed this job updates rolling statistics for a patient's
4
4
  # last past 12 HD sessions.
5
-
5
+ # Because this job will be triggered again the next time an HD Sessions is
6
+ # created, it is not crucial to keep each event around - ie they have a short
7
+ # shelf-life. For this reason we only retry 3 times then delete failed jobs.
6
8
  module Renalware
7
9
  module HD
8
10
  class UpdateRollingPatientStatisticsJob < ApplicationJob
9
11
  queue_as :hd_patient_statistics
10
- queue_with_priority 1
12
+ queue_with_priority 4
11
13
 
12
14
  # :reek:UtilityFunction
13
15
  def perform(patient)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/letters"
4
+ require "attr_extras"
5
+
6
+ module Renalware
7
+ module Letters
8
+ module Printing
9
+ class BatchPrintJob < ApplicationJob
10
+ include UsingTempFolder
11
+
12
+ # Returns the name of a temp file containing the pdf data
13
+ def perform(batch, user)
14
+ in_a_temporary_folder do |dir|
15
+ Dir.chdir(dir) do
16
+ BatchCompilePdfs.call(batch, user)
17
+ end
18
+ end
19
+ end
20
+
21
+ def max_attempts
22
+ 2
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -6,7 +6,7 @@ module Renalware
6
6
  module PatientsRansackHelper
7
7
  extend ActiveSupport::Concern
8
8
 
9
- UUID_REGEXP = /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/
9
+ UUID_REGEXP = /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/.freeze
10
10
 
11
11
  included do
12
12
  class_eval do
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renalware
4
+ module UsingTempFolder
5
+ extend ActiveSupport::Concern
6
+
7
+ def in_a_temporary_folder
8
+ Dir.mktmpdir(nil, Rails.root.join("tmp").to_s) do |dir|
9
+ yield Pathname(dir)
10
+ # temp dir removed here
11
+ end
12
+ end
13
+ end
14
+ end
@@ -5,15 +5,14 @@ require_dependency "renalware/clinics"
5
5
  module Renalware
6
6
  module Clinics
7
7
  class Appointment < ApplicationRecord
8
+ include Accountable
8
9
  belongs_to :patient, touch: true
9
10
  belongs_to :clinic
10
- belongs_to :user
11
+ belongs_to :consultant, class_name: "Renal::Consultant"
11
12
 
12
13
  validates :starts_at, presence: true
13
14
  validates :patient_id, presence: true
14
15
  validates :clinic_id, presence: true
15
- validates :user, presence: true
16
-
17
16
  validates :starts_at, timeliness: { type: :datetime }
18
17
 
19
18
  def starts_on
@@ -13,7 +13,11 @@ module Renalware
13
13
  end
14
14
 
15
15
  def call
16
- search.result.includes(:user, :clinic, patient: [current_modality: [:description]])
16
+ search.result.includes(
17
+ :clinic,
18
+ :consultant,
19
+ patient: [current_modality: [:description]]
20
+ )
17
21
  end
18
22
 
19
23
  def search
@@ -35,7 +35,11 @@ module Renalware
35
35
 
36
36
  if session.save
37
37
  # Might be cleaner if something listened for this event and created this job there?
38
- UpdateRollingPatientStatisticsJob.perform_later(patient) unless session.open?
38
+ # UpdateRollingPatientStatisticsJob.perform_later(patient) unless session.open?
39
+ unless session.open?
40
+ Delayed::Job.enqueue UpdateRollingPatientStatisticsDjJob.new(patient.id)
41
+ end
42
+
39
43
  broadcast(:save_success, session)
40
44
  else
41
45
  session.type = session_type # See method comment
@@ -30,7 +30,7 @@ module Renalware
30
30
  end
31
31
 
32
32
  def rolling_stats_for_this_patient
33
- @patient_rolling_stats ||= begin
33
+ @rolling_stats_for_this_patient ||= begin
34
34
  PatientStatistics.where(patient: patient,
35
35
  rolling: true,
36
36
  year: nil,
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/letters"
4
+
5
+ module Renalware
6
+ module Letters
7
+ class Batch < ApplicationRecord
8
+ include Accountable
9
+ attr_accessor(
10
+ :url,
11
+ :enclosures_present,
12
+ :state_eq,
13
+ :author_id_eq,
14
+ :created_by_id_eq,
15
+ :letterhead_id_eq,
16
+ :page_count_in_array,
17
+ :s
18
+ )
19
+
20
+ enum status: { queued: 0, processing: 10, awaiting_printing: 15, failure: 20, success: 30 }
21
+ has_many(
22
+ :items,
23
+ dependent: :restrict_with_exception,
24
+ class_name: "Renalware::Letters::BatchItem"
25
+ )
26
+ has_many(
27
+ :letters,
28
+ through: :items,
29
+ class_name: "Renalware::Letters::Letter"
30
+ )
31
+
32
+ def percent_complete
33
+ ((items.where(status: :compiled).count.to_f / batch_items_count) * 100).ceil
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/letters"
4
+
5
+ module Renalware
6
+ module Letters
7
+ class BatchItem < ApplicationRecord
8
+ belongs_to :letter
9
+ belongs_to :batch, counter_cache: true
10
+ enum status: { queued: 0, compiled: 10 }
11
+
12
+ def self.policy_class
13
+ BasePolicy
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renalware
4
+ module Letters
5
+ module Lists
6
+ # Form object to help us build and parse the appropriate filters for the
7
+ # letters_lists controller #show action, where we display all/batch_printable letters etc.
8
+ # For example, when displaying batchprintable letters we need to disable the enclosures
9
+ # filter (setting it to No), and restrict the optionsin the letter states dropdown so only
10
+ # Approved or Completed letters can be batch printed.
11
+ # We delegate logic to an instance of a private class which is (or is a subclass of)
12
+ # AllLetters.
13
+ # If we need to add another filter type, create another sub class of AllLetters.
14
+ class Form
15
+ delegate_missing_to :@handler
16
+
17
+ def initialize(named_filter:, params: {})
18
+ @handler = create_filter_specific_object_to_handle_all_requests(named_filter, params)
19
+ end
20
+
21
+ def create_filter_specific_object_to_handle_all_requests(named_filter, params)
22
+ handler_klass = "#{self.class.name}::#{named_filter.to_s.classify}Letters"
23
+ handler_klass.constantize.new(params)
24
+ end
25
+
26
+ class AllLetters
27
+ include ActiveModel::Model
28
+ include Virtus::Model
29
+
30
+ attribute :s, String # sort order, not really part of the form
31
+ attribute :enclosures_present, Boolean
32
+ attribute :state_eq, Integer
33
+ attribute :author_id_eq, Integer
34
+ attribute :created_by_id_eq, Integer
35
+ attribute :letterhead_id_eq, Integer
36
+ attribute :page_count_in_array, Integer
37
+
38
+ def letter_state_options(states = Letters::Letter.states)
39
+ states.map do |state|
40
+ label = I18n.t(state.to_sym, scope: "enums.letter.for_receptionists.state")
41
+ [label, state]
42
+ end
43
+ end
44
+
45
+ def author_options
46
+ @author_options ||= User.author.ordered
47
+ end
48
+
49
+ def typist_options
50
+ @typist_options ||= User.ordered
51
+ end
52
+
53
+ def letterhead_options
54
+ @letterhead_options ||= Letters::Letterhead.ordered
55
+ end
56
+
57
+ def page_count_options
58
+ [["1 or 2", "[1,2]"], ["3 or 4", "[3,4]"], ["5 or 6", "[5,6]"]]
59
+ end
60
+
61
+ def disabled_inputs
62
+ []
63
+ end
64
+
65
+ def allow_blank_inputs
66
+ [:state_eq, :page_count_in_array]
67
+ end
68
+ end
69
+
70
+ class BatchPrintableLetters < AllLetters
71
+ def initialize(params)
72
+ super
73
+ # These are the default values that must match the filters when the page is first
74
+ # loaded, so the right results are displayed
75
+ self.page_count_in_array ||= "[1,2]"
76
+ end
77
+
78
+ def letter_state_options
79
+ super([:approved])
80
+ end
81
+
82
+ def disabled_inputs
83
+ [:enclosures_present, :state_eq]
84
+ end
85
+
86
+ def allow_blank_inputs
87
+ []
88
+ end
89
+
90
+ def enclosures_present
91
+ false
92
+ end
93
+
94
+ def state_eq
95
+ @state_eq || :approved
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -44,7 +44,7 @@ module Renalware
44
44
  end
45
45
 
46
46
  # Note the letter must be a LetterPresenter which has a #to_html method
47
- # The to_html method should (and does on the LetterPrsenter class) render the complete
47
+ # The to_html method should (and does on the LetterPresenter class) render the complete
48
48
  # html including surrounding layout with inline css and images. This way if the
49
49
  # layout changes or the image is changed for example, the cache for the pdf is no longer
50
50
  # valid and a new key and cache entry will be created.
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/letters"
4
+ require "attr_extras"
5
+
6
+ module Renalware
7
+ module Letters
8
+ module Printing
9
+ # - takes a batch of letters
10
+ # - compiles each letter along with its recipient cover sheets
11
+ # - adds blank pages to make sure the letter always starts on an odd page
12
+ # - appends all PDF letters
13
+ # 2-page documents (from the contents of the /2 folder)
14
+ # - moves these compiled PDFs to a network share.
15
+ # - removes the temp folder structure - auto
16
+ class BatchCompilePdfs
17
+ include PdfCombining
18
+ PAGE_COUNTS = %w(2 3 4 5 6 7 8 9 10).freeze
19
+
20
+ def self.call(batch, user)
21
+ new(batch, user).call
22
+ end
23
+
24
+ def initialize(batch, user)
25
+ @batch = batch
26
+ @user = user
27
+ @dir = Pathname(Dir.pwd)
28
+ Rails.logger.info "Compiling letter PDFs for batch #{batch.id} in folder #{dir}"
29
+ end
30
+
31
+ # rubocop:disable Metrics/AbcSize
32
+ def call
33
+ batch.status = :processing
34
+ batch.save_by!(user)
35
+ batch.items.each do |item|
36
+ validate_letter(item.letter)
37
+ assemble_letter_pdfs(item.letter, dir)
38
+ item.update(status: :compiled)
39
+ end
40
+ batch.filepath = append_files
41
+ batch.status = :awaiting_printing
42
+ batch.save_by!(user)
43
+ end
44
+ # rubocop:enable Metrics/AbcSize
45
+
46
+ private
47
+
48
+ attr_reader :batch, :dir, :user
49
+
50
+ def append_files
51
+ folder = Rails.root.join("tmp", "batched_letters")
52
+ FileUtils.mkdir_p folder
53
+ pdf_file = folder.join("#{batch.id}.pdf")
54
+ glob = Dir.glob(dir.join("*.pdf"))
55
+ combine_multiple_pdfs_into_file(filepath: pdf_file, glob: glob) if glob.any?
56
+ Pathname(pdf_file).to_s
57
+ end
58
+
59
+ def page_count_allowing_for_cover_sheet(letter)
60
+ (letter.page_count + 1).to_s
61
+ end
62
+
63
+ def validate_letter(letter)
64
+ raise "Letter has no page_count" if letter.page_count.to_i.zero?
65
+ if letter.page_count.to_i > PAGE_COUNTS.last.to_i
66
+ raise "Letter page count (#{letter.page_count}) unexpected"
67
+ end
68
+ end
69
+
70
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
71
+ # TODO: refactor
72
+ def assemble_letter_pdfs(letter, dir)
73
+ Rails.logger.info "Interleaving address sheets and letters for letter #{letter.id}"
74
+ letter_has_an_odd_number_of_pages = letter.page_count.odd?
75
+ filenames = []
76
+ working_folder = dir.join("letter_#{letter.id}")
77
+ FileUtils.mkdir_p working_folder
78
+
79
+ output_filepath = dir.join("compiled_letter_#{letter.id}.pdf")
80
+
81
+ Dir.chdir(working_folder) do
82
+ letter_filename = write_letter_to_pdf_file(letter)
83
+ letter.recipients.each do |recipient|
84
+ Rails.logger.info " Recipient #{recipient.id}"
85
+ filenames << write_recipient_cover_sheet_pdf(letter, recipient)
86
+ filenames << letter_filename
87
+ filenames << blank_page_filename if letter_has_an_odd_number_of_pages
88
+ end
89
+ combine_multiple_pdfs_using_filenames(filenames, working_folder, output_filepath)
90
+ end
91
+ end
92
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
93
+
94
+ # Remove letter and recipient working files
95
+ def remove_working_files
96
+ FileUtils.rm Dir.glob(dir.join("*.pdf"))
97
+ end
98
+
99
+ def write_letter_to_pdf_file(letter)
100
+ filename = "original_letter_#{letter.id}.pdf"
101
+ File.open(filename, "wb") do |file|
102
+ file.write(PdfRenderer.call(letter))
103
+ end
104
+ filename
105
+ end
106
+
107
+ def write_recipient_cover_sheet_pdf(letter, recipient)
108
+ filename = "letter_#{letter.id}_address_sheet_for_recipient_#{recipient.id}.pdf"
109
+ RecipientAddressPagePdf.new(recipient).render_file filename
110
+ filename
111
+ end
112
+
113
+ def blank_page_filename
114
+ @blank_page_filename ||= begin
115
+ Renalware::Engine.root.join("app", "assets", "pdf", "blank_page.pdf").to_s
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end