In this section you will extend the application to create and store data by adding notes to exams in the worklist.

Prerequisites

Setting up a new schema and connection

Overview

Bridge stores clinical data in a PostgreSQL relational database. PostgreSQL databases have named logical namespaces called schema. Queries can only be made in a single database, but queries for can traverse multiple schemas. The core clinical data model in Bridge is in the public schema (public is the default schema name in a PostgreSQL database). Applications can have their own schema for storing application-specific information, such as configurations and user preferences. This tutorial application will create and use a schema to store notes.

Best Practice While in theory an application could be designed to not require its own schema, in practice you will need to make the authorization for an application configurable for an app to be approved for distribution. At a minimum, this requires a configuration table for the application to save these settings.

Database setup

To simplify the process of creating a new role and schema for the application, you should create a series of SQL and bash scripts. The packaging requirements for distributing an app in Part 11 of the tutorial will provide further details and background.

Start by creating a SQL script to define the new role, schema, and grant privileges. Make the directory db_scripts in the root of the application, then create db_scripts/create.sql:

create user railsdevelopertutorial password '2893ourj8923urjl';
create schema AUTHORIZATION railsdevelopertutorial;
revoke all on all tables in schema public from railsdevelopertutorial;
grant usage on schema public to railsdevelopertutorial;
grant select on all tables in schema public to railsdevelopertutorial;
alter user railsdevelopertutorial set search_path to railsdevelopertutorial,public;

Now we want to build the table definitions and any indexes and put that in db_scripts/schema.sql. We want to provide a way to create a free text note that's associated with a user and a rad exam. Here is what our schema file looks like:

CREATE SEQUENCE rails_developer_tutorial_notes_id_seq
        START WITH 1
        INCREMENT BY 1
        NO MAXVALUE
        NO MINVALUE
        CACHE 1;

CREATE TABLE rails_developer_tutorial_notes (
        id bigint DEFAULT nextval('rails_developer_tutorial_notes_id_seq'::regclass) NOT NULL,
        employee_id bigint NOT NULL,
        rad_exam_id bigint NOT NULL,
        note text NOT NULL,
        created timestamp with time zone NOT NULL
);

ALTER TABLE rails_developer_tutorial_notes
        ADD CONSTRAINT rails_developer_tutorial_notes_id PRIMARY KEY (id);

Best Practice - The columns rad_exam_id and employee_id (both type bigint) are foreign keys to the clinical data in the public schema. DO NOT store patient MRNs, accession numbers, or other clinical information that could substitute for a foreign key. Changes to the clinical data caused by an event like a patient merge would not be reflected in an application copying clinical data, whereas applications using foreign keys will be maintained by the platform.

Setup your config/database.yml file to connect to the database. The development and production environments will be different.

development:
  adapter: postgresql
  host: localhost
  username: railsdevelopertutorial
  password: 2893ourj8923urjl
  database: harbinger

production:
  adapter: postgresql
  jndi: java:/jdbc/railsdevelopertutorial

Though we will not need to create a wildfly data source for development you will need one as part of your packaged application for production. Edit ./db_scripts/create-wildfly-datasource.sh and add the following:

$CLI -c "data-source add --jndi-name=java:/jdbc/railsdevelopertutorial --name=railsdevelopertutorialPool --connection-url=jdbc:postgresql://localhost:5432/harbinger --driver-name=postgres --user-name=railsdevelopertutorial --password=2893ourj8923urjl --max-pool-size=8 --min-pool-size=2 --idle-timeout-minutes=5 --flush-strategy=Gracefully"

Now build a ./db_scripts/install.sh script to use the files you've created to install your database.

#!/bin/bash
echo 'Creating schema, user, tables, etc.'
psql -e -f create.sql -U postgres harbinger
export PGPASSWORD=2893ourj8923urjl
psql -e -f schema.sql -U railsdevelopertutorial harbinger

Run the script ./db_scripts/install.sh to install your database.

Rails setup

Earlier in the tutorial, the Rails database connection was disabled because the application only used the SDK to retrieve clinical data. Now that the application has its own schema, you will configure Rails to be able to access this schema. Edit the Gemfile to add the required adapter gems:

gem "activerecord-jdbc-adapter", "~> 1.3.8", platform: :jruby
gem "activerecord-jdbcpostgresql-adapter", platform: :jruby
gem 'activerecord-bogacs', :require => 'active_record/bogacs'

Application initialization will need to change to replace the require rails/... imperatives with a single require. The GlassFish JDBC pooling requires the use of bogacs to prevent ActiveRecord pooling. Use the FalsePool provided by bogacs to prevent nested connection pools. Edit application.rb:

# Load the SDK when in development mode
# The SDK is a shared library when in production
if ENV['DEV_MODE'] == "on" then
  require File.join(File.dirname(__FILE__),'..','sdk','harbinger-sdk-standalone.jar')
end

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module RailsDeveloperTutorial
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

    # Do not swallow errors in after_commit/after_rollback callbacks.
    config.active_record.raise_in_transactional_callbacks = true
  end
end

# As this never leaves the GF pooling environment make sure this always runs
pool_class = ActiveRecord::Bogacs::FalsePool
ActiveRecord::ConnectionAdapters::ConnectionHandler.connection_pool_class = pool_class

Note that the rails_in_transaction-callbacks configuration was uncommented. Additionally, edit config/environments/development.rb to uncomment config.active_record.migration_error = :page_load.

Create config/database.yml to define how to connect to the GlassFish JDBC pools:

development:
  adapter: postgresql
  jndi: jdbc/railsdevelopertutorial

production:
  adapter: postgresql
  jndi: jdbc/railsdevelopertutorial

This tutorial does not cover testing, but if you plan to implement test, you would also add a test database configuration in this file.

Creating the clinical notes model and controller

With the database connection configured, models can be created. Build a Note model and that uses the rails_developer_tutorial_notes table by creating app/models/note.rb:

class Note < ActiveRecord::Base
  self.table_name = "rails_developer_tutorial_notes"

  scope :for_exam, lambda {|exam| where(["rad_exam_id = ?",exam.getId]) }

  def employee(entity_manager=nil)
    @employee ||= Java::HarbingerSdkData::Employee.withId(self.employee_id,entity_manager)
  end

end

An ActiveRecord model is a class that inherits from ActiveRecord::Base, typically with a camel case version of the table name. Alternatively, to avoid a very long name for this model, you can set the table name manually. There are two functions, the first is a query scope to find any notes associated with an exam. The second is an instance method to get the employee record from the SDK using a note object.

Build a controller to handle the form post that will create a note. Create app/controllers/notes_controller.rb:

class NotesController < ApplicationController
  before_filter :general_authentication
  before_filter :get_entity_manager
  after_filter :close_entity_manager

  def create
    # Get the logged in employee
    employee = Java::HarbingerSdkData::Employee.withUserName(session[:username],@entity_manager)
    # Create our ActiveRecord Object
    note = Note.new({ employee_id: employee.getId,
                      rad_exam_id: params[:rad_exam_id],
                      note: params[:note],
                      created: Time.now()
                    })
    # Save it to the database
    if note.save
      # Render our notes/note partial with some local variables
      render :partial => "notes/note", locals: {note: note, employee: employee}
    else
      # Render our error status if it fails to save
      render :status => 500, :text => "Failed to save the note"
    end
  end

end

Best Practice - While this function works, it is a trivial example that would not be suitable for a production app: there is no form validation and minimal error handling. Given the subject of the tutorial is learning the tools to develop with Bridge, error handling and form validation are not implemented for the sake of brevity and focus.

Create a new route in our config/routes.rb file and build the view (partial):

Edit config/routes.rb:

post 'notes/create' => "notes#create"

Create the new view directory app/views/notes and create app/views/notes/_note.html.erb:

<div class="note">
  <h4><%= note.employee.name %> - <%= formatd(note.created) %></h4>
  <p><%= note.note %></p>
</div>

With the Notes model, controller, route, and partial view completed, notes can be added to the existing worklist. There are many design approaches to consider for a clinical notes area: consider a modal, sidebar, or integration with the existing table. Favoring simplicity, the tutorial will integrate the note into the table. Edit app/views/worklist/index.html:

<% content_for(:javascript_includes, javascript_include_tag("pages/worklist.js")) %>
<div class="container-fluid">
  <div class="row">
    <div class="col-xs-12">
      <h1>Ready to read worklist</h1>
      <table class="table table-bordered">
    <thead>
      <tr>
        <th>Accession #</th>
        <th>Patient MRN</th>
        <th>Patient Name</th>
        <th>Completed At</th>
        <th>Age</th>
        <th>Notes</th>
        <% authorized(["radiologist","ai-staff"]) do %>
        <th></th>
        <% end %>
      </tr>
    </thead>
    <tbody>
      <% @exams.each do |exam| %>
      <tr class="data-row">
        <td><%= exam.accession %></td>
        <td><%= exam.patientMrn.mrn %></td>
        <td><%= exam.patientMrn.patient.name %></td>
        <td><%= formatd(exam.radExamTime.endExam) %></td>
        <td><%= age(exam.radExamTime.endExam) %></td>
        <% notes = Note.for_exam(exam) %>
        <td><span class="badge <%= 'highlight' if notes.size > 0 %>"><%= notes.size %></span></td>
        <% authorized(["radiologist","ai-staff"]) do %>
        <td class="launch-group">
          <div class="data">
        <div class="integration-definition"><%= exam.imageViewer %></div>
        <div class="exam-object"><%= exam.integrationJson %></div>
          </div>
          <button class="btn btn-xs btn-primary <%= 'disabled' if exam.imageViewer.blank? %>">View Images</button>
        </td>
            <% end %>
      </tr>
      <tr class="note-row">
        <td colspan="<%= authorized?(["radiologist","ai-staff"]) ? 7 : 6 %>">
          <div class="notes">
        <% notes.each do |note| %>
        <%= render partial: "notes/note", locals: {note: note, employee: note.employee(@entity_manager)} %>
        <% end %>
          </div>
          <%= form_tag({controller: :notes, action: :create}, {method: :post, class: "form"}) do %>
          <input type="hidden" name="rad_exam_id" value="<%= exam.id %>" />
          <div class="form-group">
        <label>Clinical Note</label>
        <textarea name="note" class="form-control"></textarea>
          </div>
          <input type="submit" class="btn btn-primary" value="Add" />
          <% end %>
        </td>
      </tr>
      <% end %>
    </tbody>
      </table>
    </div>
  </div>
</div>

Best Practice - The above code is large for a view: it includes quite a bit of logic mixed with the markup. This was done to see all the relevant code together, but for the sake of maintenance, it would be wise to break out components of this view into helpers and partials.

The JavaScript and CSS styling remain to be implemented, but this markup accomplishes the following:

To complete the CSS styling, edit app/assets/stylesheets/application.scss:

/*
 *= require_self
 */

@import "bootstrap-sprockets";
@import "bootstrap";

@import "font-awesome-sprockets";
@import "font-awesome";

.data { display: none; }

/* Don't show the note row by default */
.note-row { display: none; }
/* Make the rows show a pointer hand instead of an arrow cursor */
.data-row { cursor: pointer; }
/* Striped the table taking into account the extra row */
.data-row:nth-child(4n+1) { background-color: #f9f9f9; }

/* Stripe the notes */
.notes .note:nth-child(odd) { background-color: #f9f9f9; }
/* Some notes styling */
.notes .note { border: 1px solid #ddd; border-radius: 5px; padding: 5px; margin-bottom: 8px; }
.notes .note h4 { font-size: 12px; font-weight: bold; }

/* Increase the visibility of badges with a number > 0 */
.badge.highlight { background-color: #6F256F; }

Finally, add the JavaScript to drive everything in app/assets/javascripts/pages/worklist.js:

$(document).ready(function() {
    $(".launch-group button").bind("click", function(e) {
    //Prevent attempting launch when the button is disabled
    if (!($(this).hasClass('disabled'))) {
        var data = $(this).siblings(".data");
        var integration = $.parseJSON(data.find(".integration-definition").html());
        var exam = $.parseJSON(data.find(".exam-object").html());
        $.harbingerjs.integration.view(integration,exam);
    }
    });

    // Click handler to toggle the notes row
    $(".data-row").bind('click',function(e) {
    //Prevent toggle comments when clicking on the view images button
    if (!($(e.target).hasClass("btn"))) {
        $(this).next().toggle(); //Toggle the display of the note row
    }
    });

    // Capture form submission
    $("form").submit(function(e) {
    var self = $(this); //Allows access to the form within the ajax callbacks
    e.preventDefault(); //prevent submission as it will be done via ajax
    //Make an ajax post and handle the relative root of the url with harbingerjs
    $.ajax($.harbingerjs.core.url("/notes/create"),
           {data: $(this).serializeArray(),
        method: "post",
        //run when save is successful
        success: function(response) {
            var badge = self.parents("tr").prev().find("td .badge");
            var badge_value = parseInt(badge.text()) + 1;
            //Update the badge with an incremented value
            badge.text(badge_value);
            //Add the highlight class if there is now a comment
            if (badge_value > 0) badge.addClass("highlight");
            //Append the note markup to the .notes div
            self.siblings(".notes").append(response);
            self.find('textarea, input[type="submit"]').prop('disabled',false); //re-enable
            self.find('textarea').val(""); // clear textarea
        },
        //run if there is a serverside error
        error: function() {
            //Log errors to the console if it exists
            if (typeof console !== 'undefined') { console.log("Error adding a note",arguments) }
            self.find('textarea, input[type="submit"]').prop('disabled',false); //re-enable
        },
        //run before sending the request
        beforeSend: function() {
            self.find('textarea, input[type="submit"]').prop('disabled',true); //disable form
        }});
    });
});

Whenever you specify a URL (either in an Ajax call or as part of an image src, etc.) you need to account for where the application will be when deployed. All Bridge apps are deployed to a relative root /[appname] in production. Calling out to our NotesController's create action would require the path /notes/create in a development environment, but a production environment would use /railsdevelopertutorial/notes/create. The best way to address this is to use $.harbingerjs.core.url as above.

You must add a helper call to <%= harbingerjs_setup() %> inside the <head> tag of the application layout. Ensure that call is made in the app/views/layouts/application.html.erb before the call to load application.js. The full <head> tag:

<head>
  <title>Rails Developer Tutorial</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= harbingerjs_setup() %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
  <%= yield(:javascript_includes) %>

</head>

Best Practice - You need to use $.harbingerjs.core.url in any project with AJAX calls. The function and helper are very lightweight, so add them to any new application you write as part of the initial setup.

You should now be able to click on an exam row in your worklist to see and add notes.