Skip to content

Commit

Permalink
Consider external trainings when qualifying #262
Browse files Browse the repository at this point in the history
  • Loading branch information
amaierhofer committed Mar 27, 2024
1 parent ad3b73e commit 38bc79b
Show file tree
Hide file tree
Showing 15 changed files with 843 additions and 2 deletions.
12 changes: 10 additions & 2 deletions app/controllers/external_trainings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ class ExternalTrainingsController < CrudController
before_render_form :load_event_kinds

def create
super(location: history_group_person_path(@group, @person))
super(location: history_group_person_path(@group, @person)) do
qualifier.issue
end
end

def destroy
super(location: history_group_person_path(@group, @person))
super(location: history_group_person_path(@group, @person)) do
qualifier.revoke
end
end

private
Expand All @@ -37,6 +41,10 @@ def build_entry
@person.external_trainings.build
end

def qualifier
@qualifier ||= ExternalTrainings::Qualifier.new(@person, entry, 'participant')
end

def load_event_kinds
@event_kinds ||= Event::Kind.list
end
Expand Down
64 changes: 64 additions & 0 deletions app/domain/external_trainings/qualifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

# Copyright (c) 2012-2024, Schweizer Alpen-Club. This file is part of
# hitobito_sac_cas and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_sac_cas.

module ExternalTrainings
class Qualifier < Event::Qualifier
ROLE = 'participant'.freeze

private

def issue_qualifications
with_adjusted_qualifications do
super
end
end

def revoke_qualifications
with_adjusted_qualifications do
super
end
end

def with_adjusted_qualifications
destroy_later_qualifications
yield
create_later_qualifications
end

def create_later_qualifications
sorted_later_events.each do |event|
QualifyAction.new(person, event, qualification_kinds(event.kind)).run
ProlongAction.new(person, event, prolongation_kinds(event.kind), role).run
end
end

def destroy_later_qualifications
@person.qualifications
.where(qualification_kind: qualifying_and_prolonging_kinds)
.where('qualified_at > ?', @event.qualification_date)
.destroy_all
end

def sorted_later_events
(courses_loader.load - [@event]).sort_by(&:qualification_date)
end

def courses_loader
@courses_loader ||= Event::TrainingDays::CoursesLoader.new(
@person.id,
ROLE,
qualifying_and_prolonging_kinds,
@event.qualification_date,
Time.zone.now
)
end

def qualifying_and_prolonging_kinds
qualification_kinds + prolongation_kinds
end
end
end
27 changes: 27 additions & 0 deletions app/domain/sac_cas/event/training_days/course_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

# Copyright (c) 2012-2024, Schweizer Alpen-Club. This file is part of
# hitobito_sac_cas and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_sac_cas.

module SacCas::Event::TrainingDays::CourseLoader

def load
super + load_external_trainings
end

private

def load_external_trainings
ExternalTraining.between(@start_date, @end_date)
.includes(event_kind: { event_kind_qualification_kinds: :qualification_kind })
.where(event_kind_qualification_kinds: {
qualification_kind_id: @qualification_kind_ids,
category: :prolongation,
role: @role,
})
.order('start_at DESC')
.distinct
end
end
26 changes: 26 additions & 0 deletions app/domain/sac_cas/event/training_days/courses_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

# Copyright (c) 2012-2024, Schweizer Alpen-Club. This file is part of
# hitobito_sac_cas and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_sac_cas.

module SacCas::Event::TrainingDays::CoursesLoader

def load
super + load_external_trainings
end

def load_external_trainings
ExternalTraining.between(@start_date, @end_date)
.where(person: @person_id)
.includes(event_kind: { event_kind_qualification_kinds: :qualification_kind })
.where(event_kind_qualification_kinds: {
qualification_kind_id: @qualification_kind_ids,
category: :prolongation,
role: @role,
})
.order('start_at DESC')
.distinct
end
end
14 changes: 14 additions & 0 deletions app/models/external_training.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,22 @@ class ExternalTraining < ActiveRecord::Base

scope :list, -> { order(created_at: :desc) }

def self.between(start_date, end_date)
where('start_at <= :end_date AND finish_at >= :start_date ',
start_date: start_date, end_date: end_date).distinct
end

def to_s
name
end

def start_date
start_at
end

def qualification_date
finish_at
end

alias_method :kind, :event_kind
end
3 changes: 3 additions & 0 deletions lib/hitobito_sac_cas/wagon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class Wagon < Rails::Engine
## Decorators
RoleDecorator.prepend SacCas::RoleDecorator

## Domain
Event::TrainingDays::CoursesLoader.prepend SacCas::Event::TrainingDays::CoursesLoader

## Resources
GroupResource.include SacCas::GroupResource
PersonResource.include SacCas::PersonResource
Expand Down
119 changes: 119 additions & 0 deletions spec/controllers/external_trainings_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# frozen_string_literal: true

# Copyright (c) 2024, Schweizer Alpen-Club. This file is part of
# hitobito_sac_cas and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_sac_cas

require 'spec_helper'

describe ExternalTrainingsController do

let(:person) { people(:mitglied) }
let(:group) { groups(:bluemlisalp_mitglieder) }
let(:ski_course) { event_kinds(:ski_course) }
let(:ski_leader) { qualification_kinds(:ski_leader) }
let(:start_at) { Date.new(2024, 1, 1) }

before { sign_in(people(:admin)) }

describe 'POST#create' do
let(:ski_leader_qualis) { person.qualifications.where(qualification_kind: ski_leader) }
let(:params) {
{
group_id: group.id,
person_id: person.id,
external_training: {
event_kind_id: ski_course.id,
start_at: start_at,
finish_at: start_at,
training_days: 1,
name: 'Skikurs'
}
}
}

it 'creates training without qualification' do
expect do
post :create, params: params
expect(response).to redirect_to(history_group_person_path(group, person))
end.to change { ExternalTraining.count }.by(1)
.and not_change { Qualification.count }
end

it 'creates qualification if event qualifies' do
create_event_kind_quali_kind(ski_course, ski_leader, category: :qualification)
expect do
post :create, params: params
end.to change { ski_leader_qualis.count }.by(1)
end

context 'existing qualification' do
let!(:quali) do
Fabricate(:qualification, qualification_kind: ski_leader, person: person, start_at: 3.years.ago, qualified_at: 3.years.ago)
end

it 'prolongs qualification if criteria matches' do
ski_leader.update!(required_training_days: nil)
expect do
post :create, params: params
end.to change { ski_leader_qualis.count }.by(1)
end

it 'noops if qualification is too old' do
quali.update_columns(finish_at: Date.new(2015, 1, 1))
expect do
post :create, params: params
end.not_to change { ski_leader_qualis.count }
end

context 'training days' do
it 'prolongs qualification if training has enough training days' do
expect do
post :create, params: params.deep_merge(external_training: { training_days: 2 })
end.to change { ski_leader_qualis.count }
end

it 'noops if training has not enough training days' do
expect do
post :create, params: params.deep_merge(external_training: { training_days: 1.5 })
end.to not_change { ski_leader_qualis.count }
end
end
end
end

describe 'POST#destroy' do
let(:ski_leader_qualis) { person.qualifications.where(qualification_kind: ski_leader) }
let(:params) { { group_id: group.id, person_id: person.id, id: training.id } }
let!(:training) { Fabricate(:external_training, person: person) }

before { create_event_kind_quali_kind(ski_course, ski_leader) }

it 'removes training' do
expect do
delete :destroy, params: params
expect(response).to redirect_to(history_group_person_path(group, person))
end.to change { ExternalTraining.count }.by(-1)
end

it 'removes training and corresponding qualification' do
qualification = Fabricate(:qualification, qualification_kind: ski_leader, person: person, qualified_at: training.finish_at)

expect do
delete :destroy, params: params
expect(response).to redirect_to(history_group_person_path(group, person))
end.to change { ExternalTraining.count }.by(-1)
.and change { Qualification.count }.by(-1)
end
end

def create_event_kind_quali_kind(event_kind, quali_kind, category: :qualification)
Event::KindQualificationKind.create!(
event_kind: event_kind,
qualification_kind: quali_kind,
category: category,
role: :participant
)
end
end
70 changes: 70 additions & 0 deletions spec/domain/event/qualifier_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

# Copyright (c) 2012-2024, Schweizer Alpen-Club. This file is part of
# hitobito_sac_cas and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_sac_cas.

require 'spec_helper'

describe Event::Qualifier do
let(:ski_course) { event_kinds(:ski_course) }
let(:ski_leader) { qualification_kinds(:ski_leader) }
let(:person) { people(:mitglied) }
let(:today) { Date.new(2024, 3, 26) }

describe 'prolonging' do
let(:participation) { create_course_participation(start_at: today, training_days: 1) }
subject(:qualifier) { described_class.for(participation) }
let(:start_dates) { person.qualifications.order(:start_at).pluck(:start_at) }

it 'does issue if event itself has sufficient training days' do
create_qualification(today - 1.year)
participation.event.update!(training_days: 2)
expect { qualifier.issue }.to change { person.qualifications.count }.by(1)
expect(start_dates).to eq [today - 1.year, today]
end

it 'does issue if event combined with training has sufficient training days' do
create_qualification(today - 1.year)
create_external_training(today - 5.months, training_days: 1)
expect { qualifier.issue }.to change { person.qualifications.count }.by(1)
expect(start_dates).to eq [today - 1.year, today - 5.months]
end

it 'does not issue if qualification date would be earlier than latest qualification' do
create_qualification(today - 1.year)
create_external_training(today - 15.months, training_days: 1)
expect { qualifier.issue }.not_to change { person.qualifications.count }
expect(start_dates).to eq [today - 1.year]
end

it 'does not issue if training is after course qualification date' do
create_qualification(today - 1.year)
create_external_training(today + 1.day, training_days: 1)
expect { qualifier.issue }.not_to change { person.qualifications.count }
expect(start_dates).to eq [today - 1.year]
end
end

def create_qualification(start_at, qualified_at = start_at)
Fabricate(:qualification, person: person, qualification_kind: ski_leader, start_at: start_at, qualified_at: qualified_at)
end

def create_external_training(start_at, training_days:)
Fabricate(:external_training, {
person: person,
event_kind: ski_course,
start_at: start_at,
finish_at: start_at,
training_days: training_days
})
end

def create_course_participation(training_days: nil, start_at:, qualified: false)
course = Fabricate.build(:course, kind: ski_course, training_days: training_days)
course.dates.build(start_at: start_at)
course.save!
Fabricate(:event_participation, event: course, person: person, qualified: qualified)
end
end
Loading

0 comments on commit 38bc79b

Please sign in to comment.