renalware-core 2.0.0.pre.rc10 → 2.0.0.pre.rc11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/renalware/modules/_dashboard.scss +12 -3
  3. data/app/assets/stylesheets/renalware/modules/_letters.scss +0 -6
  4. data/app/assets/stylesheets/renalware/modules/_pathology.scss +5 -0
  5. data/app/assets/stylesheets/renalware/modules/_patients.scss +24 -0
  6. data/app/assets/stylesheets/renalware/modules/_users.scss +36 -0
  7. data/app/controllers/renalware/admin/cache_controller.rb +17 -0
  8. data/app/controllers/renalware/admin/users_controller.rb +1 -0
  9. data/app/controllers/renalware/api/token_authenticated_api_controller.rb +25 -0
  10. data/app/controllers/renalware/api/v1/patients/patients_controller.rb +17 -0
  11. data/app/controllers/renalware/concerns/devise_controller_methods.rb +4 -1
  12. data/app/controllers/renalware/devise/sessions_controller.rb +0 -29
  13. data/app/controllers/renalware/pathology/historical_observation_results_controller.rb +8 -10
  14. data/app/controllers/renalware/pathology/recent_observation_results_controller.rb +8 -10
  15. data/app/controllers/renalware/renal/aki_alerts_controller.rb +6 -1
  16. data/app/controllers/renalware/reporting/audits_controller.rb +1 -1
  17. data/app/controllers/renalware/system/errors_controller.rb +3 -1
  18. data/app/controllers/renalware/transplants/wait_lists_controller.rb +12 -3
  19. data/app/models/renalware/admin.rb +4 -0
  20. data/app/models/renalware/api.rb +6 -0
  21. data/app/models/renalware/clinics/current_observations.rb +1 -0
  22. data/app/models/renalware/events/event_query.rb +1 -1
  23. data/app/models/renalware/feeds/hl7_message.rb +16 -1
  24. data/app/models/renalware/hd/mdm_patients_query.rb +1 -1
  25. data/app/models/renalware/letters/pdf_letter_cache.rb +5 -1
  26. data/app/models/renalware/medications/prescription.rb +1 -0
  27. data/app/models/renalware/pathology/create_observations_grouped_by_date_table.rb +39 -0
  28. data/app/models/renalware/pathology/observation.rb +1 -1
  29. data/app/models/renalware/pathology/observation_digest.rb +12 -0
  30. data/app/models/renalware/pathology/observation_requests_attributes_builder.rb +2 -1
  31. data/app/models/renalware/pathology/observations_grouped_by_date_query.rb +91 -0
  32. data/app/models/renalware/pathology/observations_grouped_by_date_table.rb +59 -0
  33. data/app/models/renalware/pd/mdm_patients_query.rb +3 -1
  34. data/app/models/renalware/renal/aki_alert.rb +1 -0
  35. data/app/models/renalware/reporting/audit.rb +2 -0
  36. data/app/models/renalware/system/update_user.rb +0 -1
  37. data/app/models/renalware/transplants/registrations/wait_list_query.rb +9 -5
  38. data/app/models/renalware/transplants.rb +2 -0
  39. data/app/models/renalware/user.rb +26 -8
  40. data/app/policies/renalware/admin/cache_policy.rb +15 -0
  41. data/app/presenters/renalware/admin/users/summary_part.rb +23 -0
  42. data/app/presenters/renalware/events/summary_part.rb +15 -8
  43. data/app/presenters/renalware/hd/mdm_presenter.rb +1 -1
  44. data/app/presenters/renalware/letters/summary_part.rb +4 -4
  45. data/app/presenters/renalware/mdm_presenter.rb +18 -9
  46. data/app/presenters/renalware/pathology/historical_observation_results/html_table_view.rb +15 -1
  47. data/app/presenters/renalware/problems/summary_part.rb +4 -6
  48. data/app/presenters/renalware/summary_part.rb +5 -4
  49. data/app/views/renalware/admin/cache/show.html.slim +20 -0
  50. data/app/views/renalware/admin/feeds/files/index.html.slim +0 -1
  51. data/app/views/renalware/admin/users/_summary_part.html.slim +3 -0
  52. data/app/views/renalware/admin/users/index.html.slim +25 -8
  53. data/app/views/renalware/admissions/_summary_part.html.slim +11 -12
  54. data/app/views/renalware/api/v1/patients/patients/show.json.jbuilder +17 -0
  55. data/app/views/renalware/dashboard/dashboards/_content.html.slim +3 -0
  56. data/app/views/renalware/devise/registrations/edit.html.slim +2 -0
  57. data/app/views/renalware/events/events/_summary_part.html.slim +4 -5
  58. data/app/views/renalware/letters/_summary_part.html.slim +15 -14
  59. data/app/views/renalware/letters/letters/_table.html.slim +2 -1
  60. data/app/views/renalware/mdm/_pathology.html.slim +4 -2
  61. data/app/views/renalware/medications/_summary_part.html.slim +1 -1
  62. data/app/views/renalware/navigation/_renal.html.slim +1 -1
  63. data/app/views/renalware/navigation/_renalware_admin.html.slim +1 -0
  64. data/app/views/renalware/pathology/_navigation.html.slim +1 -1
  65. data/app/views/renalware/pathology/historical_observation_results/_table.html.slim +13 -0
  66. data/app/views/renalware/pathology/historical_observation_results/index.html.slim +3 -3
  67. data/app/views/renalware/pathology/observation_requests/_table.html.slim +1 -1
  68. data/app/views/renalware/pathology/recent_observation_results/_table.html.slim +18 -0
  69. data/app/views/renalware/pathology/recent_observation_results/index.html.slim +3 -3
  70. data/app/views/renalware/patients/clinical_summaries/show.html.slim +7 -4
  71. data/app/views/renalware/patients/side_menu/_actions.html.slim +26 -21
  72. data/app/views/renalware/problems/problems/_problem.html.slim +3 -0
  73. data/app/views/renalware/problems/problems/_summary_part.html.slim +16 -5
  74. data/app/views/renalware/renal/aki_alerts/edit.html.slim +4 -0
  75. data/app/views/renalware/renal/aki_alerts/index.html.slim +8 -0
  76. data/app/views/renalware/transplants/mdm/_pathology_cmvdna.html.slim +3 -1
  77. data/app/views/renalware/transplants/wait_lists/show.html.slim +2 -2
  78. data/config/initializers/inflections.rb +1 -0
  79. data/config/locales/renalware/mdm.yml +2 -2
  80. data/config/locales/renalware/renal/aki_alerts.en.yml +4 -0
  81. data/config/routes.rb +22 -5
  82. data/config/{schedule.rb → schedule.rb.example} +0 -0
  83. data/db/functions/update_current_observation_set_from_trigger_v03.sql +93 -0
  84. data/db/functions/update_current_observation_set_from_trigger_v04.sql +93 -0
  85. data/db/migrate/20180202184954_create_view_pathology_observation_digests.rb +5 -0
  86. data/db/migrate/20180206225525_update_fn_update_current_observation_set_from_trigger.rb +9 -0
  87. data/db/migrate/20180208150629_add_authentication_token_to_users.rb +5 -0
  88. data/db/migrate/20180213124203_add_cancelled_to_pathology_observations.rb +9 -0
  89. data/db/migrate/20180213125734_update_fn_update_current_obs_set_trgger.rb +9 -0
  90. data/db/migrate/20180214124317_add_cols_to_aki_alerts.rb +9 -0
  91. data/db/migrate/20180216132741_disable_some_audits.rb +8 -0
  92. data/db/seeds/default/transplants/transplant_donor_stages.rb +2 -2
  93. data/db/views/pathology_observation_digests_v01.sql +20 -0
  94. data/lib/renalware/version.rb +1 -1
  95. data/spec/factories/pathology/observation_requests.rb +31 -0
  96. data/spec/support/login_macros.rb +4 -2
  97. metadata +37 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 020dc0513f4623aebbe15a14363cd05525eaed0405a8deacb057dcd585e39114
4
- data.tar.gz: 89f150581ec3fb071333af4682c971c0564cf71f0c6284d20941e2a0839c14e3
3
+ metadata.gz: 57dc3391a5faf70954e6f6ce4e04ccfa81b4089bcccd51c9b1e8f826554325e8
4
+ data.tar.gz: 153f6ce8fd7006f45d9d1a2349c0d365edcfb0effe190457c808420c921fd93f
5
5
  SHA512:
6
- metadata.gz: 92079d58e50dcc7a2475ee28850baaaf804e129b15accef98d542fcca39bb5da6691135f307f4098636ca134ad8673c6c139aad6cb656eb90451dced4f4cacdc
7
- data.tar.gz: 994ec364d3c9bed98075d17742390327685c5b46465ab016531c25db804e4500709fc4a6907b8e99eb45857785884db3638097a54dae9e0c88836595b369ec3c
6
+ metadata.gz: 0473fadd4cee90562f9bb1cf86a77510fd7642fb3a25be9aff729d9b3e02ed76064eb0ee213852879c0cfabb00a819b7f6a3f3931ea26fbc26539a056a2063ec
7
+ data.tar.gz: 8807fe96b88c7930d1b931fb81c4571394538d7759b27c94695157b2956033063da7a9176976e977c48632ba47391a2d32f43fcf44f85c2e52b5b01e56f2775b
@@ -1,10 +1,19 @@
1
1
  .dashboard-content {
2
2
  margin-top: 10px;
3
3
 
4
- p.empty-section {
4
+ h3 {
5
+ font-size: 1.1rem;
6
+ font-weight: bold;
7
+ }
8
+
9
+ p {
5
10
  padding-left: 0;
6
- color: $dashboard-muted-color;
7
- clear: both;
11
+
12
+ &.empty-section {
13
+ padding-left: 0;
14
+ color: $dashboard-muted-color;
15
+ clear: both;
16
+ }
8
17
  }
9
18
  }
10
19
 
@@ -1,9 +1,3 @@
1
- // When the letter summary part is included in the clinical summary, make it 12 cols wide.
2
- .page--clinical_summaries {
3
- .summary-part--letters {
4
- @include grid-column(12);
5
- }
6
- }
7
1
 
8
2
  table.contacts tbody td{
9
3
  vertical-align: top;
@@ -23,6 +23,11 @@ table#observations {
23
23
  border: 1px solid #ddd;
24
24
  font-size: 0.8rem;
25
25
  }
26
+
27
+ &.transposed {
28
+ tr { display: block; float: left; }
29
+ th, td { display: block; }
30
+ }
26
31
  }
27
32
 
28
33
  .current-observations {
@@ -377,3 +377,27 @@ form {
377
377
  }
378
378
  }
379
379
  }
380
+
381
+ .page--clinical_summaries {
382
+ .summary-part--letters {
383
+ @include grid-column(12);
384
+ }
385
+
386
+ .summary-part--problems,
387
+ .summary-part--prescriptions {
388
+ @include grid-column(12);
389
+
390
+ @media #{$large-up} {
391
+ @include grid-column(6);
392
+ }
393
+ }
394
+
395
+ .summary-part--events,
396
+ .summary-part--admissions {
397
+ @include grid-column(12);
398
+
399
+ @media #{$xxlarge-up} {
400
+ @include grid-column(6);
401
+ }
402
+ }
403
+ }
@@ -0,0 +1,36 @@
1
+ .admin-users {
2
+ td.approved {
3
+ text-align: center;
4
+ color: $nhs-light-green;
5
+
6
+ .unapproved {
7
+ color: $nhs-red;
8
+ font-weight: bold;
9
+ }
10
+ }
11
+
12
+ .tag {
13
+ color: $white;
14
+ padding: .05rem .5rem .15rem .5rem;
15
+ border: none;
16
+ margin: .1rem .3rem .2rem 0;
17
+ border-radius: .3rem;
18
+ background-color: $mid-grey;
19
+
20
+ &.devops {
21
+
22
+ }
23
+
24
+ &.clinical {
25
+ background-color: $nhs-green;
26
+ }
27
+
28
+ &.admin {
29
+ background-color: $nhs-blue;
30
+ }
31
+
32
+ &.super_admin {
33
+ background-color: $nhs-red;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,17 @@
1
+ require_dependency "renalware/admin"
2
+
3
+ module Renalware
4
+ module Admin
5
+ class CacheController < BaseController
6
+ def show
7
+ authorize [:renalware, :admin, :cache], :show?
8
+ end
9
+
10
+ def destroy
11
+ authorize [:renalware, :admin, :cache], :destroy?
12
+ Rails.cache.clear
13
+ redirect_to admin_cache_path, notice: "Cache successfully cleared"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -6,6 +6,7 @@ module Renalware
6
6
  query = params.fetch(:q, {})
7
7
  query[:s] ||= "family_name"
8
8
  search = User
9
+ .includes(:roles)
9
10
  .where.not(username: :systemuser)
10
11
  .search(query)
11
12
  users = search.result(distinct: true).page(page).per(per_page)
@@ -0,0 +1,25 @@
1
+ #
2
+ # From https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
3
+ #
4
+ module Renalware
5
+ module API
6
+ class TokenAuthenticatedApiController < ApplicationController
7
+ before_action :authenticate_user_from_token!
8
+ before_action :authenticate_user! # fallback
9
+
10
+ private
11
+
12
+ def authenticate_user_from_token!
13
+ username = params[:username].presence
14
+ user = username && User.find_by(username: username)
15
+
16
+ # Notice how we use Devise.secure_compare to compare the token
17
+ # in the database with the token given in the params, mitigating
18
+ # timing attacks.
19
+ if user && Devise.secure_compare(user.authentication_token, params[:token])
20
+ sign_in user, store: false
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ require_dependency "renalware/patients"
2
+ require_dependency "renalware/api"
3
+
4
+ module Renalware
5
+ module API
6
+ module V1
7
+ module Patients
8
+ class PatientsController < TokenAuthenticatedApiController
9
+ def show
10
+ patient = Patient.find_by!(local_patient_id: params[:id])
11
+ render locals: { patient: patient }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -37,6 +37,7 @@ module Renalware
37
37
  devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :password, :remember_me])
38
38
  end
39
39
 
40
+ # rubocop:disable Metrics/MethodLength
40
41
  def configure_account_update_parameters
41
42
  devise_parameter_sanitizer.permit(
42
43
  :account_update,
@@ -49,10 +50,12 @@ module Renalware
49
50
  :password_confirmation,
50
51
  :current_password,
51
52
  :professional_position,
52
- :signature
53
+ :signature,
54
+ :with_extended_validation
53
55
  ]
54
56
  )
55
57
  end
58
+ # rubocop:enable Metrics/MethodLength
56
59
  end
57
60
  end
58
61
  end
@@ -17,35 +17,6 @@ module Renalware
17
17
  max_duration_has_passed ? dashboard_path : super
18
18
  end
19
19
 
20
- # Important note: Since Devise 4.4.0 the gem's SessionsController#create will do an implicit
21
- # check to see if the resource (User signing-in) is valid (or rather, I think it checks for the
22
- # presence of anything in resource.errors). If the user is not valid it now
23
- # skips the redirect to the path specified in .after_sign_in_path_for.
24
- # The result for us was that the user _could_ log on, but they stayed on the login screen
25
- # (the appearance of the menu indicated they were in fact logged in). The redirect was not
26
- # happening because the user was invalid (ie there were validation errors and
27
- # user.valid? == false).
28
- # The reason for the validation errors is that we have some conditional validation in User;
29
- # for instance only validate #signature during an update, not on
30
- # a create - after all #signature is something they set up later in their 'profile'.
31
- # So what we have to do here is stop Devise from thinking the user in invalid by skipping
32
- # validations.
33
- # See https://github.com/plataformatec/devise/issues/4742#issuecomment-355154023
34
- # Note that ideally we should move signature, professional position etc to a Profile model
35
- # and then we could remove the conditional validation.
36
- def create
37
- super do |resource|
38
- # This first line clears any existing errors that prevent Devise redirecting on successful
39
- # login. It does not negate any password validations however. Its just a workaround to get
40
- # the redirect to work.
41
- resource.errors.clear
42
- # This second line means resource.valid? returns true ie conditional on: :update validations
43
- # are skipped. This line is not necessary (clearing the error above actually solves the
44
- # problem) but does no harm and is belt and braces.
45
- resource.skip_validation = true
46
- end
47
- end
48
-
49
20
  private
50
21
 
51
22
  def last_sign_in_at
@@ -3,19 +3,17 @@ require_dependency "renalware/pathology"
3
3
  module Renalware
4
4
  module Pathology
5
5
  class HistoricalObservationResultsController < Pathology::BaseController
6
+ include Renalware::Concerns::Pageable
6
7
  before_action :load_patient
7
8
 
8
9
  def index
9
- table_view = HistoricalObservationResults::HTMLTableView.new(view_context)
10
- presenter = HistoricalObservationResults::Presenter.new
11
- service = ViewObservationResults.new(@patient.observations, presenter)
12
- service.call(params)
13
-
14
- render :index, locals: {
15
- rows: presenter.view_model,
16
- paginator: presenter.paginator,
17
- table: table_view
18
- }
10
+ observations_table = CreateObservationsGroupedByDateTable.new(
11
+ patient: patient,
12
+ observation_descriptions: RelevantObservationDescription.all,
13
+ page: page || 1,
14
+ per_page: 25
15
+ ).call
16
+ render :index, locals: { table: observations_table }
19
17
  end
20
18
  end
21
19
  end
@@ -3,19 +3,17 @@ require_dependency "renalware/pathology"
3
3
  module Renalware
4
4
  module Pathology
5
5
  class RecentObservationResultsController < Pathology::BaseController
6
+ include Renalware::Concerns::Pageable
6
7
  before_action :load_patient
7
8
 
8
9
  def index
9
- table_view = RecentObservationResults::HTMLTableView.new(view_context)
10
- presenter = RecentObservationResults::Presenter.new
11
- service = ViewObservationResults.new(@patient.observations, presenter)
12
- service.call(params)
13
-
14
- render :index, locals: {
15
- rows: presenter.view_model,
16
- paginator: presenter.paginator,
17
- table: table_view
18
- }
10
+ observations_table = CreateObservationsGroupedByDateTable.new(
11
+ patient: patient,
12
+ observation_descriptions: RelevantObservationDescription.all,
13
+ page: page || 1,
14
+ per_page: per_page || 100
15
+ ).call
16
+ render :index, locals: { table: observations_table }
19
17
  end
20
18
  end
21
19
  end
@@ -37,7 +37,12 @@ module Renalware
37
37
  end
38
38
 
39
39
  def aki_alert_params
40
- params.require(:renal_aki_alert).permit(:notes, :action_id, :hotlist, :hospital_ward_id)
40
+ params
41
+ .require(:renal_aki_alert)
42
+ .permit(
43
+ :notes, :action_id, :hotlist, :hospital_ward_id,
44
+ :max_cre, :cre_date, :max_aki, :aki_date
45
+ )
41
46
  end
42
47
  end
43
48
  end
@@ -7,7 +7,7 @@ module Renalware
7
7
 
8
8
  def index
9
9
  authorize Audit, :index?
10
- render locals: { audits: present(Audit.all, AuditPresenter) }
10
+ render locals: { audits: present(Audit.enabled, AuditPresenter) }
11
11
  end
12
12
 
13
13
  def show
@@ -13,7 +13,9 @@ module Renalware
13
13
 
14
14
  def generate_test_internal_server_error
15
15
  raise "This is an intentionally raised error - please ignore it. " \
16
- "It is used only to test system integration"
16
+ "It is used only to test system integration. " \
17
+ "The rest of this messages is padding to test that the title is truncated to 256 " \
18
+ "characters#{'.' * 100}"
17
19
  end
18
20
  end
19
21
  end
@@ -6,19 +6,28 @@ module Renalware
6
6
  include Renalware::Concerns::Pageable
7
7
 
8
8
  def show
9
- query = Registrations::WaitListQuery.new(quick_filter: params[:filter], q: params[:q])
10
9
  registrations = query.call.page(page).per(per_page || 50)
11
10
  authorize registrations
12
11
  render locals: {
13
12
  path_params: path_params,
14
13
  registrations: registrations,
15
- q: query.search }
14
+ q: query.search
15
+ }
16
16
  end
17
17
 
18
18
  private
19
19
 
20
+ def query
21
+ @query ||= begin
22
+ Registrations::WaitListQuery.new(
23
+ named_filter: params[:named_filter],
24
+ q: params[:q]
25
+ )
26
+ end
27
+ end
28
+
20
29
  def path_params
21
- params.permit([:controller, :action, :filter])
30
+ params.permit([:controller, :action, :named_filter])
22
31
  end
23
32
  end
24
33
  end
@@ -0,0 +1,4 @@
1
+ module Renalware
2
+ module Admin
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Renalware
2
+ module API
3
+ module V1
4
+ end
5
+ end
6
+ end
@@ -3,6 +3,7 @@ require "attr_extras"
3
3
 
4
4
  module Renalware
5
5
  module Clinics
6
+ # TODO: Move this to a view?
6
7
  class CurrentObservations
7
8
  NULL_DATE = nil
8
9
  pattr_initialize :patient
@@ -8,7 +8,7 @@ module Renalware
8
8
  def initialize(patient:, query: {})
9
9
  @query = query
10
10
  @patient = patient
11
- @query[:s] = "created_at DESC" if @query[:s].blank?
11
+ @query[:s] = "datetime DESC" if @query[:s].blank?
12
12
  end
13
13
 
14
14
  def call
@@ -51,6 +51,7 @@ module Renalware
51
51
  end
52
52
 
53
53
  class Observation < SimpleDelegator
54
+ attr_reader :cancelled
54
55
  alias_attribute :date_time, :observation_date
55
56
  alias_attribute :value, :observation_value
56
57
 
@@ -60,7 +61,21 @@ module Renalware
60
61
 
61
62
  # TODO: Implement comment extraction
62
63
  def comment
63
- ""
64
+ @comment || ""
65
+ end
66
+
67
+ # Some messages may come through with result text like
68
+ # ##TEST CANCELLED## Insufficient specimen received
69
+ # in which case replace with something more concise.
70
+ # We could save the actual message somewhere
71
+ def observation_value
72
+ if super.upcase.at("CANCELLED")
73
+ @comment = super
74
+ @cancelled = true
75
+ ""
76
+ else
77
+ super
78
+ end
64
79
  end
65
80
 
66
81
  # Because some units of measurement, such as 10^12/L for WBC, contain a caret, the caret
@@ -4,7 +4,7 @@ module Renalware
4
4
  include ModalityScopes
5
5
  include PatientPathologyScopes
6
6
  MODALITY_NAMES = "HD".freeze
7
- DEFAULT_SEARCH_PREDICATE = "hgb_date".freeze
7
+ DEFAULT_SEARCH_PREDICATE = "hgb_date desc".freeze
8
8
  attr_reader :q, :relation
9
9
 
10
10
  def initialize(relation: HD::Patient.all, q:)
@@ -36,8 +36,12 @@ module Renalware
36
36
  end
37
37
 
38
38
  # Note the letter must be a LetterPresenter which has a #to_html method
39
+ # The to_html method should (and does on the LetterPrsenter class) render the complete
40
+ # html including surrounding layout with inline css and images. This way if the layout changes
41
+ # or the image is changed for example, the cache for the pdf is no longer valid and a new
42
+ # key and cache entry will be created.
39
43
  def self.cache_key_for(letter)
40
- "#{letter.id}-#{Digest::MD5.hexdigest(letter.to_html)}"
44
+ "letter:pdf:#{letter.id}:#{Digest::MD5.hexdigest(letter.to_html)}"
41
45
  end
42
46
  end
43
47
  end
@@ -47,6 +47,7 @@ module Renalware
47
47
  scope :ordered, -> { includes(:drug).order("drugs.name") }
48
48
  scope :with_medication_route, -> { includes(:medication_route) }
49
49
  scope :with_drugs, -> { includes(drug: :drug_types) }
50
+ scope :with_classifications, -> { includes(drug: :classifications) }
50
51
  scope :with_termination, -> { includes(termination: [:created_by]) }
51
52
  scope :current, lambda { |date = Date.current|
52
53
  eager_load(:termination)
@@ -0,0 +1,39 @@
1
+ require_dependency "renalware/pathology"
2
+
3
+ module Renalware
4
+ module Pathology
5
+ class CreateObservationsGroupedByDateTable
6
+ attr_reader :patient, :observation_descriptions, :options
7
+
8
+ def initialize(patient:, observation_descriptions:, **options)
9
+ @patient = patient
10
+ @observation_descriptions = observation_descriptions
11
+ @options = options
12
+ end
13
+
14
+ def call
15
+ if observation_descriptions.blank?
16
+ raise(ArgumentError, "No observation_descriptions supplied")
17
+ end
18
+ create_observations_table
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_grouped_observations
24
+ ObservationsGroupedByDateQuery.new(
25
+ patient: patient,
26
+ observation_descriptions: observation_descriptions,
27
+ **options
28
+ )
29
+ end
30
+
31
+ def create_observations_table
32
+ ObservationsGroupedByDateTable.new(
33
+ relation: fetch_grouped_observations,
34
+ observation_descriptions: observation_descriptions
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,7 +7,7 @@ module Renalware
7
7
  belongs_to :description, class_name: "ObservationDescription"
8
8
 
9
9
  validates :description, presence: true
10
- validates :result, presence: true
10
+ validates :result, presence: true, unless: ->(obs) { obs.cancelled? }
11
11
  validates :observed_at, presence: true
12
12
 
13
13
  scope :ordered, -> { order(observed_at: :desc) }
@@ -0,0 +1,12 @@
1
+ require_dependency "renalware/pathology"
2
+
3
+ module Renalware
4
+ module Pathology
5
+ # Backed by a view, returns pathology results grouped by the day the observation
6
+ # was made. Please check if this is used in code - it may not be,
7
+ # However the underlying view is a useful way of investigating a patient's results
8
+ # so please do not remove the view.
9
+ class ObservationDigest < ApplicationRecord
10
+ end
11
+ end
12
+ end
@@ -76,7 +76,8 @@ module Renalware
76
76
  description_id: observation_description.id,
77
77
  observed_at: parse_time(observation.date_time),
78
78
  result: observation.value,
79
- comment: observation.comment
79
+ comment: observation.comment,
80
+ cancelled: observation.cancelled
80
81
  }
81
82
  end.compact
82
83
  end
@@ -0,0 +1,91 @@
1
+ require_dependency "renalware/pathology"
2
+ require "attr_extras"
3
+
4
+ module Renalware
5
+ module Pathology
6
+ # A custom relation-like object, implementing a kaminiari-like pagination interface.
7
+ # Its a query object but means to be used like a relation. If passed into a view you can
8
+ # do = paginate(relation).
9
+ # See ObservationsGroupedByDateTable for intended usage.
10
+ #
11
+ # .all() returns a jsonb hash of OBX results for each day a patient had an observation.
12
+ # Only returns observations whose code matches observation_descriptions
13
+ #
14
+ # Example usage:
15
+ # observation_descriptions = ..
16
+ # rows = ObservationsGroupedByDateQuery.new(
17
+ # patient: patient,
18
+ # observation_descriptions: observation_descriptions,
19
+ # per_page: 50,
20
+ # page: 1
21
+ # )
22
+ #
23
+ # Example output:
24
+ # patient_id observation_date observations
25
+ # ------------------------------------------
26
+ # 1 2018-02-02 {"CYA": "14"}
27
+ # 1 2016-06-15 {"CMVDNA": "0.10"}
28
+ # 1 2016-03-15 {"NA": "137", "TP": "74", "ALB": "48", "ALP": "71", ...
29
+ # 1 2016-02-29 {"NA": "136", "TP": "78", "ALB": "47", "ALP": "71", ...
30
+ #
31
+ #
32
+ class ObservationsGroupedByDateQuery
33
+ attr_reader :patient, :observation_descriptions, :page, :limit
34
+ alias :current_page :page
35
+ alias :limit_value :limit
36
+
37
+ def initialize(patient:, observation_descriptions:, page: 1, per_page: 50)
38
+ @patient = patient
39
+ @observation_descriptions = observation_descriptions
40
+ @page = Integer(page)
41
+ @limit = Integer(per_page)
42
+ end
43
+
44
+ def total_pages
45
+ result = conn.execute(to_count_sql)
46
+ total = result.getvalue(0, 0)
47
+ (total.to_f / limit).ceil
48
+ end
49
+
50
+ def offset
51
+ (page - 1) * limit
52
+ end
53
+
54
+ def all
55
+ conn.execute(to_paginated_sql)
56
+ end
57
+
58
+ private
59
+
60
+ def to_sql
61
+ <<-SQL.squish
62
+ select obs_req.patient_id, cast(observed_at as date) as observed_on,
63
+ jsonb_object_agg(obs_desc.code, obs.result) results
64
+ from pathology_observations obs
65
+ inner join pathology_observation_requests obs_req on obs.request_id = obs_req.id
66
+ inner join pathology_observation_descriptions obs_desc on obs.description_id = obs_desc.id
67
+ where patient_id = #{conn.quote(patient.id)}
68
+ and obs.description_id in (#{observation_description_ids})
69
+ group by patient_id, observed_on
70
+ order by patient_id asc, observed_on desc
71
+ SQL
72
+ end
73
+
74
+ def to_count_sql
75
+ "select count(*) from (#{to_sql}) as query"
76
+ end
77
+
78
+ def to_paginated_sql
79
+ to_sql + " limit #{limit} offset #{offset}"
80
+ end
81
+
82
+ def conn
83
+ ActiveRecord::Base.connection
84
+ end
85
+
86
+ def observation_description_ids
87
+ observation_descriptions.map(&:id).join(",")
88
+ end
89
+ end
90
+ end
91
+ end