From 7981de85a8c318ce4c96140ff6fc014bb6719cdb Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 30 Nov 2025 13:29:30 +1000 Subject: [PATCH 01/21] docs: fix typo in API root, add doc_version The document still said "documentaion". Nuts. I have also added the doc version number. I defaulted to the version of the branch. --- app/api/api_root.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 983749c551..a5a6f0bdb4 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -153,10 +153,10 @@ class ApiRoot < Grape::API add_swagger_documentation \ base_path: nil, - api_version: 'v1', + doc_version: 'v10.0.0', hide_documentation_path: true, info: { - title: 'Doubtfire API Documentaion', + title: 'Doubtfire API Documentation', description: 'Doubtfire is a modern, lightweight learning management system.', license: 'AGPL v3.0', license_url: 'https://github.com/doubtfire-lms/doubtfire-api/blob/master/LICENSE' From 524c2ebf016597057ffcdd04e5b9c8780b7ac150 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 25 Apr 2026 20:24:28 +1000 Subject: [PATCH 02/21] feat(ai_eff): rename weighting in test files --- lib/helpers/database_populator.rb | 2 +- lib/tasks/simulate_jplag_submissions.rake | 2 +- test/api/comments/comment_test.rb | 8 ++--- test/api/comments/extension_test.rb | 12 +++---- test/api/comments/scorm_extension_test.rb | 8 ++--- test/api/comments/status_test.rb | 2 +- test/api/groups_api_test.rb | 8 ++--- test/api/scorm_api_test.rb | 2 +- test/api/tasks_api_test.rb | 18 +++++----- test/api/test_attempts_test.rb | 14 ++++---- test/api/unit_roles_test.rb | 2 +- test/api/units/feedback_test.rb | 4 +-- test/api/units/task_definitions_api_test.rb | 12 +++---- test/factories/units_factory.rb | 2 +- test/models/group_test.rb | 2 +- test/models/task_definition_test.rb | 4 +-- test/models/task_status_test.rb | 18 +++++----- test/models/task_test.rb | 32 +++++++++--------- ...COS10001-ImportTasksWithTutorialStream.csv | 2 +- ...10001-ImportTasksWithoutTutorialStream.csv | 2 +- test_files/COS10001-Tasks.csv | 2 +- .../COS10001-TasksUnorderedUploadReqs.csv | 2 +- test_files/COS10001-TestImportTasks.csv | 8 ++--- .../COS10001-Tasks-Prerequisites.csv | 2 +- test_files/csv_test_files/COS10001-Tasks.csv | 2 +- test_files/csv_test_files/COS10001-Tasks.xlsx | Bin 8874 -> 8837 bytes .../unit_csv_imports/import_group_tasks.csv | 2 +- 27 files changed, 87 insertions(+), 87 deletions(-) diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index 57911a98bc..49e5cb7de7 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -670,7 +670,7 @@ def generate_tasks_for_unit(unit, unit_details) abbreviation: "A#{count + 1}", unit_id: unit.id, description: faker_random_sentence(5, 10), - weighting: BigDecimal("2"), + estimated_hours: BigDecimal("2"), target_date: target_date, upload_requirements: up_reqs, start_date: start_date, diff --git a/lib/tasks/simulate_jplag_submissions.rake b/lib/tasks/simulate_jplag_submissions.rake index 33c1a8975c..85e9d3063e 100644 --- a/lib/tasks/simulate_jplag_submissions.rake +++ b/lib/tasks/simulate_jplag_submissions.rake @@ -27,7 +27,7 @@ namespace :db do unit_id: unit.id, tutorial_stream: unit.tutorial_streams.first, description: faker_random_sentence(5, 10), - weighting: BigDecimal("2"), + estimated_hours: BigDecimal("2"), target_date: target_date, upload_requirements: [ { diff --git a/test/api/comments/comment_test.rb b/test/api/comments/comment_test.rb index 1e1fc558a1..91b1efe54f 100644 --- a/test/api/comments/comment_test.rb +++ b/test/api/comments/comment_test.rb @@ -241,7 +241,7 @@ def test_student_reply_to_other_student_in_same_group tutorial_stream: unit.tutorial_streams.first, name: 'Task to switch from ind to group after submission', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 1.week, target_date: Time.zone.now - 1.day, @@ -484,7 +484,7 @@ def test_read_receipts_for_task_status_comments tutorial_stream: unit.tutorial_streams.first, name: 'test_read_receipts_for_task_status_comments', description: 'test_read_receipts_for_task_status_comments', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -527,7 +527,7 @@ def test_project_plan_task_comments_dont_show_in_inbox tutorial_stream: unit.tutorial_streams.first, name: 'test_project_plan_task_comments_dont_show_in_inbox', description: 'test_project_plan_task_comments_dont_show_in_inbox', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -574,7 +574,7 @@ def test_discussed_in_class_task_comments_dont_show_in_inbox tutorial_stream: unit.tutorial_streams.first, name: 'test_discussed_in_class_task_comments_dont_show_in_inbox', description: 'test_discussed_in_class_task_comments_dont_show_in_inbox', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, diff --git a/test/api/comments/extension_test.rb b/test/api/comments/extension_test.rb index 7c707ab798..eaa3f06d52 100644 --- a/test/api/comments/extension_test.rb +++ b/test/api/comments/extension_test.rb @@ -19,7 +19,7 @@ def test_extension_application tutorial_stream: project.tutorial_enrolments.first.tutorial.tutorial_stream, name: 'status task change', description: 'status task change test', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.day, @@ -94,7 +94,7 @@ def test_extension_application tutorial_stream: unit.tutorial_streams.first, name: 'status task change', description: 'status task change test', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.day, @@ -152,7 +152,7 @@ def test_disallow_student_extensions tutorial_stream: unit.tutorial_streams.first, name: 'status task change', description: 'status task change test', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.day, @@ -201,7 +201,7 @@ def test_extension_on_resubmit tutorial_stream: unit.tutorial_streams.first, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.day, @@ -257,7 +257,7 @@ def test_extension_in_inbox tutorial_stream: unit.tutorial_streams.first, name: 'status task change', description: 'status task change test', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.day, @@ -317,7 +317,7 @@ def test_flexible_dates tutorial_stream: unit.tutorial_streams.first, name: 'Flexible Dates Test', description: 'Flexible Dates Test', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.day, diff --git a/test/api/comments/scorm_extension_test.rb b/test/api/comments/scorm_extension_test.rb index f206b8f0f5..cafaffdc1b 100644 --- a/test/api/comments/scorm_extension_test.rb +++ b/test/api/comments/scorm_extension_test.rb @@ -20,7 +20,7 @@ def test_scorm_extension_request tutorial_stream: unit.tutorial_streams.first, name: 'Scorm extension request', description: 'Scorm extension request', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -82,7 +82,7 @@ def test_read_by_main_tutor tutorial_stream: unit.tutorial_streams.first, name: 'Scorm extension request', description: 'Scorm extension request', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -138,7 +138,7 @@ def test_auto_grant_for_tutor tutorial_stream: unit.tutorial_streams.first, name: 'Scorm extension request', description: 'Scorm extension request', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -188,7 +188,7 @@ def test_scorm_extension_assessment tutorial_stream: unit.tutorial_streams.first, name: 'Scorm extension', description: 'Scorm extension', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, diff --git a/test/api/comments/status_test.rb b/test/api/comments/status_test.rb index 9c2e729b12..51de89d48b 100644 --- a/test/api/comments/status_test.rb +++ b/test/api/comments/status_test.rb @@ -20,7 +20,7 @@ def test_status_comments abbreviation: 'test_status_comments', name: 'test_status_comments', description: 'test_status_comments', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, diff --git a/test/api/groups_api_test.rb b/test/api/groups_api_test.rb index 9c71389519..24336cc64a 100644 --- a/test/api/groups_api_test.rb +++ b/test/api/groups_api_test.rb @@ -33,7 +33,7 @@ def test_group_submission_with_extensions tutorial_stream: unit.tutorial_streams.first, name: 'Task to switch from ind to group after submission', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 1.week, target_date: Time.zone.now - 1.day, @@ -99,7 +99,7 @@ def test_comment_on_group_task_without_group tutorial_stream: unit.tutorial_streams.first, name: 'Task to switch from ind to group after submission', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 1.week, target_date: Time.zone.now - 1.day, @@ -146,7 +146,7 @@ def test_pdf_comment_on_group_task tutorial_stream: unit.tutorial_streams.first, name: 'Task to switch from ind to group after submission', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 1.week, target_date: Time.zone.now - 1.day, @@ -189,7 +189,7 @@ def test_pdf_comment_on_group_task_without_group tutorial_stream: unit.tutorial_streams.first, name: 'Task to switch from ind to group after submission', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 1.week, target_date: Time.zone.now - 1.day, diff --git a/test/api/scorm_api_test.rb b/test/api/scorm_api_test.rb index 22287d1888..bb75f8cf97 100644 --- a/test/api/scorm_api_test.rb +++ b/test/api/scorm_api_test.rb @@ -26,7 +26,7 @@ def test_serve_scorm_content tutorial_stream: unit.tutorial_streams.first, name: 'Task scorm', description: 'Task with scorm test', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, diff --git a/test/api/tasks_api_test.rb b/test/api/tasks_api_test.rb index 4d2f996016..14a03e0cb3 100644 --- a/test/api/tasks_api_test.rb +++ b/test/api/tasks_api_test.rb @@ -50,7 +50,7 @@ def test_get_task_submission_details_creates_session_and_activity tutorial_stream: unit.tutorial_streams.first, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -146,7 +146,7 @@ def test_time_exceeded_grade tutorial_stream: unit.tutorial_streams.first, name: 'Task past due', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -186,7 +186,7 @@ def test_extension_reverts_time_exceeded tutorial_stream: unit.tutorial_streams.first, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -264,7 +264,7 @@ def test_extension_reverts_time_exceeded_auto_apply tutorial_stream: unit.tutorial_streams.first, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -438,7 +438,7 @@ def test_can_submit_ipynb tutorial_stream: unit.tutorial_streams.first, name: 'Code task', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -479,7 +479,7 @@ def test_invalid_latex_in_ipynb tutorial_stream: unit.tutorial_streams.first, name: 'Code task', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -520,7 +520,7 @@ def test_download_task_pdf tutorial_stream: unit.tutorial_streams.first, name: 'Code task', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -778,7 +778,7 @@ def test_check_in_comment tutorial_stream: unit.tutorial_streams.first, name: 'Code task', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -826,7 +826,7 @@ def test_requires_discussion_blocks_complete_until_discussed_comment_added tutorial_stream: unit.tutorial_streams.first, name: 'Discussion required task', description: 'Task that requires discussion before complete', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, diff --git a/test/api/test_attempts_test.rb b/test/api/test_attempts_test.rb index be0c02ae5e..4ace1f4ac5 100644 --- a/test/api/test_attempts_test.rb +++ b/test/api/test_attempts_test.rb @@ -20,7 +20,7 @@ def test_get_task_attempts tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts', description: 'Test attempts', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -53,7 +53,7 @@ def test_get_task_attempts tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts new', description: 'Test attempts new', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -108,7 +108,7 @@ def test_get_latest tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts', description: 'Test attempts', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -177,7 +177,7 @@ def test_review_attempt tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts', description: 'Test attempts', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -273,7 +273,7 @@ def test_post_attempt tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts', description: 'Test attempts', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -358,7 +358,7 @@ def test_update_attempt tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts', description: 'Test attempts', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -452,7 +452,7 @@ def test_delete_attempt tutorial_stream: unit.tutorial_streams.first, name: 'Test attempts', description: 'Test attempts', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, diff --git a/test/api/unit_roles_test.rb b/test/api/unit_roles_test.rb index 5e24941fd0..d66bbcac70 100644 --- a/test/api/unit_roles_test.rb +++ b/test/api/unit_roles_test.rb @@ -220,7 +220,7 @@ def test_observer_unit_role task_def: { name: "Sample Task", description: "This is a test task", - weighting: 10, + estimated_hours: 10, target_grade: 50, start_date: Time.zone.today, target_date: Time.zone.today + 7.days, diff --git a/test/api/units/feedback_test.rb b/test/api/units/feedback_test.rb index 1b51591544..bb03ac6a2d 100644 --- a/test/api/units/feedback_test.rb +++ b/test/api/units/feedback_test.rb @@ -40,7 +40,7 @@ def test_tasks_for_task_inbox tutorial_stream: unit.tutorial_streams.first, name: 'Code task', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -57,7 +57,7 @@ def test_tasks_for_task_inbox tutorial_stream: unit.tutorial_streams.first, name: 'Code task2', description: 'Code task2', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, diff --git a/test/api/units/task_definitions_api_test.rb b/test/api/units/task_definitions_api_test.rb index 00967f32b0..73a3bfb6e4 100644 --- a/test/api/units/task_definitions_api_test.rb +++ b/test/api/units/task_definitions_api_test.rb @@ -38,7 +38,7 @@ def test_task_definition_cud tutorial_stream_abbr: unit.tutorial_streams.first.abbreviation, name: 'New Task Def', description: 'First task def', - weighting: 4, + estimated_hours: 4, target_grade: 1, group_set_id: unit.group_sets.first.id, start_date: unit.start_date, @@ -65,14 +65,14 @@ def test_task_definition_cud assert_json_matches_model td, last_response_body, all_task_def_keys 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 4, td.estimated_hours data_to_put = { task_def: { tutorial_stream_abbr: unit.tutorial_streams.last.abbreviation, name: 'New Task Def 1', description: 'First task def 1', - weighting: 2, + estimated_hours: 2, target_grade: 2, group_set_id: nil, start_date: unit.start_date + 2.days, @@ -98,7 +98,7 @@ def test_task_definition_cud assert_json_matches_model td, last_response_body, all_task_def_keys 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 2, td.estimated_hours end def test_post_invalid_file_tasksheet @@ -245,7 +245,7 @@ def test_submission_creates_folders tutorial_stream: unit.tutorial_streams.first, name: 'test_submission_creates_folders', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -299,7 +299,7 @@ def test_change_to_group_after_submissions tutorial_stream: unit.tutorial_streams.first, name: 'Task to switch from ind to group after submission', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, diff --git a/test/factories/units_factory.rb b/test/factories/units_factory.rb index 7cc6de1090..b8cb0c3550 100644 --- a/test/factories/units_factory.rb +++ b/test/factories/units_factory.rb @@ -11,7 +11,7 @@ upload_requirements { [{'key' => 'file0','name' => 'Imported Code','type' => 'code'}] } start_date { unit.start_date + rand(1..12).weeks } sequence(:abbreviation) { |n| "#{GradeHelper.short_grade_for target_grade}#{((unit.start_date - start_date) / 1.week).floor + 1}.#{n}" } - weighting { rand(1..5) } + estimated_hours { rand(1..5) } target_date { start_date + rand(1..2).weeks } group_set { nil } tutorial_stream { unit.tutorial_streams.sample } diff --git a/test/models/group_test.rb b/test/models/group_test.rb index 3a9cfad589..27bf9ff902 100644 --- a/test/models/group_test.rb +++ b/test/models/group_test.rb @@ -150,7 +150,7 @@ def test_submit_with_others_having_extensions tutorial_stream: unit.tutorial_streams.first, name: 'Task for test', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now + 3.days, target_date: Time.zone.now + 1.week, diff --git a/test/models/task_definition_test.rb b/test/models/task_definition_test.rb index 77f9d9ed5f..9f1c26cfe2 100644 --- a/test/models/task_definition_test.rb +++ b/test/models/task_definition_test.rb @@ -15,7 +15,7 @@ def test_default_quality_points tutorial_stream: test_unit.tutorial_streams.first, name: 'Test quality points', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: test_unit.start_date + 1.week, target_date: test_unit.start_date + 2.weeks, @@ -45,7 +45,7 @@ def test_default_tii_settings tutorial_stream: test_unit.tutorial_streams.first, name: 'Test tii settings', description: 'test def', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: test_unit.start_date + 1.week, target_date: test_unit.start_date + 2.weeks, diff --git a/test/models/task_status_test.rb b/test/models/task_status_test.rb index e5081c59d3..9ac5b4f62a 100644 --- a/test/models/task_status_test.rb +++ b/test/models/task_status_test.rb @@ -21,7 +21,7 @@ def test_status_chanaged_with_extenssion unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -69,7 +69,7 @@ def test_status_changed_task_definition_assess_in_portfolio_only unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -118,7 +118,7 @@ def test_status_changed_unit_has_assess_in_portfolio_tasks unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now - 1.week, @@ -164,7 +164,7 @@ def test_tutor_cant_signoff_tasks_complete_assess_in_portfolio_only unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -227,7 +227,7 @@ def test_tutor_can_update_assess_in_portfolio_tasks_to_working_on_it unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -275,7 +275,7 @@ def test_student_cant_update_assess_in_portfolio_without_files unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -335,7 +335,7 @@ def test_student_cant_update_assess_in_portfolio_if_task_def_not_aip unit_id: unit.id, name: 'Task past due - for revert', description: 'Task past due', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 2.weeks, target_date: Time.zone.now + 1.week, @@ -394,7 +394,7 @@ def test_submission_for_assess_in_portfolio tutorial_stream: unit.tutorial_streams.first, name: 'Task with image2', description: 'img task2', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -435,7 +435,7 @@ def test_assess_in_portfolio_submissions_dont_show_in_tutor_inbox tutorial_stream: unit.tutorial_streams.first, name: 'Task with image2', description: 'img task2', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, diff --git a/test/models/task_test.rb b/test/models/task_test.rb index cab193b681..c31e8b95c5 100644 --- a/test/models/task_test.rb +++ b/test/models/task_test.rb @@ -55,7 +55,7 @@ def test_pdf_creation_with_gif tutorial_stream: unit.tutorial_streams.first, name: 'Task with image', description: 'img task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -100,7 +100,7 @@ def test_image_upload tutorial_stream: unit.tutorial_streams.first, name: 'Task with image2', description: 'img task2', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -142,7 +142,7 @@ def test_pdf_creation_with_jpg tutorial_stream: unit.tutorial_streams.first, name: 'Task with image', description: 'img task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -187,7 +187,7 @@ def test_pdf_with_quotes_in_task_title tutorial_stream: unit.tutorial_streams.first, name: '"Quoted Task"', description: 'Task with quotes in name', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -333,7 +333,7 @@ def test_ipynb_to_pdf tutorial_stream: unit.tutorial_streams.first, name: 'Task with ipynb', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -431,7 +431,7 @@ def test_code_submission_with_long_lines tutorial_stream: unit.tutorial_streams.first, name: 'Task with super ling lines in code submission', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -505,7 +505,7 @@ def test_code_submission_with_long_lines tutorial_stream: unit.tutorial_streams.first, name: 'Task with super ling lines in code submission', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -579,7 +579,7 @@ def test_code_submission_with_long_lines tutorial_stream: unit.tutorial_streams.first, name: 'Task with super ling lines in code submission', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -653,7 +653,7 @@ def test_pdf_validation_on_submit tutorial_stream: unit.tutorial_streams.first, name: 'PDF Test Task', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -722,7 +722,7 @@ def test_pdf_creation_fails_on_invalid_pdf tutorial_stream: unit.tutorial_streams.first, name: 'PDF Test Task', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -769,7 +769,7 @@ def test_pax_crash_does_not_stop_pdf_creation tutorial_stream: unit.tutorial_streams.first, name: 'PDF Test Task', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -1099,7 +1099,7 @@ def test_portfolio_evidence_path tutorial_stream: unit.tutorial_streams.first, name: 'Test task', description: 'Code task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date + 1.week, target_date: unit.start_date + 2.weeks, @@ -1206,7 +1206,7 @@ def test_extension_on_trigger_transition tutorial_stream: unit.tutorial_streams.first, name: 'Test task', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: unit.start_date, target_date: unit.start_date + 1.week, @@ -1262,7 +1262,7 @@ def test_trigger_time_exceeded tutorial_stream: unit.tutorial_streams.first, name: 'Test task', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 3.week, target_date: Time.zone.now - 2.week, @@ -1347,7 +1347,7 @@ def test_assessment_lock_to_tutorial_stream tutorial_stream: tutorial_stream_main, name: 'Test task for main', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 0, start_date: Time.zone.now - 3.weeks, target_date: Time.zone.now - 2.weeks, @@ -1366,7 +1366,7 @@ def test_assessment_lock_to_tutorial_stream tutorial_stream: tutorial_stream_hd, name: 'Test task for HD', description: 'Test task', - weighting: 4, + estimated_hours: 4, target_grade: 3, start_date: Time.zone.now - 3.weeks, target_date: Time.zone.now - 2.weeks, diff --git a/test_files/COS10001-ImportTasksWithTutorialStream.csv b/test_files/COS10001-ImportTasksWithTutorialStream.csv index bc92146d61..80b23df923 100644 --- a/test_files/COS10001-ImportTasksWithTutorialStream.csv +++ b/test_files/COS10001-ImportTasksWithTutorialStream.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] diff --git a/test_files/COS10001-ImportTasksWithoutTutorialStream.csv b/test_files/COS10001-ImportTasksWithoutTutorialStream.csv index a5e4a89700..d51c1e5f6e 100644 --- a/test_files/COS10001-ImportTasksWithoutTutorialStream.csv +++ b/test_files/COS10001-ImportTasksWithoutTutorialStream.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] diff --git a/test_files/COS10001-Tasks.csv b/test_files/COS10001-Tasks.csv index 54036fe3c2..54aaa32c4b 100644 --- a/test_files/COS10001-Tasks.csv +++ b/test_files/COS10001-Tasks.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] diff --git a/test_files/COS10001-TasksUnorderedUploadReqs.csv b/test_files/COS10001-TasksUnorderedUploadReqs.csv index 7a4d27adc4..806404fe26 100644 --- a/test_files/COS10001-TasksUnorderedUploadReqs.csv +++ b/test_files/COS10001-TasksUnorderedUploadReqs.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file1"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file5"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file3"",""name"":""Screenshot"",""type"":""image""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] diff --git a/test_files/COS10001-TestImportTasks.csv b/test_files/COS10001-TestImportTasks.csv index 6e62fde57a..8924a5964b 100644 --- a/test_files/COS10001-TestImportTasks.csv +++ b/test_files/COS10001-TestImportTasks.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,tutorial_stream -Imported Task 1,TEST_TASK1,Imported!,4,0,true,10,true,"[{""key"":""file0"",""name"":""Imported Code"",""type"":""code""}]",9,Mon,9,Mon,9,Wed,prac-1 -Imported Task 2,TEST_TASK2,Imported!,4,0,true,10,true,"[{""key"":""file0"",""name"":""Imported Code"",""type"":""code""}]",9,Mon,9,Mon,9,Wed,prac-1 -Imported Task 3,TEST_TASK3,Imported!,4,0,true,10,true,"[{""key"":""file0"",""name"":""Imported Code"",""type"":""code""}]",9,Mon,9,Mon,9,Wed,prac-1 +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,max_quality_pts,is_graded,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,tutorial_stream +Imported Task 1,TEST_TASK1,Imported!,4,0,true,10,true,"[{""key"":""file0"",""name"":""Imported Code"",""type"":""code""}]",9,Mon,9,Mon,9,Wed,prac-1 +Imported Task 2,TEST_TASK2,Imported!,4,0,true,10,true,"[{""key"":""file0"",""name"":""Imported Code"",""type"":""code""}]",9,Mon,9,Mon,9,Wed,prac-1 +Imported Task 3,TEST_TASK3,Imported!,4,0,true,10,true,"[{""key"":""file0"",""name"":""Imported Code"",""type"":""code""}]",9,Mon,9,Mon,9,Wed,prac-1 diff --git a/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv b/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv index a399ef52e7..4163143f10 100644 --- a/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv +++ b/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites,discussion_prompts +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites,discussion_prompts Assignment 12,A12,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,13,Mon,,,,,,import-tasks,FALSE,[],[] Pass task 1,1.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] Pass task 2,2.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] diff --git a/test_files/csv_test_files/COS10001-Tasks.csv b/test_files/csv_test_files/COS10001-Tasks.csv index 0bdc01213d..b81401540b 100644 --- a/test_files/csv_test_files/COS10001-Tasks.csv +++ b/test_files/csv_test_files/COS10001-Tasks.csv @@ -1,4 +1,4 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites,discussion_prompts +name,abbreviation,description,estimated_hours,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites,discussion_prompts Assignment 12,A12,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,13,Mon,,,,,,import-tasks,FALSE,[],[] Pass task 1,1.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] Pass task 2,2.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] diff --git a/test_files/csv_test_files/COS10001-Tasks.xlsx b/test_files/csv_test_files/COS10001-Tasks.xlsx index dfe25c7d0ab35f9fc60573cda218c4e3f704cc7b..f7bf81f3aa1912e93d557f947906ffd47c6de5d9 100644 GIT binary patch delta 3040 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 delta 3094 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 Date: Sat, 25 Apr 2026 19:31:59 +1000 Subject: [PATCH 03/21] feat(ai_eff): rename weighting to estimated hours --- app/api/entities/task_definition_entity.rb | 2 +- app/api/task_definitions_api.rb | 8 ++++---- app/models/project.rb | 4 ++-- app/models/task.rb | 2 +- app/models/task_definition.rb | 8 ++++---- ...ge_weighting_to_estimated_hours_in_task_definitions.rb | 5 +++++ db/schema.rb | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20260425091151_change_weighting_to_estimated_hours_in_task_definitions.rb diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index 04c15e74d0..cf8b6b95c4 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -12,7 +12,7 @@ def staff?(my_role) expose :abbreviation expose :name expose :description - expose :weighting + expose :estimated_hours expose :target_grade with_options(format_with: :date_only) do diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 8da40c69ad..0196a61e86 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -19,7 +19,7 @@ class TaskDefinitionsApi < Grape::API optional :tutorial_stream_abbr, type: String, desc: 'The abbreviation of tutorial stream' requires :name, type: String, desc: 'The name of this task def' requires :description, type: String, desc: 'The description of this task def' - requires :weighting, type: Integer, desc: 'The weighting of this task' + requires :estimated_hours, type: Integer, desc: 'The estimated number of hours to complete this task' requires :target_grade, type: Integer, desc: 'Minimum grade for task' optional :group_set_id, type: Integer, desc: 'Related group set' requires :start_date, type: Date, desc: 'The date when the task should be started' @@ -57,7 +57,7 @@ class TaskDefinitionsApi < Grape::API .permit( :name, :description, - :weighting, + :estimated_hours, :target_grade, :start_date, :target_date, @@ -115,7 +115,7 @@ class TaskDefinitionsApi < Grape::API optional :tutorial_stream_abbr, type: String, desc: 'The abbreviation of the tutorial stream' optional :name, type: String, desc: 'The name of this task def' optional :description, type: String, desc: 'The description of this task def' - optional :weighting, type: Integer, desc: 'The weighting of this task' + optional :estimated_hours, type: Integer, desc: 'The estimated number of hours to complete this task' optional :target_grade, type: Integer, desc: 'Target grade for task' optional :group_set_id, type: Integer, desc: 'Related group set' optional :start_date, type: Date, desc: 'The date when the task should be started' @@ -171,7 +171,7 @@ class TaskDefinitionsApi < Grape::API .permit( :name, :description, - :weighting, + :estimated_hours, :target_grade, :start_date, :target_date, diff --git a/app/models/project.rb b/app/models/project.rb index 1444730f9b..2c50c28e4e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -464,7 +464,7 @@ def discuss_and_demonstrate_tasks # get the weight of all tasks completed or marked as ready to assess # def completed_tasks_weight - ready_or_complete_tasks.empty? ? 0.0 : ready_or_complete_tasks.map { |task| task.task_definition.weighting }.inject(:+) + ready_or_complete_tasks.empty? ? 0.0 : ready_or_complete_tasks.map { |task| task.task_definition.estimated_hours }.inject(:+) end def convert_hash_to_pct(hash, total) @@ -594,7 +594,7 @@ def assigned_task_defs end def total_task_weight - assigned_task_defs.map(&:weighting).inject(:+) + assigned_task_defs.map(&:estimated_hours).inject(:+) end def remaining_days diff --git a/app/models/task.rb b/app/models/task.rb index ee3e76564c..a15c6d4e54 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -837,7 +837,7 @@ def assessed? end def weight - task_definition.weighting.to_f + task_definition.estimated_hours.to_f end def add_text_comment(user, text, reply_to_id = nil) diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 8d2fa7c5cd..6effea69eb 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -104,7 +104,7 @@ def self.permissions validate :unit_must_be_same validate :tutorial_stream_present? - validates :weighting, presence: true + validates :estimated_hours, presence: true validate :check_existing_prerequisites @@ -542,7 +542,7 @@ def to_csv_row end def self.csv_columns - [:name, :abbreviation, :description, :weighting, :target_grade, :restrict_status_updates, :max_quality_pts, + [:name, :abbreviation, :description, :estimated_hours, :target_grade, :restrict_status_updates, :max_quality_pts, :is_graded, :plagiarism_warn_pct, :scorm_enabled, :scorm_allow_review, :scorm_bypass_test, :scorm_time_delay_enabled, :scorm_attempt_limit, :group_set, :upload_requirements, :start_week, :start_day, :target_week, :target_day, :due_week, :due_day, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites, :discussion_prompts] @@ -572,7 +572,7 @@ def self.task_def_for_csv_row(unit, row) result = TaskDefinition.find_or_create_by(unit_id: unit.id, tutorial_stream: tutorial_stream, name: name, abbreviation: abbreviation) do |td| td.target_date = target_date td.start_date = start_date - td.weighting = row[:weighting].to_i + td.estimated_hours = row[:estimated_hours].to_i end new_task = true end @@ -581,7 +581,7 @@ def self.task_def_for_csv_row(unit, row) result.unit_id = unit.id result.abbreviation = abbreviation result.description = "#{row[:description]}".strip - result.weighting = row[:weighting].to_i + result.estimated_hours = row[:estimated_hours].to_i result.target_grade = row[:target_grade].to_i result.restrict_status_updates = %w(Yes y Y yes true TRUE 1).include? "#{row[:restrict_status_updates]}".strip result.max_quality_pts = row[:max_quality_pts].to_i diff --git a/db/migrate/20260425091151_change_weighting_to_estimated_hours_in_task_definitions.rb b/db/migrate/20260425091151_change_weighting_to_estimated_hours_in_task_definitions.rb new file mode 100644 index 0000000000..f56e8a7d61 --- /dev/null +++ b/db/migrate/20260425091151_change_weighting_to_estimated_hours_in_task_definitions.rb @@ -0,0 +1,5 @@ +class ChangeWeightingToEstimatedHoursInTaskDefinitions < ActiveRecord::Migration[8.0] + def change + rename_column :task_definitions, :weighting, :estimated_hours + end +end diff --git a/db/schema.rb b/db/schema.rb index 73fc5ebd58..8a0082cae3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_03_22_230239) do +ActiveRecord::Schema[8.0].define(version: 2026_04_25_091151) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -420,7 +420,7 @@ t.bigint "unit_id" t.string "name" t.string "description", limit: 4096 - t.decimal "weighting", precision: 10 + t.decimal "estimated_hours", precision: 10 t.datetime "target_date", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false From d337b35e529fb3e1ab352f29c917c32c3db37fdb Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 26 Apr 2026 16:18:35 +1000 Subject: [PATCH 04/21] feat(ai_eff): prediction endpoint and Sidekiq job This commit is inspired by PR#87 and PR#85 that attempts to implement the AI Task Effort prediction feature. Co-authored-by: jtalev Co-authored-by: officialid130-13e13 --- app/api/entities/task_definition_entity.rb | 1 + app/api/task_definitions_api.rb | 22 +++++++++++++ app/sidekiq/predict_effort_job.rb | 33 +++++++++++++++++++ ...425165056_add_predicted_effort_to_tasks.rb | 5 +++ db/schema.rb | 3 +- 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/sidekiq/predict_effort_job.rb create mode 100644 db/migrate/20260425165056_add_predicted_effort_to_tasks.rb diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index cf8b6b95c4..042130d1b2 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -13,6 +13,7 @@ def staff?(my_role) expose :name expose :description expose :estimated_hours + expose :predicted_effort expose :target_grade with_options(format_with: :date_only) do diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 0196a61e86..7628dfd328 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -20,6 +20,7 @@ class TaskDefinitionsApi < Grape::API requires :name, type: String, desc: 'The name of this task def' requires :description, type: String, desc: 'The description of this task def' requires :estimated_hours, type: Integer, desc: 'The estimated number of hours to complete this task' + optional :predicted_effort, type: Float, desc: 'The predicted effort of the task based on task features' requires :target_grade, type: Integer, desc: 'Minimum grade for task' optional :group_set_id, type: Integer, desc: 'Related group set' requires :start_date, type: Date, desc: 'The date when the task should be started' @@ -58,6 +59,7 @@ class TaskDefinitionsApi < Grape::API :name, :description, :estimated_hours, + :predicted_effort, :target_grade, :start_date, :target_date, @@ -116,6 +118,7 @@ class TaskDefinitionsApi < Grape::API optional :name, type: String, desc: 'The name of this task def' optional :description, type: String, desc: 'The description of this task def' optional :estimated_hours, type: Integer, desc: 'The estimated number of hours to complete this task' + optional :predicted_effort, type: Float, desc: 'The predicted effort of the task based on task features' optional :target_grade, type: Integer, desc: 'Target grade for task' optional :group_set_id, type: Integer, desc: 'Related group set' optional :start_date, type: Date, desc: 'The date when the task should be started' @@ -172,6 +175,7 @@ class TaskDefinitionsApi < Grape::API :name, :description, :estimated_hours, + :predicted_effort, :target_grade, :start_date, :target_date, @@ -926,6 +930,24 @@ class TaskDefinitionsApi < Grape::API present job, with: Entities::SidekiqJobEntity end + desc 'Predict the effort required for a task description' + params do + requires :unit_id, type: Integer, desc: 'The unit that has the task definition' + requires :task_def_id, type: Integer, desc: 'The task definition to predict effort for' + end + post '/units/:unit_id/task_definitions/:task_def_id/predict_effort' do + unit = Unit.find(params[:unit_id]) + unless authorise? current_user, unit, :get_students + error!({ error: "Not authorised to run prediction." }, 403) + end + + td = unit.task_definitions.find(params[:task_def_id]) + + PredictEffortJob.perform_async(td.id) + + present status: "Prediction queued" + end + # desc 'Retrieve the contents of the overseer execution script' # params do # requires :unit_id, type: Integer, desc: 'The unit that has the task definition' diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb new file mode 100644 index 0000000000..b6fca57afb --- /dev/null +++ b/app/sidekiq/predict_effort_job.rb @@ -0,0 +1,33 @@ +require "net/http" +require "json" + +class PredictEffortJob + include Sidekiq::Worker + + def perform(task_def_id) + td = TaskDefinition.find(task_def_id) + payload = build_payload(td) + Rails.logger.info("ML payload: #{payload.to_json}") + response = Net::HTTP.post( + URI("#{ENV.fetch('ML_SERVICE_URL')}/predict"), + payload.to_json, + "Content-Type" => "application/json" + ) + + result = JSON.parse(response.body) + + td.update(predicted_effort: result["predicted_effort"]) + end + + private + + def build_payload(task_def) + { + estimated_hours: task_def.estimated_hours, + target_grade: task_def.target_grade, + start_date: task_def.start_date, + due_date: task_def.due_date # , + # TODO: task sheet for TF-IDF + } + end +end diff --git a/db/migrate/20260425165056_add_predicted_effort_to_tasks.rb b/db/migrate/20260425165056_add_predicted_effort_to_tasks.rb new file mode 100644 index 0000000000..33c40ce6a6 --- /dev/null +++ b/db/migrate/20260425165056_add_predicted_effort_to_tasks.rb @@ -0,0 +1,5 @@ +class AddPredictedEffortToTasks < ActiveRecord::Migration[8.0] + def change + add_column :task_definitions, :predicted_effort, :float + end +end diff --git a/db/schema.rb b/db/schema.rb index 8a0082cae3..355fbb76df 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_04_25_091151) do +ActiveRecord::Schema[8.0].define(version: 2026_04_25_165056) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -450,6 +450,7 @@ t.boolean "use_resources_for_jplag_base_code", default: false, null: false t.boolean "lock_assessments_to_tutorial_stream", default: false, null: false t.boolean "requires_discussion", default: false, null: false + t.float "predicted_effort" t.index ["abbreviation", "unit_id"], name: "index_task_definitions_on_abbreviation_and_unit_id", unique: true t.index ["group_set_id"], name: "index_task_definitions_on_group_set_id" t.index ["name", "unit_id"], name: "index_task_definitions_on_name_and_unit_id", unique: true From 05b690d92fd2e77a49d33a3d86cb74a4ae93c6c1 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 26 Apr 2026 21:02:07 +1000 Subject: [PATCH 05/21] feat(ai_eff): default value for predicted effort Also, the addition of the default value to any record that has null values in the column. --- ...set_default_predicted_effort_on_task_definitions.rb | 10 ++++++++++ db/schema.rb | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20260426101725_set_default_predicted_effort_on_task_definitions.rb diff --git a/db/migrate/20260426101725_set_default_predicted_effort_on_task_definitions.rb b/db/migrate/20260426101725_set_default_predicted_effort_on_task_definitions.rb new file mode 100644 index 0000000000..e7f53d2235 --- /dev/null +++ b/db/migrate/20260426101725_set_default_predicted_effort_on_task_definitions.rb @@ -0,0 +1,10 @@ +class SetDefaultPredictedEffortOnTaskDefinitions < ActiveRecord::Migration[8.0] + def up + change_column_default :task_definitions, :predicted_effort, 1.0 + TaskDefinition.where(predicted_effort: nil).update_all(predicted_effort: 1.0) + end + + def down + change_column_default :task_definitions, :predicted_effort, nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 355fbb76df..3fc9a273f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_04_25_165056) do +ActiveRecord::Schema[8.0].define(version: 2026_04_26_101725) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -450,7 +450,7 @@ t.boolean "use_resources_for_jplag_base_code", default: false, null: false t.boolean "lock_assessments_to_tutorial_stream", default: false, null: false t.boolean "requires_discussion", default: false, null: false - t.float "predicted_effort" + t.float "predicted_effort", default: 1.0 t.index ["abbreviation", "unit_id"], name: "index_task_definitions_on_abbreviation_and_unit_id", unique: true t.index ["group_set_id"], name: "index_task_definitions_on_group_set_id" t.index ["name", "unit_id"], name: "index_task_definitions_on_name_and_unit_id", unique: true From a1f5dbbf276ef6df0f110075eef6fcb184d494b8 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 26 Apr 2026 22:32:44 +1000 Subject: [PATCH 06/21] fix(ai_eff): mutated uri in the Sidekiq job --- app/sidekiq/predict_effort_job.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb index b6fca57afb..972b5a47c6 100644 --- a/app/sidekiq/predict_effort_job.rb +++ b/app/sidekiq/predict_effort_job.rb @@ -9,13 +9,13 @@ def perform(task_def_id) payload = build_payload(td) Rails.logger.info("ML payload: #{payload.to_json}") response = Net::HTTP.post( - URI("#{ENV.fetch('ML_SERVICE_URL')}/predict"), + URI("#{ENV.fetch('ML_SERVICE_URL')}predict"), payload.to_json, "Content-Type" => "application/json" ) result = JSON.parse(response.body) - + Rails.logger.info("FastAPI response: #{response.body}") td.update(predicted_effort: result["predicted_effort"]) end From 3a0eef482e6eea81419b0abb016902677f553502 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 20:47:13 +1000 Subject: [PATCH 07/21] add allow prediction flag on units migration --- .../20260504104656_add_prediction_enabled_flag_to_units.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb diff --git a/db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb b/db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb new file mode 100644 index 0000000000..6964f2dfe0 --- /dev/null +++ b/db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb @@ -0,0 +1,5 @@ +class AddPredictionEnabledFlagToUnits < ActiveRecord::Migration[8.0] + def change + add_column :units, :allow_effort_predictions, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fc9a273f2..eeb3593988 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_04_26_101725) do +ActiveRecord::Schema[8.0].define(version: 2026_05_04_104656) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -744,6 +744,7 @@ t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false t.integer "feedback_warning_threshold_days", default: 5 t.integer "feedback_overflow_threshold_days", default: 7 + t.boolean "allow_effort_predictions", default: false, null: false t.index ["draft_task_definition_id"], name: "index_units_on_draft_task_definition_id" t.index ["main_convenor_id"], name: "index_units_on_main_convenor_id" t.index ["overseer_image_id"], name: "index_units_on_overseer_image_id" From 6f459a89cf6d96d043344441bb31639945a0d720 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 21:10:48 +1000 Subject: [PATCH 08/21] expose allow_effort_predictions in entity --- app/api/entities/unit_entity.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/entities/unit_entity.rb b/app/api/entities/unit_entity.rb index 4743f67511..3a296123a6 100644 --- a/app/api/entities/unit_entity.rb +++ b/app/api/entities/unit_entity.rb @@ -49,6 +49,9 @@ def can_read_unit_config?(my_role) expose :allow_student_change_tutorial, unless: :summary_only expose :allow_flexible_dates, unless: :summary_only expose :mark_late_submissions_as_assess_in_portfolio, unless: :summary_only + expose :feedback_warning_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } + expose :feedback_overflow_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } + expose :allow_effort_predictions expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos, unless: :summary_only expose :tutorial_streams, using: TutorialStreamEntity, unless: :summary_only @@ -65,7 +68,5 @@ def can_read_unit_config?(my_role) # unit.group_memberships.where(active: true) # end - expose :feedback_warning_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } - expose :feedback_overflow_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } end end From c0523daaa0bd5fd75b0211d80db5ea8a7a3d53d7 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 21:17:16 +1000 Subject: [PATCH 09/21] add allow_effort_prediction to crud endpoints --- app/api/units_api.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/api/units_api.rb b/app/api/units_api.rb index a20c8a3d7a..0f4e82019a 100644 --- a/app/api/units_api.rb +++ b/app/api/units_api.rb @@ -92,6 +92,7 @@ class UnitsApi < Grape::API optional :assessment_enabled, type: Boolean optional :feedback_warning_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its highlighted in the tutors inbox' optional :feedback_overflow_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its added to overflow marking' + optional :allow_effort_predictions, type: Boolean, desc: 'Turn on/off ability for admins to run AI effort predictions for tasks belonging to this unit' mutually_exclusive :teaching_period_id, :start_date mutually_exclusive :teaching_period_id, :end_date @@ -126,7 +127,8 @@ class UnitsApi < Grape::API :overseer_image_id, :assessment_enabled, :feedback_warning_threshold_days, - :feedback_overflow_threshold_days + :feedback_overflow_threshold_days, + :allow_effort_predictions, ) if unit.teaching_period_id.present? && (unit_parameters.key?(:start_date) || unit_parameters['teaching_period_id'] == -1) @@ -174,6 +176,7 @@ class UnitsApi < Grape::API optional :allow_student_change_tutorial, type: Boolean, desc: 'Can turn on/off student ability to change tutorials', default: true optional :feedback_warning_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its highlighted in the tutors inbox' optional :feedback_overflow_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its added to overflow marking' + optional :allow_effort_predictions, type: Boolean, desc: 'Turn on/off ability for admins to run AI effort predictions for tasks belonging to this unit', default: false mutually_exclusive :teaching_period_id, :start_date mutually_exclusive :teaching_period_id, :end_date @@ -205,7 +208,8 @@ class UnitsApi < Grape::API :portfolio_auto_generation_date, :allow_student_change_tutorial, :feedback_warning_threshold_days, - :feedback_overflow_threshold_days + :feedback_overflow_threshold_days, + :allow_effort_predictions, ) # Ensure the user is authorised to convene units From 42ee620b20b7496ae817faa64b802e3999b6dd7c Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 22:31:54 +1000 Subject: [PATCH 10/21] return job ID from prediction endpoint --- app/api/task_definitions_api.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 7628dfd328..3f10c79f3c 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -943,9 +943,12 @@ class TaskDefinitionsApi < Grape::API td = unit.task_definitions.find(params[:task_def_id]) - PredictEffortJob.perform_async(td.id) + job_id = PredictEffortJob.perform_async(td.id) - present status: "Prediction queued" + present( + status: "Prediction queued", + job_id: job_id + ) end # desc 'Retrieve the contents of the overseer execution script' From 96da485a83ff8008525db9d924ccc3794afc2924 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 22:57:10 +1000 Subject: [PATCH 11/21] error handling on prediction job enqueue --- app/api/task_definitions_api.rb | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 3f10c79f3c..1ed5b4efa4 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -943,12 +943,29 @@ class TaskDefinitionsApi < Grape::API td = unit.task_definitions.find(params[:task_def_id]) - job_id = PredictEffortJob.perform_async(td.id) + begin + job_id = PredictEffortJob.perform_async(td.id) - present( - status: "Prediction queued", - job_id: job_id - ) + error!({ error: "Failed to enqueue prediction job" }, 500) if job_id.nil? + + present( + { + job_id: job_id, + message: "Prediction queued", + success: true + } + ) + rescue StandardError => e + Rails.logger.error("Failed to enqueue prediction job: #{e.message}") + + error!( + { + message: "Failed to enqueue job", + error: "Could not queue prediction job", + success: false + }, 500 + ) + end end # desc 'Retrieve the contents of the overseer execution script' From 158acbddd73984df64ad56fba2a34022f969cbf6 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 23:15:09 +1000 Subject: [PATCH 12/21] add initiator to sidekiq job - adding an initiator to the job allows the user who enqueued the job to retrieve info about the job to aid in polling for results after initial enqueueing takes place --- app/api/sidekiq_api.rb | 6 +++++- app/api/task_definitions_api.rb | 11 +++++------ app/sidekiq/predict_effort_job.rb | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/api/sidekiq_api.rb b/app/api/sidekiq_api.rb index 47c92a4c74..ab1df55b63 100644 --- a/app/api/sidekiq_api.rb +++ b/app/api/sidekiq_api.rb @@ -15,8 +15,12 @@ class SidekiqApi < Grape::API get '/sidekiq/:id' do job_id = params[:id] job_data = Sidekiq::Status.get_all(job_id) - initiator = Sidekiq::Status.get(job_id, :initiator) + + if initiator.nil? + error!({ error: 'Job not found or has no owner' }, 404) + end + if current_user.id != initiator.to_i error!({ error: 'You do not have permission to access this job' }, 403) end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 1ed5b4efa4..ea60b787da 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -944,14 +944,13 @@ class TaskDefinitionsApi < Grape::API td = unit.task_definitions.find(params[:task_def_id]) begin - job_id = PredictEffortJob.perform_async(td.id) - - error!({ error: "Failed to enqueue prediction job" }, 500) if job_id.nil? + job_id = PredictEffortJob.perform_async(td.id, current_user.id) + error!({ error: 'Failed to enqueue prediction job' }, 500) if job_id.nil? present( { job_id: job_id, - message: "Prediction queued", + message: 'Prediction queued', success: true } ) @@ -960,8 +959,8 @@ class TaskDefinitionsApi < Grape::API error!( { - message: "Failed to enqueue job", - error: "Could not queue prediction job", + message: 'Failed to enqueue job', + error: e.message, success: false }, 500 ) diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb index 972b5a47c6..d69f7b55da 100644 --- a/app/sidekiq/predict_effort_job.rb +++ b/app/sidekiq/predict_effort_job.rb @@ -3,8 +3,10 @@ class PredictEffortJob include Sidekiq::Worker + include Sidekiq::Status::Worker - def perform(task_def_id) + def perform(task_def_id, user_id) + store initiator: user_id td = TaskDefinition.find(task_def_id) payload = build_payload(td) Rails.logger.info("ML payload: #{payload.to_json}") From bb93aec16ca3a2339a4d8c4899ee9bed6940ec11 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Wed, 6 May 2026 00:31:37 +1000 Subject: [PATCH 13/21] More error handling --- app/sidekiq/predict_effort_job.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb index d69f7b55da..987e3ea16d 100644 --- a/app/sidekiq/predict_effort_job.rb +++ b/app/sidekiq/predict_effort_job.rb @@ -9,9 +9,14 @@ def perform(task_def_id, user_id) store initiator: user_id td = TaskDefinition.find(task_def_id) payload = build_payload(td) + + ml_url = ENV.fetch('ML_SERVICE_URL') + if ml_url.blank? + raise StandardError, "ML_SERVICE_URL is not configured" + end Rails.logger.info("ML payload: #{payload.to_json}") response = Net::HTTP.post( - URI("#{ENV.fetch('ML_SERVICE_URL')}predict"), + URI("#{ml_url}predict"), payload.to_json, "Content-Type" => "application/json" ) @@ -19,6 +24,17 @@ def perform(task_def_id, user_id) result = JSON.parse(response.body) Rails.logger.info("FastAPI response: #{response.body}") td.update(predicted_effort: result["predicted_effort"]) + store result: result + rescue StandardError => e + Rails.logger.error("PredictEffortJob failed: #{e.message}") + + store( + status: 'failed', + message: e.message, + result: { error: e.message } + ) + + raise e end private From 749814a0dc2d9717fccbd2882b8ff23d8bd16de8 Mon Sep 17 00:00:00 2001 From: Josh Talev <139172317+jtalev@users.noreply.github.com> Date: Wed, 6 May 2026 21:19:22 +1000 Subject: [PATCH 14/21] removes package install that breaks build (#1) Co-authored-by: josh.talev --- texlive.Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/texlive.Dockerfile b/texlive.Dockerfile index c68816ce24..1ca6c9acff 100644 --- a/texlive.Dockerfile +++ b/texlive.Dockerfile @@ -53,7 +53,6 @@ RUN tlmgr install \ paralist \ pdfcol \ pdflscape \ - pdfmanagement-testphase \ pdfpages \ tagpdf \ tcolorbox \ From 90d26ab9efa563f1cf09b957abf42330666f2085 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Mon, 11 May 2026 19:29:53 +1000 Subject: [PATCH 15/21] refactor(ai_eff): change job checking to make a little more sense --- app/api/sidekiq_api.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/sidekiq_api.rb b/app/api/sidekiq_api.rb index ab1df55b63..4f97346a5b 100644 --- a/app/api/sidekiq_api.rb +++ b/app/api/sidekiq_api.rb @@ -17,8 +17,8 @@ class SidekiqApi < Grape::API job_data = Sidekiq::Status.get_all(job_id) initiator = Sidekiq::Status.get(job_id, :initiator) - if initiator.nil? - error!({ error: 'Job not found or has no owner' }, 404) + if job_data.nil? || job_data.empty? + error!({ error: 'Job not found' }, 404) end if current_user.id != initiator.to_i From f82d58d490ea55756e8625ed74c0e9b9b63b1e37 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 16 May 2026 17:35:58 +1000 Subject: [PATCH 16/21] feat(ai_eff): remove failing line rubocop --- app/api/sidekiq_api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/sidekiq_api.rb b/app/api/sidekiq_api.rb index 4f97346a5b..e195ec480e 100644 --- a/app/api/sidekiq_api.rb +++ b/app/api/sidekiq_api.rb @@ -17,7 +17,7 @@ class SidekiqApi < Grape::API job_data = Sidekiq::Status.get_all(job_id) initiator = Sidekiq::Status.get(job_id, :initiator) - if job_data.nil? || job_data.empty? + if job_data.blank? error!({ error: 'Job not found' }, 404) end From d52a79839c316e0371d50e554003129d9979f453 Mon Sep 17 00:00:00 2001 From: Josh Talev <139172317+jtalev@users.noreply.github.com> Date: Sat, 16 May 2026 19:02:36 +1000 Subject: [PATCH 17/21] Return job id from prediction endpoint (#3) * return job ID from prediction endpoint * error handling on prediction job enqueue * add initiator to sidekiq job - adding an initiator to the job allows the user who enqueued the job to retrieve info about the job to aid in polling for results after initial enqueueing takes place * More error handling --------- Co-authored-by: josh.talev --- app/api/sidekiq_api.rb | 6 +++++- app/api/task_definitions_api.rb | 25 ++++++++++++++++++++++--- app/sidekiq/predict_effort_job.rb | 22 ++++++++++++++++++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/api/sidekiq_api.rb b/app/api/sidekiq_api.rb index 47c92a4c74..ab1df55b63 100644 --- a/app/api/sidekiq_api.rb +++ b/app/api/sidekiq_api.rb @@ -15,8 +15,12 @@ class SidekiqApi < Grape::API get '/sidekiq/:id' do job_id = params[:id] job_data = Sidekiq::Status.get_all(job_id) - initiator = Sidekiq::Status.get(job_id, :initiator) + + if initiator.nil? + error!({ error: 'Job not found or has no owner' }, 404) + end + if current_user.id != initiator.to_i error!({ error: 'You do not have permission to access this job' }, 403) end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 7628dfd328..ea60b787da 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -943,9 +943,28 @@ class TaskDefinitionsApi < Grape::API td = unit.task_definitions.find(params[:task_def_id]) - PredictEffortJob.perform_async(td.id) - - present status: "Prediction queued" + begin + job_id = PredictEffortJob.perform_async(td.id, current_user.id) + error!({ error: 'Failed to enqueue prediction job' }, 500) if job_id.nil? + + present( + { + job_id: job_id, + message: 'Prediction queued', + success: true + } + ) + rescue StandardError => e + Rails.logger.error("Failed to enqueue prediction job: #{e.message}") + + error!( + { + message: 'Failed to enqueue job', + error: e.message, + success: false + }, 500 + ) + end end # desc 'Retrieve the contents of the overseer execution script' diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb index 972b5a47c6..987e3ea16d 100644 --- a/app/sidekiq/predict_effort_job.rb +++ b/app/sidekiq/predict_effort_job.rb @@ -3,13 +3,20 @@ class PredictEffortJob include Sidekiq::Worker + include Sidekiq::Status::Worker - def perform(task_def_id) + def perform(task_def_id, user_id) + store initiator: user_id td = TaskDefinition.find(task_def_id) payload = build_payload(td) + + ml_url = ENV.fetch('ML_SERVICE_URL') + if ml_url.blank? + raise StandardError, "ML_SERVICE_URL is not configured" + end Rails.logger.info("ML payload: #{payload.to_json}") response = Net::HTTP.post( - URI("#{ENV.fetch('ML_SERVICE_URL')}predict"), + URI("#{ml_url}predict"), payload.to_json, "Content-Type" => "application/json" ) @@ -17,6 +24,17 @@ def perform(task_def_id) result = JSON.parse(response.body) Rails.logger.info("FastAPI response: #{response.body}") td.update(predicted_effort: result["predicted_effort"]) + store result: result + rescue StandardError => e + Rails.logger.error("PredictEffortJob failed: #{e.message}") + + store( + status: 'failed', + message: e.message, + result: { error: e.message } + ) + + raise e end private From c5a217f941b3e25213b92909ad1b26ff2495b306 Mon Sep 17 00:00:00 2001 From: Josh Talev <139172317+jtalev@users.noreply.github.com> Date: Sat, 16 May 2026 19:02:54 +1000 Subject: [PATCH 18/21] Allow prediction flag on units (#2) * add allow prediction flag on units migration * expose allow_effort_predictions in entity * add allow_effort_prediction to crud endpoints --------- Co-authored-by: josh.talev --- app/api/entities/unit_entity.rb | 5 +++-- app/api/units_api.rb | 8 ++++++-- ...20260504104656_add_prediction_enabled_flag_to_units.rb | 5 +++++ db/schema.rb | 3 ++- 4 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb diff --git a/app/api/entities/unit_entity.rb b/app/api/entities/unit_entity.rb index 4743f67511..3a296123a6 100644 --- a/app/api/entities/unit_entity.rb +++ b/app/api/entities/unit_entity.rb @@ -49,6 +49,9 @@ def can_read_unit_config?(my_role) expose :allow_student_change_tutorial, unless: :summary_only expose :allow_flexible_dates, unless: :summary_only expose :mark_late_submissions_as_assess_in_portfolio, unless: :summary_only + expose :feedback_warning_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } + expose :feedback_overflow_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } + expose :allow_effort_predictions expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos, unless: :summary_only expose :tutorial_streams, using: TutorialStreamEntity, unless: :summary_only @@ -65,7 +68,5 @@ def can_read_unit_config?(my_role) # unit.group_memberships.where(active: true) # end - expose :feedback_warning_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } - expose :feedback_overflow_threshold_days, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) } end end diff --git a/app/api/units_api.rb b/app/api/units_api.rb index a20c8a3d7a..0f4e82019a 100644 --- a/app/api/units_api.rb +++ b/app/api/units_api.rb @@ -92,6 +92,7 @@ class UnitsApi < Grape::API optional :assessment_enabled, type: Boolean optional :feedback_warning_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its highlighted in the tutors inbox' optional :feedback_overflow_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its added to overflow marking' + optional :allow_effort_predictions, type: Boolean, desc: 'Turn on/off ability for admins to run AI effort predictions for tasks belonging to this unit' mutually_exclusive :teaching_period_id, :start_date mutually_exclusive :teaching_period_id, :end_date @@ -126,7 +127,8 @@ class UnitsApi < Grape::API :overseer_image_id, :assessment_enabled, :feedback_warning_threshold_days, - :feedback_overflow_threshold_days + :feedback_overflow_threshold_days, + :allow_effort_predictions, ) if unit.teaching_period_id.present? && (unit_parameters.key?(:start_date) || unit_parameters['teaching_period_id'] == -1) @@ -174,6 +176,7 @@ class UnitsApi < Grape::API optional :allow_student_change_tutorial, type: Boolean, desc: 'Can turn on/off student ability to change tutorials', default: true optional :feedback_warning_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its highlighted in the tutors inbox' optional :feedback_overflow_threshold_days, type: Integer, desc: 'Number of days since a submission without feedback before its added to overflow marking' + optional :allow_effort_predictions, type: Boolean, desc: 'Turn on/off ability for admins to run AI effort predictions for tasks belonging to this unit', default: false mutually_exclusive :teaching_period_id, :start_date mutually_exclusive :teaching_period_id, :end_date @@ -205,7 +208,8 @@ class UnitsApi < Grape::API :portfolio_auto_generation_date, :allow_student_change_tutorial, :feedback_warning_threshold_days, - :feedback_overflow_threshold_days + :feedback_overflow_threshold_days, + :allow_effort_predictions, ) # Ensure the user is authorised to convene units diff --git a/db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb b/db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb new file mode 100644 index 0000000000..6964f2dfe0 --- /dev/null +++ b/db/migrate/20260504104656_add_prediction_enabled_flag_to_units.rb @@ -0,0 +1,5 @@ +class AddPredictionEnabledFlagToUnits < ActiveRecord::Migration[8.0] + def change + add_column :units, :allow_effort_predictions, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fc9a273f2..eeb3593988 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_04_26_101725) do +ActiveRecord::Schema[8.0].define(version: 2026_05_04_104656) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -744,6 +744,7 @@ t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false t.integer "feedback_warning_threshold_days", default: 5 t.integer "feedback_overflow_threshold_days", default: 7 + t.boolean "allow_effort_predictions", default: false, null: false t.index ["draft_task_definition_id"], name: "index_units_on_draft_task_definition_id" t.index ["main_convenor_id"], name: "index_units_on_main_convenor_id" t.index ["overseer_image_id"], name: "index_units_on_overseer_image_id" From c846cfb953daebc8950bb442269e3697a54689b5 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 16 May 2026 19:14:02 +1000 Subject: [PATCH 19/21] feat(ai_eff): suggested fix from #3 --- app/sidekiq/predict_effort_job.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb index 987e3ea16d..5526bd379f 100644 --- a/app/sidekiq/predict_effort_job.rb +++ b/app/sidekiq/predict_effort_job.rb @@ -15,12 +15,16 @@ def perform(task_def_id, user_id) raise StandardError, "ML_SERVICE_URL is not configured" end Rails.logger.info("ML payload: #{payload.to_json}") + response = Net::HTTP.post( URI("#{ml_url}predict"), payload.to_json, "Content-Type" => "application/json" ) + unless response.is_a?(Net::HTTPSuccess) + raise StandardError, "ML service returned #{response.code}: #{response.body}" + end result = JSON.parse(response.body) Rails.logger.info("FastAPI response: #{response.body}") td.update(predicted_effort: result["predicted_effort"]) From a8fa2ef72cf5c3088865a916c67f0b4bfc99b1b9 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 16 May 2026 20:04:20 +1000 Subject: [PATCH 20/21] docs: change error message --- app/api/sidekiq_api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/sidekiq_api.rb b/app/api/sidekiq_api.rb index 1ef67b2c37..8f7875c8cd 100644 --- a/app/api/sidekiq_api.rb +++ b/app/api/sidekiq_api.rb @@ -18,7 +18,7 @@ class SidekiqApi < Grape::API initiator = Sidekiq::Status.get(job_id, :initiator) if job_data.blank? || initiator.nil? - error!({ error: 'Job not found' }, 404) + error!({ error: 'Job not found or has no owner' }, 404) end if current_user.id != initiator.to_i From 260ad01550cdd09121ead226d1e21baaec4d1fff Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 17 May 2026 14:43:54 +1000 Subject: [PATCH 21/21] feat(ai_eff): add target date to ML payload --- app/sidekiq/predict_effort_job.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/sidekiq/predict_effort_job.rb b/app/sidekiq/predict_effort_job.rb index 987e3ea16d..26235192a8 100644 --- a/app/sidekiq/predict_effort_job.rb +++ b/app/sidekiq/predict_effort_job.rb @@ -44,6 +44,7 @@ def build_payload(task_def) estimated_hours: task_def.estimated_hours, target_grade: task_def.target_grade, start_date: task_def.start_date, + target_date: task_def.target_date, due_date: task_def.due_date # , # TODO: task sheet for TF-IDF }