diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index 04c15e74d0..d06b3033a5 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -57,6 +57,14 @@ def staff?(my_role) expose :has_jplag_report?, as: :has_jplag_report, if: ->(unit, options) { staff?(options[:my_role]) } expose :is_graded expose :max_quality_pts + + expose :estimated_days do |task_def, _options| + task_def.estimated_days + end + + expose :estimated_hours do |task_def, _options| + task_def.estimated_hours + end expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false # expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) } expose :assessment_enabled diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 8da40c69ad..25157f8297 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -33,6 +33,7 @@ class TaskDefinitionsApi < Grape::API requires :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer' + optional :estimated_hours, type: Integer, desc: 'Estimated time to complete task, measured in hours' optional :similarity_language, type: String, desc: 'The language to use for code similarity checks' optional :scorm_enabled, type: Boolean, desc: 'Whether SCORM assessment is enabled for this task' optional :scorm_allow_review, type: Boolean, desc: 'Whether a student is allowed to review their completed test attempts' @@ -74,6 +75,7 @@ class TaskDefinitionsApi < Grape::API :max_quality_pts, :assessment_enabled, :overseer_image_id, + :estimated_hours, :similarity_language, :assess_in_portfolio_only, :requires_discussion, @@ -86,6 +88,9 @@ class TaskDefinitionsApi < Grape::API task_params[:unit_id] = unit.id task_params[:upload_requirements] = params[:task_def][:upload_requirements].present? ? JSON.parse(params[:task_def][:upload_requirements]) : [] + hours = task_params.delete(:estimated_hours).to_i + task_params[:estimated_time_minutes] = (hours * 60) + task_def = TaskDefinition.new(task_params) # Set the tutorial stream @@ -134,6 +139,7 @@ class TaskDefinitionsApi < Grape::API optional :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer' + optional :estimated_hours, type: Integer, desc: 'Estimated time to complete task, measured in hours' optional :similarity_language, type: String, desc: 'The language to use for code similarity checks' optional :assess_in_portfolio_only, type: Boolean, desc: 'Whether a task can only be signed off during portfolio assessment' optional :requires_discussion, type: Boolean, desc: 'Whether task must be discussed in class before it can be signed off as complete' @@ -188,6 +194,7 @@ class TaskDefinitionsApi < Grape::API :max_quality_pts, :assessment_enabled, :overseer_image_id, + :estimated_hours, :similarity_language, :assess_in_portfolio_only, :requires_discussion, @@ -199,17 +206,20 @@ class TaskDefinitionsApi < Grape::API if params[:task_def][:upload_requirements].present? upload_reqs = JSON.parse(params[:task_def][:upload_requirements]) task_params[:upload_requirements] = upload_reqs + end - # Ensure we permit all of the passed in upload requirements - if task_params[:upload_requirements].is_a? Array - # Force permit - the model validates the details - task_params[:upload_requirements].each(&:permit!) - end + hours = task_params.delete(:estimated_hours).to_i + task_params[:estimated_time_minutes] = (hours * 60) - # Ensure changes to a TD defined as a 'draft task definition' are validated - if unit.draft_task_definition_id == params[:id] && (upload_reqs.length != 1 || upload_reqs[0]['type'] != 'document') - error!({ error: 'Task is marked as the draft learning summary. A draft learning summary task can only contain a single document upload.' }, 403) - end + # Ensure we permit all of the passed in upload requirements + if task_params[:upload_requirements].is_a? Array + # Force permit - the model validates the details + task_params[:upload_requirements].each(&:permit!) + end + + # Ensure changes to a TD defined as a 'draft task definition' are validated + if unit.draft_task_definition_id == params[:id] && (upload_reqs.length != 1 || upload_reqs[0]['type'] != 'document') + error!({ error: 'Task is marked as the draft learning summary. A draft learning summary task can only contain a single document upload.' }, 403) end # Bulk update task definition with permitted parameters diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 8d2fa7c5cd..8bae03b2e3 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -853,6 +853,10 @@ def read_file_from_resources(filename) nil end + def estimated_hours + (estimated_time_minutes.to_i / 60) % 24 + end + private def delete_associated_files() diff --git a/db/migrate/20260326104829_add_estimated_time_minutes_to_task_definitions.rb b/db/migrate/20260326104829_add_estimated_time_minutes_to_task_definitions.rb new file mode 100644 index 0000000000..9b7983818d --- /dev/null +++ b/db/migrate/20260326104829_add_estimated_time_minutes_to_task_definitions.rb @@ -0,0 +1,9 @@ +class AddEstimatedTimeMinutesToTaskDefinitions < ActiveRecord::Migration[8.0] + def up + add_column :task_definitions, :estimated_time_minutes, :integer, null: true, default: 0, comment: "Estimated time to complete task, measured in minutes" + end + + def down + remove_column :task_definitions, :estimated_time_minutes + end +end diff --git a/db/schema.rb b/db/schema.rb index 73fc5ebd58..f15918295f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -440,6 +440,7 @@ t.boolean "assessment_enabled", default: false t.bigint "overseer_image_id" t.string "tii_group_id" + t.integer "estimated_time_minutes", default: 0, comment: "Estimated time to complete task, measured in minutes" t.string "similarity_language" t.boolean "scorm_enabled", default: false t.boolean "scorm_allow_review", default: false diff --git a/test/api/units/task_definitions_api_test.rb b/test/api/units/task_definitions_api_test.rb index 00967f32b0..61055f4359 100644 --- a/test/api/units/task_definitions_api_test.rb +++ b/test/api/units/task_definitions_api_test.rb @@ -24,7 +24,8 @@ def all_task_def_keys 'restrict_status_updates', 'plagiarism_warn_pct', 'is_graded', - 'max_quality_pts' + 'max_quality_pts', + 'estimated_hours' ] end @@ -49,7 +50,8 @@ def test_task_definition_cud upload_requirements: '[ { "key": "file0", "name": "Shape Class", "type": "document" } ]', plagiarism_warn_pct: 80, is_graded: false, - max_quality_pts: 0 + max_quality_pts: 0, + estimated_hours: 0 } } @@ -66,6 +68,8 @@ def test_task_definition_cud assert_equal [{ "key" => "file0", "name" => "Shape Class", "type" => "document" }], td.upload_requirements assert_equal unit.tutorial_streams.first.id, td.tutorial_stream_id assert_equal 4, td.weighting + assert_equal (24 * 60), td.estimated_time_minutes + assert_equal 0, last_response_body['estimated_hours'] data_to_put = { task_def: { @@ -83,7 +87,8 @@ def test_task_definition_cud upload_requirements: [ { "key": "file0", "name": "Other Class", "type": "document" } ].to_json, plagiarism_warn_pct: 80, is_graded: false, - max_quality_pts: 0 + max_quality_pts: 0, + estimated_hours: 3 } } @@ -99,6 +104,8 @@ def test_task_definition_cud assert_equal unit.tutorial_streams.last.id, td.tutorial_stream_id assert_equal [{ "key" => "file0", "name" => "Other Class", "type" => "document" }], td.upload_requirements assert_equal 2, td.weighting + assert_equal 3060, td.estimated_time_minutes + assert_equal 3, last_response_body['estimated_hours'] end def test_post_invalid_file_tasksheet