Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lab test user selection filter #32

Merged
merged 8 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ bundle-add: up bin-bundle-install
routes: up rails-routes
generate: up rails-generate
db\:migrate: up migrate-db
db\:rollback: up rollback-db
db\:setup: up setup-db
db\:reset: up reset-db
db\:seed: up seed-db
Expand Down Expand Up @@ -47,6 +48,10 @@ wait-db:
migrate-db:
docker-compose exec -T health-keeper-app rails db:migrate

STEP ?= 1
rollback-db:
docker-compose exec -T health-keeper-app rails db:rollback STEP=$(STEP)

setup-db:
docker-compose exec -T health-keeper-app rails db:setup

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ If you want to add a `p-1` class then it should be `tw-p-1` now.
But it does not apply to the states, for example, if you want to add a `p-2` on hover, then your class should be `hover:tw-p-2`.

## Useful commands
- In order to reinit everything run `make reinit`. It will drop all containers and recreate everything from the scratch.
- In order to reinit everything run `make reinit`. It will drop all containers and recreate everything from the scratch.
### DB management:
- In order to set up DB run `make db:setup`.
- In order to recreate DB run `make db:reset`.
- In order to (re)populate DB with a testing data run `make db:seed`.
- In order to get a list of routes run `make routes`.
- In order to use generator run `make generate options='generate options'`, e.g. `make generate options='resource student name:string school:belongs_to'`.
- In order to apply migrations run `make db:migrate`.
- In order to rollback migrations run `make db:rollback`. It rollbacks 1 migration by default. If you need more, just add a `STEP` option for that: `make db:rollback STEP=2`
### Gem management:
- In order to install all gems from `Gemfile.lock` run `make bundle-install`.
- In order to install a gem run `make bundle-add gem='gem_name'`, e.g. `make bundle-add gem='gmaps4rails'`.
### Others:
- In order to get a list of routes run `make routes`.
- In order to use generator run `make generate options='generate options'`, e.g. `make generate options='resource student name:string school:belongs_to'`.
- In order to get access to inside the given docker container run `make sh c='container_name'`, e.g. `make sh c='health-keeper-app'`.
There you can do your stuff the same as within manual set up, e.g. run `rails about`.

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def update_assigned_users
format.html { redirect_to admin_user_url(@user), notice: t('.success') }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit_roles, status: :unprocessable_entity }
format.html { render :edit_assigned_users, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
Expand Down
1 change: 1 addition & 0 deletions app/controllers/biomarkers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class BiomarkersController < ApplicationController

# GET /biomarkers or /biomarkers.json
def index
authorize Biomarker
@biomarkers = policy_scope(Biomarker.all)
end

Expand Down
1 change: 1 addition & 0 deletions app/controllers/health_records_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class HealthRecordsController < ApplicationController

# GET /health_records or /health_records.json
def index
authorize HealthRecord
@health_records = policy_scope(HealthRecord.all)
end

Expand Down
26 changes: 25 additions & 1 deletion app/controllers/lab_tests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@
class LabTestsController < ApplicationController
before_action :set_lab_test, only: %i[show edit update destroy]
before_action :build_lab_test, only: %i[create]
before_action :set_filter_by_user_id, only: %i[index]

# GET /lab_tests or /lab_tests.json
def index
authorize LabTest
@recordables = policy_scope(LabTest)
.select(:recordable_id, :created_at)
.where(user_id: @chosen_user_id)
.order(:created_at)
.group(:recordable_id, :created_at)
@biomarkers = policy_scope(Biomarker)
.includes(:reference_ranges, :lab_tests)
.where(lab_tests: { user_id: current_user.id })
.where(lab_tests: { user_id: @chosen_user_id })

respond_to do |format|
format.html
format.turbo_stream do
render partial: 'lab_tests/index_table',
locals: { recordables: @recordables, biomarkers: @biomarkers }
end
end
end

# GET /lab_tests/1 or /lab_tests/1.json
Expand Down Expand Up @@ -89,11 +100,24 @@ def build_lab_test
@lab_test = current_user.lab_tests.build(lab_test_params)
end

def set_filter_by_user_id
current_user_selected = filter_params.empty? ||
!filter_params[:user_id] ||
# To exclude not assigned users selecting. TODO: move this to Pundit policy.
current_user.assigned_users.map(&:id).exclude?(filter_params[:user_id].to_i)

@chosen_user_id = current_user_selected ? current_user.id : filter_params[:user_id]
end

# Only allow a list of trusted parameters through.
def lab_test_params
params
.require(:lab_test)
.permit(:user_id, :biomarker_id, :value, :unit, :reference_range_id, :recordable_type, :recordable_id, :notes,
:created_at, :updated_at)
end

def filter_params
params.permit(:user_id)
end
end
1 change: 1 addition & 0 deletions app/controllers/measurements_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class MeasurementsController < ApplicationController

# GET /measurements or /measurements.json
def index
authorize Measurement
@measurements = policy_scope(Measurement.all)
end

Expand Down
1 change: 1 addition & 0 deletions app/controllers/reference_ranges_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class ReferenceRangesController < ApplicationController

# GET /reference_ranges or /reference_ranges.json
def index
authorize ReferenceRange
@reference_ranges = policy_scope(ReferenceRange.all)
end

Expand Down
21 changes: 21 additions & 0 deletions app/helpers/users_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,28 @@ def assigned_user_ids_list(user)
user.assigned_users_ids
end

# @return [[string, int]] Pairs of user full name and id ready to use in select field
# Adds current user pair if it was not selected
# Or empty array if there are no assigned users
def assigned_users_list_for_select(user, current_user)
return [] if user.assigned_users.empty?

current_user_included = false
assigned_users = user.assigned_users.map do |assigned_user|
current_user_included = true if assigned_user.id == current_user.id
[assigned_user.full_name, assigned_user.id]
end

assigned_users << [current_user.full_name, current_user.id] unless current_user_included

assigned_users
end

def users_list_for_select(user)
user.users_list.map { [_1.full_name, _1.id] }
end

def assigned_users?
current_user.full_access_roles_can? && !current_user.assigned_users.empty?
end
end
9 changes: 9 additions & 0 deletions app/javascript/controllers/search_form_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = [ "form" ]

search() {
this.formTarget.requestSubmit()
}
}
6 changes: 6 additions & 0 deletions app/models/concerns/role_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ def remove_roles(new_roles)
end

def validate_roles(new_roles)
if new_roles.empty?
errors.add(:base, t('errors.messages.empty_roles_detected'))

return false
end

valid_roles = Role.pluck(:name)
invalid_roles = new_roles - valid_roles
unless invalid_roles.empty?
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/users/edit_assigned_users.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</div>

<div>
<%= form.submit class: "tw-my-4 tw-bg-blue-500 tw-hover:bg-blue-700 tw-text-white tw-font-bold tw-py-2 tw-px-4 tw-rounded" %>
<%= form.submit class: "tw-my-4 tw-bg-blue-500 tw-hover:bg-blue-700 tw-text-white tw-font-bold tw-py-2 tw-px-4 tw-rounded" %>
</div>
<% end %>

Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/users/edit_roles.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<div>
<%= form.label :roles, style: "display: block" %>
<%= form.select :roles,
options_for_select(users_list_for_select(@user), user_role_names_list(@user)),
options_for_select(@roles, user_role_names_list(@user)),
{},
multiple: true %>
</div>
Expand Down
6 changes: 6 additions & 0 deletions app/views/lab_tests/_index_table.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div id="lab_tests" class="tw-relative tw-overflow-x-auto tw-shadow-md tw-sm:tw-rounded-lg">
<table class="tw-w-full tw-text-sm tw-text-left tw-text-right tw-rounded-md">
<%= render "index_table_head", recordables: @recordables %>
<%= render "index_table_body", recordables: @recordables, biomarkers: @biomarkers %>
</table>
</div>
40 changes: 29 additions & 11 deletions app/views/lab_tests/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
<section>
<header class="tw-my-2 tw-flex tw-flex-center tw-justify-between tw-items-center">
<h1 class="tw-text-lg">Lab Tests</h1>
<%= link_to "New lab test", new_lab_test_path, class: "btn btn-primary" %>
</header>

<div id="lab_tests" class="tw-relative tw-overflow-x-auto tw-shadow-md tw-sm:tw-rounded-lg">
<table class="tw-w-full tw-text-sm tw-text-left tw-text-right tw-rounded-md">
<%= render "index_table_head", recordables: @recordables %>
<%= render "index_table_body", recordables: @recordables, biomarkers: @biomarkers %>
</table>
</div>
<%= turbo_frame_tag "lab_tests", class: "shadow overflow-hidden rounded border-b border-gray-200" do %>

<header class="tw-my-2 tw-flex tw-flex-center tw-justify-between tw-items-center">
<h1 class="tw-text-lg">Lab Tests</h1>

<% if assigned_users? %>
<div class="flex justify-end mb-1">
<%= form_with url: lab_tests_path, method: :get, data: { controller: "search-form", search_form_target: "form", turbo_frame: "lab_tests" } do |form| %>
<%= form.select :user_id,
options_for_select(
assigned_users_list_for_select(current_user, current_user),
@chosen_user_id
),
{ include_blank: false },
class: "border-blue-500 rounded",
data: {
action: "change->search-form#search"
}
%>
<% end %>
</div>
<% end %>

<%= link_to "New lab test", new_lab_test_path, class: "btn btn-primary" %>
</header>

<%= render "index_table", recordables: @recordables, biomarkers: @biomarkers %>

<% end %>

</section>
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ en:
errors:
messages:
invalid_roles_detected: "Invalid roles detected: %{invalid_roles}"
empty_roles_detected: "You can not leave user without any role"
remove_role_failure: "Failed to remove role: %{role}"
add_role_failure: "Failed to add role: %{role}"
unexpected_error: "Unexpected error while updating user"
Expand Down
46 changes: 46 additions & 0 deletions spec/helpers/users_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,50 @@
end
end
end

describe '#assigned_users_list_for_select' do
let(:current_user) { instance_double(User, full_name: 'Current User', id: 999) }
let(:user_with_assigned_users) do
instance_double(
User,
assigned_users: [
instance_double(User, full_name: 'Alice Johnson', id: 101),
instance_double(User, full_name: 'Bob Smith', id: 102)
]
)
end
let(:user_with_assigned_users_and_current) do
instance_double(
User,
assigned_users: [
instance_double(User, full_name: 'Alice Johnson', id: 101),
instance_double(User, full_name: 'Bob Smith', id: 102),
instance_double(User, full_name: 'Current User', id: 999)
]
)
end

context 'when the user has assigned users and no current' do
it 'returns a list of arrays with user names and IDs for select' do
result = helper.assigned_users_list_for_select(user_with_assigned_users, current_user)
expect(result).to eq([['Alice Johnson', 101], ['Bob Smith', 102], ['Current User', 999]])
end
end

context 'when the user has assigned users with current' do
it 'returns a list of arrays with user names and IDs for select' do
result = helper.assigned_users_list_for_select(user_with_assigned_users_and_current, current_user)
expect(result).to eq([['Alice Johnson', 101], ['Bob Smith', 102], ['Current User', 999]])
end
end

context 'when the user has no assigned users' do
let(:user_with_no_assigned_users) { instance_double(User, assigned_users: []) }

it 'returns an empty array' do
result = helper.assigned_users_list_for_select(user_with_no_assigned_users, current_user)
expect(result).to eq([])
end
end
end
end
14 changes: 14 additions & 0 deletions spec/requests/admin/users_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@
expect(assigns(:user).errors[:base]).to include('Invalid roles detected: invalid_role')
end
end

context 'with empty roles' do
let(:empty_roles) { [] }

it 'returns an unprocessable entity status' do
post update_roles_admin_user_path(user), params: { user: { roles: empty_roles } }
expect(response).to have_http_status(:unprocessable_entity)
end

it 'assigns errors' do
post update_roles_admin_user_path(user), params: { user: { roles: empty_roles } }
expect(assigns(:user).errors[:base]).to include('You can not leave user without any role')
end
end
end

describe 'GET /edit_assigned_users' do
Expand Down
Loading