From c582c145ebf8457127a39c876e4db04e93653a9d Mon Sep 17 00:00:00 2001 From: Gustavo Molinari Date: Wed, 22 Jan 2025 13:19:32 -0300 Subject: [PATCH] [FS-8] Add attendances admin panel (#23) --- .../admin/attendances_controller.rb | 24 +++++++++ app/policies/admin/attendance_policy.rb | 24 +++++++++ .../admin/attendances/_attendance.html.erb | 6 +++ app/views/admin/attendances/index.html.erb | 15 ++++++ .../admin/base/_navigation_links.html.erb | 1 + config/locales/en.yml | 3 ++ config/routes.rb | 1 + spec/policies/admin/attendance_policy_spec.rb | 52 +++++++++++++++++++ spec/support/system_tests.rb | 2 + spec/system/admin/admin_attendances_spec.rb | 27 ++++++++++ 10 files changed, 155 insertions(+) create mode 100644 app/controllers/admin/attendances_controller.rb create mode 100644 app/policies/admin/attendance_policy.rb create mode 100644 app/views/admin/attendances/_attendance.html.erb create mode 100644 app/views/admin/attendances/index.html.erb create mode 100644 spec/policies/admin/attendance_policy_spec.rb create mode 100644 spec/system/admin/admin_attendances_spec.rb diff --git a/app/controllers/admin/attendances_controller.rb b/app/controllers/admin/attendances_controller.rb new file mode 100644 index 0000000..2d8c759 --- /dev/null +++ b/app/controllers/admin/attendances_controller.rb @@ -0,0 +1,24 @@ +module Admin + class AttendancesController < BaseController + before_action :set_attendance, only: [ :destroy ] + + def index + @attendances = authorize policy_scope(Current.company.attendances).order(created_at: :desc) + end + + def destroy + @attendance.destroy! + + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.remove(dom_id(@attendance)) } + format.html { redirect_to admin_attendances_path, notice: notice_message } + end + end + + private + + def set_attendance + @attendance = authorize policy_scope(Current.company.attendances).find(params[:id]) + end + end +end diff --git a/app/policies/admin/attendance_policy.rb b/app/policies/admin/attendance_policy.rb new file mode 100644 index 0000000..d9a5375 --- /dev/null +++ b/app/policies/admin/attendance_policy.rb @@ -0,0 +1,24 @@ +module Admin + class AttendancePolicy < ApplicationPolicy + # NOTE: Up to Pundit v2.3.1, the inheritance was declared as + # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`. + # In most cases the behavior will be identical, but if updating existing + # code, beware of possible changes to the ancestors: + # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5 + + def index? + owner? + end + + def destroy? + owner? && record.company == user.company + end + + class Scope < ApplicationPolicy::Scope + # NOTE: Be explicit about which records you allow access to! + def resolve + scope.all + end + end + end +end diff --git a/app/views/admin/attendances/_attendance.html.erb b/app/views/admin/attendances/_attendance.html.erb new file mode 100644 index 0000000..66daa5f --- /dev/null +++ b/app/views/admin/attendances/_attendance.html.erb @@ -0,0 +1,6 @@ +<%= tag.tr id: dom_id(attendance) do %> + <%= attendance.customer.name %> + <%= l attendance.attended_at %> + + <%= link_to t(:delete), [:admin, attendance], data: { turbo_method: :delete, turbo_confirm: t(:confirm) } if policy([:admin, attendance]).destroy? %> +<% end %> diff --git a/app/views/admin/attendances/index.html.erb b/app/views/admin/attendances/index.html.erb new file mode 100644 index 0000000..2cb82da --- /dev/null +++ b/app/views/admin/attendances/index.html.erb @@ -0,0 +1,15 @@ +
+
<%= Attendance.model_name.human %>
+
+ + + + + + + + <%= render @attendances %> + +
<%= Attendance.human_attribute_name :customer %><%= Attendance.human_attribute_name :attended_at %>
+
+
diff --git a/app/views/admin/base/_navigation_links.html.erb b/app/views/admin/base/_navigation_links.html.erb index 2f07e7a..c902ad1 100644 --- a/app/views/admin/base/_navigation_links.html.erb +++ b/app/views/admin/base/_navigation_links.html.erb @@ -1,2 +1,3 @@ <%= nav_item t(".edit_company"), edit_admin_company_path if Current.user.company? %> <%= nav_item Activity, admin_activities_path if policy([:admin, Activity]).index? %> +<%= nav_item Attendance, admin_attendances_path if policy([:admin, Attendance]).index? %> diff --git a/config/locales/en.yml b/config/locales/en.yml index d5f64a1..068143a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -38,6 +38,9 @@ en: name: Activity name duration_minutes: Duration (minutes) max_capacity: Max capacity + attendance: + customer: Customer + attended_at: Date company: name: Company name subdomain: Company subdomain diff --git a/config/routes.rb b/config/routes.rb index 3bf8659..8bc3a82 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,7 @@ constraints CompanyConstraint.new do namespace :admin do resources :activities + resources :attendances, only: [ :index, :destroy ] resource :company, only: [ :edit, :update ] root to: "companies#show" diff --git a/spec/policies/admin/attendance_policy_spec.rb b/spec/policies/admin/attendance_policy_spec.rb new file mode 100644 index 0000000..0497c89 --- /dev/null +++ b/spec/policies/admin/attendance_policy_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +RSpec.describe Admin::AttendancePolicy, type: :policy do + let(:company) { build(:company) } + + subject { described_class } + + before { Current.company = company } + + permissions :index? do + context "when the user is not logged in" do + let(:user) { nil } + + it { is_expected.not_to permit(user, Attendance) } + end + + context "when the user is not the owner" do + let(:user) { build(:user, :company) } + + it { is_expected.not_to permit(user, Attendance) } + end + + context "when the user is the owner" do + let(:user) { build(:user, :company, owner: company) } + + it { is_expected.to permit(user, Attendance) } + end + end + + permissions :destroy? do + let(:attendance) { build(:attendance, company:) } + + context "when the user is not logged in" do + let(:user) { nil } + + it { is_expected.not_to permit(user, attendance) } + end + + context "when the attendance is from a different company" do + let(:user) { build(:user, :company, owner: company) } + let(:attendance) { build(:attendance) } + + it { is_expected.not_to permit(user, attendance) } + end + + context "when the attendance is from the user company" do + let(:user) { build(:user, :company, owner: company) } + + it { is_expected.to permit(user, attendance) } + end + end +end diff --git a/spec/support/system_tests.rb b/spec/support/system_tests.rb index 7efed1b..2d18ce7 100644 --- a/spec/support/system_tests.rb +++ b/spec/support/system_tests.rb @@ -6,4 +6,6 @@ config.before(:each, type: :system, js: true) do driven_by :selenium_chrome_headless end + + config.include ActionView::RecordIdentifier, type: :system end diff --git a/spec/system/admin/admin_attendances_spec.rb b/spec/system/admin/admin_attendances_spec.rb new file mode 100644 index 0000000..0e8a07b --- /dev/null +++ b/spec/system/admin/admin_attendances_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe "Admin::Attendances", type: :system do + let(:subdomain) { "something" } + let(:company) { create(:company, subdomain:) } + let(:user) { create(:user, :company, owner: company) } + + before { sign_in(user) } + + let!(:attendance) { create(:attendance, company:) } + + it "can list all and delete the attendances" do + visit admin_root_url(subdomain:) + + within("#web-nav") do + find("a[href='#{admin_attendances_path}']").click + end + + expect(page).to have_content attendance.customer.name + expect(page).to have_content I18n.l(attendance.attended_at) + + within("##{dom_id(attendance)}") { click_on I18n.t(:delete) } + + expect(page).not_to have_content attendance.customer.name + expect(page).not_to have_content I18n.l(attendance.attended_at) + end +end