renalware-core 2.0.132 → 2.0.133

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/renalware/core.js.erb +1 -1
  3. data/app/assets/stylesheets/renalware/core.scss +1 -1
  4. data/app/assets/stylesheets/renalware/modules/_clinics.scss +1 -2
  5. data/app/assets/stylesheets/renalware/modules/_patients.scss +1 -1
  6. data/app/assets/stylesheets/renalware/partials/_dashboards.scss +15 -0
  7. data/app/components/renalware/application_component.rb +22 -0
  8. data/app/components/renalware/events/biopsies_component.html.slim +14 -0
  9. data/app/components/renalware/events/biopsies_component.rb +20 -0
  10. data/app/components/renalware/letters/letters_in_progress_component.html.slim +8 -0
  11. data/app/components/renalware/letters/letters_in_progress_component.rb +32 -0
  12. data/app/components/renalware/letters/unread_electronic_ccs_component.html.slim +7 -0
  13. data/app/components/renalware/letters/unread_electronic_ccs_component.rb +24 -0
  14. data/app/components/renalware/messaging/unread_messages_component.html.slim +11 -0
  15. data/app/components/renalware/messaging/unread_messages_component.rb +24 -0
  16. data/app/components/renalware/patients/bookmarks_component.html.slim +11 -0
  17. data/app/components/renalware/patients/bookmarks_component.rb +22 -0
  18. data/app/controllers/renalware/clinical/body_compositions_controller.rb +1 -1
  19. data/app/controllers/renalware/pathology/code_groups_controller.rb +44 -0
  20. data/app/controllers/renalware/transplants/wait_lists_controller.rb +17 -8
  21. data/app/models/renalware/clinical/body_composition.rb +7 -0
  22. data/app/models/renalware/hd/mdm_patients_query.rb +10 -1
  23. data/app/models/renalware/pathology/code_group.rb +10 -3
  24. data/app/models/renalware/pathology/code_group_membership.rb +2 -0
  25. data/app/models/renalware/pathology/current_observation_set.rb +16 -3
  26. data/app/models/renalware/pathology/observations_jsonb_serializer.rb +1 -1
  27. data/app/models/renalware/pathology/version.rb +11 -0
  28. data/app/models/renalware/system/component.rb +25 -0
  29. data/app/models/renalware/system/dashboard.rb +20 -0
  30. data/app/models/renalware/system/dashboard_component.rb +17 -0
  31. data/app/models/renalware/transplants.rb +8 -1
  32. data/app/models/renalware/transplants/registrations/wait_list_form.rb +16 -0
  33. data/app/models/renalware/transplants/registrations/wait_list_query.rb +17 -3
  34. data/app/models/renalware/ukrdc/incoming/file_list.rb +1 -1
  35. data/app/models/renalware/ukrdc/incoming/import_surveys.rb +16 -0
  36. data/app/models/renalware/ukrdc/outgoing/rendering/base.rb +22 -0
  37. data/app/models/renalware/ukrdc/outgoing/rendering/dialysis_session.rb +2 -2
  38. data/app/models/renalware/ukrdc/outgoing/rendering/hd_session_observations.rb +1 -1
  39. data/app/models/renalware/user.rb +9 -0
  40. data/app/policies/renalware/hd/closed_session_policy.rb +1 -1
  41. data/app/policies/renalware/pathology/code_group_policy.rb +23 -0
  42. data/app/presenters/renalware/hd/protocol_presenter.rb +8 -2
  43. data/app/views/renalware/clinical/body_compositions/_form.html.slim +6 -0
  44. data/app/views/renalware/clinical/body_compositions/_table.html.slim +4 -0
  45. data/app/views/renalware/dashboard/bookmarks/_bookmark.html.slim +5 -5
  46. data/app/views/renalware/dashboard/bookmarks/_table.html.slim +5 -5
  47. data/app/views/renalware/dashboard/dashboards/_content.html.slim +4 -41
  48. data/app/views/renalware/dashboard/letters/_letter.html.slim +2 -2
  49. data/app/views/renalware/hd/mdm_patients/_patient.html.slim +5 -1
  50. data/app/views/renalware/hd/mdm_patients/_table.html.slim +2 -1
  51. data/app/views/renalware/hd/protocols/_recent_pathology.html.slim +5 -12
  52. data/app/views/renalware/mdm/{_biopsies.html.slim → _biopsies.html.slim.dead} +0 -0
  53. data/app/views/renalware/mdm_patients/_patient.html.slim +5 -1
  54. data/app/views/renalware/mdm_patients/_table.html.slim +1 -1
  55. data/app/views/renalware/medications/prescriptions/_tables.html.slim +27 -23
  56. data/app/views/renalware/messaging/internal/receipts/_receipt.html.slim +1 -1
  57. data/app/views/renalware/modalities/modalities/index.html.slim +17 -16
  58. data/app/views/renalware/navigation/_super_admin.html.slim +1 -0
  59. data/app/views/renalware/pathology/code_groups/edit.html.slim +8 -0
  60. data/app/views/renalware/pathology/code_groups/index.html.slim +16 -0
  61. data/app/views/renalware/pathology/code_groups/show.html.slim +34 -0
  62. data/app/views/renalware/problems/problems/index.html.slim +9 -5
  63. data/app/views/renalware/surveys/_eq5d_summary_part.html.slim +17 -17
  64. data/app/views/renalware/surveys/_pos_s_summary_part.html.slim +61 -60
  65. data/app/views/renalware/transplants/mdm/_bottom.html.slim +1 -1
  66. data/app/views/renalware/transplants/mdm_patients/_patient.html.slim +5 -1
  67. data/app/views/renalware/transplants/mdm_patients/_table.html.slim +1 -1
  68. data/app/views/renalware/transplants/wait_lists/_registration.html.slim +1 -0
  69. data/app/views/renalware/transplants/wait_lists/show.html.slim +24 -1
  70. data/config/locales/renalware/clinical/body_composition.yml +4 -0
  71. data/config/routes/pathology.rb +1 -0
  72. data/db/migrate/20200114151225_add_clinical_body_composition_cols.rb +11 -0
  73. data/db/migrate/20200127165951_create_pathology_versions.rb +16 -0
  74. data/db/migrate/20200127170711_add_created_by_to_pathology_code_groups.rb +15 -0
  75. data/db/migrate/20200129093835_create_system_dashboards.rb +73 -0
  76. data/db/seeds/default/pathology/code_groups.rb +15 -0
  77. data/db/seeds/default/pathology/seeds.rb +2 -1
  78. data/lib/core_extensions/active_record/migration_helpers.rb +3 -2
  79. data/lib/renalware/engine.rb +1 -1
  80. data/lib/renalware/version.rb +1 -1
  81. data/lib/tasks/spec.rake +23 -21
  82. data/spec/factories/clinical/body_compositions.rb +3 -1
  83. data/spec/factories/pathology/code_group_memberships.rb +7 -0
  84. data/spec/factories/pathology/code_groups.rb +13 -0
  85. data/spec/factories/transplants/registration_status_descriptions.rb +5 -0
  86. data/spec/support/devise_spec_helper.rb +24 -8
  87. data/spec/support/ukrdc_helpers.rb +2 -1
  88. metadata +53 -24
@@ -10,6 +10,8 @@ module Renalware
10
10
  # together in groups on the page for clarity) and within that group might have a position wich
11
11
  # determines its order in the subgroup.
12
12
  class CodeGroupMembership < ApplicationRecord
13
+ include Accountable
14
+ has_paper_trail class_name: "Renalware::Pathology::Version", on: [:create, :update, :destroy]
13
15
  validates :position_within_subgroup, presence: true
14
16
  validates :subgroup, presence: true
15
17
  belongs_to :code_group
@@ -30,9 +30,18 @@ module Renalware
30
30
  validates :patient, presence: true
31
31
  serialize :values, ObservationsJsonbSerializer
32
32
 
33
+ # Select values frm the set where the code matches the code or array of codes
34
+ # requested.
35
+ # When the code is not found in the set, return an empty hash for that code.
36
+ # When the patient has no current_observation_set, return an empty hash for each code.
37
+ # We need to be sure to extend the HashWithIndifferentAccess returned from
38
+ # #select with the ObservationSetMethods so a user can call eg {..}.hgb_date
39
+ # or {..}.plt etc without error
33
40
  def values_for_codes(codes)
34
- codes = Array(codes)
35
- values.select { |code, _| codes.include?(code) }
41
+ hash = Array(codes).each_with_object(HashWithIndifferentAccess.new) do |code, hash|
42
+ hash[code] = values[code] || CurrentObservationSet.null_values_hash
43
+ end
44
+ hash.extend(ObservationSetMethods)
36
45
  end
37
46
 
38
47
  def self.null_values_hash
@@ -42,7 +51,11 @@ module Renalware
42
51
 
43
52
  class NullObservationSet
44
53
  def values
45
- ObservationsJsonbSerializer.load({})
54
+ ObservationsJsonbSerializer.load(HashWithIndifferentAccess.new)
55
+ end
56
+
57
+ def values_for_codes(codes)
58
+ CurrentObservationSet.null_values_hash
46
59
  end
47
60
  end
48
61
  end
@@ -34,7 +34,7 @@ module Renalware
34
34
 
35
35
  def observation_hash_or_hash_element_for(code, suffix)
36
36
  obs_hash = self[code]
37
- return nil if obs_hash.nil? # the patient may not have this observation in the set
37
+ return nil if obs_hash.blank? # the patient may not have this code in the set, or it might be {}
38
38
  return obs_hash[:result] if suffix == "_result"
39
39
  return Date.parse(obs_hash[:observed_at]) if suffix == "_observed_at"
40
40
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/pathology"
4
+
5
+ module Renalware
6
+ module Pathology
7
+ class Version < PaperTrail::Version
8
+ self.table_name = :pathology_versions
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/system"
4
+
5
+ module Renalware
6
+ module System
7
+ # A component is the description in data of an ActionView::Component
8
+ # in the app/components folder inside this engine.
9
+ # Components encapsulate reusable elements of the UI.
10
+ # Dashboard-compatible components must have a current_user arg in their
11
+ # ctor.
12
+ # A component can be included for example on a dashboard (see Dashboard
13
+ # and DashboardComponent) provided component.dashboard == true.
14
+ # A component has these properies
15
+ # - class_name eg "Renalware::Letters::LettersInProgress"
16
+ # - name eg "Letters In Progress"
17
+ # - dashboard - if true then this component can be added to a dashboard
18
+ # - roles - the roles required for a user to be able to add this component
19
+ # to their dashboard
20
+ class Component < ApplicationRecord
21
+ validates :class_name, presence: true
22
+ validates :name, presence: true, uniqueness: true
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/system"
4
+
5
+ module Renalware
6
+ module System
7
+ # The desription in data of a dashboard displayed in the UI, for example
8
+ # the 'default' dashboard a user see when they log in, a dashboard for
9
+ # an HD nurse, or a user's customised dashboard (if we get to build that!),
10
+ # A dashboard comprises components (see Component and DashboardComponent).
11
+ class Dashboard < ApplicationRecord
12
+ belongs_to :user
13
+ has_many :dashboard_components, dependent: :restrict_with_exception
14
+ has_many :components, through: :dashboard_components
15
+ validates :name, uniqueness: true
16
+ validates :name, presence: { if: -> { user_id.nil? } }
17
+ validates :user, presence: { if: -> { name.nil? } }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/system"
4
+
5
+ module Renalware
6
+ module System
7
+ # Represents an instance of a component on a dashboard
8
+ class DashboardComponent < ApplicationRecord
9
+ belongs_to :component
10
+ belongs_to :dashboard
11
+ validates :position, presence: true
12
+ validates :position, uniqueness: { scope: [:dashboard_id] }
13
+ validates :dashboard, presence: true
14
+ validates :component, presence: true
15
+ end
16
+ end
17
+ end
@@ -2,7 +2,14 @@
2
2
 
3
3
  module Renalware
4
4
  module Transplants
5
- WAITLIST_FILTERS = %w(active suspended active_and_suspended working_up status_mismatch).freeze
5
+ WAITLIST_FILTERS = %w(
6
+ all
7
+ active
8
+ suspended
9
+ active_and_suspended
10
+ working_up
11
+ status_mismatch
12
+ ).freeze
6
13
 
7
14
  def self.table_name_prefix
8
15
  "transplant_"
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "renalware/transplants"
4
+
5
+ module Renalware
6
+ module Transplants
7
+ module Registrations
8
+ class WaitListForm
9
+ include ActiveModel::Model
10
+ include Virtus::Model
11
+
12
+ attribute :ukt_recipient_number, String
13
+ end
14
+ end
15
+ end
16
+ end
@@ -6,9 +6,10 @@ module Renalware
6
6
  module Transplants
7
7
  module Registrations
8
8
  class WaitListQuery
9
- def initialize(named_filter:, q: nil)
9
+ def initialize(named_filter:, ukt_recipient_number: nil, q: nil)
10
10
  @named_filter = named_filter&.to_sym || :active
11
11
  @q = (q || ActionController::Parameters.new).permit(:s, :q)
12
+ @ukt_recipient_number = ukt_recipient_number
12
13
  end
13
14
 
14
15
  def call
@@ -16,6 +17,7 @@ module Renalware
16
17
  .result
17
18
  .extending(Scopes)
18
19
  .apply_filter(named_filter)
20
+ .having_ukt_recipient_number(ukt_recipient_number)
19
21
  end
20
22
 
21
23
  def search
@@ -37,6 +39,14 @@ module Renalware
37
39
  # At some point we will have turn this into a mapping object or hash because there will
38
40
  # probably not be a 1 to 1 mapping from wait list to UKT status.
39
41
  module Scopes
42
+ def having_ukt_recipient_number(number)
43
+ return all if number.blank?
44
+
45
+ where(<<-SQL, number)
46
+ transplant_registrations.document -> 'codes' ->> 'uk_transplant_patient_recipient_number' = ?
47
+ SQL
48
+ end
49
+
40
50
  # rubocop:disable Metrics/MethodLength
41
51
  def apply_filter(filter)
42
52
  case filter
@@ -71,10 +81,13 @@ module Renalware
71
81
 
72
82
  private
73
83
 
74
- attr_reader :q, :named_filter
84
+ attr_reader :q, :named_filter, :ukt_recipient_number
75
85
 
86
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
76
87
  def query_for_filter(filter)
77
88
  case filter
89
+ when :all
90
+ {}
78
91
  when :active
79
92
  { current_status_in: :active }
80
93
  when :suspended
@@ -89,6 +102,7 @@ module Renalware
89
102
  {} # See Scopes
90
103
  end
91
104
  end
105
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
92
106
 
93
107
  class QueryableRegistration < ActiveType::Record[Registration]
94
108
  scope :current_status_in, lambda { |codes|
@@ -133,7 +147,7 @@ module Renalware
133
147
  private_class_method :ransackable_scopes
134
148
 
135
149
  def self.ransackable_scopes(_auth_object = nil)
136
- %i(current_status_in)
150
+ %i(current_status_in ukt_recipient_number_eq)
137
151
  end
138
152
  end
139
153
  end
@@ -6,7 +6,7 @@ module Renalware
6
6
  module UKRDC
7
7
  module Incoming
8
8
  class FileList
9
- pattr_initialize [pattern: "*.xml", paths: Paths.new]
9
+ pattr_initialize [pattern: "survey*.xml", paths: Paths.new]
10
10
 
11
11
  # Helper which yields each file in the incoming folder.
12
12
  def each_file
@@ -18,6 +18,13 @@ module Renalware
18
18
 
19
19
  def call
20
20
  Incoming::FileList.new(paths: paths).each_file do |filepath|
21
+ if already_imported?(filepath)
22
+ logger.info "Skipping: #{filepath} already successfully imported"
23
+ FileUtils.mv filepath, paths.archive.join(filepath.basename)
24
+ next
25
+ end
26
+
27
+ logger.info "Processing: #{filepath}"
21
28
  import_surveys_from_file(filepath)
22
29
  end
23
30
  rescue StandardError => e
@@ -27,6 +34,15 @@ module Renalware
27
34
 
28
35
  private
29
36
 
37
+ # If a file arrives that we have already imported successfully then skip it.
38
+ # This assumes filenames are unique across time, which they should be.
39
+ # Note we match on the whole file path, so if the location of the folder changes
40
+ # and we are presented with familiar files, then they will be imported again. Its unlikely
41
+ # those two things will happen together though.
42
+ def already_imported?(filepath)
43
+ TransmissionLog.exists?(file_path: filepath.to_s, status: :imported)
44
+ end
45
+
30
46
  # Import all surverys (they will be for the same patient) in the XML file.
31
47
  # Note that #with_logging yields a block that will catch and save any error to
32
48
  # ukrdc_transmission_logs
@@ -13,6 +13,28 @@ module Renalware
13
13
  yield(elem) if block_given?
14
14
  elem
15
15
  end
16
+
17
+ # Example output:
18
+ # "123 approx" => 123
19
+ # "123" => 123
20
+ # 123 => 123
21
+ # 123.1 => 123
22
+ # 0 => nil
23
+ # "n/a" => nil
24
+ def coerce_to_integer(value)
25
+ value.to_i > 0 && value.presence&.to_i
26
+ end
27
+
28
+ # Example output:
29
+ # "123 approx" => 123.0
30
+ # "123" => 123.0
31
+ # 123 => 123.0
32
+ # 123.1 => 123.1
33
+ # 0 => nil
34
+ # "n/a" => nil
35
+ def coerce_to_float(value)
36
+ value.to_f > 0.0 && value.presence&.to_f
37
+ end
16
38
  end
17
39
  end
18
40
  end
@@ -67,8 +67,8 @@ module Renalware
67
67
  elem << create_node("QHD20", session.access_rr02_code)
68
68
  elem << create_node("QHD21", session.access_rr41_code)
69
69
  elem << create_node("QHD22", "N") # Access in two sites simultaneously
70
- elem << create_node("QHD30", session.blood_flow)
71
- elem << create_node("QHD31", session.duration_in_minutes) # Time Dialysed in Minutes
70
+ elem << create_node("QHD30", coerce_to_integer(session.blood_flow))
71
+ elem << create_node("QHD31", coerce_to_integer(session.duration_in_minutes))
72
72
  elem << create_node("QHD32", session.sodium_content) # Sodium in Dialysate
73
73
  elem << create_node("QHD33") # TODO: Needling Method
74
74
  end
@@ -41,7 +41,7 @@ module Renalware
41
41
  {
42
42
  "blood_pressure.systolic" => observations.blood_pressure&.systolic,
43
43
  "blood_pressure.diastolic" => observations.blood_pressure&.diastolic,
44
- "weight" => observations.weight
44
+ "weight" => coerce_to_float(observations.weight)
45
45
  }
46
46
  end
47
47
  end
@@ -92,6 +92,15 @@ module Renalware
92
92
  OpenSSL::HMAC.hexdigest(digest, key, id.to_s)
93
93
  end
94
94
 
95
+
96
+ # We implement a simple can? method ion the use because in places we pass a current user
97
+ # from an ActionView::Component to a partial, and in the specs the partial says it cannot
98
+ # Example usage user.can?(:edit, letter)
99
+ # def can?(method, record)
100
+ # method = :"#{method}?" unless method.to_s.ends_with("?")
101
+ # Pundit.policy(self, record).public_send(method.to_sym)
102
+ # end
103
+
95
104
  private
96
105
 
97
106
  def build_authentication_token
@@ -10,7 +10,7 @@ module Renalware
10
10
  def edit?
11
11
  return false unless record.persisted?
12
12
 
13
- user_is_super_admin? || !record.immutable?
13
+ user_is_admin? || user_is_super_admin? || !record.immutable?
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renalware
4
+ module Pathology
5
+ class CodeGroupPolicy < BasePolicy
6
+ def create?
7
+ user_is_super_admin?
8
+ end
9
+
10
+ def update?
11
+ create?
12
+ end
13
+
14
+ def destroy?
15
+ create?
16
+ end
17
+
18
+ def edit?
19
+ create?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -56,8 +56,14 @@ module Renalware
56
56
  end
57
57
 
58
58
  def recent_pathology
59
- current_observation_set = Pathology.cast_patient(patient).current_observation_set
60
- current_observation_set&.values || Pathology::CurrentObservationSet.null_values_hash
59
+ current_observation_set = Pathology.cast_patient(patient).fetch_current_observation_set
60
+ current_observation_set ||= Pathology::NullObservationSet.new
61
+ current_observation_set.values_for_codes(codes_to_show)
62
+ end
63
+
64
+ def codes_to_show
65
+ code_group_name = "hd_session_form_recent"
66
+ @codes_to_show ||= Pathology::CodeGroup.descriptions_for_group(code_group_name).pluck(:code)
61
67
  end
62
68
 
63
69
  def patient_title
@@ -6,6 +6,12 @@
6
6
  as: :user_picker,
7
7
  collection: Renalware::User.ordered,
8
8
  wrapper: :horizontal_medium
9
+ = f.input :pre_post_hd,
10
+ collection: Renalware::Clinical::BodyComposition.pre_post_hds.collect{ |c| [c[0].titleize, c[0]] },
11
+ include_blank: "Not Applicable",
12
+ wrapper: :horizontal_small
13
+ = f.input :weight,
14
+ wrapper: :horizontal_tiny
9
15
  = f.input :overhydration,
10
16
  wrapper: :horizontal_small
11
17
  = f.input :volume_of_distribution,
@@ -6,6 +6,8 @@ table
6
6
  th.col-width-date= t(".assessed_on")
7
7
  th= t(".modality_description")
8
8
  th= t(".assessor")
9
+ th.col-width-tiny= t(".pre_post_hd")
10
+ th.col-width-tiny= t(".weight")
9
11
  th.col-width-tiny= t(".overhydration")
10
12
  th.col-width-tiny= t(".volume_of_distribution")
11
13
  th.col-width-tiny= t(".total_body_water")
@@ -29,6 +31,8 @@ table
29
31
  td= body_composition.assessed_on
30
32
  td= body_composition.modality_description&.name
31
33
  td= body_composition.assessor
34
+ td= body_composition.pre_post_hd&.titleize
35
+ td= body_composition.weight
32
36
  td= body_composition.overhydration
33
37
  td= body_composition.volume_of_distribution
34
38
  td= body_composition.total_body_water
@@ -4,13 +4,13 @@ tr class=("urgent" if bookmark.urgent?)
4
4
  td= default_patient_link(patient)
5
5
  td= patient.nhs_number
6
6
  td= patient.hospital_identifier
7
- td= I18n.l(patient.born_on)
7
+ td.show-for-medium-up= I18n.l(patient.born_on)
8
8
  td= patient.age
9
9
  td= patient.sex
10
- td= patient.current_modality
11
- td= bookmark.notes
12
- td= bookmark.tags
13
- td= I18n.l bookmark.created_at
10
+ td.col-width-medium-with-ellipsis= patient.current_modality
11
+ td.show-for-medium-up= bookmark.notes
12
+ td.show-for-medium-up= bookmark.tags
13
+ td= I18n.l bookmark.created_at&.to_date
14
14
  td.nowrap= render "renalware/patients/bookmarks/delete",
15
15
  bookmark: bookmark,
16
16
  link_text: "Remove"