0

I have a model called hardships with an array value for the users that have voted to approve that hardship (:approvals => [] in the hardship_params).

I'm trying to create a list of hardships on the index page for hardships which any given current_user (using devise) has NOT approved.

The array is formed with user ids, so a typical value for @hardship.approvals would be ["2", "3"] if the second and third users had approved it.

I'm having trouble with the logic to call this from the controller. I've looked at SO posts like this but can't glean an answer that works for Rails 5.2 and Postgres.

I'm looking for something like this:

@need_approval = Hardship.where.not(approvals.include?(current_user.id.to_s))

However, I know that doesn't work because approvals isn't a variable and the syntax is all wrong. Can anyone help me Rails-ify this so it gets the info I need?

ADDITIONAL INFORMATION

Here's my hardships table:

  create_table "hardships", force: :cascade do |t|
    t.string "full_name"
    t.date "date"
    t.string "position"
    t.string "branch"
    t.string "email_non_toca"
    t.string "mobile"
    t.string "address"
    t.string "city"
    t.string "state"
    t.string "zip"
    t.string "bank_name"
    t.string "bank_phone"
    t.string "bank_address"
    t.date "start_date"
    t.boolean "accident", default: false
    t.boolean "catastrophe", default: false
    t.boolean "counseling", default: false
    t.boolean "family_emergency", default: false
    t.boolean "health", default: false
    t.boolean "memorial", default: false
    t.boolean "other_hardship", default: false
    t.string "other_hardship_description"
    t.decimal "requested_amount"
    t.text "hardship_description"
    t.decimal "self_fund"
    t.string "intent_signature"
    t.date "intent_signature_date"
    t.string "release_signature"
    t.date "release_signature_date"
    t.string "status", default: "Application Started"
    t.string "final_decision", default: "Not Decided"
    t.bigint "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.boolean "returned", default: false
    t.text "approvals", default: [], array: true
    t.text "rejections", default: [], array: true
    t.index ["user_id"], name: "index_hardships_on_user_id"
  end
Liz
  • 1,369
  • 2
  • 26
  • 61
  • Why aren't you using an association between `Hardship` and `User`? – jvillian Jul 18 '19 at 18:51
  • @jvillian Because a user can either approve, reject, or request to modify a `hardship` (as well as two other types of applications...`scholarship` and `charity`) and that seemed like a not DRY amount of join tables to create. – Liz Jul 18 '19 at 18:54
  • @Liz does `approvals` from here `@hardship.approvals` column or table ? – 7urkm3n Jul 18 '19 at 19:22
  • @jvillian I added my hardships schema to the OP. – Liz Jul 18 '19 at 19:47
  • Hang on @Liz, I'm working on it... – jvillian Jul 18 '19 at 19:54
  • Like the other comments said, you should be using a join table for this. But if you decide not to, check https://stackoverflow.com/a/31569083/2697183 (which also recommends a join table) – AbM Jul 18 '19 at 20:55
  • @Liz - Sorry that took a while. Extended answer, below. IMO, quite DRY. – jvillian Jul 18 '19 at 21:19

1 Answers1

2

If I were you, I think I would be tempted to create a ApplicationAction model. Something like:

# == Schema Information
#
# Table name: application_actions
#
#  id               :bigint           not null, primary key
#  user_id          :integer
#  application_type :string
#  application_id   :integer
#  action           :integer
#  created_at       :datetime         not null
#  updated_at       :datetime         not null
#

class ApplicationAction < ApplicationRecord
  belongs_to :user 
  belongs_to :application, polymorphic: true

  enum action: {
    approved:             0,
    rejected:             1,
    requested_to_modify:  2
  }  
end

Then, in your User model, do something like:

# == Schema Information
#
# Table name: users
#
#  id         :bigint           not null, primary key
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class User < ApplicationRecord
  has_many :application_actions

  %i(
    approved
    rejected
    requested_to_modify
  ).each do |action_sym|
    %w(
      hardship
      scholarship
      charity
    ).each do |application_type|

      # Create the associations: 
      #   - hardship_applications
      #   - scholarship_applications
      #   - charity_applications
      has_many "#{application_type}_applications".to_sym,
        -> { distinct },
        through:      :application_actions,
        source:       :application,
        source_type:  application_type.camelize

      # Define the methods:
      #   - approved_application_actions
      #   - rejected_application_actions
      #   - requested_to_modify_application_actions
      define_method("#{action_sym}_application_actions") do 
        application_actions.send(action_sym)
      end

      # Define the methods:
      #   - approved_hardship_applications
      #   - rejected_hardship_applications
      #   - requested_to_modify_hardship_applications
      #   - approved_scholarship_applications
      #   - rejected_scholarship_applications
      #   - requested_to_modify_scholarship_applications
      #   - approved_charity_applications
      #   - rejected_charity_applications
      #   - requested_to_modify_charity_applications
      define_method("#{action_sym}_#{application_type}_applications") do 
        send("#{application_type}_applications").
          joins(:application_actions).
          where(
            application_actions: {
              action: ApplicationAction.actions[action_sym]
            }
          )
      end

      # Define the methods:
      #   - hardship_applications_not_approved
      #   - hardship_applications_not_rejected
      #   - hardship_applications_not_requested_to_modify
      #   - scholarship_applications_not_approved
      #   - scholarship_applications_not_rejected
      #   - scholarship_applications_not_requested_to_modify
      #   - charity_applications_not_approved
      #   - charity_applications_not_rejected
      #   - charity_applications_not_requested_to_modify
      define_method("#{application_type}_applications_not_#{action_sym}") do 
        application_type.
          camelize.
          constantize.
          where.
          not(id: send("#{action_sym}_#{application_type}_applications"))
      end

    end
  end

end

And, in your Hardship model, do something like:

# == Schema Information
#
# Table name: hardships
#
#  id               :bigint           not null, primary key
#  application_type :integer          default(NULL)
#  created_at       :datetime         not null
#  updated_at       :datetime         not null
#

class Hardship < ApplicationRecord
  has_many :application_actions, as: :application

  enum application_type: {
    accident:           0,
    catastrophe:        1,
    counseling:         2,
    family_emergency:   3,
    health:             4,
    memorial:           5,
    other_hardship:     6
  }
end

Then, if I run that through a quick RSpec test:

require 'rails_helper'

RSpec.describe 'Hardship Applications' do
  before(:each) do 
    @user_1               = User.create!
    @user_2               = User.create!
    @hardship_1           = Hardship.create!
    @user_1.
      application_actions.
      create!(application: @hardship_1).
      approved!
    @user_2.
      application_actions.
      create!(application: @hardship_1).
      rejected!
  end
  it "user_1 approved_hardship_applications to include hardship_1" do 
    expect(@user_1.approved_hardship_applications).to include(@hardship_1)
  end
  it "user_1 hardship_applications_not_approved NOT to include hardship_1" do 
    expect(@user_1.hardship_applications_not_approved).not_to include(@hardship_1)
  end
  it "user_1 rejected_hardship_applications NOT to include hardship_1" do 
    expect(@user_1.rejected_hardship_applications).not_to include(@hardship_1)
  end
  it "user_2 approved_hardship_applications NOT to include hardship_1" do 
    expect(@user_2.approved_hardship_applications).not_to include(@hardship_1)
  end
  it "user_2 hardship_applications_not_approved to include hardship_1" do
    expect(@user_2.hardship_applications_not_approved).to include(@hardship_1)
  end
  it "user_2 rejected_hardship_applications to include hardship_1" do 
    expect(@user_2.rejected_hardship_applications).to include(@hardship_1)
  end
end

I get...

Hardship Applications
  user_1 approved_hardship_applications to include hardship_1
  user_1 hardship_applications_not_approved NOT to include hardship_1
  user_1 rejected_hardship_applications NOT to include hardship_1
  user_2 approved_hardship_applications NOT to include hardship_1
  user_2 hardship_applications_not_approved to include hardship_1
  user_2 rejected_hardship_applications to include hardship_1

Finished in 0.13431 seconds (files took 0.90021 seconds to load)
6 examples, 0 failures

So, you can do:

@need_approval = current_user.hardship_applications_not_approved

instead of:

@need_approval = Hardship.where.not(approvals.include?(current_user.id.to_s))

Also you'll note that in the Hardship model, I made all your application types into an enum. This will cut down on empty fields (assuming a Hardship application can be of only one application type).

There appear to be other opportunities for reducing redundancy. For instance, you could create PhysicalAddress, PhoneNumber, and EmailAddress models and associate these with each type of application. You could also create your application status to an enum and default it to application_started. Same with final_decision.

jvillian
  • 19,953
  • 5
  • 31
  • 44
  • This is awesome and incredibly detailed! It took me a while to understand it all, but truly fantastic. Thank you! – Liz Jul 20 '19 at 18:26
  • Sure thing, so glad it helped! LMK if you have any additional questions about this. – jvillian Jul 20 '19 at 18:28