DEV Community

Cover image for Plan My MD Visit Part 1
fentybit
fentybit

Posted on • Updated on

Plan My MD Visit Part 1

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.

Alt Text

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.

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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.

Alt Text

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.

GitHub logo 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!

YouTube Demo

DEV Blog

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)

Collapse
 
mcnchem profile image
Chem

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!

Collapse
 
fentybit profile image
fentybit

Thank you so much Chem. :)
Rails works like magic!