Skip to content

Commit

Permalink
First pass at the progress activity query.
Browse files Browse the repository at this point in the history
This seems to be working but feels like it needs more testing to be sure
  • Loading branch information
msquance-stem committed Dec 19, 2024
1 parent 8aecc26 commit d139b3c
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 0 deletions.
54 changes: 54 additions & 0 deletions app/services/programmes/progress_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Programmes
class ProgressQuery
def initialize(programme, active_state, enrolled, completed_groups = [])
@programme = programme
@active_state = active_state # :active, :lapsing, :lapsed
@enrolled = enrolled
@completed_groups = completed_groups
end

def call
query = if @enrolled
User.joins(:user_programme_enrolments, achievements: {activity: :programme_activities})
.where(
user_programme_enrolments: {programme: @programme},
programme_activities: {programme: @programme, legacy: false},
achievements: {created_at: dates_by_state}
)
else
User.joins(:user_programme_enrolments, achievements: {activity: :programme_activities})
.where(
programme_activities: {programme: @programme, legacy: false},
achievements: {created_at: dates_by_state}
)
.where.not(
user_programme_enrolments: {programme: @programme}
)
end
if @completed_groups.any?
query = query.group(:id)
@completed_groups.each do |group|
query = if group.instance_of? ProgrammeActivityGroupings::CreditCounted
query.having("SUM(CASE WHEN \"programme_activities\".\"programme_activity_grouping_id\" = ? THEN activities.credit END) >= ?", group.id, group.metadata["required_credit_count"])
else
query.having("COUNT(CASE WHEN \"programme_activities\".\"programme_activity_grouping_id\" = ? THEN 1 END) >= ?", group.id, group.required_for_completion)
end
end
end
query.distinct
end

def dates_by_state
case @active_state
when :active
DateTime.now.months_ago(4)..DateTime.now
when :lapsing
DateTime.now.months_ago(8)..DateTime.now.months_ago(4)
when :lapsed
DateTime.now.months_ago(1000)..DateTime.now.months_ago(8)
else
DateTime.now.months_ago(4)..DateTime.now
end
end
end
end
1 change: 1 addition & 0 deletions spec/factories/programme_activities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
factory :programme_activity do
programme
activity
legacy { false }
end
end
241 changes: 241 additions & 0 deletions spec/services/programmes/progress_query_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
require "rails_helper"

RSpec.describe Programmes::ProgressQuery do
let(:programme) { create(:programme) }
let!(:course_group) {
create(:programme_activity_groupings_credit_counted, programme:, metadata: {required_credit_count: 20})
}
let!(:community_group_1) { create(:programme_activity_grouping, programme:, community: true, required_for_completion: 2) }
let!(:community_group_2) { create(:programme_activity_grouping, programme:, community: true) }
let!(:course) {
act = create(:activity, credit: 10)
create(:programme_activity, activity: act, programme_activity_grouping: course_group, programme:)
act
}
let!(:course_20c) {
act = create(:activity, credit: 20)
create(:programme_activity, activity: act, programme_activity_grouping: course_group, programme:)
act
}
let!(:community_activity_1) {
act = create(:activity)
create(:programme_activity, activity: act, programme_activity_grouping: community_group_1, programme:)
act
}
let!(:community_activity_2) {
act = create(:activity)
create(:programme_activity, activity: act, programme_activity_grouping: community_group_2, programme:)
act
}

let!(:community_activity_1b) {
act = create(:activity)
create(:programme_activity, activity: act, programme_activity_grouping: community_group_1, programme:)
act
}

let(:other_programme) { create(:programme) }
let!(:other_course_group) { create(:programme_activity_groupings_credit_counted, programme: other_programme) }
let!(:other_activity) {
act = create(:activity)
create(:programme_activity, activity: act, programme_activity_grouping: other_course_group, programme: other_programme)
act
}
let!(:other_community_group_1) { create(:programme_activity_grouping, programme: other_programme, community: true) }
let!(:other_community_activity_1) {
act = create(:activity)
create(:programme_activity, activity: act, programme_activity_grouping: other_community_group_1, programme: other_programme)
act
}

def unenrolled_user
user = create(:user)
create(:user_programme_enrolment, user:, programme: other_programme)
user
end

def enrolled_user
user = create(:user)
create(:user_programme_enrolment, user:, programme:)
user
end

def active_achievement(user, activity)
date = Faker::Time.between(from: DateTime.now.months_ago(4), to: DateTime.now.days_ago(1))
create(:completed_achievement, activity:, user:, created_at: date, updated_at: date)
user
end

def lapsing_achievement(user, activity)
date = Faker::Time.between(from: DateTime.now.months_ago(8), to: DateTime.now.months_ago(4))
create(:completed_achievement, activity:, user:, created_at: date, updated_at: date)
user
end

def lapsed_achievement(user, activity)
date = Faker::Time.between(from: DateTime.now.months_ago(12), to: DateTime.now.months_ago(8))
create(:completed_achievement, activity:, user:, created_at: date, updated_at: date)
user
end

it "enrolled_user should not have user_programme_enrolment" do
# Test is to ensure that nothing has changed with auto enrolment that might impact this test suite
expect(UserProgrammeEnrolment.where(user: unenrolled_user, programme:)).to be_empty
end

context "dates_by_state" do
it "should return correct dates for active" do
active_range = described_class.new(programme, :active, true).dates_by_state
expect(active_range.first.to_date).to eq(DateTime.now.months_ago(4).to_date)
expect(active_range.last.to_date).to eq(DateTime.now.to_date)
end

it "should return correct dates for lasping" do
lapsing_range = described_class.new(programme, :lapsing, true).dates_by_state
expect(lapsing_range.first.to_date).to eq(DateTime.now.months_ago(8).to_date)
expect(lapsing_range.last.to_date).to eq(DateTime.now.months_ago(4).to_date)
end
it "should return correct dates for lasping" do
lapsed_range = described_class.new(programme, :lapsed, true).dates_by_state
expect(lapsed_range.first.to_date).to eq(DateTime.now.months_ago(1000).to_date)
expect(lapsed_range.last.to_date).to eq(DateTime.now.months_ago(8).to_date)
end

it "should default to active range for non matching key" do
active_range = described_class.new(programme, :non_sense, true).dates_by_state
expect(active_range.first.to_date).to eq(DateTime.now.months_ago(4).to_date)
expect(active_range.last.to_date).to eq(DateTime.now.to_date)
end
end

context "active user" do
context "enrolled" do
let!(:user_1) { active_achievement(enrolled_user, course) }
let!(:user_2) {
active_achievement(enrolled_user, community_activity_1)
}
let!(:user_3) {
user = active_achievement(enrolled_user, community_activity_1)
active_achievement(user, community_activity_1b)
}
let!(:user_4) { active_achievement(enrolled_user, course_20c) }
let!(:user_lapsing) { lapsing_achievement(enrolled_user, community_activity_1) }
let!(:user_lapsed) { lapsed_achievement(enrolled_user, community_activity_1) }
let!(:no_match_user) { active_achievement(unenrolled_user, community_activity_1) }

context "with no groups" do
it "shoud return correct users" do
result = described_class.new(programme, :active, true).call
expect(result).to contain_exactly(user_1, user_2, user_3, user_4)
expect(result).not_to include no_match_user
expect(result).not_to include user_lapsing
expect(result).not_to include user_lapsed
end
end

context "with single group" do
it "using required_for_completion should return correct users" do
result = described_class.new(programme, :active, true, [community_group_1]).call
expect(result).to contain_exactly(user_3)
expect(result).not_to include user_1
expect(result).not_to include user_2
expect(result).not_to include no_match_user
expect(result).not_to include user_lapsing
expect(result).not_to include user_lapsed
end

context "with credits" do
it "should not include users without enough credits" do
result = described_class.new(programme, :active, true, [course_group]).call
expect(result).not_to contain_exactly(user_1)
end
it "should include users with enough credits" do
result = described_class.new(programme, :active, true, [course_group]).call
expect(result).to contain_exactly(user_4)
end
end
end

context "with mulitple groups" do
let!(:user_both) {
user = active_achievement(enrolled_user, community_activity_1)
active_achievement(user, community_activity_1b)
active_achievement(user, course_20c)
}
it "using required_for_completion should return correct users" do
result = described_class.new(programme, :active, true, [community_group_1, course_group]).call
expect(result).to contain_exactly(user_both)
end
end
end

context "unenrolled" do
let!(:user_1) { active_achievement(enrolled_user, course) }
let!(:not_enrolled_user) { active_achievement(unenrolled_user, community_activity_1) }

it "with no groups" do
result = described_class.new(programme, :active, false).call
expect(result).to contain_exactly(not_enrolled_user)
expect(result).not_to include user_1
end
end
end

context "lapsing user" do
context "enrolled" do
let!(:user_1) { lapsing_achievement(enrolled_user, course) }
let!(:user_2) { lapsing_achievement(enrolled_user, community_activity_1) }
let!(:user_active) { active_achievement(enrolled_user, community_activity_1) }
let!(:user_lapsed) { lapsed_achievement(enrolled_user, community_activity_1) }
let!(:no_match_user) { lapsing_achievement(unenrolled_user, community_activity_1) }

it "with no groups" do
result = described_class.new(programme, :lapsing, true).call
expect(result).to contain_exactly(user_1, user_2)
expect(result).not_to include no_match_user
expect(result).not_to include user_active
expect(result).not_to include user_lapsed
end
end

context "unenrolled" do
let!(:user_1) { lapsing_achievement(enrolled_user, course) }
let!(:not_enrolled_user) { lapsing_achievement(unenrolled_user, community_activity_1) }

it "with no groups" do
result = described_class.new(programme, :lapsing, false).call
expect(result).to contain_exactly(not_enrolled_user)
expect(result).not_to include user_1
end
end
end

context "lapsed user" do
context "enrolled" do
let!(:user_1) { lapsed_achievement(enrolled_user, course) }
let!(:user_2) { lapsed_achievement(enrolled_user, community_activity_1) }
let!(:user_active) { active_achievement(enrolled_user, community_activity_1) }
let!(:user_lapsing) { lapsing_achievement(enrolled_user, community_activity_1) }
let!(:no_match_user) { active_achievement(unenrolled_user, community_activity_1) }

it "with no groups" do
result = described_class.new(programme, :lapsed, true).call
expect(result).to contain_exactly(user_1, user_2)
expect(result).not_to include no_match_user
expect(result).not_to include user_lapsing
expect(result).not_to include user_active
end
end

context "unenrolled" do
let!(:user_1) { lapsed_achievement(enrolled_user, course) }
let!(:not_enrolled_user) { lapsed_achievement(unenrolled_user, community_activity_1) }

it "with no groups" do
result = described_class.new(programme, :lapsed, false).call
expect(result).to contain_exactly(not_enrolled_user)
expect(result).not_to include user_1
end
end
end
end

0 comments on commit d139b3c

Please sign in to comment.