Skip to content

Commit

Permalink
Score calculation variants (#22)
Browse files Browse the repository at this point in the history
* Defines the logic for calculating average scores, based on the EvaluationProgram's program_type
  • Loading branch information
CharlieIGG authored Jan 4, 2020
1 parent 7e7577d commit 79792d8
Show file tree
Hide file tree
Showing 30 changed files with 453 additions and 171 deletions.
2 changes: 2 additions & 0 deletions .guard_history
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ all rspec
rspec
all rspec
all
EvaluatioEvaluai
all
2 changes: 1 addition & 1 deletion app/models/evaluation_criterium.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class EvaluationCriterium < ApplicationRecord
has_many :program_criteria
has_many :evaluation_scores
has_many :evaluation_programs, -> { distinct }, through: :program_criteria
has_many :project_evaluation_summaries, -> { distinct }, through: :evaluation_programs
has_many :project_evaluations, -> { distinct }, through: :evaluation_programs

validates :name, uniqueness: true
end
17 changes: 11 additions & 6 deletions app/models/evaluation_program.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,33 @@
# Table name: evaluation_programs
#
# id :bigint not null, primary key
# name :string
# start_at :datetime
# name :string not null
# start_at :datetime not null
# end_at :datetime
# program_type :integer default("project_follow_up"), not null
# created_at :datetime not null
# updated_at :datetime not null
# criteria_scale_max :float not null
# criteria_scale_min :float not null
# criteria_step_size :float default(1.0), not null
#


#
# A wrapper for evaluations. A project (Startup) might be part of several
# EvaluationPrograms, some lasting hours, some lasting months, and each with different evaluators
#
class EvaluationProgram < ApplicationRecord
MAXIMUM_VALID_SCORE = 100
AVAILABLE_STEP_SIZES = [0.5, 1, 5, 10]
AVAILABLE_STEP_SIZES = [0.5, 1, 5, 10].freeze

enum program_type: {
project_follow_up: 0,
competition: 1
}

has_many :project_evaluation_summaries
has_many :project_evaluations
has_many :program_criteria
has_many :projects, through: :project_evaluation_summaries
has_many :projects, through: :project_evaluations
has_many :evaluation_criteria, through: :program_criteria

validates :name, uniqueness: true, presence: true
Expand Down
37 changes: 18 additions & 19 deletions app/models/evaluation_score.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
#
# Table name: evaluation_scores
#
# id :bigint not null, primary key
# program_criterium_id :bigint not null
# project_evaluation_summary_id :bigint not null
# total :float
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint not null, primary key
# program_criterium_id :bigint not null
# project_evaluation_id :bigint not null
# total :float default(0.0), not null
# weighed_total :float default(0.0), not null
# created_at :datetime not null
# updated_at :datetime not null
#

#
Expand All @@ -18,23 +19,15 @@
#
class EvaluationScore < ApplicationRecord
belongs_to :program_criterium
belongs_to :project_evaluation_summary
has_one :evaluation_program, through: :program_criterium
belongs_to :project_evaluation
has_one :evaluation_program, through: :project_evaluation

delegate :name, :weight, to: :program_criterium

validates :program_criterium_id, uniqueness: { scope: :project_evaluation_summary_id }
validates :program_criterium_id, uniqueness: { scope: :project_evaluation_id }
validates_with EvaluationScoreValidator

after_commit :update_summary
after_destroy :update_summary

def update_summary
project_evaluation_summary.scores[name] = score_summary
# TODO: This operation should probably be extracted to the corresponding
# controller actions in order to preven redundancy.
project_evaluation_summary.recalculate_total_score
project_evaluation_summary.save
end
before_save :calculate_weighted_total

def minimum
evaluation_program.criteria_scale_min
Expand All @@ -54,4 +47,10 @@ def score_summary
"weighed_points": (points.to_f / maximum) * weight
}.with_indifferent_access
end

private

def calculate_weighted_total
self.weighed_total = (total.to_f / maximum) * weight
end
end
6 changes: 3 additions & 3 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
# updated_at :datetime not null
#


#
# Projects to evaluate, these might be Startups or something similar
#
class Project < ApplicationRecord
has_many :project_evaluation_summaries
has_many :evaluation_programs, through: :project_evaluation_summaries
has_many :project_evaluations
has_many :project_program_summaries
has_many :evaluation_programs, through: :project_program_summaries
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,45 @@

# == Schema Information
#
# Table name: project_evaluation_summaries
# Table name: project_evaluations
#
# id :bigint not null, primary key
# total_score :float default(0.0)
# evaluation_program_id :bigint not null
# project_id :bigint not null
# program_start :date
# evaluator_id :bigint not null
# created_at :datetime not null
# updated_at :datetime not null
# timestamp :datetime
# scores :jsonb
#


#
# This class depicts the track record for a Project in any given EvaluationProgram
#
class ProjectEvaluationSummary < ApplicationRecord
class ProjectEvaluation < ApplicationRecord
belongs_to :evaluation_program
belongs_to :project
belongs_to :evaluator, class_name: 'User'
has_many :evaluation_scores

validates :evaluator_id, uniqueness: { scope: :evaluation_program_id },
if: :enforce_evaluator_uniqueness?
validates_numericality_of :total_score,
less_than_or_equal_to: EvaluationProgram::MAXIMUM_VALID_SCORE,
greater_than_or_equal_to: 0

def recalculate_total_score
self.total_score = calculate_total_score
self.total_score = evaluation_scores.sum(:weighed_total)
total_score
end

def recalculate_total_score!
update(total_score: recalculate_total_score)
end

def calculate_total_score
return 0 unless scores.count.positive?
def enforce_evaluator_uniqueness?
return false unless evaluation_program.present?

scores.map { |_k, score| score[:weighed_points] }.compact.sum
evaluation_program.competition?
end
end
33 changes: 33 additions & 0 deletions app/models/project_program_summary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: project_program_summaries
#
# id :bigint not null, primary key
# average_score :float default(0.0), not null
# latest_increase_percent :float
# evaluation_program_id :bigint not null
# project_id :bigint not null
# scores_summary :jsonb not null
# created_at :datetime not null
# updated_at :datetime not null
#

class ProjectProgramSummary < ApplicationRecord
belongs_to :evaluation_program
belongs_to :project

validates :project_id, uniqueness: { scope: :evaluation_program_id }
validates_numericality_of :average_score,
less_than_or_equal_to: EvaluationProgram::MAXIMUM_VALID_SCORE,
greater_than_or_equal_to: 0

delegate :program_type, to: :evaluation_program

def project_evaluations
project.project_evaluations.where(
evaluation_program_id: evaluation_program.id
)
end
end
3 changes: 2 additions & 1 deletion app/models/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@


class Role < ApplicationRecord
ADMIN_ROLES = %i[superadmin admin].freeze
AVAILABLE_ROLES = %i[superadmin program_admin program_evaluator
project_leader project_member].freeze
has_and_belongs_to_many :users, join_table: :users_roles

belongs_to :resource,
Expand Down
78 changes: 78 additions & 0 deletions app/services/project_program_summary_updater.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

#
# This class manages the logic behind determining how a Summary's scores (JSONB) should
# be updated, and then performs the update.
#
class ProjectProgramSummaryUpdater
PROGRAM_TYPE_METHODS = {
project_follow_up: :take_latest_evaluation,
competition: :take_all_evaluations
}.with_indifferent_access

def initialize(project_id, evaluation_program_id)
@summary = ProjectProgramSummary.find_by(
project_id: project_id, evaluation_program_id: evaluation_program_id
)
return unless @summary

@type = @summary.program_type
end

def run
assign_summary_scores
assign_average_score
@summary.save!
end

private

def assign_summary_scores
@summary.scores_summary['evaluation_count'] = evaluations.count
@summary.scores_summary['criteria'] = generate_criteria_summary
end

def assign_average_score
@summary.average_score = evaluations.average(:total_score)
end

def evaluations
@evaluations ||= send(PROGRAM_TYPE_METHODS[@type])
end

def take_latest_evaluation
@summary.project_evaluations.order(timestamp: :desc).limit(1)
end

def take_all_evaluations
@summary.project_evaluations
end

def generate_criteria_summary
sql = <<-SQL
SELECT
evaluation_criteria.name,
program_criteria.weight,
evaluation_programs.criteria_scale_max AS maximum,
evaluation_programs.criteria_scale_min AS minimum,
AVG(evaluation_scores.total) AS total,
AVG(evaluation_scores.weighed_total) AS weighed_total
FROM evaluation_scores
LEFT JOIN program_criteria
ON evaluation_scores.program_criterium_id = program_criteria.id
LEFT JOIN evaluation_programs
ON program_criteria.evaluation_program_id = evaluation_programs.id
LEFT JOIN evaluation_criteria
ON program_criteria.evaluation_criterium_id = evaluation_criteria.id
WHERE evaluation_scores.project_evaluation_id IN (#{evaluations.pluck(:id).join(', ')})
GROUP BY
evaluation_criteria.name, program_criteria.weight,
evaluation_programs.criteria_scale_max,
evaluation_programs.criteria_scale_min
ORDER BY
evaluation_criteria.name
SQL
scores = EvaluationScore.connection.execute(sql).to_a
scores.inject({}) { |hash, score| hash.merge(score['name'] => score) }
end
end
7 changes: 5 additions & 2 deletions db/migrate/20191113010414_create_evaluation_programs.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true

class CreateEvaluationPrograms < ActiveRecord::Migration[6.0]
def change
create_table :evaluation_programs do |t|
t.string :name
t.datetime :start_at
t.string :name, null: false
t.datetime :start_at, null: false, default: -> { 'NOW()' }
t.datetime :end_at
t.integer :program_type, null: false, default: 0

t.timestamps
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# frozen_string_literal: true

class CreateProjectEvaluationSummaries < ActiveRecord::Migration[6.0]
class CreateProjectEvaluations < ActiveRecord::Migration[6.0]
def change
create_table :project_evaluation_summaries do |t|
create_table :project_evaluations do |t|
t.float :total_score, default: 0.0
t.references :evaluation_program, null: false, foreign_key: true
t.references :project, null: false, foreign_key: true
t.date :program_start
t.references :evaluator, index: true, null: false, foreign_key: { to_table: :users }

t.timestamps
end
Expand Down
5 changes: 3 additions & 2 deletions db/migrate/20191230211748_create_evaluation_scores.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ class CreateEvaluationScores < ActiveRecord::Migration[6.0]
def change
create_table :evaluation_scores do |t|
t.references :program_criterium, null: false, foreign_key: true
t.references :project_evaluation_summary, null: false, foreign_key: true
t.float :total
t.references :project_evaluation, null: false, foreign_key: true
t.float :total, null: false, default: 0.0
t.float :weighed_total, null: false, default: 0.0

t.timestamps
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

class AddTimestampAndScoresToProjectEvaluationSummaries < ActiveRecord::Migration[6.0]
def change
add_column :project_evaluation_summaries, :timestamp, :datetime
add_column :project_evaluation_summaries, :scores, :jsonb, default: {}
add_column :project_evaluations, :timestamp, :datetime
end
end
15 changes: 15 additions & 0 deletions db/migrate/20200102223447_create_project_program_summaries.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class CreateProjectProgramSummaries < ActiveRecord::Migration[6.0]
def change
create_table :project_program_summaries do |t|
t.float :average_score, null: false, default: 0.0
t.float :latest_increase_percent
t.references :evaluation_program, null: false, foreign_key: true
t.references :project, null: false, foreign_key: true
t.jsonb :scores_summary, null: false, default: {}

t.timestamps
end
end
end
Loading

0 comments on commit 79792d8

Please sign in to comment.