CSC/ECE 517 Fall 2024 - E2486.Reimplement deadline rights and deadline types
Project 4 Design Doc
Issues
1. Determining Late Submissions:
- There is a need to calculate whether submissions or reviews are late based on the due dates.
- Issue: The current system does not have a straightforward way to identify late submissions or track submission dates easily.
2. Handling Late Team Formation:
- Teams that form after the due date should incur a penalty.
- Issue: Team formation dates may not be explicitly tracked in the current system, or if they are, they are not used for penalty calculation.
3. Handling Review Penalties:
- Participants who fail to complete reviews on time should incur a penalty as well.
- Issue: Reviews are often tied to the response maps or individual participants, so there's a challenge in ensuring penalties are applied consistently across different models.
4. Penalty Application:
- The penalty calculation should be easily extensible, and transparent for debugging.
- Issue: Penalties may need to be applied in multiple places within the codebase, but should be kept modular and readable.
5. Code Maintainability and Readability:
- The code should remain maintainable as penalties could apply to more parts of the application in the future.
- Issue: Repeating penalty logic across models may lead to code duplication, increasing maintenance cost.
Proposed Solutions
A. Introduction of a PenaltyCalculator Mixin
- Solution: A new module `PenaltyCalculator` will be created to handle the penalty calculations. This module will contain methods for calculating submission, review, and team formation penalties.
- Benefit: It makes the penalty calculation logic reusable, keeps the code DRY, and ensures the logic is centralized.
B. Tracking Submission Dates
- Solution: Ensure that submission dates are explicitly tracked in the `Submission` model. Add a method to check if a submission is late based on the due date.
- Benefit: Provides a straightforward way to identify late submissions and apply penalties accordingly.
C. Tracking Team Formation Dates
- Solution: Use the team formation date in the `Team` model to track when a team is formed. Use this date to calculate penalties for late team formation.
- Benefit: Ensures that penalties for late team formation are applied consistently and accurately.
D. Handling Review Penalties
- Solution: Extend the `PenaltyCalculator` mixin to include methods for calculating penalties for late reviews. Ensure that review deadlines are tracked and used in penalty calculations.
- Benefit: Ensures that penalties for late reviews are applied consistently across different models.
E. Extensible Penalty Application
- Solution: Design the `PenaltyCalculator` mixin to be easily extensible, allowing new types of penalties to be added in the future. Use clear and transparent methods for debugging.
- Benefit: Keeps the penalty calculation logic modular and readable, making it easier to maintain and extend.
F. Code Maintainability and Readability
- Solution: Centralize the penalty calculation logic in the `PenaltyCalculator` mixin to avoid code duplication. Ensure that the mixin is included in relevant models where penalties need to be applied.
- Benefit: Reduces code duplication, making the codebase easier to maintain and ensuring that penalty logic is consistent across the application.
UML Diagram
Implementation of PenaltyCalculator Module
module PenaltyCalculator def calculate_penalty(submission_date, due_date, penalty_rate) return 0 if submission_date <= due_date days_late = (submission_date.to_date - due_date.to_date).to_i penalty = days_late * penalty_rate penalty end end
Summary of Proposed File Changes
- app/lib/penalty_calculator.rb (new file)
- app/models/assignment.rb (modify to include mixin and penalty calculation method)
- app/models/submission.rb (modify to include mixin and penalty calculation method)
- app/models/participant.rb (modify to include mixin and penalty calculation method)
- app/models/team.rb (modify to include mixin and penalty calculation method)
- config/application.rb (modify to ensure lib directory is autoloaded)
- spec/lib/penalty_calculator_spec.rb (new file for unit tests)
- spec/models/assignment_spec.rb (modify to include tests for penalty calculation)
- spec/models/submission_spec.rb (modify to include tests for penalty calculation)
- spec/models/participant_spec.rb (modify to include tests for penalty calculation)
- spec/models/team_spec.rb (modify to include tests for penalty calculation)
Final Implementation
Resolving Issues
1. Determining Late Submissions &&
2. Handling Late Team Formation &&
3. Handling Review Penalties
- Our solution simplifies the different types of penalties and enables a single type of penalty to be used for all applications.
- Solution: Update calculate_penalty in late_policy.rb to determine penalty units and calculate total penalty
def calculate_penalty(submission_time, due_date) return 0 if submission_time <= due_date time_diff = submission_time - due_date penalty_units = case penalty_unit when 'Minute' time_diff / 60 when 'Hour' time_diff / 3600 when 'Day' time_diff / 86400 else raise 'Invalid penalty unit' end raise 'Penalty per unit is missing' if penalty_per_unit.nil? penalty = penalty_units * penalty_per_unit [penalty, max_penalty].min.round(2) end
4. Penalty Application:
- Solution: Include ScoreCalculationHelper mixin in response.rb and update aggregate_questionnaire_score to calculate and apply penalty to score
class Response < ApplicationRecord include ScoreCalculationHelper
def aggregate_questionnaire_score # only count the scorable questions, only when the answer is not nil # we accept nil as answer for scorable questions, and they will not be counted towards the total score sum = 0 scores.each do |s| question = Question.find(s.question_id) # For quiz responses, the weights will be 1 or 0, depending on if correct sum += s.answer * question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion) end sum penalty = LatePolicy.calculate_penalty(submission_time, due_date) apply_penalty(sum, penalty) end
5. Code Maintainability and Readability:
- Solution: Maintaining penalty calculation and application in its own module ensures readability and limits potential for duplicate code elsewhere in the codebase
Implementation of PenaltyCalculator Module
module ScoreCalculationHelper def weighted_score(scores, weights) total_weight = weights.sum # Multiply each score by its weight, then divide by the total weight weighted_scores = scores.zip(weights).map { |score, weight| score * weight } weighted_scores.sum / total_weight end def apply_penalty(score, penalty) score - (score * penalty / 100.0) end end
Design Patterns and Principles
While not an explicit implementation, our extension of LatePolicy logic adhered to the Decorator pattern by extending penalty behaviors and adding new parameters like penalty_per_unit and max_penalty. Additonally, we were able to harness the Open/Closed principle in both LatePolicy and DeadlineType by those classes allowing us to create new penalty units (Minutes/Hours/Days) and new types of deadlines for bidding for topics and reviews. Another example of a classic foundational principle, encapsulation, can be found in our implementation of calculate_penalty. Allowing the LatePolicy to do the calculation keeps the logic for the penalties separate from the response and any other classes that might use late policies.
Test Cases
Testing is primarily centered around score_calculation_helper.rb with some additional testing in late_policy.rb due to its close ties with penalty enforcement. Other adjustments include stubbing DueDates to ensure the DueDate object returns the proper due_at for your tests:
allow(DueDate).to receive(:find_by).and_return(double('DueDate', due_at: due_date))
Some benefits to stubbing DueDate this way:
- Consistency: The due_date object is now controlled, avoiding reliance on any database or external factors.
- Flexibility: You can easily modify the due_date within each test case.
- Isolates the behavior of LatePolicy from database interactions.
- Makes tests faster and more predictable.
- Focuses solely on the calculate_penalty logic
Stubbing example:
describe '#calculate_penalty' do let(:late_policy) { LatePolicy.new(penalty_unit: 'InvalidUnit', penalty_per_unit: 10, max_penalty: 100) } let(:due_date) { Time.now } before do # Stub the due_date object with the desired behavior allow(DueDate).to receive(:find_by).and_return(double('DueDate', due_at: due_date)) end it 'raises an error if the penalty unit is invalid' do submission_time = Time.now + 1.hour # Expecting an error due to invalid penalty unit expect { late_policy.calculate_penalty(submission_time, due_date) }.to raise_error('Invalid. Penalty unit must be Minute, Hour or Day') end end
As our helper is tasked only with computing and applying penalties, our testing revolved primarily around ensuring scoring is correct after penalties are applied.
Example score_calculation_helper.rb test for computation:
describe '#apply_penalty' do it 'reduces the score by the correct percentage penalty' do score = 100 penalty = 20 expect(apply_penalty(score, penalty)).to eq(80.0) end end
Example test for handling negative numbers:
describe '#apply_penalty' do it 'raises an error if penalty is negative' do score = 100 penalty = -20 expect { apply_penalty(score, penalty) }.to raise_error(ArgumentError) end end
Late policies add in a small additional layer of complexity by seeing how late a submission is and using that to determine the penalty that should be applied.
Example late_policy_spec.rb test for applying a max penalty:
describe '#calculate_penalty' do it 'returns the max penalty when calculated penalty exceeds max_penalty' do submission_time = Time.now + 10.days due_date = Time.now late_policy = LatePolicy.new(penalty_unit: 'Day', penalty_per_unit: 20, max_penalty: 50) # Time difference is 10 days, calculated penalty would be 10 * 20 = 200 # But since the max penalty is 50, it should return 50 expect(late_policy.calculate_penalty(submission_time, due_date)).to eq(50) end end