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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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