Commit 8b52ab85 authored by Greg Gard's avatar Greg Gard

adding discard gem; validations module, patient validations

parent 3c898ef6
......@@ -42,13 +42,21 @@ gem 'bootsnap', '>= 1.1.0', require: false
# ********************************
# emt gems
#
# bootstrap 3 sass
# https://github.com/twbs/bootstrap-sass
# bootstrap 3 sass - https://github.com/twbs/bootstrap-sass
gem 'jquery-rails'
# gem 'sass-rails', '>= 3.2' # already included above
gem 'bootstrap-sass', '~> 3.3.7'
gem 'kaminari' # pagination - could use will_paginate instead, but this is more modern/cleaner
# pagination - https://github.com/kaminari/kaminari
# - could use will_paginate instead, but this is more modern/cleaner
# - sadly, it still does table counts by default. drag on production under load.
# had same issues with will_paginate.
gem 'kaminari'
# logical deletes - https://github.com/jhawthorn/discard
# - see readme has interesting commends on acts_as_paranoid etc. overall, he says you
# are/should be responsible for setting scopes and not use automagical gook for this
gem 'discard'
# **********************
# per env
......
......@@ -78,6 +78,8 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.0.5)
crass (1.0.4)
discard (1.0.0)
activerecord (>= 4.2, < 6)
erubi (1.7.1)
execjs (2.7.0)
ffi (1.9.23)
......@@ -219,6 +221,7 @@ DEPENDENCIES
capybara (>= 2.15, < 4.0)
chromedriver-helper
coffee-rails (~> 4.2)
discard
jbuilder (~> 2.5)
jquery-rails
kaminari
......
......@@ -20,14 +20,14 @@ main { @extend .container };
}
&-btn {
@extend .btn;
@extend .btn, .btn-sm;
margin: 5px 5px 5px 0;
}
// use with btn force override eg form
&-brand {
background-color: $brand !important;
color: $white !important;
background-color: $brand;
a {
color: $white;
font-size: 1.5rem;
......@@ -37,6 +37,19 @@ main { @extend .container };
}
}
&-danger {
background-color: $danger !important;
color: $white !important;
a {
color: $white;
font-size: 1.5rem;
&:hover{
}
}
}
&-card {
@extend .panel, .panel-default;
......@@ -60,6 +73,25 @@ main { @extend .container };
> input, > textarea, > select, > checkbox { @extend .form-control; }
}
&-errors {
@extend .well, .well-sm;
background-color: $warn !important;
> h5 {
padding: 0;
margin: 0;
}
}
}
&-page-notifications {
@extend .well, .well-sm;
background-color: $info;
> h5 {
padding: 0;
margin: 0;
}
}
}
\ No newline at end of file
......@@ -22,7 +22,9 @@ $white-90: rgba($white, 0.9);
$brand: #0983d5;
$lightGrey: rgb(245, 248, 250);
$info: #eeffee;
$warn: #ffcccc;
$danger: #ff0000;
@function hd($color) {
@return darken($color, 10%);
......
class ApplicationController < ActionController::Base
before_action :set_presenter
before_action :set_resource, only: [:show, :edit, :update, :destroy]
# but not index or any other non-rest eg dashboard
before_action :set_resource, only: [:new, :create, :show, :edit, :update, :destroy]
# override in subclasses to load a different presenter by default
def self.default_presenter
......@@ -17,14 +19,34 @@ class ApplicationController < ActionController::Base
# - could use inherited hook to trigger call if we needed earlier
# - not using sti here but prevent sti kids from having own resource params
def self.resource_class
@@resource_classes[name] ||= Object.get_const(controller_name.singularize).base_class
@@resource_classes[name] ||= Object.const_get(controller_name.classify).base_class
end
# *******************************
# instance methods
#
# instance methods - keep as much as possible from being exposed to router
#
private
attr_accessor :presenter, :resource, :resource_params
# more sugar
def user_errors
EMT.user_errors
end
def log_stack_error(e)
logger.error "[STACK] #{self.class.name}##{action_name}: #{e}"
end
def stack_error_message(action = nil)
msg = EMT.stack_error_message
case action
when :now
flash.now[:error] = msg
end
msg
end
# all controllers get a presenter even if just base class
def set_presenter(klass = self.class.default_presenter)
self.presenter = klass.new(
......@@ -34,24 +56,38 @@ class ApplicationController < ActionController::Base
controller: self
)
end
# uses discard on all models - ie disallow updates on discards
# maybe use admin page to show all discards and allow updates there.
def set_resource(klass = self.class.resource_class)
self.resource = klass.find(params[:id])
instance_variable_set :"@#{klass.name.underscore}", resource
self.resource = case action_name.to_s.downcase
when 'new'
klass.new
when 'create'
set_resource_params(klass)
klass.new(resource_params)
when 'update'
set_resource_params(klass)
klass.kept.find(params[:id])
else
# set_resource_params(klass)
klass.kept.find(params[:id])
end
instance_variable_set :"@#{klass.name.underscore}", resource
resource
rescue => e
log_stack_error e
redirect_to klass, notice: stack_error_message
end
def set_resource_params(_resource = resource)
return unless _resource
params.require(resource.class.name.underscore.to_sym).permit(resource.class.params_whitelist)
# override resource_params in kids or set whitelist in models
def set_resource_params(klass)
@resource_params = params.require(klass.name.underscore.to_sym).permit(klass.params_whitelist)
end
# override in kids or set model whitelist
def resource_params
@resource_params ||= set_resource_params
end
end
# sugar so we don't have to deal with @ivars all the time. rails imports ivars
......
#
# old school get/post non-ajax "break my back button" controller
# TODO: need a jquery/bootstrap/sass friendly datepicker
#
class PatientsController < ApplicationController
set_resource_class Patient
def index
@patients = Patient.order(:last_name, :first_name, :middle_name).page params[:page]
@patients = Patient.kept.order(:last_name, :first_name, :middle_name).page params[:page]
end
def new
@patient = Patient.new
end
def create
@patient = Patient.new(params[:patient])
@patient.save!
redirect_to patients_path, notice: "Patient created"
rescue *user_errors => e
render :new
rescue => e
log_stack_error e
render :new
end
def show
redirect_to edit_patient_path(@patient)
end
def edit
rescue *user_errors => e
redirect_to patients_path, notice: e
rescue => e
log_stack_error e
redirect_to patients_path, notice: stack_error_message
end
def update
@patient.update_attributes! resource_params
@patient.update! resource_params
redirect_to patients_path, notice: "Patient updated"
rescue *user_errors => e
render :edit
rescue => e
logger.error e
flash.now[:error] = "Unable to save"
log_stack_error e
stack_error_message(:now)
render :edit
end
# TODO: undiscard
def destroy
@patient.discard
redirect_to patients_path, notice: "Patient deleted"
end
rescue *user_errors => e
redirect_to patients_path, notice: e
# ************************
private
rescue => e
log_stack_error e
redirect_to patients_path, notice: stack_error_message
end
......
......@@ -34,19 +34,33 @@ module ApplicationHelper
return if messages.empty?
%Q[<div class='emt-page-notifications'>
<h5>Notice:</h5>
<h5>Notifications:</h5>
<ul>#{messages.map{|m| tag.li m}.join}</ul>
</div>].html_safe
end
# expects model from controller
def emt_form_errors(resource)
return unless resource && resource.errors.present?
%Q[<div id="emt-form-errors">
<h5>#{pluralize(resource.errors.count, "error")} errors:</h5>
<ul>#{resource.errors.full_messages.map{|msg| tag.li msg}}</ul>
return unless resource && !resource.errors.empty?
%Q[<div class="emt-form-errors">
<h5>Unable to process your request due to these error(s):</h5>
<ul>#{resource.errors.full_messages.map{|msg| tag.li msg}.join}</ul>
</div>].html_safe
end
def emt_form_button(resource)
submit_tag "Save", class: 'emt-btn emt-brand'
end
def emt_delete_button(resource)
return if !resource || resource.new_record?
link_to('Delete', resource,
method: :delete,
data: { confirm: 'Are you sure?' },
class: 'emt-btn emt-danger'
)
end
end
module PatientsHelper
end
\ No newline at end of file
......@@ -4,4 +4,36 @@
#
module EMT
# ***************************
# custom errors
DEFAULT_STACK_ERROR_MESSAGE = "There was a system error."
class UserError < StandardError
end
def self.stack_error_message
DEFAULT_STACK_ERROR_MESSAGE
end
#
# differentiates between stack errors and user errors so we can:
# - not log routine validation/user errors
# - know that app error messages are suitable for users
# - useage (or replace redirects with json messages and ensure render json):
# def index
# ..some code using !bang methods
# rescue *EMT.user_errors => e
# redirect_to path, notice: e
# rescue => e
# logger.error e
# redirect_to path, notice: "System error"
# end
#
def self.user_errors
[UserError, ::ActiveRecord::RecordInvalid]
end
end
module EMT
module Validations
# try to avoid using \A\z and other regex implementation-specific functionality
# so these can be exported to js as is for client-side validations
REGEXES = {
person_name_part: /^[0-9a-zA-Z.-]{1,50}$/
}.freeze
TODAY = Date.today
NOW = Time.now
DOB_RANGE = ((TODAY.year - 100)..TODAY.year)
# included methods
# - make all default to required: true
# - pass in model so that we have access to _before_type_cast values for dates and
# have consistent api across all data types
# - methods return a match object or valid date etc else nil
module ValidationMethods
def self.included(klass)
REGEXES.each do |name, pattern|
class_eval <<-EVAL
def valid_#{name}(m, v, required: true)
_v = m.send("\#{v}_before_type_cast").to_s
return true if _v.blank? && !required
#{pattern.inspect}.match(_v)
rescue
nil
end
EVAL
end
klass.extend self
end
# - see tests for Date.parse behavior
# - if valid and block given passes in valid date and returns value of block
def valid_date(m, v, required: true)
_v = m.send("#{v}_before_type_cast").to_s
return true if _v.blank? && !required
_d = Date.parse(_v)
block_given? ? yield(_d) : _d
rescue
nil
end
def valid_dob(*args)
valid_date(*args){|_v| DOB_RANGE.include? _v.year}
end
end
end
end
\ No newline at end of file
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include EMT::Validations::ValidationMethods
# override in kids else u won't be able to save anything viz strong params
# this is not an effort to undo strong params, just relocate the logic
......
class Patient < ApplicationRecord
include Discard::Model
enum gender: {male: 1, female: 2, other: 0}
def self.gender_options
genders.map{|k, v| [k.to_s.capitalize, k]}
end
# TODO: make mr auto-assigned? if so then disallow mass assign, admins only on renaming?
def self.params_whitelist
[:mr, :first_name, :middle_name, :last_name, :dob, :sex]
[:mr, :first_name, :middle_name, :last_name, :dob, :gender]
end
# *************************
# validations/callbacks
#
validate do |m|
m.errors.add :last_name, "missing/invalid" unless valid_person_name_part(m, :last_name)
m.errors.add :first_name, "missing/invalid" unless valid_person_name_part(m, :first_name)
m.errors.add :middle_name, "invalid" unless valid_person_name_part(m, :middle_name, required: false)
m.errors.add :dob, "invalid" unless valid_dob(m, :dob, required: false)
end
# ***************************
......
class Transfer < ApplicationRecord
end
\ No newline at end of file
<%= form_with model: patient, class: 'emt-patients-form emt-form' do |f| %>
<%= form_with model: patient, local: true, class: 'emt-patients-form emt-form' do |f| %>
<%= emt_form_errors(patient) %>
<div>
<%= f.label :mr, "Medical Record Number" %>
<%= f.text_field :mr %>
......@@ -22,19 +22,16 @@
</div>
<div>
<%= f.label :dob_name %>
<%= f.text_field :first_name %>
<%= f.label :dob, 'DOB' %>
<%= f.text_field :dob, value: patient.fmt_dob %>
</div>
<div>
<%= f.label :sex %>
<%= f.text_field :sex %>
<%= f.label :gender, "Sex" %>
<%= f.select :gender, patient.class.gender_options, prompt: 'Select gender' %>
</div>
<div>
<%= f.submit "Save", class: 'emt-btn emt-brand' %>
</div>
<%= emt_form_button(patient) %>
<%= emt_delete_button(patient) %>
<% end %>
\ No newline at end of file
<% end %>
<div class='emt-patients'>
<%= link_to "Add Patient", new_patient_path, class: 'emt-btn emt-brand' %>
<%= paginate @patients %>
<table class='emt-table'>
<tr>
......
<div class='emt-patients-new'>
<%= render 'form', patient: @patient %>
</div>
\ No newline at end of file
#
# rails g migration add_discard_to_patients discarded_at:datetime:index
#
class AddDiscardToPatients < ActiveRecord::Migration[5.2]
def change
add_column :patients, :discarded_at, :datetime
add_index :patients, :discarded_at
end
end
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_05_28_085156) do
ActiveRecord::Schema.define(version: 2018_05_29_064337) do
create_table "admissions", force: :cascade do |t|
t.integer "facility_id", null: false
......@@ -86,6 +86,8 @@ ActiveRecord::Schema.define(version: 2018_05_28_085156) do
t.integer "gender"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "discarded_at"
t.index ["discarded_at"], name: "index_patients_on_discarded_at"
t.index ["last_name", "first_name"], name: "index_patients_on_last_name_and_first_name"
t.index ["mr"], name: "index_patients_on_mr", unique: true
end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment