From deb9840d50489734f87f8dd9fe20794178b0a085 Mon Sep 17 00:00:00 2001 From: aksafan Date: Sun, 29 Dec 2024 22:57:39 -0600 Subject: [PATCH 1/4] Fix bundle gems sync inside docker --- .rubocop.yml | 3 ++- Dockerfile | 16 +--------------- docker-compose.override.yml | 8 ++++++++ docker-compose.yml | 4 ---- 4 files changed, 11 insertions(+), 20 deletions(-) create mode 100644 docker-compose.override.yml diff --git a/.rubocop.yml b/.rubocop.yml index f8d8e07..69aad8b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,4 +23,5 @@ Metrics/MethodLength: Max: 20 Style/Documentation: - Enabled: false \ No newline at end of file + Enabled: false + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a73b039..e061625 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,17 +16,6 @@ RUN apt-get update -qq && apt-get install -y \ build-essential \ curl -# Install rbenv -RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv && \ - echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc && \ - echo 'eval "$(rbenv init -)"' >> ~/.bashrc && \ - git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build && \ - echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc - -# Install the specified Ruby version using rbenv -ENV PATH="/root/.rbenv/bin:/root/.rbenv/shims:$PATH" -RUN rbenv install $RUBY_VERSION && rbenv global $RUBY_VERSION - # Set the working directory WORKDIR /app @@ -35,14 +24,11 @@ COPY Gemfile /app/Gemfile COPY Gemfile.lock /app/Gemfile.lock # Install Gems dependencies -RUN gem install bundler && bundle install +RUN gem install bundler && bundle install --jobs 4 --retry 3 # Copy the application code COPY . /app -# Precompile assets (optional, if using Rails with assets) -RUN bundle exec rake assets:precompile - # Expose the port the app runs on EXPOSE 3000 diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..972b446 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,8 @@ +services: + health-keeper-app: + volumes: + - ./:/app + - bundle_cache:/usr/local/bundle + +volumes: + bundle_cache: diff --git a/docker-compose.yml b/docker-compose.yml index 79de2d1..65bc163 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,9 +5,6 @@ services: - '3000:3000' depends_on: - health-keeper-postgres - volumes: - - ./:/app - - gem_cache:/usr/local/bundle/gems tty: true health-keeper-postgres: @@ -23,4 +20,3 @@ services: volumes: health-keeper-postgres: - gem_cache: From 3cfe8b06330f112a20c039063c9d369a0a2edb12 Mon Sep 17 00:00:00 2001 From: aksafan Date: Sun, 29 Dec 2024 23:05:42 -0600 Subject: [PATCH 2/4] Add user management with role assignment --- Gemfile | 3 + Gemfile.lock | 3 + app/controllers/admin/users_controller.rb | 90 +++++++++++++++++++ app/helpers/users_helper.rb | 11 +++ app/models/user.rb | 79 ++++++++++++++-- app/policies/user_policy.rb | 41 +++++++++ app/views/admin/users/_form.html.erb | 32 +++++++ .../admin/users/_index_table_body.html.erb | 24 +++++ .../admin/users/_index_table_head.html.erb | 24 +++++ app/views/admin/users/_user.html.erb | 31 +++++++ app/views/admin/users/_user.json.jbuilder | 4 + app/views/admin/users/edit.html.erb | 8 ++ app/views/admin/users/edit_roles.html.erb | 33 +++++++ app/views/admin/users/index.html.erb | 13 +++ app/views/admin/users/index.json.jbuilder | 3 + app/views/admin/users/show.html.erb | 15 ++++ app/views/admin/users/show.json.jbuilder | 3 + app/views/layouts/_navbar.html.erb | 5 ++ config/locales/en.yml | 15 ++++ config/routes.rb | 8 ++ 20 files changed, 438 insertions(+), 7 deletions(-) create mode 100644 app/controllers/admin/users_controller.rb create mode 100644 app/helpers/users_helper.rb create mode 100644 app/policies/user_policy.rb create mode 100644 app/views/admin/users/_form.html.erb create mode 100644 app/views/admin/users/_index_table_body.html.erb create mode 100644 app/views/admin/users/_index_table_head.html.erb create mode 100644 app/views/admin/users/_user.html.erb create mode 100644 app/views/admin/users/_user.json.jbuilder create mode 100644 app/views/admin/users/edit.html.erb create mode 100644 app/views/admin/users/edit_roles.html.erb create mode 100644 app/views/admin/users/index.html.erb create mode 100644 app/views/admin/users/index.json.jbuilder create mode 100644 app/views/admin/users/show.html.erb create mode 100644 app/views/admin/users/show.json.jbuilder diff --git a/Gemfile b/Gemfile index d7f49ca..e1390ef 100644 --- a/Gemfile +++ b/Gemfile @@ -87,6 +87,9 @@ group :development do gem 'rubocop-rspec', require: false gem 'rubocop-rspec_rails', require: false + # To see in what paths rails are looking for translations + gem 'i18n-debug' + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler" diff --git a/Gemfile.lock b/Gemfile.lock index 4f7dccb..7f5c0a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,6 +132,8 @@ GEM activesupport (>= 6.1) i18n (1.14.6) concurrent-ruby (~> 1.0) + i18n-debug (1.2.0) + i18n (< 2) importmap-rails (2.0.2) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -361,6 +363,7 @@ DEPENDENCIES email_validator factory_bot_rails faker + i18n-debug importmap-rails jbuilder pg (~> 1.1) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000..b2adade --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Admin + class UsersController < ApplicationController + before_action :set_user, only: %i[show edit update destroy update edit_roles update_roles] + before_action :set_roles_list, only: %i[edit_roles update_roles] + before_action :set_user_params_roles, only: %i[update_roles] + + # GET /admin/users or /admin/users.json + def index + @users = policy_scope(User.all) + end + + # GET /admin/users/1 or /admin/users/1.json + def show + authorize @user + end + + # GET /admin/users/1/edit + def edit + authorize @user + end + + # PATCH/PUT /admin/users/1 or /admin/users/1.json + def update + authorize @user + + respond_to do |format| + if @user.update(user_params) + format.html { redirect_to admin_user_url(@user), notice: t('.success') } + format.json { render :show, status: :ok, location: @user } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @user.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /admin/users/1 or /admin/users/1.json + def destroy + authorize @user + @user.destroy! + + respond_to do |format| + format.html { redirect_to admin_users_path, status: :see_other, notice: t('.success') } + format.json { head :no_content } + end + end + + # GET /admin/users/1/edit_roles or /admin/users/1/edit_roles.json + def edit_roles + authorize @user + end + + # PATCH/PUT /admin/users/1/update_roles or /admin/users/1/update_roles.json + def update_roles + authorize @user + + respond_to do |format| + if @user.update_roles?(@user_params_roles) + 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.json { render json: @user.errors, status: :unprocessable_entity } + end + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_user + @user = User.find(params[:id]) + end + + def set_roles_list + @roles = Role.all.map { |role| [role.name.capitalize, role.name] } + end + + def set_user_params_roles + @user_params_roles = user_params[:roles].compact_blank + end + + # Only allow a list of trusted parameters through. + def user_params + params.require(:user).permit(:first_name, :last_name, :email, roles: []) + end + end +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..32eb038 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module UsersHelper + def get_user_roles_list_as_string(user) + user.user_roles_names.map { |name| User::Roles::ROLES_MAP[name.to_sym] }.join(', ') + end + + def get_user_role_ids_list(user) + user.user_roles_names + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 3858dd1..09bd22a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class User < ApplicationRecord + include ActionView::Helpers::TranslationHelper + rolify resourcify after_commit :assign_default_role, on: :create @@ -15,40 +17,96 @@ class User < ApplicationRecord has_many :measurements, dependent: :destroy has_and_belongs_to_many :roles, join_table: :users_roles # rubocop:disable Rails/HasAndBelongsToMany + accepts_nested_attributes_for :roles + validates :email, presence: true, uniqueness: true, email: true def full_name "#{first_name} #{last_name}" end + def user_roles_names + roles.pluck(:name) + end + def full_access_roles_can? - has_any_role?(*User::Roles::FULL_ACCESS_ROLES) + has_any_role?(*Roles::FULL_ACCESS_ROLES) end def all_roles_can? - has_any_role?(*User::Roles::ROLES) + has_any_role?(*Roles::ROLES) end def admin? - has_role?(User::Roles::ADMIN) + has_role?(Roles::ADMIN) end def doctor? - has_role?(User::Roles::DOCTOR) + has_role?(Roles::DOCTOR) end def health_coach? - has_role?(User::Roles::HEALTH_COACH) + has_role?(Roles::HEALTH_COACH) end def user? - has_role?(User::Roles::USER) + has_role?(Roles::USER) + end + + def update_roles?(new_roles) + User.transaction do + synchronize_roles(new_roles) + + raise ActiveRecord::Rollback unless errors.empty? + rescue StandardError => e + handle_unexpected_transaction_error(e) + + raise ActiveRecord::Rollback + end + are_errors_empty = errors.empty? + + unless are_errors_empty + Rails.logger.error("Role update failed for Account ID #{id}: #{errors.full_messages.join(', ')}") + end + + are_errors_empty end private def assign_default_role - add_role(User::Roles::USER) if roles.blank? + add_role(Roles::USER) if roles.blank? + end + + def synchronize_roles(new_roles) + return unless validate_roles(new_roles) + + current_roles = roles.pluck(:name) + + roles_to_remove = current_roles - new_roles + roles_to_remove.each do |role| + remove_role(role) || errors.add(:base, t('errors.messages.remove_role_failure', role: 'role')) + end + + roles_to_add = new_roles - current_roles + roles_to_add.each do |role| + add_role(role) || errors.add(:base, t('errors.messages.add_role_failure', role: 'role')) + end + end + + def validate_roles(new_roles) + valid_roles = Role.pluck(:name) + invalid_roles = new_roles - valid_roles + unless invalid_roles.empty? + errors.add(:base, t('errors.messages.invalid_roles_detected', invalid_roles: invalid_roles.join(', '))) + end + + invalid_roles.empty? + end + + def handle_unexpected_transaction_error(error) + errors.add(:base, t('errors.messages.unexpected_error')) + Rails.logger.error("Unexpected error while synchronizing roles: #{error.message}") end class Roles < User @@ -59,5 +117,12 @@ class Roles < User ROLES = [ADMIN, DOCTOR, HEALTH_COACH, USER].freeze FULL_ACCESS_ROLES = [ADMIN, DOCTOR, HEALTH_COACH].freeze + + ROLES_MAP = { + ADMIN => 'Admin', + DOCTOR => 'Doctor', + HEALTH_COACH => 'Health coach', + USER => 'User' + }.freeze end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 0000000..20025fc --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class UserPolicy < ApplicationPolicy + def index? + user.admin? + end + + def show? + user.admin? + end + + def create? + user.admin? + end + + def update? + user.admin? + end + + def destroy? + user.admin? + end + + def edit_roles? + user.admin? + end + + def update_roles? + user.admin? + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user.admin? + scope.all + else + scope.where(user: user) + end + end + end +end diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb new file mode 100644 index 0000000..10b2950 --- /dev/null +++ b/app/views/admin/users/_form.html.erb @@ -0,0 +1,32 @@ +<%= form_with model: @user, url: admin_user_path(@user), method: :patch do |form| %> + <% if @user.errors.any? %> +
+

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

+ +
    + <% @user.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :first_name, style: "display: block" %> + <%= form.text_field :first_name %> +
+ +
+ <%= form.label :last_name, style: "display: block" %> + <%= form.text_field :last_name %> +
+ +
+ <%= form.label :email, style: "display: block" %> + <%= form.text_field :email %> +
+ +
+ <%= 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" %> +
+<% end %> diff --git a/app/views/admin/users/_index_table_body.html.erb b/app/views/admin/users/_index_table_body.html.erb new file mode 100644 index 0000000..774fe0a --- /dev/null +++ b/app/views/admin/users/_index_table_body.html.erb @@ -0,0 +1,24 @@ + + <% @users.each do |user| %> + + + <%= user.full_name %> + + + <%= user.email %> + + + <%= get_user_roles_list_as_string user %> + + + <%= format_date_with_time user.created_at %> + + + <%= format_date_with_time user.updated_at %> + + + <%= link_to "Show details", admin_user_url(user), class: "tw-font-medium tw-text-blue-600 tw-dark:tw-text-blue-500 tw-hover:tw-underline" %> + + + <% end %> + diff --git a/app/views/admin/users/_index_table_head.html.erb b/app/views/admin/users/_index_table_head.html.erb new file mode 100644 index 0000000..a2dc70b --- /dev/null +++ b/app/views/admin/users/_index_table_head.html.erb @@ -0,0 +1,24 @@ + + + + + User + + + Email + + + Role + + + Created date + + + Updated date + + + Show + + + + \ No newline at end of file diff --git a/app/views/admin/users/_user.html.erb b/app/views/admin/users/_user.html.erb new file mode 100644 index 0000000..7c62222 --- /dev/null +++ b/app/views/admin/users/_user.html.erb @@ -0,0 +1,31 @@ +
+
+ +

+ User Name: + <%= user.full_name %> +

+ +

+ Email: + <%= user.email %> +

+ +

+ Role: + <%= get_user_roles_list_as_string user %> +

+ +

+ Created at: + <%= format_date_with_time user.created_at %> +

+ +

+ Updated at: + <%= format_date_with_time user.updated_at %> +

+ +
+ +
diff --git a/app/views/admin/users/_user.json.jbuilder b/app/views/admin/users/_user.json.jbuilder new file mode 100644 index 0000000..2ce5a95 --- /dev/null +++ b/app/views/admin/users/_user.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.extract! user, :id, :first_name, :last_name, :email, :created_at, :updated_at +json.url admin_user_url(user, format: :json) diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb new file mode 100644 index 0000000..c6cda3f --- /dev/null +++ b/app/views/admin/users/edit.html.erb @@ -0,0 +1,8 @@ +

Editing user

+ +<%= render "form", user: @user %> + +
+ <%= link_to "Show this user", admin_user_url(@user) %> | + <%= link_to "Back to users", admin_users_path %> +
diff --git a/app/views/admin/users/edit_roles.html.erb b/app/views/admin/users/edit_roles.html.erb new file mode 100644 index 0000000..11f1993 --- /dev/null +++ b/app/views/admin/users/edit_roles.html.erb @@ -0,0 +1,33 @@ +

Editing user roles

+ +<%= form_with model: @user, url: update_roles_admin_user_path(@user), method: :post do |form| %> + <% if @user.errors.any? %> +
+

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

+ +
    + <% @user.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :roles, style: "display: block" %> + <%= form.select :roles, + options_for_select(@roles, get_user_role_ids_list(@user)), + {}, + multiple: true %> +
+ +
+ <%= 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" %> +
+<% end %> + +
+ <%= link_to "Show this user", admin_user_url(@user) %> | + <%= link_to "Back to users", admin_users_path %> +
+ diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb new file mode 100644 index 0000000..0b04feb --- /dev/null +++ b/app/views/admin/users/index.html.erb @@ -0,0 +1,13 @@ +
+
+

Users

+
+ +
+ + <%= render "index_table_head" %> + <%= render "index_table_body", users: @users %> +
+
+ +
diff --git a/app/views/admin/users/index.json.jbuilder b/app/views/admin/users/index.json.jbuilder new file mode 100644 index 0000000..2ff955e --- /dev/null +++ b/app/views/admin/users/index.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.array! @users, partial: 'users/user', as: :user diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb new file mode 100644 index 0000000..eb66256 --- /dev/null +++ b/app/views/admin/users/show.html.erb @@ -0,0 +1,15 @@ +<%= render "user", user: @user %> + +
+ <%= link_to "Edit this user", edit_admin_user_path(@user) %> | + <%= link_to "Change this user roles", edit_roles_admin_user_path(@user) %> | + <%= link_to "Back to users", admin_users_path %> + + <%= button_to( + "Remove this user", + edit_admin_user_path(@user), + method: :delete, + data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, + class: "tw-mt-6 tw-mr-6 tw-mb-6 tw-bg-red-500 tw-hover:bg-blue-700 tw-text-white tw-font-bold tw-py-2 tw-px-4 tw-rounded" + ) %> +
diff --git a/app/views/admin/users/show.json.jbuilder b/app/views/admin/users/show.json.jbuilder new file mode 100644 index 0000000..48cca18 --- /dev/null +++ b/app/views/admin/users/show.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.partial! 'users/user', user: @user diff --git a/app/views/layouts/_navbar.html.erb b/app/views/layouts/_navbar.html.erb index 793092e..01896dc 100644 --- a/app/views/layouts/_navbar.html.erb +++ b/app/views/layouts/_navbar.html.erb @@ -21,6 +21,11 @@ + <% if user_signed_in? %> Welcome <%= current_user.full_name %> diff --git a/config/locales/en.yml b/config/locales/en.yml index fd727f4..7d261de 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -62,7 +62,22 @@ en: success: "Lab test was successfully updated." destroy: success: "Lab test was successfully removed." + admin: + users: + update: + success: "User was successfully updated." + destroy: + success: "User was successfully removed." + update_roles: + success: "User roles were successfully updated." application: user_not_authorized: failure: 'You are not authorized to perform this action.' + + errors: + messages: + invalid_roles_detected: "Invalid roles detected: %{invalid_roles}" + remove_role_failure: "Failed to remove role: %{role}" + add_role_failure: "Failed to add role: %{role}" + unexpected_error: "Unexpected error while synchronizing roles" diff --git a/config/routes.rb b/config/routes.rb index 5efdc48..87b6d42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,14 @@ resources :measurements resources :lab_tests resources :health_records + namespace :admin do + resources :users, only: %i[index show edit update destroy] do + member do + get :edit_roles + post :update_roles + end + end + end devise_for :users # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html From 88413abb264ee939d5dc8ac4c5d5a434895921a4 Mon Sep 17 00:00:00 2001 From: aksafan Date: Mon, 30 Dec 2024 02:14:22 -0600 Subject: [PATCH 3/4] Add user management tests --- Dockerfile | 3 + Gemfile | 7 +- Gemfile.lock | 8 +- .../stylesheets/application.tailwind.css | 10 -- app/controllers/admin/users_controller.rb | 4 +- ...30074802_add_unique_index_to_roles_name.rb | 7 + db/schema.rb | 3 +- spec/factories/roles.rb | 7 + spec/factories/users.rb | 13 ++ spec/helpers/users_helper_spec.rb | 61 +++++++++ spec/rails_helper.rb | 5 + spec/requests/admin/users_spec.rb | 120 ++++++++++++++++++ 12 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20241230074802_add_unique_index_to_roles_name.rb create mode 100644 spec/factories/roles.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/helpers/users_helper_spec.rb create mode 100644 spec/requests/admin/users_spec.rb diff --git a/Dockerfile b/Dockerfile index e061625..8f58215 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,9 @@ RUN gem install bundler && bundle install --jobs 4 --retry 3 # Copy the application code COPY . /app +# Precompile assets (optional, if using Rails with assets) +RUN bundle exec rake assets:precompile + # Expose the port the app runs on EXPOSE 3000 diff --git a/Gemfile b/Gemfile index e1390ef..d457281 100644 --- a/Gemfile +++ b/Gemfile @@ -58,7 +58,8 @@ gem 'email_validator' # Styling gem 'bootstrap' -gem 'sassc-rails' +gem 'sassc', '~> 2.4' +gem 'sassc-rails', '~> 2.1.2' gem 'tailwindcss-rails', '~> 2.7' gem 'dotenv-rails' @@ -66,6 +67,10 @@ gem 'dotenv-rails' # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" +group :test do + gem 'rails-controller-testing' +end + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem 'debug', platforms: %i[mri windows] diff --git a/Gemfile.lock b/Gemfile.lock index 7f5c0a1..f26c755 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,6 +219,10 @@ GEM activesupport (= 7.1.5.1) bundler (>= 1.15.0) railties (= 7.1.5.1) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -370,6 +374,7 @@ DEPENDENCIES puma (>= 5.0) pundit rails (~> 7.1.3, >= 7.1.3.4) + rails-controller-testing rails-html-sanitizer (>= 1.6.1) rolify rspec-rails @@ -379,7 +384,8 @@ DEPENDENCIES rubocop-rails rubocop-rspec rubocop-rspec_rails - sassc-rails + sassc (~> 2.4) + sassc-rails (~> 2.1.2) sprockets-rails stimulus-rails tailwindcss-rails (~> 2.7) diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 8666d2f..b5c61c9 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -1,13 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -/* - -@layer components { - .btn-primary { - @apply py-2 px-4 bg-blue-200; - } -} - -*/ diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b2adade..5b9adaf 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -52,7 +52,7 @@ def edit_roles authorize @user end - # PATCH/PUT /admin/users/1/update_roles or /admin/users/1/update_roles.json + # POST /admin/users/1/update_roles or /admin/users/1/update_roles.json def update_roles authorize @user @@ -75,7 +75,7 @@ def set_user end def set_roles_list - @roles = Role.all.map { |role| [role.name.capitalize, role.name] } + @roles = Role.distinct.pluck(:name).map { |role| [role.capitalize, role] } end def set_user_params_roles diff --git a/db/migrate/20241230074802_add_unique_index_to_roles_name.rb b/db/migrate/20241230074802_add_unique_index_to_roles_name.rb new file mode 100644 index 0000000..4ec54d2 --- /dev/null +++ b/db/migrate/20241230074802_add_unique_index_to_roles_name.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUniqueIndexToRolesName < ActiveRecord::Migration[7.1] + def change + add_index :roles, :name, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 561b22c..c74842f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_11_26_065249) do +ActiveRecord::Schema[7.1].define(version: 2024_12_30_074802) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -77,6 +77,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" + t.index ["name"], name: "index_roles_on_name", unique: true t.index ["resource_type", "resource_id"], name: "index_roles_on_resource" end diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb new file mode 100644 index 0000000..8d697f7 --- /dev/null +++ b/spec/factories/roles.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :role do |f| + f.name { Faker::Job.unique.position } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..4855f02 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do |f| + f.first_name { Faker::Name.first_name } + f.last_name { Faker::Name.last_name } + f.email { Faker::Internet.unique.email } + f.password { 'password' } + trait :admin do + after(:create) { |user| user.add_role(:admin) } + end + end +end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb new file mode 100644 index 0000000..0f1c418 --- /dev/null +++ b/spec/helpers/users_helper_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UsersHelper do + let(:user) { instance_double(User, user_roles_names: %w[admin doctor health_coach]) } + + describe '#get_user_roles_list_as_string' do + context 'when the user has valid roles' do + it 'returns a comma-separated string of role names' do + result = helper.get_user_roles_list_as_string(user) + expect(result).to eq('Admin, Doctor, Health coach') + end + end + + context 'when the user has no roles' do + let(:user) { instance_double(User, user_roles_names: []) } + + it 'returns an empty string' do + result = helper.get_user_roles_list_as_string(user) + expect(result).to eq('') + end + end + + context 'when the user has invalid roles' do + let(:user) { instance_double(User, user_roles_names: %w[invalid_role]) } + + it 'returns nil for invalid roles' do + result = helper.get_user_roles_list_as_string(user) + expect(result).to eq('') + end + end + end + + describe '#get_user_role_ids_list' do + context 'when the user has roles' do + it 'returns the list of role IDs' do + result = helper.get_user_role_ids_list(user) + expect(result).to eq(%w[admin doctor health_coach]) + end + end + + context 'when the user has no roles' do + let(:user) { instance_double(User, user_roles_names: []) } + + it 'returns an empty array' do + result = helper.get_user_role_ids_list(user) + expect(result).to eq([]) + end + end + + context 'when the user has invalid roles' do + let(:user) { instance_double(User, user_roles_names: %w[invalid_role]) } + + it 'returns the invalid roles as-is' do + result = helper.get_user_role_ids_list(user) + expect(result).to eq(['invalid_role']) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 99ebdaf..fdd0294 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -8,6 +8,7 @@ abort('The Rails environment is running in production mode!') if Rails.env.production? require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! +require 'devise' # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -65,3 +66,7 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") end + +RSpec.configure do |config| + config.include Devise::Test::IntegrationHelpers, type: :request +end diff --git a/spec/requests/admin/users_spec.rb b/spec/requests/admin/users_spec.rb new file mode 100644 index 0000000..0d3b6c6 --- /dev/null +++ b/spec/requests/admin/users_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin::UsersController' do + let(:admin) { create(:user, :admin) } + let(:user) { create(:user) } + let(:valid_attributes) { { first_name: 'John', last_name: 'Doe', email: 'john.doe@example.com' } } + let(:invalid_attributes) { { email: '' } } + let(:roles) { %w[admin doctor user] } + + before do + sign_in admin + end + + describe 'GET /index' do + it 'renders a successful response' do + get admin_users_path + expect(response).to be_successful + end + + it 'authorizes and displays all users' do + users_amount = 3 + create_list(:user, users_amount) + get admin_users_path + expect(assigns(:users).count).to eq(users_amount + 1) # Include the admin user + end + end + + describe 'GET /show' do + it 'renders the user details' do + get admin_user_path(user) + expect(response).to be_successful + end + end + + describe 'GET /edit' do + it 'renders the edit form' do + get edit_admin_user_path(user) + expect(response).to be_successful + end + end + + describe 'PATCH /update' do + context 'with valid parameters' do + it 'redirects to the user page' do + patch admin_user_path(user), params: { user: valid_attributes } + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'updates the user attributes' do + patch admin_user_path(user), params: { user: valid_attributes } + expect(user.reload.first_name).to eq('John') + end + end + + context 'with invalid parameters' do + it 'renders the edit form with errors' do + patch admin_user_path(user), params: { user: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe 'DELETE /destroy' do + it 'deletes the user' do + user_to_delete = create(:user) + expect do + delete admin_user_path(user_to_delete) + end.to change(User, :count).by(-1) + end + + it 'redirects to the users index' do + user_to_delete = create(:user) + delete admin_user_path(user_to_delete) + expect(response).to redirect_to(admin_users_path) + end + end + + describe 'GET /edit_roles' do + it 'renders the edit roles form' do + get edit_roles_admin_user_path(user) + expect(response).to be_successful + end + + it 'assigns roles list' do + roles.each { |role| Role.find_or_create_by!(name: role) } # Prevent duplicate role creation + get edit_roles_admin_user_path(user) + expect(assigns(:roles)).to match_array(roles.map { |r| [r.capitalize, r] }) + end + end + + describe 'POST /update_roles' do + context 'with valid roles' do + it 'redirects to the user page' do + roles.each { |role| Role.find_or_create_by!(name: role) } + post update_roles_admin_user_path(user), params: { user: { roles: %w[admin doctor] } } + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'updates the user roles' do + roles.each { |role| Role.find_or_create_by!(name: role) } + post update_roles_admin_user_path(user), params: { user: { roles: %w[admin doctor] } } + expect(user.reload.roles.map(&:name)).to include('admin', 'doctor') + end + end + + context 'with invalid roles' do + it 'returns an unprocessable entity status' do + post update_roles_admin_user_path(user), params: { user: { roles: %w[invalid_role] } } + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'assigns errors to the user object' do + post update_roles_admin_user_path(user), params: { user: { roles: %w[invalid_role] } } + expect(assigns(:user).errors[:base]).to include('Invalid roles detected: invalid_role') + end + end + end +end From ec063aa21c4692408bfa71a801a4ef82378a27fe Mon Sep 17 00:00:00 2001 From: aksafan Date: Mon, 30 Dec 2024 02:40:34 -0600 Subject: [PATCH 4/4] Fix CI --- .github/workflows/workflow.yml | 2 ++ Gemfile | 2 +- Gemfile.lock | 21 +++++++++------------ config/application.rb | 4 ++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index f4079d5..710ec63 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -70,5 +70,7 @@ jobs: run: | bundler exec rails db:create bundler exec rails db:migrate + - name: Enhance prepare commands with tailwindcss:build + run: bundle exec rake spec:prepare - name: Run tests run: bundler exec rspec diff --git a/Gemfile b/Gemfile index d457281..690d8e2 100644 --- a/Gemfile +++ b/Gemfile @@ -60,7 +60,7 @@ gem 'email_validator' gem 'bootstrap' gem 'sassc', '~> 2.4' gem 'sassc-rails', '~> 2.1.2' -gem 'tailwindcss-rails', '~> 2.7' +gem 'tailwindcss-rails', '~> 3.1' gem 'dotenv-rails' diff --git a/Gemfile.lock b/Gemfile.lock index f26c755..81879fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -313,18 +313,15 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.1) - tailwindcss-rails (2.7.7) - railties (>= 7.0.0) - tailwindcss-rails (2.7.7-aarch64-linux) - railties (>= 7.0.0) - tailwindcss-rails (2.7.7-arm-linux) - railties (>= 7.0.0) - tailwindcss-rails (2.7.7-arm64-darwin) - railties (>= 7.0.0) - tailwindcss-rails (2.7.7-x86_64-darwin) - railties (>= 7.0.0) - tailwindcss-rails (2.7.7-x86_64-linux) + tailwindcss-rails (3.1.0) railties (>= 7.0.0) + tailwindcss-ruby + tailwindcss-ruby (3.4.17) + tailwindcss-ruby (3.4.17-aarch64-linux) + tailwindcss-ruby (3.4.17-arm-linux) + tailwindcss-ruby (3.4.17-arm64-darwin) + tailwindcss-ruby (3.4.17-x86_64-darwin) + tailwindcss-ruby (3.4.17-x86_64-linux) thor (1.3.2) tilt (2.4.0) timeout (0.4.1) @@ -388,7 +385,7 @@ DEPENDENCIES sassc-rails (~> 2.1.2) sprockets-rails stimulus-rails - tailwindcss-rails (~> 2.7) + tailwindcss-rails (~> 3.1) turbo-rails tzinfo-data web-console diff --git a/config/application.rb b/config/application.rb index 821cbb6..fab9667 100644 --- a/config/application.rb +++ b/config/application.rb @@ -40,5 +40,9 @@ class Application < Rails::Application # Don't generate system test files. config.generators.system_tests = nil + + # Fixes broken tailwind.css build for CI + # @see https://github.com/rails/tailwindcss-rails/issues/153#issuecomment-1225895063 + config.assets.css_compressor = nil end end