Skip to content

Commit bf4ec01

Browse files
authored
feat: allow custom target dates per target grade (doubtfire-lms#575)
* feat: allow custom target dates per target grade * refactor: rename to target dates for clarity * refactor: throw error if target date is before start date * chore: require flexible dates to be enabled
1 parent b0bf0b2 commit bf4ec01

File tree

8 files changed

+93
-1
lines changed

8 files changed

+93
-1
lines changed

app/api/entities/task_definition_entity.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def staff?(my_role)
1919
expose :target_date
2020
expose :due_date
2121
expose :start_date
22+
# expose :p_target_date, expose_nil: false
23+
expose :c_target_date, expose_nil: false
24+
expose :d_target_date, expose_nil: false
25+
expose :hd_target_date, expose_nil: false
2226
end
2327

2428
expose :upload_requirements, expose_nil: false do |task_definition, options|

app/api/task_definitions_api.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class TaskDefinitionsApi < Grape::API
136136
optional :assess_in_portfolio_only, type: Boolean, desc: 'Whether a task can only be signed off during portfolio assessment'
137137
optional :use_resources_for_jplag_base_code, type: Boolean, desc: 'Include the common base code from task resources for JPlag comparisons'
138138
optional :lock_assessments_to_tutorial_stream, type: Boolean, desc: 'Only allow tutors in this tutorial stream to assess this task'
139+
# optional :p_target_date, type: Date, desc: 'Pass due date override'
140+
optional :c_target_date, type: Date, desc: 'Credit due date override'
141+
optional :d_target_date, type: Date, desc: 'Distinction due date override'
142+
optional :hd_target_date, type: Date, desc: 'High Distinction due date override'
139143
end
140144
end
141145
put '/units/:unit_id/task_definitions/:id' do
@@ -146,6 +150,10 @@ class TaskDefinitionsApi < Grape::API
146150
error!({ error: 'Not authorised to create a task definition of this unit' }, 403)
147151
end
148152

153+
# strip these out so TaskDefinition#update! never sees them
154+
grade_due_overrides = params[:task_def].slice('p_target_date', 'c_target_date', 'd_target_date', 'hd_target_date')
155+
params[:task_def].except!('p_target_date', 'c_target_date', 'd_target_date', 'hd_target_date')
156+
149157
task_params = ActionController::Parameters.new(params)
150158
.require(:task_def)
151159
.permit(
@@ -218,6 +226,25 @@ class TaskDefinitionsApi < Grape::API
218226
end
219227
end
220228

229+
grade_map = { 'c_target_date' => 1, 'd_target_date' => 2, 'hd_target_date' => 3 }
230+
231+
grade_due_overrides.each do |key, date|
232+
next if date.blank?
233+
next unless grade_map.key?(key) # skip p_target_date
234+
235+
if task_def.start_date > date
236+
error!({ error: 'Target date cannot be earlier than start date' }, 400)
237+
end
238+
239+
unless unit.allow_flexible_dates
240+
error!({ error: 'This unit must have Allow Flexible Dates enabled to modify target dates per grade' }, 403)
241+
end
242+
243+
TaskDefinitionGradeDueDate
244+
.find_or_initialize_by(task_definition: task_def, target_grade: grade_map[key])
245+
.update!(target_due_date: date)
246+
end
247+
221248
present task_def, with: Entities::TaskDefinitionEntity, my_role: unit.role_for(current_user)
222249
end
223250

app/models/task_definition.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def self.permissions
8181
has_many :task_prerequisites, dependent: :destroy
8282
has_many :prerequisites, through: :task_prerequisites, source: :prerequisite
8383

84+
has_many :grade_due_dates,
85+
class_name: "TaskDefinitionGradeDueDate",
86+
dependent: :destroy
87+
8488
has_many :discussion_prompts, dependent: :destroy
8589

8690
serialize :upload_requirements, coder: JSON
@@ -109,6 +113,22 @@ def self.permissions
109113
include TaskDefinitionTiiModule
110114
include TaskDefinitionSimilarityModule
111115

116+
# def p_target_date
117+
# due_date
118+
# end
119+
120+
def c_target_date
121+
grade_due_dates.find { |g| g.target_grade == 1 }&.target_due_date
122+
end
123+
124+
def d_target_date
125+
grade_due_dates.find { |g| g.target_grade == 2 }&.target_due_date
126+
end
127+
128+
def hd_target_date
129+
grade_due_dates.find { |g| g.target_grade == 3 }&.target_due_date
130+
end
131+
112132
def unit_must_be_same
113133
if unit.present? and tutorial_stream.present? and not unit.eql? tutorial_stream.unit
114134
errors.add(:unit, "should be same as the unit in the associated tutorial stream")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class TaskDefinitionGradeDueDate < ApplicationRecord
2+
belongs_to :task_definition
3+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class CreateTaskDefinitionGradeDueDates < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :task_definition_grade_due_dates do |t|
4+
t.references :task_definition, null: false
5+
t.integer :target_grade, null: false
6+
t.datetime :target_due_date, null: false
7+
8+
t.timestamps
9+
end
10+
11+
add_index :task_definition_grade_due_dates,
12+
[:task_definition_id, :target_grade],
13+
unique: true,
14+
name: "idx_td_grade_due_unique"
15+
end
16+
end

db/schema.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.0].define(version: 2026_01_13_033152) do
13+
ActiveRecord::Schema[8.0].define(version: 2026_02_22_043513) do
1414
create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
1515
t.string "name", null: false
1616
t.string "abbreviation", null: false
@@ -396,6 +396,16 @@
396396
t.index ["user_id"], name: "index_task_comments_on_user_id"
397397
end
398398

399+
create_table "task_definition_grade_due_dates", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
400+
t.bigint "task_definition_id", null: false
401+
t.integer "target_grade", null: false
402+
t.datetime "target_due_date", null: false
403+
t.datetime "created_at", null: false
404+
t.datetime "updated_at", null: false
405+
t.index ["task_definition_id", "target_grade"], name: "idx_td_grade_due_unique", unique: true
406+
t.index ["task_definition_id"], name: "index_task_definition_grade_due_dates_on_task_definition_id"
407+
end
408+
399409
create_table "task_definitions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
400410
t.bigint "unit_id"
401411
t.string "name"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FactoryBot.define do
2+
factory :task_definition_grade_due_date do
3+
4+
end
5+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "test_helper"
2+
3+
describe TaskDefinitionGradeDueDate do
4+
# it "does a thing" do
5+
# value(1+1).must_equal 2
6+
# end
7+
end

0 commit comments

Comments
 (0)