Happy New Year 2021, fellow devs!
Moving along from my previous Intro to Rails study, Rails Intro::Mandalorian, my cohort is approaching the Ruby on Rails deadline week. Since Rails provides more powerful web application frameworks compared to my previous Sinatra Project Build, it allows swift implementation of a more complex model associations and corresponding attributes.
The pandemic that has been lingering all around us since early 2020 has caused an apparent challenge to the current healthcare system. It is a testament to the new modern demand of digital healthcare. The conventional healthcare system is currently overflown with predominantly Covid-19 patients, exceeding their abilities to facilitate care of other medical patient needs. The Minimum Viable Product (MVP) of Plan My MD Visit app is to create a virtual patient system where the automation benefits patient 24/7 seeking virtual medical assistance, improving overall patient well-being. My application carries essential features and minimal attributes of tele-health platforms. Patients make their visits to the clinic, laboratory or hospital only when necessary and/or advised by medical professionals.
Table Of Contents
1. User Story
2. API Searching Saga
3. Creating Active Record Model
4. Action Controllers
5. Authentication and Authorization Terms
6. Action Views
7. Lastly, as always - Lessons Learned
1. User Story
I spent most of my time at the inception of this project, thinking who the users are and what their experiences would be. At first, I came up with 5 different models that encapsulate the majority party in the healthcare platform; users as either patients, doctors, nurses, administrators, healthcare teams and healthcare providers. The brainstorming exercise goes hand in hand with my attempt in data searching β which presents its own sets of challenges.
I focused on user experience on the patient side, creating an ease of scheduling and having the capability to access medical assistance. Patients should be able to sort doctors' specialties based on their medical needs, and assemble their own healthcare teams. For example, one patient has atrocious eczema symptoms. She should be able to schedule a virtual appointment with available in-network doctors, and create a dermatology care team which comprises of herself as the patient, doctor, appointment time and other necessary care team's attributes. The patient has a certain ownership of her care team, resulting from the automated system in streamlining health provider workflow.
2. API Searching Saga
Once I envisioned the final outputs, the very next thing would be to acquire a list of doctors and their specialties. Being able to feed my application with this data allows new users (as patients) to sign up and have immediate capability to schedule appointments. API search can be fun and gnarly at the same time. I found success from other developers utilizing BetterDoctor API, and got really excited with their doctors and subsequent information. After spending a day on debugging sessions, I learned BetterDoctor API is officially deprecated. π’
Fret not! I moved on, spent another day searching for an Open API, and landed on Centers for Medicare and Medicaid Services. Their datasets are officially used on Medicare.gov, and limited to eligible professionals (EPs) listed on Medicare Care Compare. As a result, I had to scratch out the Healthcare Provider
model from my original domain modeling. The rest of my models still meet the purpose of fulfilling MVP and lay the groundwork for my Content Management System (CMS). The datasets seed about 450+ healthcare professionals with varying specialties. I had to refresh my memory on how to parse JSON data from REST API from my first capstone project. Thanks to Ruby gems OpenURI and Nokogiri, I successfully created my doctors collection in JSON format. I had to fudge a few doctors' attributes in order to execute model validations properly.
3. Creating Active Record Model
Rails pre-dominantly identifies core components in MVC (Model-View-Controller) paradigm. Most of the logic of object relationships, such as patient
has_many doctors
, reside in the Active Record models. Controllers allow exposure of our database from these AR model associations and rendered views. This modular approach creates distinction of each MVC responsibility, which outlines the Rails architectural framework patterns.
$ rails new plan-my-md-visit
Upon instantiation of the command prompt, Rails executes its conventional file system from app
, config
, db
to Gemfile
, and many more. There are 4 main models: User
, Patient
, Doctor
and Healthcare Team
with their model associations as follows:
user has_one
:patient
user has_one
:doctor
patient belongs_to
:user
patient has_many
:healthcare_teams
patient has_many
:doctors, through:
:healthcare_teams
healthcare_team belongs_to
:patient
healthcare_team belongs_to
:doctor
doctor belongs_to
:user
doctor has_many
:healthcare_teams
doctor has_many
:patients, through: healthcare_teams
Rails Generators streamline the effort of setting up models and their table migrations.
$ rails g model Doctor user:references gender:string specialty:string hospital:string address:string city:string state:string zipcode:integer --no-test-framework
Similar implementations occurred in the other models. Following building the Active Record associations, testing Rails models in rails console
is a necessity for quality control. I am pleased with my basic schema composition.
ActiveRecord::Schema.define(version: 2021_01_04_042023) do
create_table "users", force: :cascade do |t|
t.string "firstname"
t.string "lastname"
t.string "username"
t.string "email"
t.string "password_digest"
t.boolean "admin", default: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "patients", force: :cascade do |t|
t.integer "user_id"
t.text "medical_record"
t.text "test_results"
t.text "medications"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "doctors", force: :cascade do |t|
t.integer "user_id"
t.string "gender"
t.string "specialty"
t.string "hospital"
t.string "address"
t.string "city"
t.string "state"
t.integer "zipcode"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "healthcare_teams", force: :cascade do |t|
t.integer "patient_id"
t.integer "doctor_id"
t.datetime "appointment"
t.text "test_result"
t.text "treatment_plans"
t.text "prescriptions"
t.decimal "billing"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
Validations protect our database, and I always think that it is wise to set validations early (prior to controllers and views). Other developers might think otherwise. I identified a few rudimentary Active Record Validations, mainly to avoid seeding any bad data. Each user should be unique, and in most web applications, there should only be one particular username or email associated with the account. I have no angst towards Elon Musk's son named X Γ A-12, but my application currently only allows alphabetic letters.
class User < ActiveRecord::Base
validates :username, presence: true, uniqueness: true
validates :email, presence: true, uniqueness: true
validates :firstname, presence: true, format: { without: /[0-9]/, message: "Numbers are not allowed." }
validates :lastname, presence: true, format: { without: /[0-9]/, message: "Numbers are not allowed." }
...
end
Aside from model associations and validations, I implemented the Law of Demeter design principle by adopting the "one dot" rule. For example, in order to retrieve doctor's fullname, the proper code would be doctor.user.fullname
following our model associations and their attributes. I encapsulated the logic in the Doctor
model, in reference to the object's user reference, and shortened the code to doctor.fullname
.
class Doctor < ActiveRecord::Base
...
def fullname
self.user ? self.user.fullname : nil
end
end
Active Record provides more! I found it extremely useful when implementing AREL (A Relational Algebra) in building optimized queries. It helped me in sorting out the intended outcomes of doctors.json
datasets with minimal time complexity. I was able to sort doctors' specialties with this query interface, Doctor.select(:specialty).distinct.order(specialty: :asc)
.
4. Action Controllers
I took the liberty of developing overall user experience (as patients) with Figma. My previous Designlab's UX & UI courses proved to be useful when creating this basic mid-fidelity wireframing. The exercise helped me to map the different route helpers, HTTP verbs, paths and controller actions following RESTful design principles.
Users as Patients
Upon signing up or logging in, the patient users have access to view their care teams. Overall mappings of the patient users as follows:
Sessions Controller
HTTP Verb | Method | Path | Description |
---|---|---|---|
GET |
new | /signin | user logging in |
POST |
create | /signin | user authentication |
POST |
destroy | /logout | user logging out |
Users Controller
HTTP Verb | Method | Path | Description |
---|---|---|---|
GET |
new | /users/new | render form for new user creation |
POST |
create | /users/:id | create a new user |
GET |
show | /users/:id | show a single user |
GET |
edit | /users/:id/edit | render the form for editing |
PATCH |
update | /users/:id | update a user |
DELETE |
destroy | /users/:id | delete a user |
Patients Controller
HTTP Verb | Method | Path | Description |
---|---|---|---|
GET |
new | /patients/new | render form for new patient creation |
POST |
create | /patients/:id | create a new user patient |
GET |
show | /patients/:id | user patient main page |
Doctors Controller
HTTP Verb | Method | Path | Description |
---|---|---|---|
GET |
index | /doctors | view doctors with specialty filter |
GET |
show | /doctors/:id | show a single doctor information |
Healthcare Teams Controller
HTTP Verb | Method | Path | Description |
---|---|---|---|
GET |
new | /select_specialty | user patient select doctor specialty |
Healthcare Teams Controller (nested resources)
HTTP Verb | Method | Path |
---|---|---|
GET |
index | /patients/:patient_id/healthcare_teams |
GET |
new | /patients/:patient_id/healthcare_teams/new |
POST |
new | /patients/:patient_id/healthcare_teams/new |
POST |
create | /patients/:patient_id/healthcare_teams/:id |
GET |
show | /patients/:patient_id/healthcare_teams/:id |
There are restrictions for patient users, for instance updating doctor's profile information or viewing other patient users information. My app has to account for such intrusion. In the Application Controller
, private methods current_user
and current_patient
were created to police this access policy. The two methods can be propagated on other controllers when granting certain privileges based on current_user
's identity. The following is a current_patient
examples of a user.
class PatientsController < ApplicationController
helper_method :current_user, :current_patient
def show
@patient = Patient.find_by(id: params[:id])
if @patient != current_patient
flash[:alert] = "Error URL path."
redirect_to patient_path(current_patient)
end
end
...
end
Users as Admins
The admin group should have the greatest privileges when performing Create, Read, Update and Delete (CRUD) functions on User
, Patient
, Doctor
and Healthcare Team
models. Doctors might have the capability to update Care Team's attributes such as treatment plans, test results and medications. However, they are not allowed to update patient profiles as they are restricted to administrative access.
Rails.application.routes.draw do
...
# Only Admin can see Users Lists
resources :users, except: [:index]
resources :doctors, only: [:index, :show]
# Admin privileges
namespace :admin do
resources :users, only: [:index, :show, :edit, :update, :destroy]
resources :patients, only: [:edit, :update]
resources :doctors, only: [:edit, :update]
resources :healthcare_teams, only: [:edit, :update, :destroy]
end
end
The application of namespace routes shortened my overall scoping routes, wrapping /users
, /patients
, /doctors
and /healthcare_teams
resources under /admin
. One finicky note on destroy
action. A user is either a patient or a doctor. When deleting a user, their subsequent patient or doctor information should also be deleted from our database.
class Admin::UsersController < ApplicationController
...
def destroy
@patient = @user.patient
@doctor = @user.doctor
if !@patient.nil?
@user.destroy
@patient.destroy
else
@user.destroy
@doctor.destroy
end
flash[:notice] = "User deleted."
redirect_to admin_users_path
end
end
5. Authentication and Authorization Terms
For standard user authentication (including signup, login and logout), I utilized Ruby gem bcrypt
and Active Record's Secure Password class methods. has_secure_password
needs to be included in the User
model. This implementation provides methods to set and authenticate user's password, following users' table and password_digest
attribute.
I challenged myself to set two authentication providers alongside the traditional username and password set up. Ruby gem omniauth
provides a gateway to utilizing multi-provider authentication strategies. While GitHub relies on gem omniauth-github
, Google depends on gem omniauth-google-oauth2
. Gem dotenv-rails
is required to securely save PROVIDER_CLIENT_ID
and PROVIDER_SECRET
. I defined the Rack Middleware in Rails initializer at config/initializers/omniauth.rb
.
Rails.application.config.middleware.use OmniAuth::Builder do
provider :developer unless Rails.env.production?
provider :github, ENV['GITHUB_KEY'], ENV['GITHUB_SECRET']
provider :google_oauth2, ENV["GOOGLE_KEY"],ENV["GOOGLE_SECRET"], skip_jwt: true
end
In the config/routes.rb
file, the callback routes get '/auth/:provider/callback', to: 'sessions#omniauth'
consolidates both providers (GitHub and Google). The results of authentication hash available in the callback vary from each provider. Two class methods, find_or_create_by_google(auth)
and find_or_create_by_github(auth)
, are provided in the User
model and invoked in the Sessions Controller
.
class SessionsController < ApplicationController
...
def omniauth
if auth_hash != nil && auth_hash.provider == "google_oauth2"
@user = User.find_or_create_by_google(auth)
elsif auth_hash != nil && auth_hash.provider == "github"
@user = User.find_or_create_by_github(auth)
end
session[:user_id] = @user.id
@current_patient = Patient.find_or_create_by(user_id: session[:user_id])
...
end
private
def auth
request.env['omniauth.auth']
end
end
6. Action Views
The part that requires the least amount of code, and User Interface as its focal point.
Forms forms forms!
Through Flatiron School curriculums, I became well-versed with form_for
FormBuilder features for models associated with Active Record models, and form_tag
for manual route passing where form params will be submitted. For this project, I decided to put form_with
into service. It was introduced in Rails 5.1, and the idea is to merge both implementations of form_tag
and form_for
. I might as well get comfortable with form_with view helpers and its lexical environment. At first, I was not able to display error messages, and discovered I had to add local: true
syntax. Below is a snippet of my /admin/healthcare_teams/:id/edit
route. Note on the syntax format on the nested resource, [:admin, @healthcare_team]
.
<%= render partial: '/errors', locals: { record: @healthcare_team } %>
<%= form_with model: [:admin, @healthcare_team], local: true do |f| %>
<%= f.label :appointment, "Appointment" %>
<%= f.datetime_field :appointment %>
<%= f.label :test_result, "Test Result" %>
<%= f.text_area :test_result %>
<%= f.label :treatment_plans, "Treatment Plans" %>
<%= f.text_area :treatment_plans %>
<%= f.label :prescriptions, "Prescriptions" %>
<%= f.text_area :prescriptions %>
<%= f.label :billing, "Billing" %>
<%= f.number_field :billing, min: 0, step: 0.01 %>
<%= f.submit "Update Care Team" %>
<% end %>
While Rails' doctrine is pursuant to convention over configuration, I had to customize one route get '/select_specialty', to: 'healthcare_teams#select_specialty'
. I want the patient users to have the ability to sort doctors by specialty when scheduling appointments.
CSS - Oh My!
While it was all fun in rendering many views in order to make my app more interactive, I found myself spending a tremendous amount of time on CSS clean-ups. I am thankful for MaterializeCSS, and its built-in scss
and jquery
. Though, it gave me a lot of headache with debugging and reading Stack Overflow especially when implementing their CSS classes
onto forms
, select
and button
elements. I have no CSS experience, and maybe it's about time to educate myself further via Udemy courses.
7. Lastly, as always β Lessons Learned
My struggle on this project started off when designing the overall user flow, either as patients or doctors. It was an ambitious idea at first, trying to figure out both users simultaneously. My interim cohort leader, Eriberto Guzman, suggested to focus on developing my project from the patient users point of view, and refine for other user groups as my project progresses.
My preliminary Entity Relationship Diagram (ERD) had a flaw in the User
model associations. User has_many
patients, and user has_many
doctors. I realized this during debugging sessions when setting up Admin
controllers that I had to refactor the user either as a patient or a doctor. I was relieved to find the only difference on the associations methods was minimal. For example, self.patients.build
is one of the methods for has_many
association, and it was revised to self.build_patient
when has_one
was declared.
Refactoring codes will always be a perpetual exercise as a developer. I plan on going back, and restructuring any vulnerabilities residing in my code - but first, need to deploy on Heroku and submit this project for my upcoming project assessment. So long Rails.
fentybit / PlanMyMDVisit
The MVP of Plan My MD Visit app is to create a virtual patient system where the automation benefits patient 24/7 seeking virtual medical assistance, improving overall patient well-being.
Plan My MD Visit
Domain Modeling :: Virtual Healthcare
Welcome to my simplistic version of Virtual Healthcare system.
The automation benefits patients 24/7 seeking medical assistance, improving overall patient well-being!
About
The pandemic that has been lingering all around us since early 2020 has caused an apparent challenge to the current healthcare system. It is a testament to the new modern demand of digital healthcare. The conventional healthcare system is currently overflown with predominantly Covid-19 patients, exceeding their abilities to facilitate care of other medical patient needs.
The Minimum Viable Product (MVP) of Plan My MD Visit app is to create a virtual patient system where the automation benefits patient 24/7 seeking virtual medical assistance, improving overall patient well-being. My application carries essential features and minimal attributes of tele-health platforms. Patients make their visits to the clinic, laboratory or hospital only when necessary and/or advised byβ¦
Post Scriptum:
This is my Module 3 capstone project with Flatiron School. I believe one of the catalyst to becoming a good programmer is to welcome constructive criticism. Feel free to drop a message. π
Keep Calm, and Code On.
External Sources:
Centers for Medicare and Medicaid Services
Simple Calendar
MaterializeCSS
Unsplash
Google Authentication Strategy for Rails 5 Application
fentybit | GitHub | Twitter | LinkedIn
Top comments (2)
Thank you for posting your process. I enjoy working in Ruby and JS. I look forward to dissecting this further and coming up with my own app one day! Awesome work!
Thank you so much Chem. :)
Rails works like magic!