From 13cddd2d45e7fe4592cc6ad736fb75bc6abe08ed Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Wed, 15 Jul 2026 08:03:47 +0200 Subject: [PATCH] refactor: extract dashboard event queries into DashboardQuery object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move upcoming_events, upcoming_events_for_user, all_events, and total_upcoming_events_count from DashboardController into app/queries/dashboard_query.rb as class methods. These methods assemble cross-model event listings (Workshop, Event, Meeting) with sorting and limiting logic — domain concerns, not controller responsibilities. The controller now delegates to DashboardQuery, keeping only param extraction (year_param) and a simple scope chain (top_coach_query). --- app/controllers/dashboard_controller.rb | 64 +--------------------- app/queries/dashboard_query.rb | 60 ++++++++++++++++++++ spec/queries/dashboard_query_spec.rb | 73 +++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 app/queries/dashboard_query.rb create mode 100644 spec/queries/dashboard_query_spec.rb diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 6d4e89586..82c529996 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -2,25 +2,22 @@ class DashboardController < ApplicationController before_action :is_logged_in?, only: %i[dashboard] skip_before_action :accept_terms, except: %i[dashboard show] - DEFAULT_UPCOMING_EVENTS = 5 - MAX_WORKSHOP_QUERY = 10 - helper_method :year_param def show @chapters = Chapter.active.all.order(:created_at) @user = current_user ? MemberPresenter.new(current_user) : nil - @upcoming_workshops = upcoming_events.map.each_with_object({}) do |(key, value), hash| + @upcoming_workshops = DashboardQuery.upcoming_events.map.each_with_object({}) do |(key, value), hash| hash[key] = EventPresenter.decorate_collection(value) end - @has_more_events = total_upcoming_events_count > DEFAULT_UPCOMING_EVENTS + @has_more_events = DashboardQuery.total_upcoming_events_count > DashboardQuery::DEFAULT_UPCOMING_EVENTS @testimonials = Testimonial.order(Arel.sql('RANDOM()')).limit(5).includes(:member) end def dashboard @user = MemberPresenter.new(current_user) - @ordered_events = upcoming_events_for_user.map.each_with_object({}) do |(key, value), hash| + @ordered_events = DashboardQuery.upcoming_events_for_user(current_user).map.each_with_object({}) do |(key, value), hash| hash[key] = EventPresenter.decorate_collection(value) end @announcements = current_user.announcements.active @@ -55,59 +52,4 @@ def top_coach_query .order(Arel.sql('COUNT(member_id) DESC')) .select(:member_id) end - - def upcoming_events - workshops = Workshop.eager_load(:chapter, :sponsors, :organisers, :permissions) - .today_and_upcoming - .limit(MAX_WORKSHOP_QUERY) - .to_a - sorted_events = all_events(workshops).sort_by(&:date_and_time) - - limited_events = [] - dates_shown = 0 - prev_date = nil - - sorted_events.each do |event| - dates_shown += 1 if event.date != prev_date - prev_date = event.date - break if dates_shown > 3 || limited_events.size >= DEFAULT_UPCOMING_EVENTS - - limited_events << event - end - - limited_events.group_by(&:date) - end - - def total_upcoming_events_count - workshop_count = Workshop.upcoming.count - event_count = Event.upcoming.count - meeting_exists = Meeting.next.present? ? 1 : 0 - - workshop_count + event_count + meeting_exists - end - - def upcoming_events_for_user - chapter_workshops = Workshop.upcoming - .eager_load(:chapter, :sponsors, :organisers, :permissions) - .where(chapter: current_user.chapters) - .to_a - - accepted_workshops = current_user.workshop_invitations.accepted - .joins(:workshop) - .merge(Workshop.upcoming) - .eager_load(workshop: %i[chapter sponsors organisers permissions]) - .map(&:workshop) - - all_events(chapter_workshops + accepted_workshops) - .sort_by(&:date_and_time) - .group_by(&:date) - end - - def all_events(workshops) - meeting = Meeting.eager_load(:venue, :organisers, :permissions).next - events = Event.eager_load(:venue, :sponsors, :sponsorships, :permissions, - :organisers).upcoming.take(DEFAULT_UPCOMING_EVENTS) - - [*workshops, *events, meeting].uniq.compact - end end diff --git a/app/queries/dashboard_query.rb b/app/queries/dashboard_query.rb new file mode 100644 index 000000000..3d2f2b2ed --- /dev/null +++ b/app/queries/dashboard_query.rb @@ -0,0 +1,60 @@ +class DashboardQuery + DEFAULT_UPCOMING_EVENTS = 5 + MAX_WORKSHOP_QUERY = 10 + + def self.upcoming_events + workshops = Workshop.eager_load(:chapter, :sponsors, :organisers, :permissions) + .today_and_upcoming + .limit(MAX_WORKSHOP_QUERY) + .to_a + sorted_events = all_events(workshops).sort_by(&:date_and_time) + + limited_events = [] + dates_shown = 0 + prev_date = nil + + sorted_events.each do |event| + dates_shown += 1 if event.date != prev_date + prev_date = event.date + break if dates_shown > 3 || limited_events.size >= DEFAULT_UPCOMING_EVENTS + + limited_events << event + end + + limited_events.group_by(&:date) + end + + def self.upcoming_events_for_user(member) + chapter_workshops = Workshop.upcoming + .eager_load(:chapter, :sponsors, :organisers, :permissions) + .where(chapter: member.chapters) + .to_a + + accepted_workshops = member.workshop_invitations.accepted + .joins(:workshop) + .merge(Workshop.upcoming) + .eager_load(workshop: %i[chapter sponsors organisers permissions]) + .map(&:workshop) + + all_events(chapter_workshops + accepted_workshops) + .sort_by(&:date_and_time) + .group_by(&:date) + end + + def self.total_upcoming_events_count + workshop_count = Workshop.upcoming.count + event_count = Event.upcoming.count + meeting_exists = Meeting.next.present? ? 1 : 0 + + workshop_count + event_count + meeting_exists + end + + def self.all_events(workshops) + meeting = Meeting.eager_load(:venue, :organisers, :permissions).next + events = Event.eager_load(:venue, :sponsors, :sponsorships, :permissions, + :organisers).upcoming.take(DEFAULT_UPCOMING_EVENTS) + + [*workshops, *events, meeting].uniq.compact + end + private_class_method :all_events +end diff --git a/spec/queries/dashboard_query_spec.rb b/spec/queries/dashboard_query_spec.rb new file mode 100644 index 000000000..9af66d54f --- /dev/null +++ b/spec/queries/dashboard_query_spec.rb @@ -0,0 +1,73 @@ +RSpec.describe DashboardQuery do + describe '.upcoming_events' do + it 'returns an empty hash when there are no upcoming events' do + expect(described_class.upcoming_events).to eq({}) + end + + it 'includes upcoming events and meetings alongside workshops' do + workshop = Fabricate(:workshop) + event = Fabricate(:event) + meeting = Fabricate(:meeting) + + result = described_class.upcoming_events + + expect(result.values.flatten).to include(workshop, event, meeting) + end + + it 'limits to exactly 3 distinct dates' do + Fabricate(:workshop, date_and_time: 1.day.from_now) + Fabricate(:workshop, date_and_time: 2.days.from_now) + Fabricate(:workshop, date_and_time: 3.days.from_now) + Fabricate(:workshop, date_and_time: 4.days.from_now) + + result = described_class.upcoming_events + + expect(result.keys.length).to eq(3) + end + + it 'excludes past workshops' do + Fabricate(:workshop, date_and_time: 1.month.ago) + + expect(described_class.upcoming_events).to eq({}) + end + end + + describe '.upcoming_events_for_user' do + it 'returns events for the member chapters and accepted workshops' do + chapter = Fabricate(:chapter) + member = Fabricate(:member, groups: [Fabricate(:students, chapter: chapter)]) + workshop = Fabricate(:workshop, chapter: chapter) + Fabricate(:attending_workshop_invitation, member: member, workshop: workshop) + + result = described_class.upcoming_events_for_user(member) + + expect(result.values.flatten).to include(workshop) + end + + it 'returns an empty hash when the member has no upcoming events' do + member = Fabricate(:member) + + expect(described_class.upcoming_events_for_user(member)).to eq({}) + end + end + + describe '.total_upcoming_events_count' do + it 'returns 0 when there are no upcoming events' do + expect(described_class.total_upcoming_events_count).to eq(0) + end + + it 'counts upcoming workshops, events, and meetings' do + Fabricate(:workshop) + Fabricate(:event) + Fabricate(:meeting) + + expect(described_class.total_upcoming_events_count).to eq(3) + end + + it 'does not count past events' do + Fabricate(:past_workshop) + + expect(described_class.total_upcoming_events_count).to eq(0) + end + end +end