From 148cbba601020eb41d975bb39f17a72378fc36ad Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:25:25 +1100 Subject: [PATCH 01/46] fix: ensure long comments are appended on a new page #303 --- app/views/layouts/application.pdf.erbtex | 1 + app/views/portfolio/portfolio_pdf.pdf.erb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/application.pdf.erbtex b/app/views/layouts/application.pdf.erbtex index 8e3db64fe..b05166180 100644 --- a/app/views/layouts/application.pdf.erbtex +++ b/app/views/layouts/application.pdf.erbtex @@ -42,6 +42,7 @@ \usepackage{lastpage} \usepackage{multirow} \usepackage[colorlinks]{hyperref} +\usepackage{longtable} \hypersetup{colorlinks, linkcolor=black, filecolor=black, diff --git a/app/views/portfolio/portfolio_pdf.pdf.erb b/app/views/portfolio/portfolio_pdf.pdf.erb index 318dc625b..e92fa1882 100644 --- a/app/views/portfolio/portfolio_pdf.pdf.erb +++ b/app/views/portfolio/portfolio_pdf.pdf.erb @@ -257,7 +257,7 @@ end # if there are linked outcomes end # if outcomes exist if task.comments.count > 0 %> - \begin{tabular}{p{3cm}|p{3cm}|p{9cm}} + \begin{longtable}{p{3cm}|p{3cm}|p{9cm}} \textbf{Date} & \textbf{Author} & \textbf{Comment} \\ \hline <% task.comments.each do |comment| @@ -266,7 +266,7 @@ end # if there are linked outcomes <% end # comments loop %> - \end{tabular} + \end{longtable} <% end # if comments exist From 5e3b4cc437fd296519e904a37e53305b579140dd Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:47:03 +1100 Subject: [PATCH 02/46] feat: env option to skip jplag submission clusters (#551) - Avoids a JPlag v6.2.0 bug that will occasionally hang forever during cluster calcuations --- app/models/similarity/unit_similarity_module.rb | 5 ++++- config/application.rb | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/similarity/unit_similarity_module.rb b/app/models/similarity/unit_similarity_module.rb index 64cc5f0f2..e5f3144e7 100644 --- a/app/models/similarity/unit_similarity_module.rb +++ b/app/models/similarity/unit_similarity_module.rb @@ -302,8 +302,11 @@ def run_jplag_on_done_files(task_definition, tasks_dir, tasks_with_files, unit_c base_code_string = use_base_code ? "--base-code=#{tasks_dir_split}/base" : "" + skip_cluster_check = Doubtfire::Application.config.jplag_skip_cluster_check + skip_cluster_string = skip_cluster_check ? '--cluster-skip' : '' + # Run JPLAG on the extracted files. JPlag container should already be in the /jplag/ workdir. - docker_command = "docker exec -i jplag java -jar jplag-jar-with-dependencies.jar #{tasks_dir_split}/submissions #{base_code_string} -l #{file_lang} --similarity-threshold=#{similarity_threshold} #{min_token_string} -M RUN -r #{results_dir}/#{task_definition.abbreviation}-result --overwrite" + docker_command = "docker exec -i jplag java -jar jplag-jar-with-dependencies.jar #{tasks_dir_split}/submissions #{base_code_string} -l #{file_lang} --similarity-threshold=#{similarity_threshold} #{min_token_string} #{skip_cluster_string} -M RUN -r #{results_dir}/#{task_definition.abbreviation}-result --overwrite" logger.debug "Executing command: #{docker_command}" system(docker_command) diff --git a/config/application.rb b/config/application.rb index 2b54558e5..2ce14ea91 100644 --- a/config/application.rb +++ b/config/application.rb @@ -74,6 +74,7 @@ def self.fetch_boolean_env(name) # variable. config.jplag_report_dir = ENV['DF_JPLAG_REPORT_DIR'] || Rails.root.join('jplag/results').to_s config.jplag_min_tokens = ENV.fetch('DF_JPLAG_MIN_TOKENS', -1) + config.jplag_skip_cluster_check = ENV['DF_JPLAG_SKIP_CLUSTER_CHECK'].present? && (ENV['DF_JPLAG_SKIP_CLUSTER_CHECK'].to_s.downcase == "true" || ENV['DF_JPLAG_SKIP_CLUSTER_CHECK'].to_i == 1) # ==> File size limits # Sets the global file size limit per upload requirement From 08d609ca20835d28e68d545017c18a38cb95dc7e Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:48:57 +1100 Subject: [PATCH 03/46] chore: delete old marking sessions with no duration (#549) --- lib/tasks/maintenance.rake | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/tasks/maintenance.rake b/lib/tasks/maintenance.rake index 5f68a071c..f6ba8ebfc 100644 --- a/lib/tasks/maintenance.rake +++ b/lib/tasks/maintenance.rake @@ -24,6 +24,12 @@ namespace :maintenance do end end + # Destroy old marking sessions that have less than 1 minute duration + MarkingSession + .where("end_time IS NOT NULL AND end_time < ?", 24.hours.ago) + .where("TIMESTAMPDIFF(SECOND, start_time, end_time) <= ?", 60) + .find_each(&:destroy!) + AuthToken.destroy_old_tokens end From e469e0bc9a58dc2232f53afb21d501fe804d4584 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:27:53 +1100 Subject: [PATCH 04/46] refactor: split comments into 2000 character chunks - ensures the same comment continues on the following page --- app/views/portfolio/portfolio_pdf.pdf.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/portfolio/portfolio_pdf.pdf.erb b/app/views/portfolio/portfolio_pdf.pdf.erb index e92fa1882..64704249c 100644 --- a/app/views/portfolio/portfolio_pdf.pdf.erb +++ b/app/views/portfolio/portfolio_pdf.pdf.erb @@ -261,8 +261,12 @@ end # if there are linked outcomes \textbf{Date} & \textbf{Author} & \textbf{Comment} \\ \hline <% task.comments.each do |comment| + full_text = comment.comment + chunks = full_text.scan(/.{1,2000}/m) # split into 2000-char chunks %> - <%= lesc comment.created_at.localtime.strftime("%Y/%m/%d %H:%M") %> & <%= lesc comment.user.name %> & <%= lesc comment.comment %> \\ + <% chunks.each_with_index do |chunk, i| %> + <%= lesc comment.created_at.localtime.strftime("%Y/%m/%d %H:%M") %> & <%= lesc comment.user.name %> & <%if i > 0 %>\textit{\textcolor{gray}{[continued...]}}<% end %> <%= lesc chunk %> <% if i < chunks.size - 1 %> \textit{\textcolor{gray}{[comment has been split due to length]}}<% end %> \\ + <% end %> <% end # comments loop %> From b8954ad87b042f8c1aad6b44b730f781834a5001 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:50:49 +1100 Subject: [PATCH 05/46] chore(release): 10.0.0-57 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2be50929..318b224db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-57](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-56...v10.0.0-57) (2025-11-25) + + +### Features + +* env option to skip jplag submission clusters ([#551](https://github.com/b0ink/doubtfire-deploy/issues/551)) ([5e3b4cc](https://github.com/b0ink/doubtfire-deploy/commit/5e3b4cc437fd296519e904a37e53305b579140dd)) + + +### Bug Fixes + +* ensure long comments are appended on a new page [#303](https://github.com/b0ink/doubtfire-deploy/issues/303) ([148cbba](https://github.com/b0ink/doubtfire-deploy/commit/148cbba601020eb41d975bb39f17a72378fc36ad)) + ## [10.0.0-56](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-55...v10.0.0-56) (2025-11-10) From d1d839ba10ee369faf354d758dd1c746bea9e4a9 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:31:55 +1100 Subject: [PATCH 06/46] fix: allow pdf gen and overseer job to be queued at the same time --- app/sidekiq/accept_overseer_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index db0880f4b..a40fdec5a 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -9,7 +9,7 @@ class AcceptOverseerJob sidekiq_options lock: :until_executed, # TODO: should students be allowed to submit a new task submission when the previous overseer job has not started/completed? - lock_args_method: ->(args) { [args.first] }, + lock_args_method: ->(args) { [args.first, 'overseer-assessment'] }, on_conflict: :reject, retry: 1 From c925c6f2a2b6ae8abb32b37a4b2274c131d8ffd2 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:34:00 +1100 Subject: [PATCH 07/46] chore(release): 10.0.0-58 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 318b224db..4c8918cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-58](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-57...v10.0.0-58) (2025-11-25) + + +### Bug Fixes + +* allow pdf gen and overseer job to be queued at the same time ([d1d839b](https://github.com/b0ink/doubtfire-deploy/commit/d1d839ba10ee369faf354d758dd1c746bea9e4a9)) + ## [10.0.0-57](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-56...v10.0.0-57) (2025-11-25) From 7981de85a8c318ce4c96140ff6fc014bb6719cdb Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 30 Nov 2025 13:29:30 +1000 Subject: [PATCH 08/46] 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 983749c55..a5a6f0bdb 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 ab38360a5c847a1db0d03547fc432b000a61ebb9 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:51:01 +1100 Subject: [PATCH 09/46] feat: discussion prompts (#546) * feat: init discussion prompts * chore: allow created by user to be null * chore: mount discussion prompt api * feat: discussion prompt api * test: ensure discussion prompt permissions * refactor: check permissions first * chore: return new entity * chore: remove unneeded fields * feat: expose count of discussion prompts * refactor: simplify discussion prompt schema * feat: import and export discussion prompts via task definition csv * test: add discussion prompts header to csv * chore: rollover discussion prompts * test: ensure rollover duplicates discussion prompts * chore: add discussion prompts header * chore: add discussion prompts header * test: ensure dicussion prompts are exported to csv * refactor: remove project specific prompts for task definition * refactor: remove project specific tests * chore: add discussion prompts header --- app/api/api_root.rb | 2 + app/api/discussion_prompts_api.rb | 122 ++++++++++++ app/api/entities/discussion_prompt_entity.rb | 8 + app/api/entities/task_definition_entity.rb | 4 + app/helpers/authorisation_helpers.rb | 3 +- app/models/discussion_prompt.rb | 7 + app/models/project.rb | 9 +- app/models/task_definition.rb | 38 +++- app/models/unit.rb | 11 +- ...0251110000046_create_discussion_prompts.rb | 10 + db/schema.rb | 11 +- test/api/discussion_prompts_api_test.rb | 179 ++++++++++++++++++ test/models/task_definition_test.rb | 2 +- test/models/unit_model_test.rb | 52 +++++ ...COS10001-ImportTasksWithTutorialStream.csv | 74 ++++---- ...10001-ImportTasksWithoutTutorialStream.csv | 74 ++++---- test_files/COS10001-Tasks.csv | 76 ++++---- .../COS10001-Tasks-Prerequisites.csv | 12 +- test_files/csv_test_files/COS10001-Tasks.csv | 10 +- test_files/csv_test_files/COS10001-Tasks.xlsx | Bin 8866 -> 8874 bytes .../unit_csv_imports/import_group_tasks.csv | 6 +- 21 files changed, 572 insertions(+), 138 deletions(-) create mode 100644 app/api/discussion_prompts_api.rb create mode 100644 app/api/entities/discussion_prompt_entity.rb create mode 100644 app/models/discussion_prompt.rb create mode 100644 db/migrate/20251110000046_create_discussion_prompts.rb create mode 100644 test/api/discussion_prompts_api_test.rb diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 983749c55..6f82404da 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -102,6 +102,7 @@ class ApiRoot < Grape::API mount WebcalApi mount WebcalPublicApi mount MarkingSessionsApi + mount DiscussionPromptsApi mount Feedback::FeedbackChipApi @@ -150,6 +151,7 @@ class ApiRoot < Grape::API AuthenticationHelpers.add_auth_to D2lIntegrationApi::D2lApi AuthenticationHelpers.add_auth_to Feedback::FeedbackChipApi AuthenticationHelpers.add_auth_to MarkingSessionsApi + AuthenticationHelpers.add_auth_to DiscussionPromptsApi add_swagger_documentation \ base_path: nil, diff --git a/app/api/discussion_prompts_api.rb b/app/api/discussion_prompts_api.rb new file mode 100644 index 000000000..799f921bf --- /dev/null +++ b/app/api/discussion_prompts_api.rb @@ -0,0 +1,122 @@ +require 'grape' + +class DiscussionPromptsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + before do + authenticated? + end + + desc "Get all the discussion prompts for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + end + get '/task_definitions/:task_definition_id/discussion_prompts' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :get_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + result = task_definition.discussion_prompts + .order(priority: :desc) + + present result, with: Entities::DiscussionPromptEntity, user: current_user + end + + desc "Create a new discussion prompt for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + requires :content, type: String, desc: 'Discussion prompt content' + requires :priority, type: Integer, desc: 'The priority of the disucssion prompt' + end + post '/task_definitions/:task_definition_id/discussion_prompts' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :create_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + unit = task_definition.unit + + content = params[:content].to_s + priority = params[:priority].to_i + + discussion_prompt = DiscussionPrompt.create!({ + task_definition: task_definition, + content: content, + priority: priority + }) + + present discussion_prompt, with: Entities::DiscussionPromptEntity + end + + desc "Update a discussion prompt for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + requires :content, type: String, desc: 'Discussion prompt content' + requires :priority, type: Integer, desc: 'The priority of the disucssion prompt' + requires :id, type: Integer, desc: 'The ID of the discussion prompt' + end + put '/task_definitions/:task_definition_id/discussion_prompts/:id' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :create_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + discussion_prompt = task_definition.discussion_prompts.find(params[:id]) + + content = params[:content].to_s + priority = params[:priority].to_i + + discussion_prompt.update({ + task_definition: task_definition, + content: content, + priority: priority + }) + end + + desc "Delete a discussion prompt for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + requires :id, type: Integer, desc: 'The ID of the discussion prompt' + end + delete '/task_definitions/:task_definition_id/discussion_prompts/:id' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :create_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + discussion_prompt = task_definition.discussion_prompts.find(params[:id]) + + discussion_prompt.destroy! + present discussion_prompt.destroyed?, with: Grape::Presenters::Presenter + end + + desc "Get all the discussion prompts for a project" + params do + requires :project_id, type: Integer, desc: 'Project to fetch discussion prompts for' + end + get 'projects/:project_id/discussion_prompts' do + project = Project.find(params[:project_id]) + + # TODO: should convenor permissions exist on the project ? + unless authorise? current_user, project, :get_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + tasks_to_discuss = project.tasks.where(task_status: [TaskStatus.discuss, TaskStatus.demonstrate]) + task_definition_ids = tasks_to_discuss.pluck(:task_definition_id) + + result = DiscussionPrompt + .joins(:task_definition) + .where(task_definition_id: task_definition_ids) + .order('task_definitions.abbreviation ASC, discussion_prompts.priority DESC') + + present result, with: Entities::DiscussionPromptEntity, user: current_user + end + +end diff --git a/app/api/entities/discussion_prompt_entity.rb b/app/api/entities/discussion_prompt_entity.rb new file mode 100644 index 000000000..1ffc160f3 --- /dev/null +++ b/app/api/entities/discussion_prompt_entity.rb @@ -0,0 +1,8 @@ +module Entities + class DiscussionPromptEntity < Grape::Entity + expose :id + expose :task_definition_id + expose :content + expose :priority + end +end diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index faa8d3752..3aa2afd6a 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -57,5 +57,9 @@ def staff?(my_role) expose :lock_assessments_to_tutorial_stream, if: ->(unit, options) { staff?(options[:my_role]) } expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos + + expose :discussion_prompts_count do |task_def| + task_def.discussion_prompts.size + end end end diff --git a/app/helpers/authorisation_helpers.rb b/app/helpers/authorisation_helpers.rb index 6ebc7605d..b27fd5902 100644 --- a/app/helpers/authorisation_helpers.rb +++ b/app/helpers/authorisation_helpers.rb @@ -25,7 +25,8 @@ def get_permission_hash(role, perm_hash, _other) :get_discussion, :get_staff_note, :get_members, - :get_groups + :get_groups, + :get_discussion_prompt ].freeze # diff --git a/app/models/discussion_prompt.rb b/app/models/discussion_prompt.rb new file mode 100644 index 000000000..7bbd14c80 --- /dev/null +++ b/app/models/discussion_prompt.rb @@ -0,0 +1,7 @@ +class DiscussionPrompt < ApplicationRecord + + belongs_to :task_definition, optional: false + belongs_to :project, optional: true + belongs_to :created_by, class_name: 'User', optional: true + +end diff --git a/app/models/project.rb b/app/models/project.rb index 578b0dfbf..030b11b52 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -73,14 +73,16 @@ def self.permissions :change_campus, :get_staff_note, :create_staff_note, - :reprocess_submission + :reprocess_submission, + :get_discussion_prompt ] # What can admins do with projects? admin_role_permissions = [ :get, :get_submission, - :reprocess_submission + :reprocess_submission, + :get_discussion_prompt ] # What can auditors do with projects? @@ -88,7 +90,8 @@ def self.permissions :get, :get_submission, :get_staff_note, - :reprocess_submission + :reprocess_submission, + :get_discussion_prompt ] # What can nil users do with projects? diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 27f08f699..652d1b4ce 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -13,7 +13,9 @@ def self.permissions :update, :upload_csv, :get_los, - :create_task_prerequisite + :create_task_prerequisite, + :get_discussion_prompt, + :create_discussion_prompt ] admin_role_permissions = [ @@ -22,12 +24,16 @@ def self.permissions :update, :upload_csv, :get_los, - :create_task_prerequisite + :create_task_prerequisite, + :get_discussion_prompt, + :create_discussion_prompt ] tutor_role_permissions = [ :get_feedback_chips, - :get_los + :get_los, + :get_discussion_prompt, + :create_discussion_prompt ] auditor_role_permissions = [ @@ -72,6 +78,8 @@ def self.permissions has_many :task_prerequisites, dependent: :destroy has_many :prerequisites, through: :task_prerequisites, source: :prerequisite + has_many :discussion_prompts, dependent: :destroy + serialize :upload_requirements, coder: JSON # Model validations/constraints @@ -463,7 +471,7 @@ def propogate_date_changes date_diff def to_csv_row TaskDefinition.csv_columns - .reject { |col| [:start_week, :start_day, :target_week, :target_day, :due_week, :due_day, :upload_requirements, :group_set, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites].include? col } + .reject { |col| [:start_week, :start_day, :target_week, :target_day, :due_week, :due_day, :upload_requirements, :group_set, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites, :discussion_prompts].include? col} .map { |column| attributes[column.to_s] } + [ group_set.nil? ? "" : group_set.name, @@ -482,6 +490,12 @@ def to_csv_row abbreviation: prereq.abbreviation, task_status_id: tp.task_status_id } + end.to_json, + discussion_prompts.map do |prompt| + { + content: prompt.content, + priority: prompt.priority + } end.to_json ] # [target_date.strftime('%d-%m-%Y')] + @@ -492,7 +506,7 @@ def self.csv_columns [:name, :abbreviation, :description, :weighting, :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] + :due_week, :due_day, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites, :discussion_prompts] end def self.task_def_for_csv_row(unit, row) @@ -554,6 +568,20 @@ def self.task_def_for_csv_row(unit, row) result.tutorial_stream = unit.tutorial_streams.where(abbreviation: row[:tutorial_stream]).first end + result.discussion_prompts.destroy_all + + if row[:discussion_prompts].present? + prompts = JSON.parse(row[:discussion_prompts]) + prompts.each do |prompt| + DiscussionPrompt.create!({ + task_definition: result, + content: prompt['content'], + priority: prompt['priority'] + }) + end + + end + result.assess_in_portfolio_only = %w(Yes y Y yes true TRUE 1).include? "#{row[:assess_in_portfolio_only]}".strip if result.valid? && (row[:group_set].blank? || result.group_set.present?) diff --git a/app/models/unit.rb b/app/models/unit.rb index 085e3968b..3e7fddadb 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -378,9 +378,9 @@ def rollover(teaching_period, start_date, end_date, new_code) end end - # Duplicate task prerequisites task_definitions.each do |td| new_td = new_unit.task_definitions.find_by(abbreviation: td.abbreviation) + # Duplicate task prerequisites td.task_prerequisites.each do |prereq| new_prerequisite_td = new_unit.task_definitions.find_by(abbreviation: prereq.prerequisite.abbreviation) TaskPrerequisite.create!( @@ -389,6 +389,15 @@ def rollover(teaching_period, start_date, end_date, new_code) task_status_id: prereq.task_status_id ) end + + # Duplicate discussion prompts + td.discussion_prompts.each do |prompt| + DiscussionPrompt.create!({ + task_definition: new_td, + content: prompt.content, + priority: prompt.priority + }) + end end # Link outcomes diff --git a/db/migrate/20251110000046_create_discussion_prompts.rb b/db/migrate/20251110000046_create_discussion_prompts.rb new file mode 100644 index 000000000..789c4101a --- /dev/null +++ b/db/migrate/20251110000046_create_discussion_prompts.rb @@ -0,0 +1,10 @@ +class CreateDiscussionPrompts < ActiveRecord::Migration[8.0] + def change + create_table :discussion_prompts do |t| + t.references :task_definition, null: false + t.text :content, null: false, limit: 4096 + t.integer :priority, default: 0 + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0e65657d5..e8d8a8f4d 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: 2025_11_02_221253) do +ActiveRecord::Schema[8.0].define(version: 2025_11_10_000046) 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 @@ -90,6 +90,15 @@ t.datetime "updated_at", null: false end + create_table "discussion_prompts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "task_definition_id", null: false + t.text "content", null: false + t.integer "priority", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["task_definition_id"], name: "index_discussion_prompts_on_task_definition_id" + end + create_table "feedback_chips", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "type" t.text "chip_text" diff --git a/test/api/discussion_prompts_api_test.rb b/test/api/discussion_prompts_api_test.rb new file mode 100644 index 000000000..c7c1db70d --- /dev/null +++ b/test/api/discussion_prompts_api_test.rb @@ -0,0 +1,179 @@ +require 'test_helper' +require 'date' +require './lib/helpers/database_populator' + +class DiscussionPromptsApiTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + include TestHelpers::TestFileHelper + + def app + Rails.application + end + + def test_dicussion_prompts_filters + # Create discussion prompts for task definition + unit = FactoryBot.create(:unit) + user = FactoryBot.create(:user, :convenor) + unit.employ_staff(user, Role.convenor) + + td1 = FactoryBot.create(:task_definition, unit: unit) + td2 = FactoryBot.create(:task_definition, unit: unit) + + assert_not_nil td1 + assert_not_nil td2 + + student1 = FactoryBot.create(:user, :student) + student2 = FactoryBot.create(:user, :student) + + project1 = unit.enrol_student(student1, nil) + project2 = unit.enrol_student(student2, nil) + + # Create global prompts (for task definitions) + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Ask the student about pointers and references...', + priority: 1 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Ask the student about passing values by reference...', + priority: 2 + }) + + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Potential use of GenAI, ask the student to explain their code...', + priority: 3 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Ask student to explain why they used std:cin over read_line()...', + priority: 4 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Ask student to explain why they used std:cin over read_line()... 1', + priority: 4 + }) + + add_auth_header_for(user: user) + + get "/api/task_definitions/#{td1.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 2, last_response_body.count + + get "/api/task_definitions/#{td2.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 3, last_response_body.count + + project1_task1 = project1.task_for_task_definition(td1) + project1_task2 = project1.task_for_task_definition(td2) + + project2_task1 = project2.task_for_task_definition(td1) + project2_task2 = project2.task_for_task_definition(td2) + + project1_task1.update!(task_status: TaskStatus.discuss) + project1_task2.update!(task_status: TaskStatus.complete) + + project2_task1.update!(task_status: TaskStatus.ready_for_feedback) + project2_task2.update!(task_status: TaskStatus.complete) + + get "/api/projects/#{project1.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 2, last_response_body.count + + project1_task2.update!(task_status: TaskStatus.discuss) + + get "/api/projects/#{project1.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 5, last_response_body.count + + get "/api/projects/#{project2.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 0, last_response_body.count + end + + def test_student_dicussion_prompts_permissions + unit = FactoryBot.create(:unit) + convenor = FactoryBot.create(:user, :convenor) + tutor = FactoryBot.create(:user, :tutor) + + unit.employ_staff(convenor, Role.convenor) + unit.employ_staff(tutor, Role.tutor) + + td = FactoryBot.create(:task_definition, unit: unit) + + student = FactoryBot.create(:user, :student) + project = unit.enrol_student(student, nil) + + users_can = [convenor, tutor] + + users_cant = [student] + + users_can.each do |user| + add_auth_header_for(user: user) + + get "/api/task_definitions/#{td.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body + + post "/api/task_definitions/#{td.id}/discussion_prompts", { + content: 'test content 123', + priority: 1 + } + assert_equal 201, last_response.status, last_response_body + + last_prompt = DiscussionPrompt.last + + assert_not_nil last_prompt + assert_equal 'test content 123', last_prompt.content + assert_equal 1, last_prompt.priority + + put "/api/task_definitions/#{td.id}/discussion_prompts/#{last_prompt.id}", { + content: 'test content 456', + priority: 2 + } + assert_equal 200, last_response.status, last_response_body + last_prompt.reload + assert_equal 'test content 456', last_prompt.content + assert_equal 2, last_prompt.priority + + delete "/api/task_definitions/#{td.id}/discussion_prompts/#{last_prompt.id}" + assert_equal 200, last_response.status, last_response_body + assert_nil DiscussionPrompt.find_by(id: last_prompt.id) + + get "/api/projects/#{project.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body + end + + users_cant.each do |user| + add_auth_header_for(user: user) + + get "/api/task_definitions/#{td.id}/discussion_prompts" + assert_equal 403, last_response.status, last_response_body + + post "/api/task_definitions/#{td.id}/discussion_prompts", { + content: 'test', + priority: 0 + } + assert_equal 403, last_response.status, last_response_body + + put "/api/task_definitions/#{td.id}/discussion_prompts/1", { + content: 'test', + priority: 0 + } + assert_equal 403, last_response.status, last_response_body + + delete "/api/task_definitions/#{td.id}/discussion_prompts/1" + assert_equal 403, last_response.status, last_response_body + + get "/api/projects/#{project.id}/discussion_prompts" + assert_equal 403, last_response.status, last_response_body + end + end + +end diff --git a/test/models/task_definition_test.rb b/test/models/task_definition_test.rb index 2d47e1f45..77f9d9ed5 100644 --- a/test/models/task_definition_test.rb +++ b/test/models/task_definition_test.rb @@ -145,7 +145,7 @@ def test_export_task_definitions_csv task_defs_csv = CSV.parse unit.task_definitions_csv, headers: true task_defs_csv.each do |task_def_csv| task_def = unit.task_definitions.find_by(abbreviation: task_def_csv['abbreviation']) - keys_to_ignore = %w[tutorial_stream start_week start_day target_week target_day due_week due_day upload_requirements task_prerequisites] + keys_to_ignore = %w[tutorial_stream start_week start_day target_week target_day due_week due_day upload_requirements task_prerequisites discussion_prompts] task_def_csv.each do |key, value| unless keys_to_ignore.include?(key) assert_equal(task_def[key].to_s, value) diff --git a/test/models/unit_model_test.rb b/test/models/unit_model_test.rb index 81ea40fec..6a54c9b28 100644 --- a/test/models/unit_model_test.rb +++ b/test/models/unit_model_test.rb @@ -301,6 +301,58 @@ def test_rollover_assess_in_portfolio unit3.destroy! end + def test_rollover_of_discussion_prompts + unit = FactoryBot.create(:unit, with_students: false, task_count: 4) + td1 = unit.task_definitions.first + td2 = unit.task_definitions.second + + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Discuss pointers and references', + priority: 1 + }) + + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Discuss object oriented programming', + priority: 2 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Discuss use of AI', + priority: 3 + }) + + unit2 = unit.rollover(TeachingPeriod.find(2), nil, nil, nil) + + new_td1 = unit2.task_definitions.first + new_td2 = unit2.task_definitions.second + + assert_equal 2, new_td1.discussion_prompts.count + assert_equal 1, new_td2.discussion_prompts.count + + new_prompt1 = new_td1.discussion_prompts.first + new_prompt2 = new_td1.discussion_prompts.second + new_prompt3 = new_td2.discussion_prompts.first + + assert_not_nil new_prompt1, "Discussion prompt should be duplicated in rollover" + assert_not_nil new_prompt2, "Discussion prompt should be duplicated in rollover" + assert_not_nil new_prompt3, "Discussion prompt should be duplicated in rollover" + + assert_equal new_prompt1.task_definition.id, new_td1.id + assert_equal new_prompt1.content, 'Discuss pointers and references' + assert_equal new_prompt1.priority, 1 + + assert_equal new_prompt2.task_definition.id, new_td1.id + assert_equal new_prompt2.content, 'Discuss object oriented programming' + assert_equal new_prompt2.priority, 2 + + assert_equal new_prompt3.task_definition.id, new_td2.id + assert_equal new_prompt3.content, 'Discuss use of AI' + assert_equal new_prompt3.priority, 3 + end + def test_rollover_of_task_prerequisites unit = FactoryBot.create(:unit, with_students: false, task_count: 4) td1 = unit.task_definitions.first diff --git a/test_files/COS10001-ImportTasksWithTutorialStream.csv b/test_files/COS10001-ImportTasksWithTutorialStream.csv index 7b6ea2e63..bc92146d6 100644 --- a/test_files/COS10001-ImportTasksWithTutorialStream.csv +++ b/test_files/COS10001-ImportTasksWithTutorialStream.csv @@ -1,37 +1,37 @@ -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 -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,[] -Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]" -Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,,,,FALSE,[] +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 +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,[],[] +Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]",[] +Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,,,,FALSE,[],[] diff --git a/test_files/COS10001-ImportTasksWithoutTutorialStream.csv b/test_files/COS10001-ImportTasksWithoutTutorialStream.csv index e0d553332..a5e4a8970 100644 --- a/test_files/COS10001-ImportTasksWithoutTutorialStream.csv +++ b/test_files/COS10001-ImportTasksWithoutTutorialStream.csv @@ -1,37 +1,37 @@ -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 -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,[] -Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,,,,,FALSE,[] -Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]" -Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,,,,,FALSE,[] +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 +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,[],[] +Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]",[] +Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,,,,,FALSE,[],[] diff --git a/test_files/COS10001-Tasks.csv b/test_files/COS10001-Tasks.csv index 3845398d8..54036fe3c 100644 --- a/test_files/COS10001-Tasks.csv +++ b/test_files/COS10001-Tasks.csv @@ -1,38 +1,38 @@ -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 -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,[] -Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,TRUE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]" -Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,FALSE,[] -Test 10,T10,Test 10 tests the import task bug,1,0,TRUE,[],10,Fri,10,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[] +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 +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,[],[] +Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,TRUE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]",[] +Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,FALSE,[],[] +Test 10,T10,Test 10 tests the import task bug,1,0,TRUE,[],10,Fri,10,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[],[] diff --git a/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv b/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv index a9e2274fd..a399ef52e 100644 --- a/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv +++ b/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv @@ -1,6 +1,6 @@ -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 -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,[] -Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,[] -Distinction Task 2,5.5D.new,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]" +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 +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,[],[] +Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,[],[] +Distinction Task 2,5.5D.new,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]",[] diff --git a/test_files/csv_test_files/COS10001-Tasks.csv b/test_files/csv_test_files/COS10001-Tasks.csv index e28c64ead..0bdc01213 100644 --- a/test_files/csv_test_files/COS10001-Tasks.csv +++ b/test_files/csv_test_files/COS10001-Tasks.csv @@ -1,5 +1,5 @@ -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 -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,[] -Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]" +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 +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,[],[] +Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]",[] diff --git a/test_files/csv_test_files/COS10001-Tasks.xlsx b/test_files/csv_test_files/COS10001-Tasks.xlsx index af83229fe082be93b9dc8ac5e1f4dfda403ad6fd..dfe25c7d0ab35f9fc60573cda218c4e3f704cc7b 100644 GIT binary patch delta 2921 zcmY*bX*|>m7aokUuh+hglm=NsS+cL$_pvVxF>;Y5G14E|#?FL@>`chs*fNMPwg`z) zNU~*Z8HT2}dw=hJm-FGA59d6e&Uv2alpE90r(u>L(tvKVfMFJUKvj5k zx!JYTcr!%G-1$j0R#AZoV0i}&W3J;M>Ua+S_gV!lsE|;Ys-XXANEae) zn|*k6Mc>00y(L}&ZyQI-n(&kh*G<=~mH& zKDR21)?WT$V@Jbzpu(vKVaBbBlNlSJUB9r+P94dT=qMXGipai7D{W7A&dGv%s?p&^ zEgRAR#p0^7?qXZ;6ebnpcu-X5#*~tFj%Cl?RBY=UU3>fB6&xpCNfSH_20S9n)m0#9oEeHN|Fv7_`oq1MJN6?sDdL4UY84s zBk3rSMkWjgh!_w1Jo(Ho$R<6=DDb3b<~VRy zt9lQ(#p^|%nF?qr(Xf#1Mn}_6 za*3FE?T+3jH8Pj1h>S1f4iUcQCQff80Sg1UI1*;z>_X?;h;Q;4M)SnX>Y3@~Rzmv# z$FUYoir9wR%Wk31)Dp5bkG>{&zqg-vCn4oB@xD>n`wP_?=QJ=`X#b3;41WpC`9lIU zrGje2D}q=Ki4nT$ok?;UiRm?TO6X4H0EElAkeR0m;`?I z)=WlyYKC4X%mN=61X{-|F+%}cV#cC8dvxP0kbGv*@MoKTU&O%T0N$HS|88?m9M8ZPHz;z9#+~p+oS?o-zDVJAXS8kHF&1JoBqX-w6SF%P(HI+Wv0TS0^~C&@;7pxycxF*0s@q?}i~zY1BHN+y zynGOIDeC>K$&9P#`(dO>!QI*8j6=+~tGkq3%qKJH3X`3Rkg1z^S%7DpdK6N%L*CCp zSH~u5++`7}`baWJ5dk32BDhRHNanF3_PWAwYp>$YSY9_W+&2nj7_QPfHOl)M=auAU z|0FR)2X2jXes$h)l1N1+ta{2e`T9{H)Q@PD(8(kt$&(>$j9ef!?OuWkG4NJs6)uC4 z!j_2ib}m#bA3xGif6zDVP-0Vbwp^@?P-2B_m@d^gwqq#vK$Pm0gZe=^_jjWUoQ*L# zd+|L>-)6!Dst*TlADatn(k$EYNNz4o+QW<7`{aef%(mNKDPEXjcd!g0B^9rhzVV`C zE%p*3!^Xmgq!yBHAJW;RWjp84_}aw%#2fSr?!xF%B~MGv2S3tpNO5s}xYt3C3RHLR zlaXSzFfzDZ4H#yUtZOv5bu7()4tH~U=@iSvX$C7Te)jxQ*lqD%R_OE{0m>(ymLKkN z5gGMU^siF_{gochnp!^4=AvEc)pgv`mDKZpUWS>Xj=sK(-)7AovcHm0N{Ol|s2*4K zOWTVH+5=B6rFy(rkL0huDfRRHY0dMU#fAWG(JcUHzhk%7|vSnqouw6Ie-!p2>`9{vZ zZxTF+s^GP6&JND1FK-Q9UMS*Q?V|Vdl^7?Sr?IM?w-=L0_@))4Cc=zRw+~#5%`g!da&DN@yJ{IfUkmP%K0YBFSTyFF?Nrs% zM7V~xxQ7pu2$09-Ls8UE?JX~-F8P{-PEq;2GY$w45YM9HQuJ;zZ%DgTfVylCk^Ix7 z1v+BO$j0`$Ax50vJO?77*-Z3v4UuQ}Mg*~DJuvQ3U$eQeM`Ah2@&b$#o6(mD22B!c zZe?jAdM=K;QmLxYy&6BuKV57*pyLq-E<&Fz`mcLJ7 zwucWoip><2Z=(I_h8v%fU#V3BS9|M3r^s@DGm8dfWr{)b&w*QRBp_N50*X|?4HYl? zroqgUkDQo*>0>zY8eFEzulq8kr^*axu#DPQEh>s)J^5ml>dCfvV8{bQBe-HJS=L&l zrchVd_N_cGWv3+@>c*vlKWrVnZbX%|D}V{LOJDr&V|{OuE)%UrN%w2+?K z?U~I&N9AosY2999+6d0!_6`7I%GV4g#gz$gPh00qTifim9Z;ts*sUJ4K`vUyS-LWH zUHx@`C0o&FBA{l;+^BnFF^zR)M3YaGlZZ=SoF(3-{Beela3b64$)483N)>#ST9+7M zq4!W-`3uqash$s@goL!OuoE|__~_{5sM|^o{k8@k?+v`3uKF9#S z203@|m}mU5A^%YN61j{jKn6>$i)v-n6*GRu)Sb~rT8 z8(dJLZ0`4P9o(QlS65v30?gl#ulX=|oyC=6zRm{1#PV~sf`#p9{{5`FL2anwFilVy zq5n7~69~ljdpG}$A!f`fvj8Rm%8r3?u>WrV6I>WgF>R_W3?K#-`VXUjn=RKr1AkW! zUQ8@6&)@J|{{Mw%V$Q_G1^-%n&~GUyqy~XF{txJgQ9GweMZrX#6NkM0Eyf_wzW{Vg BV)Xz3 delta 2882 zcmV-I3%&HJMWRKp*9HaS&!}RHliUU?e^Q7VMvxF~rFN#GOgsBjnZy*UNu0GEJ~W#D zzSjv*7*->;gTRjM>*I6pJvZj&)21w}6_bjKDnjJSK^Chrk@M;`LSJX8-A9(vw908A zDi)zFQ|Qz1&p&R~LcYy~c(VYYQW08eU56N}Y{^Qhu0+i$SXl^JQVm1-id8Kcf6djB zFQMMHWI!8{D=dMyFUq*&W-sY-O{NND6;$a-&Na&;bPYqXW+xM}smBd3V3iPuxCjpqa(<97fZUG?&16M2i$$eD zYCGz`E2|beUJ4MgHD&)aTr#K~kPx^7EzLqYR}WM#twt6R!e15mhgre5e-tNdg>%u& z^#XR;Ijak?J({33#~S>6F=0_SQ>&Mw;gMSDd1?F)z0%Bt3qy!HH@fGa!v)^RaIeV+ zEiHJyP3|Bsk8}lD0xjgbBEB_g=)F{#49Uy){={_$@pWnwLi)BBdyYL$$0OV8`K~t( zCL|ptKfoCogqfg?UhV{7f1o4eLAKA9?&)T~LY%PS`8$p8&hBHI{-vrLp=Xx0-w|_ME7vrx>U`t>Q3^kok%?m6-z8OMw z7?Ke>A(wRIQux@WIU$FS*tPFhVA=Lo)z(D(<&nS!V$Z00gI8p`f7ZqwSu!+d$=jT? zdEIZ%Xpucg%XF>bfH>~JSg?)0Q@ZUzqa0|C{r=eL4P4txNosp!04W;>p6w@T&%Y)~ zJn>VLGILLa8-uv`VEOlPyPHwnK;4*!RQ?_{AR@#*CWRtOC)XB7EWO%h&#QQ%Z!+!w)0RR63lUEuKvyciz0tsD1H4q;J z007mK-3%aqliP~hFc60C3w;Np`}mkO8xvzw@jv7Fo!&sbU)RHzzqas)#j z79T&%_eyU}1zS7M!Gdvu!3!$p#TCF1S6mP~u6JJ+Et} z=}KVThiFQfchLBa@t5>z+Wm`6k~6K0Dy@&?p!0lL{w$aUlntcI`ftKS(?G+Hy9*pq z@%t=)MRx-$9%0ElVd(&K?b6wn7diPCo<0+NMs5kmr9XQki+REEhFw*jhB7CgqU<#$ z!D8Od&o2&pa~6A+u3kU@vw%4w;#t*vfzm#L5ID!U@mDGN!l;0+ZX$6r3USum6t9%p zINmcVLriR1j!R6LtKb6VCo1iqTRla=}TI;O$?NU_Uzv27R8XpCoaZM6QZ!T10nR40>&U zJT~B(6vwEOVsMg1_qR!>y%d99n)>%QImUvzv>5b`qcQCYj9qefQ`ZZ}d+ab6j26bG zUFQVtUH=CF0RR6000960l+;U4!ax)S;J?yzr3)<`9x53o0owAgynW%W))H4}XjApy zJ7fr*b3%;npx@k^&Y8KD`CFD1K~f}tO77l2on6QET_^uYwz>Y*7A@EL@*7Foe*F%z zJk7R6$E{roSIKn0wyPKGC%OJ#s+#mlCB5`uJ==V=zHI_6u&s_3+SWjeY-^$oY-^zn zZM#An*>;UKw(SP(7WPiT#DJ8{1dyVc0#Y_JKniCLNa-v9DV`-D<+B2$fbIZ)DWQ8C z1ts(VNC`axQbJEQ)~W3pklH>2R&6~!7RT8T|CtTF_&6Ja4yy-9ht&t9!wUApS1Q_f zXr9uBBh%x%4(ChIMeVe)#A5k*ROe3yfhWbhBbWx>*y`@PbZh3N>`$ zW~PQp=1@Y{aB(Q1<5~Vzv#wGKu|Im|qIlH@^#`+g5cCEK(%Qco@Bsh-os)JKAAb`; z480@qA4YSAZb}hCRk9VLaDxj)94a(&cAbrt`O4Un?F#YVnKZq0C-i7P``LbD&+_KM zS#`j~6#PnGUM#di--Vw1u+l&8zP|ma6$|(tZ1A|!6SBT(U%y&1iz>OES9&WFubYOu zEjr+f5YZQ86OscdW*Qof38CjLqJKEsyjv_jG!DpX)rIV3rLW#=l|Ah2}G~ zE~WM0aM?(^Y>xKH2kUjh1A&yT=U9*2C5q>u5oy>;8Eu{;kOmYpBa8o= zWzfUgVkId*f#aMDw33#FcYr|MnJP?laPQK%)?tpjd7B}j$LcjdF zyREzlQ+6tg+T?*mFc6Ahyv1Fb)o2V5sKGx`qbuV&Qra#Mq^)Y+TEM-F4-o;2jB`0JLx|njHV7 z^zCUl@V@nO;u+)Uoco}%`Xm{++p{65ALw;*|FB-5GL6$|n8aZ+U8iXl&$8+5dw=hx zfDc0uS?yirCCG5I@}Z0?Z{YH8D!-Fa8Wpn)9aIAaOV0D-SuV*mgE diff --git a/test_files/unit_csv_imports/import_group_tasks.csv b/test_files/unit_csv_imports/import_group_tasks.csv index d2ab3d948..2b576cfeb 100644 --- a/test_files/unit_csv_imports/import_group_tasks.csv +++ b/test_files/unit_csv_imports/import_group_tasks.csv @@ -1,3 +1,3 @@ -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,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites -Group Import 1,1GI,Test Description - Import,16,0,FALSE,0,FALSE,80,[],Group Work,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[] -Missing Group,2GI,Test Description - Import FAIL,16,0,FALSE,0,FALSE,80,[],Group Work1,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[] +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,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 +Group Import 1,1GI,Test Description - Import,16,0,FALSE,0,FALSE,80,[],Group Work,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[],[] +Missing Group,2GI,Test Description - Import FAIL,16,0,FALSE,0,FALSE,80,[],Group Work1,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[],[] From 18d439c3c26ef84a950dad76f2eddb05639cff10 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:57:53 +1100 Subject: [PATCH 10/46] refactor: overseer improvements (#554) * refactor: queue overseer job after pdf gen completes * chore: truncate long overseer assessment comments * refactor: decode text if its base64 encoded * fix: use unique work dir name * refactor: ensure submission file is renamed to match upload requirement * fix: return error if overseer output contains syntax error * chore: add base64 check method * refactor: add base64 helper * chore: add newlines --- app/api/submission/portfolio_evidence_api.rb | 32 +++++++++++--------- app/helpers/base64_helper.rb | 7 +++++ app/models/overseer_assessment.rb | 20 ++++++++++-- app/sidekiq/accept_overseer_job.rb | 20 ++++++++---- app/sidekiq/accept_submission_job.rb | 12 ++++++++ 5 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 app/helpers/base64_helper.rb diff --git a/app/api/submission/portfolio_evidence_api.rb b/app/api/submission/portfolio_evidence_api.rb index 2316277be..c6d0a67fa 100644 --- a/app/api/submission/portfolio_evidence_api.rb +++ b/app/api/submission/portfolio_evidence_api.rb @@ -6,6 +6,8 @@ class PortfolioEvidenceApi < Grape::API helpers AuthenticationHelpers helpers AuthorisationHelpers helpers FileStreamHelper + helpers Base64Helper + include LogHelper def self.logger @@ -98,18 +100,6 @@ def self.logger # Copy files to be PDFed task.accept_submission(current_user, scoop_files(params, upload_reqs), self, params[:contributions], trigger, alignments, accepted_tii_eula: params[:accepted_tii_eula]) - if task.overseer_enabled? - overseer_assessment = OverseerAssessment.create_for(task) - if overseer_assessment.present? - logger.info "Launching Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id}" - - overseer_assessment.send_to_overseer - - - else - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed #{overseer_assessment.inspect}" - end - end present task, with: Entities::TaskEntity, update_only: true end @@ -244,9 +234,16 @@ def self.logger end result = [] - yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + begin + yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + rescue Psych::SyntaxError => e + error!({ error: "Failed to parse overseer output: #{e.message}" }, 401) + end yaml_data.each do |key, value| + if base64?(value) + value = Base64.decode64(value) + end result << { label: key, result: value } end @@ -292,9 +289,16 @@ def self.logger end result = [] - yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + begin + yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + rescue Psych::SyntaxError => e + error!({ error: "Failed to parse overseer output: #{e.message}" }, 401) + end yaml_data.each do |key, value| + if base64?(value) + value = Base64.decode64(value) + end result << { label: key, result: value } end diff --git a/app/helpers/base64_helper.rb b/app/helpers/base64_helper.rb new file mode 100644 index 000000000..8222ce6f5 --- /dev/null +++ b/app/helpers/base64_helper.rb @@ -0,0 +1,7 @@ +module Base64Helper + def base64?(value) + value.is_a?(String) && Base64.strict_encode64(Base64.decode64(value)) == value + rescue ArgumentError + false + end +end diff --git a/app/models/overseer_assessment.rb b/app/models/overseer_assessment.rb index eab52e485..f15063505 100644 --- a/app/models/overseer_assessment.rb +++ b/app/models/overseer_assessment.rb @@ -199,12 +199,22 @@ def update_from_output(work_dir_path) comment_txt = '' if !yaml_file['build_message'].nil? && !yaml_file['build_message'].strip.empty? comment_txt += "Build output:\n" - comment_txt += yaml_file['build_message'] + comment_txt += if base64?(yaml_file['run_message']) + Base64.urlsafe_decode64(yaml_file['build_message']) + else + yaml_file['run_message'] + end + comment_txt += "\n" end if !yaml_file['run_message'].nil? && !yaml_file['run_message'].strip.empty? comment_txt += "\n" unless comment_txt.empty? comment_txt += "Execution output:\n" - comment_txt += yaml_file['run_message'] + comment_txt += if base64?(yaml_file['run_message']) + Base64.urlsafe_decode64(yaml_file['run_message']) + else + yaml_file['run_message'] + end + comment_txt += "\n" end if !yaml_file['message'].nil? && !yaml_file['message'].strip.empty? @@ -214,7 +224,7 @@ def update_from_output(work_dir_path) end if comment_txt.present? - update_assessment_comment(comment_txt) + update_assessment_comment(comment_txt[0, 4000]) # Truncate to 4000 characters else puts 'YAML file doesn\'t contain field `build_message` or `run_message`' end @@ -245,5 +255,9 @@ def update_from_output(work_dir_path) def delete_associated_files FileUtils.rm_rf output_path end + + def base64?(value) + value.is_a?(String) && Base64.strict_encode64(Base64.decode64(value)) == value + end end # rubocop:enable Rails/Output diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index a40fdec5a..d5d11c1ed 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -19,11 +19,13 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment at(0) total(1) - work_dir = Rails.root.join("tmp", "overseer", task_id.to_s) - FileUtils.mkdir_p(work_dir) - task = Task.find(task_id) + work_dir_name = "#{task.id}-#{overseer_assessment_id}" + + work_dir = Rails.root.join("tmp", "overseer", work_dir_name) + FileUtils.mkdir_p(work_dir) + raise "PDF is still compiling" if task.processing_pdf? || !task.has_done_file? raise "Submission file not found: #{submission}" unless File.exist?(submission) @@ -34,9 +36,15 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment next if entry.name_is_directory? parts = entry.name.split('/')[1..] - next unless parts + next unless parts.length >= 1 + + file_name = parts.first + index = file_name.to_i + + file = task.upload_requirements[index] + final_name = file['name'] - dest_path = File.join(work_dir, *parts) + dest_path = File.join(work_dir, final_name) FileUtils.mkdir_p(File.dirname(dest_path)) zip_file.extract(entry, dest_path) { true } end @@ -83,7 +91,7 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment #{volume_mount} \ --name #{container_name} \ #{docker_image_name_tag} \ - bash -c "cd /overseer/work-dir/#{task_id} && ./run.sh" + bash -c "cd /overseer/work-dir/#{work_dir_name} && ./run.sh" ) system(command) diff --git a/app/sidekiq/accept_submission_job.rb b/app/sidekiq/accept_submission_job.rb index b843f66cf..90c20026d 100644 --- a/app/sidekiq/accept_submission_job.rb +++ b/app/sidekiq/accept_submission_job.rb @@ -49,6 +49,18 @@ def perform(task_id, user_id, accepted_tii_eula) if TurnItIn.enabled? task.send_documents_to_tii(user, accepted_tii_eula: accepted_tii_eula) end + + if task.overseer_enabled? + overseer_assessment = OverseerAssessment.create_for(task) + if overseer_assessment.present? + logger.info "Launching Overseer assessment for task_def_id: #{task.task_definition.id} task_id: #{task.id}" + + overseer_assessment.send_to_overseer + + else + logger.info "Overseer assessment for task_def_id: #{task.task_definition.id} task_id: #{task.id} was not performed #{overseer_assessment.inspect}" + end + end rescue StandardError => e # to raise error message to avoid unnecessary retry logger.error e task.clear_in_process From 42389bebd1ed04508b86aadb69964d0a84353469 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:58:05 +1100 Subject: [PATCH 11/46] chore(release): 10.0.0-59 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8918cfc..82984b6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-59](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-58...v10.0.0-59) (2025-12-03) + + +### Features + +* discussion prompts ([#546](https://github.com/b0ink/doubtfire-deploy/issues/546)) ([ab38360](https://github.com/b0ink/doubtfire-deploy/commit/ab38360a5c847a1db0d03547fc432b000a61ebb9)) + ## [10.0.0-58](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-57...v10.0.0-58) (2025-11-25) From 14b91e8ed67e5f54e853c53ffc8a887468a85982 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:17:26 +1100 Subject: [PATCH 12/46] chore: remove work dir only on success --- app/sidekiq/accept_overseer_job.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index d5d11c1ed..390527a20 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -104,10 +104,9 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment if File.exist?(yaml_path) path = FileHelper.task_submission_identifier_path_with_timestamp(:done, task, timestamp) FileUtils.cp(yaml_path, path) + FileUtils.rm_rf(work_dir) end - FileUtils.rm_rf(work_dir) - logger.info "Completed overseer job" rescue StandardError => e logger.error e From 7810a098e9f2782b1b32359896ae42c8f2e49a5b Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:17:57 +1100 Subject: [PATCH 13/46] chore: set script permissions in overseer container --- app/sidekiq/accept_overseer_job.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index 390527a20..20fb44fe5 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -70,7 +70,6 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment run_sh_path = File.join(work_dir, 'run.sh') File.write(run_sh_path, script_contents) - system("chmod +x #{work_dir}/run.sh") mount = Doubtfire::Application.config.overseer_workdir_volume_mount volume_mount = if mount.nil? @@ -91,7 +90,7 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment #{volume_mount} \ --name #{container_name} \ #{docker_image_name_tag} \ - bash -c "cd /overseer/work-dir/#{work_dir_name} && ./run.sh" + bash -c "cd /overseer/work-dir/#{work_dir_name} && chmod +x ./run.sh && ./run.sh" ) system(command) From 69db8e97074c32ea083fef8ed856fd36e2756968 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:18:09 +1100 Subject: [PATCH 14/46] chore: mount correct work dir --- app/sidekiq/accept_overseer_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index 20fb44fe5..261b803bf 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -78,7 +78,7 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment # used in production, as it breaks isolation between tasks. "--volumes-from #{Doubtfire::Application.config.overseer_fallback_volume_container}" else - "-v #{mount}/#{task_id}:/overseer/work-dir/#{task_id}" + "-v #{mount}/#{task_id}:/overseer/work-dir/#{work_dir_name}" end container_name = "overseer-#{task_id}-#{timestamp}" From d3d60d65ac9bc670723afe6747b96b2b73e71e74 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:18:21 +1100 Subject: [PATCH 15/46] chore(release): 10.0.0-60 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82984b6e1..8486171d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-12-03) + ## [10.0.0-59](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-58...v10.0.0-59) (2025-12-03) From f6b70077ffcc793cd7a3fe54707ad6bbb84a6570 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:28:41 +1100 Subject: [PATCH 16/46] Revert "chore: set script permissions in overseer container" This reverts commit 7810a098e9f2782b1b32359896ae42c8f2e49a5b. --- app/sidekiq/accept_overseer_job.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index 261b803bf..d182b7a38 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -70,6 +70,7 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment run_sh_path = File.join(work_dir, 'run.sh') File.write(run_sh_path, script_contents) + system("chmod +x #{work_dir}/run.sh") mount = Doubtfire::Application.config.overseer_workdir_volume_mount volume_mount = if mount.nil? @@ -90,7 +91,7 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment #{volume_mount} \ --name #{container_name} \ #{docker_image_name_tag} \ - bash -c "cd /overseer/work-dir/#{work_dir_name} && chmod +x ./run.sh && ./run.sh" + bash -c "cd /overseer/work-dir/#{work_dir_name} && ./run.sh" ) system(command) From e61c669dabb665ef690bb14501a1af7e368cd576 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:30:10 +1100 Subject: [PATCH 17/46] chore(release): 10.0.0-61 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8486171d8..ddfdd10e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-61](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-60...v10.0.0-61) (2025-12-03) + ## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-12-03) ## [10.0.0-59](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-58...v10.0.0-59) (2025-12-03) From 84b08d8c3e6a18e38585792c873fa174e9bd338a Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:40:33 +1100 Subject: [PATCH 18/46] fix: set correct mount --- app/sidekiq/accept_overseer_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index d182b7a38..f812efa5f 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -79,7 +79,7 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment # used in production, as it breaks isolation between tasks. "--volumes-from #{Doubtfire::Application.config.overseer_fallback_volume_container}" else - "-v #{mount}/#{task_id}:/overseer/work-dir/#{work_dir_name}" + "-v #{mount}/#{work_dir_name}:/overseer/work-dir/#{work_dir_name}" end container_name = "overseer-#{task_id}-#{timestamp}" From 65c9d3f33f5a68b22f36d17c39328b702baf8232 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:40:41 +1100 Subject: [PATCH 19/46] chore(release): 10.0.0-62 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddfdd10e0..ceb8663fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-62](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-61...v10.0.0-62) (2025-12-03) + + +### Bug Fixes + +* set correct mount ([84b08d8](https://github.com/b0ink/doubtfire-deploy/commit/84b08d8c3e6a18e38585792c873fa174e9bd338a)) + ## [10.0.0-61](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-60...v10.0.0-61) (2025-12-03) ## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-12-03) From 399d9efdbad629d82db82fb6389d5f8037622efb Mon Sep 17 00:00:00 2001 From: SteveDala <131694957+SteveDala@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:47:47 +1000 Subject: [PATCH 20/46] docs: fix typo in API root, add doc_version (#555) Co-authored-by: Steven Dalamaras --- 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 6f82404da..da3bd45fa 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -155,10 +155,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 e106eebf331bd7f1b12ff1a65fa590d4fba8d9e9 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:48:04 +1100 Subject: [PATCH 21/46] feat: add basic health endpoint (#553) --- config/routes.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 60479c128..ea52a7900 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,4 +9,6 @@ mount ApiRoot => '/' mount GrapeSwaggerRails::Engine => '/api/docs' mount Sidekiq::Web => "/sidekiq" # mount Sidekiq::Web in your Rails app + + get "health" => "rails/health#show", as: :rails_health_check end From ec98b044465d2ee3afd434439c4264aeb85af6e5 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:54:07 +1100 Subject: [PATCH 22/46] chore(release): 10.0.0-63 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb8663fb..e10fc48c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-63](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-62...v10.0.0-63) (2025-12-03) + + +### Features + +* add basic health endpoint ([#553](https://github.com/b0ink/doubtfire-deploy/issues/553)) ([e106eeb](https://github.com/b0ink/doubtfire-deploy/commit/e106eebf331bd7f1b12ff1a65fa590d4fba8d9e9)) + ## [10.0.0-62](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-61...v10.0.0-62) (2025-12-03) From e8a1df571ef45228f73dbeb7db38a21c79a59251 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:20:00 +1100 Subject: [PATCH 23/46] chore: silence healthcheck requests --- config/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/application.rb b/config/application.rb index 2ce14ea91..19ea42155 100644 --- a/config/application.rb +++ b/config/application.rb @@ -27,6 +27,8 @@ class Application < Rails::Application # Load .env variables Dotenv::Rails.load + config.silence_healthcheck_path = "/health" + # ==> Authentication Method # Authentication method default is database, but possible settings # are: database, ldap, aaf, or saml. It can be overridden using the DF_AUTH_METHOD From 0ee67c3cac3f852e5488c3605dcdb634d0ced96d Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:20:13 +1100 Subject: [PATCH 24/46] chore(release): 10.0.0-64 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e10fc48c0..35f618415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-64](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-63...v10.0.0-64) (2025-12-03) + ## [10.0.0-63](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-62...v10.0.0-63) (2025-12-03) From 88e0c7a9e65a71aed0b9a8453d75af0aa52a09f4 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:26:34 +1100 Subject: [PATCH 25/46] feat: attention required task status (#557) * feat: init additional discuss task status * refactor: reveal discuss check status * refactor: add discuss check status * test: test for new discuss check status * refactor: rename to off track * refactor: rename to attention required * refactor: update status colors * refactor: rename to attention required * chore: add migration to create attention required status * chore: remove unused code * chore: remove newline --- app/api/discussion_prompts_api.rb | 2 +- app/api/submission/portfolio_evidence_api.rb | 1 + app/mailers/notifications_mailer.rb | 2 +- app/models/task.rb | 2 +- app/models/task_status.rb | 10 +++++++++- app/models/unit.rb | 2 +- app/models/unit_role.rb | 2 +- .../20251212010033_add_attention_required_status.rb | 7 +++++++ db/schema.rb | 2 +- lib/tasks/init.rake | 3 ++- test/models/task_status_test.rb | 4 ++-- vendor/assets/stylesheets/doubtfire-coverpage.css | 6 +++++- 12 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20251212010033_add_attention_required_status.rb diff --git a/app/api/discussion_prompts_api.rb b/app/api/discussion_prompts_api.rb index 799f921bf..77e9aa498 100644 --- a/app/api/discussion_prompts_api.rb +++ b/app/api/discussion_prompts_api.rb @@ -108,7 +108,7 @@ class DiscussionPromptsApi < Grape::API error!({ error: 'You do not have permission to access this project' }, 403) end - tasks_to_discuss = project.tasks.where(task_status: [TaskStatus.discuss, TaskStatus.demonstrate]) + tasks_to_discuss = project.tasks.where(task_status: [TaskStatus.discuss, TaskStatus.attention_required, TaskStatus.demonstrate]) task_definition_ids = tasks_to_discuss.pluck(:task_definition_id) result = DiscussionPrompt diff --git a/app/api/submission/portfolio_evidence_api.rb b/app/api/submission/portfolio_evidence_api.rb index c6d0a67fa..cf17a4256 100644 --- a/app/api/submission/portfolio_evidence_api.rb +++ b/app/api/submission/portfolio_evidence_api.rb @@ -22,6 +22,7 @@ def self.logger ready_for_feedback: 1, assess_in_portfolio: 1, discuss: 2, + attention_required: 0, demonstrate: 2, complete: 3 }.freeze diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 433f2e7eb..748d5bd02 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -61,7 +61,7 @@ def weekly_student_summary(project, summary_stats, did_revert_to_pass) @student_engagements = @engagements.select { |e| [TaskStatus.not_started.name, TaskStatus.need_help.name, TaskStatus.working_on_it.name, TaskStatus.ready_for_feedback.name].include? e.engagement }.count - @staff_engagements = @engagements.select { |e| [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.demonstrate.name, TaskStatus.fail.name].include? e.engagement }.count + @staff_engagements = @engagements.select { |e| [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.attention_required.name, TaskStatus.demonstrate.name, TaskStatus.fail.name].include? e.engagement }.count @task_states = project.tasks.joins(:task_status).select("count(tasks.id) as number, task_statuses.name as status").group("task_statuses.name") diff --git a/app/models/task.rb b/app/models/task.rb index 1c11c8f52..92644d42e 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -542,7 +542,7 @@ def trigger_transition(trigger: '', by_user: nil, bulk: false, group_transition: # Can only be graded if task_def is not assess_in_portfolio_only if task_definition.max_quality_pts > 0 case status - when TaskStatus.complete, TaskStatus.discuss, TaskStatus.demonstrate + when TaskStatus.complete, TaskStatus.discuss, TaskStatus.demonstrate, TaskStatus.attention_required update(quality_pts: quality) end end diff --git a/app/models/task_status.rb b/app/models/task_status.rb index 4e70314c6..b4dbac8cd 100644 --- a/app/models/task_status.rb +++ b/app/models/task_status.rb @@ -68,6 +68,10 @@ def self.assess_in_portfolio TaskStatus.find(13) end + def self.attention_required + TaskStatus.find(14) + end + class << self # Provide access to the count from the database via a new db_count method alias_method :db_count, :count @@ -80,7 +84,7 @@ class << self # Keep this hard coded! Saves cache load time. # Important: count must equal the largest id in the database def self.count - 13 + 14 end def self.status_for_name(name) @@ -111,6 +115,8 @@ def self.status_for_name(name) TaskStatus.time_exceeded when 'assess in portfolio', 'assess_in_portfolio', 'aip' TaskStatus.assess_in_portfolio + when 'attention required', 'attention_required', 'ar' + TaskStatus.attention_required else nil end @@ -135,6 +141,7 @@ def self.id_to_key(id) when 11 then :fail when 12 then :time_exceeded when 13 then :assess_in_portfolio + when 14 then :attention_required else :not_started end end @@ -153,6 +160,7 @@ def status_key return :feedback_exceeded if self == TaskStatus.feedback_exceeded return :time_exceeded if self == TaskStatus.time_exceeded return :assess_in_portfolio if self == TaskStatus.assess_in_portfolio + return :attention_required if self == TaskStatus.attention_required return :not_started end diff --git a/app/models/unit.rb b/app/models/unit.rb index 3e7fddadb..077fc5bae 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -2117,7 +2117,7 @@ def get_all_tasks_for(user, my_tutorials_only = false) # def tasks_awaiting_feedback(user) get_all_tasks_for(user) - .where('task_statuses.id IN (:ids)', ids: [TaskStatus.discuss, TaskStatus.redo, TaskStatus.demonstrate, TaskStatus.fix_and_resubmit]) + .where('task_statuses.id IN (:ids)', ids: [TaskStatus.discuss, TaskStatus.attention_required, TaskStatus.redo, TaskStatus.demonstrate, TaskStatus.fix_and_resubmit]) .order('task_definition_id') end diff --git a/app/models/unit_role.rb b/app/models/unit_role.rb index c6ee5de25..7a0050786 100644 --- a/app/models/unit_role.rb +++ b/app/models/unit_role.rb @@ -127,7 +127,7 @@ def populate_summary_stats(summary_stats, tutorial_stream, tutorial, row) data[:engagements] = all_engagements data[:total_staff_engagements] = all_engagements.count - data[:staff_engagements] = weekly_engagements.where(engagement: [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.demonstrate.name, TaskStatus.fail.name]) + data[:staff_engagements] = weekly_engagements.where(engagement: [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.attention_required.name, TaskStatus.demonstrate.name, TaskStatus.fail.name]) # Weekly task engagements for this tutorial data[:weekly_engagements_count] = weekly_engagements.count diff --git a/db/migrate/20251212010033_add_attention_required_status.rb b/db/migrate/20251212010033_add_attention_required_status.rb new file mode 100644 index 000000000..ca88e635c --- /dev/null +++ b/db/migrate/20251212010033_add_attention_required_status.rb @@ -0,0 +1,7 @@ +class AddAttentionRequiredStatus < ActiveRecord::Migration[8.0] + def change + if TaskStatus.where(name: 'Attention Required').count < 1 + TaskStatus.create name: "Attention Required", description: "This task needs to be discussed with your tutor so that you can get back on track." + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e8d8a8f4d..d2a328042 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: 2025_11_10_000046) do +ActiveRecord::Schema[8.0].define(version: 2025_12_12_010033) 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 diff --git a/lib/tasks/init.rake b/lib/tasks/init.rake index 6a4c4bbc9..2023e509a 100644 --- a/lib/tasks/init.rake +++ b/lib/tasks/init.rake @@ -41,7 +41,8 @@ namespace :db do Demonstrate: "Your work looks good, demonstrate it to your tutor to complete.", Fail: "You did not successfully demonstrate the required learning in this task.", "Time Exceeded": "You did not submit or complete the task before the appropriate deadline.", - "Assess in Portfolio": "This task will not be signed off as complete by your tutor, and will be marked directly in your portfolio." + "Assess in Portfolio": "This task will not be signed off as complete by your tutor, and will be marked directly in your portfolio.", + "Attention Required": "This task needs to be discussed with your tutor so that you can get back on track." } statuses.each do |name, desc| print "." diff --git a/test/models/task_status_test.rb b/test/models/task_status_test.rb index eab479a33..e5081c59d 100644 --- a/test/models/task_status_test.rb +++ b/test/models/task_status_test.rb @@ -508,11 +508,11 @@ def test_status_for_name end def test_staff_assigned_statuses - assert_equal TaskStatus.staff_assigned_statuses.count, 9 # number of staff tasks + assert_equal TaskStatus.staff_assigned_statuses.count, 10 # number of staff tasks end def test_id_to_key_not_started - assert_equal TaskStatus.id_to_key(14), :not_started + assert_equal TaskStatus.id_to_key(15), :not_started end end diff --git a/vendor/assets/stylesheets/doubtfire-coverpage.css b/vendor/assets/stylesheets/doubtfire-coverpage.css index d0153fa57..ba3974885 100644 --- a/vendor/assets/stylesheets/doubtfire-coverpage.css +++ b/vendor/assets/stylesheets/doubtfire-coverpage.css @@ -76,12 +76,16 @@ button.task-status.discuss { background-color: #31b0d5; color: white; } +button.task-status.attention-required { + background-color: #f1814d; + color: white; +} button.task-status.complete { background-color: #5BB75B; background-image: linear-gradient(#62c462, #51a351); color: white; } button.task-status.assess-in-portfolio { - background-color: #91b891; + background-color: #f2d85c; color: white; } From d2e49bfd57e69281923458b8853226a85c9da390 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:01:44 +1100 Subject: [PATCH 26/46] feat: overseer pipeline (#559) * feat: init overseer step pipeline * feat: add overseer step model * refactor: correctly expose sorted overseer steps * feat: expose list of overseer resource files * chore: remove name of zip file from path * feat: allow partial diff check * refactor: rerun migration * chore: remove duplicate overseer run * fix: ignore top level zip name * feat: delete overseer step * feat: overseer step result model * refactor: init refactor of new overseer step workflow * chore: overseer step result relationships * chore: remove byebug * refactor: add assessment relationship * chore: remove newlines from output * fix: set correct status * refactor: store feedback message in step result * feat: expose minimal overseer step results in overseer assessment comments * refactor: filter out overseer steps from students * refactor: dont expose step results and add endpoint to fetch them * chore: expose assessment enabled to students * fix: set timeout on overseer run script * chore: allow for empty output file * refactor: expose overseer stdout and stdin results if enabled * chore: retrieve the last set task status * refactor: break logic into separate methods * refactor: allow test submissions while overseer is disabled for a task * refactor: combine scheme changes into single migration * refactor: use seconds for overseer timeout * chore: pass in test submission argument * chore: disable execution script endpoint * refactor: clean up overseer job * refactor: only update status if halting on step * chore: remove todo * refactor: add overseer step permissions * feat: add overseer step validation * fix: get correct models * chore: remove todo * refactor: expose statuses to staff only --- app/api/api_root.rb | 2 + .../entities/overseer_assessment_entity.rb | 3 + app/api/entities/overseer_step_entity.rb | 47 ++++ .../entities/overseer_step_result_entity.rb | 30 +++ app/api/entities/task_definition_entity.rb | 10 +- app/api/overseer_steps_api.rb | 200 ++++++++++++++ app/api/task_definitions_api.rb | 82 +++--- app/models/comments/assessment_comment.rb | 3 + app/models/overseer_assessment.rb | 25 +- app/models/overseer_step.rb | 6 + app/models/overseer_step_result.rb | 5 + app/models/task.rb | 6 +- app/models/task_definition.rb | 27 +- app/sidekiq/accept_overseer_job.rb | 243 ++++++++++++++---- app/sidekiq/accept_submission_job.rb | 8 +- .../20251218031455_create_overseer_steps.rb | 70 +++++ db/schema.rb | 50 +++- test/factories/overseer_steps.rb | 5 + test/models/overseer_step_test.rb | 7 + 19 files changed, 718 insertions(+), 111 deletions(-) create mode 100644 app/api/entities/overseer_step_entity.rb create mode 100644 app/api/entities/overseer_step_result_entity.rb create mode 100644 app/api/overseer_steps_api.rb create mode 100644 app/models/overseer_step.rb create mode 100644 app/models/overseer_step_result.rb create mode 100644 db/migrate/20251218031455_create_overseer_steps.rb create mode 100644 test/factories/overseer_steps.rb create mode 100644 test/models/overseer_step_test.rb diff --git a/app/api/api_root.rb b/app/api/api_root.rb index da3bd45fa..2817baa83 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -103,6 +103,7 @@ class ApiRoot < Grape::API mount WebcalPublicApi mount MarkingSessionsApi mount DiscussionPromptsApi + mount OverseerStepsApi mount Feedback::FeedbackChipApi @@ -152,6 +153,7 @@ class ApiRoot < Grape::API AuthenticationHelpers.add_auth_to Feedback::FeedbackChipApi AuthenticationHelpers.add_auth_to MarkingSessionsApi AuthenticationHelpers.add_auth_to DiscussionPromptsApi + AuthenticationHelpers.add_auth_to OverseerStepsApi add_swagger_documentation \ base_path: nil, diff --git a/app/api/entities/overseer_assessment_entity.rb b/app/api/entities/overseer_assessment_entity.rb index b8402dcf5..2d26ab47c 100644 --- a/app/api/entities/overseer_assessment_entity.rb +++ b/app/api/entities/overseer_assessment_entity.rb @@ -7,5 +7,8 @@ class OverseerAssessmentEntity < Grape::Entity expose :status expose :created_at expose :updated_at + + expose :total_steps + expose :passed_steps end end diff --git a/app/api/entities/overseer_step_entity.rb b/app/api/entities/overseer_step_entity.rb new file mode 100644 index 000000000..b230b577c --- /dev/null +++ b/app/api/entities/overseer_step_entity.rb @@ -0,0 +1,47 @@ +module Entities + class OverseerStepEntity < Grape::Entity + expose :id + expose :task_definition_id + + def staff?(my_role) + Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil? + end + + expose :name, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :description, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :display_name + expose :display_description + + expose :run_command, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :timeout, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :sort_order, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :step_type + expose :partial_output_diff, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :stdin_input_file, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :expected_output_file, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :feedback_message, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :status_on_success, + if: ->(_obj, options) { staff?(options[:my_role]) } do |overseer_step| + TaskStatus.find_by(id: overseer_step.status_on_success_id)&.status_key || 'no_change' + end + + expose :status_on_failure, + if: ->(_obj, options) { staff?(options[:my_role]) } do |overseer_step| + TaskStatus.find_by(id: overseer_step.status_on_failure_id)&.status_key || 'no_change' + end + + expose :halt_on_success, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :halt_on_failure, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :show_expected_output, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :show_stdin, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :show_stdout, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :enabled + end +end diff --git a/app/api/entities/overseer_step_result_entity.rb b/app/api/entities/overseer_step_result_entity.rb new file mode 100644 index 000000000..eeebd0caa --- /dev/null +++ b/app/api/entities/overseer_step_result_entity.rb @@ -0,0 +1,30 @@ +module Entities + class OverseerStepResultEntity < Grape::Entity + + def staff?(my_role) + Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil? + end + + expose :id + expose :overseer_step_id + expose :exit_status + expose :pass + expose :feedback_message + + expose :stdout, if: lambda { |result, options| + staff?(options[:my_role]) || result.overseer_step&.show_stdout + } + + expose :stdin, if: lambda { |result, options| + staff?(options[:my_role]) || result.overseer_step&.show_stdin + } + + expose :expected_output, if: lambda { |result, options| + staff?(options[:my_role]) || result.overseer_step&.show_expected_output + } + + expose :stdout_sha256 + expose :stdin_sha256 + expose :expected_output_sha256 + end +end diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index 3aa2afd6a..fda5e31ce 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -50,7 +50,8 @@ def staff?(my_role) expose :is_graded expose :max_quality_pts 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, if: ->(unit, options) { staff?(options[:my_role]) } + expose :assessment_enabled expose :similarity_language, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false expose :assess_in_portfolio_only expose :use_resources_for_jplag_base_code, if: ->(unit, options) { staff?(options[:my_role]) } @@ -61,5 +62,12 @@ def staff?(my_role) expose :discussion_prompts_count do |task_def| task_def.discussion_prompts.size end + + # expose :overseer_steps, using: OverseerStepEntity, if: ->(unit, options) { staff?(options[:my_role]) } + expose :overseer_steps, using: OverseerStepEntity do |task_def, options| + task_def.overseer_steps # options[:my_role] is still available inside the entity + end + expose :overseer_resource_files, if: ->(task_def, options) { staff?(options[:my_role]) } + end end diff --git a/app/api/overseer_steps_api.rb b/app/api/overseer_steps_api.rb new file mode 100644 index 000000000..9b695b6dd --- /dev/null +++ b/app/api/overseer_steps_api.rb @@ -0,0 +1,200 @@ +require 'grape' + +class OverseerStepsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers SidekiqHelper + + before do + authenticated? + end + + desc 'Add an overseer step' + params do + requires :overseer_step, type: Hash do + requires :name, type: String + optional :description, type: String + optional :display_name, type: String + optional :display_description, type: String + optional :run_command, type: String + optional :timeout, type: Integer + # TODO: rename to execution_order || exec_order? + optional :sort_order, type: Integer + optional :partial_output_diff, type: Boolean + requires :step_type, type: String + optional :stdin_input_file, type: String + optional :expected_output_file, type: String + optional :feedback_message, type: String + optional :status_on_success, type: String + optional :status_on_failure, type: String + optional :halt_on_success, type: Boolean + optional :halt_on_failure, type: Boolean + optional :show_expected_output, type: Boolean + optional :show_stdin, type: Boolean + optional :show_stdout, type: Boolean + optional :enabled, type: Boolean + end + requires :task_def_id, type: Integer + end + post '/units/:unit_id/task_definitions/:task_def_id/overseer_steps' do + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) + end + + task_definition = TaskDefinition.find(params[:task_def_id]) + + unless authorise? current_user, task_definition, :manage_overseer_steps + error!({ error: 'Not authorised to manage overseer for this task definition' }, 403) + end + + status_on_success_param = params[:overseer_step][:status_on_success] + status_on_failure_param = params[:overseer_step][:status_on_failure] + + status_on_success_id = status_on_success_param.present? && status_on_success_param != 'no_change' ? TaskStatus.status_for_name(status_on_success_param)&.id : nil + status_on_failure_id = status_on_failure_param.present? && status_on_failure_param != 'no_change' ? TaskStatus.status_for_name(status_on_failure_param)&.id : nil + + overseer_step_params = ActionController::Parameters.new(params) + .require(:overseer_step) + .permit( + :name, + :description, + :display_name, + :display_description, + :run_command, + :timeout, + :sort_order, + :step_type, + :partial_output_diff, + :stdin_input_file, + :expected_output_file, + :feedback_message, + :status_on_success_id, + :status_on_failure_id, + :halt_on_success, + :halt_on_failure, + :show_expected_output, + :show_stdin, + :show_stdout, + :enabled + ) + .merge(task_definition_id: task_definition.id, + status_on_success_id: status_on_success_id, + status_on_failure_id: status_on_failure_id) + + result = OverseerStep.create!(overseer_step_params) + + if result.nil? + error!({ error: 'No overseer step added' }, 403) + else + present result, with: Entities::OverseerStepEntity + end + end + + desc 'Update an overseer step' + params do + requires :overseer_step, type: Hash do + optional :name, type: String + optional :description, type: String + optional :display_name, type: String + optional :display_description, type: String + optional :run_command, type: String + optional :timeout, type: Integer + optional :sort_order, type: Integer + optional :step_type, type: String + optional :partial_output_diff, type: Boolean + optional :stdin_input_file, type: String + optional :expected_output_file, type: String + optional :feedback_message, type: String + optional :status_on_success, type: String + optional :status_on_failure, type: String + optional :halt_on_success, type: Boolean + optional :halt_on_failure, type: Boolean + optional :show_expected_output, type: Boolean + optional :show_stdin, type: Boolean + optional :show_stdout, type: Boolean + optional :enabled, type: Boolean + end + requires :task_def_id, type: Integer + end + put '/units/:unit_id/task_definitions/:task_def_id/overseer_steps/:id' do + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) + end + + unit = Unit.find(params[:unit_id]) + task_definition = unit.task_definitions.find(params[:task_def_id]) + overseer_step = task_definition.overseer_steps.find(params[:id]) + + unless authorise? current_user, overseer_step.task_definition, :manage_overseer_steps + error!({ error: 'Not authorised to manage overseer for this task definition' }, 403) + end + + status_on_success_param = params[:overseer_step][:status_on_success] + status_on_failure_param = params[:overseer_step][:status_on_failure] + + status_on_success_id = status_on_success_param.present? && status_on_success_param != 'no_change' ? TaskStatus.status_for_name(status_on_success_param)&.id : nil + status_on_failure_id = status_on_failure_param.present? && status_on_failure_param != 'no_change' ? TaskStatus.status_for_name(status_on_failure_param)&.id : nil + + overseer_step_params = ActionController::Parameters.new(params) + .require(:overseer_step) + .permit( + :name, + :description, + :display_name, + :display_description, + :run_command, + :timeout, + :sort_order, + :step_type, + :partial_output_diff, + :stdin_input_file, + :expected_output_file, + :feedback_message, + :status_on_success_id, + :status_on_failure_id, + :halt_on_success, + :halt_on_failure, + :show_expected_output, + :show_stdin, + :show_stdout, + :enabled + ) + .merge( + status_on_success_id: status_on_success_id, + status_on_failure_id: status_on_failure_id + ) + + overseer_step.update!(overseer_step_params) + + present overseer_step, with: Entities::OverseerStepEntity + end + + desc 'Delete an overseer step' + delete '/overseer_steps/:id' do + overseer_step = OverseerStep.find(params[:id]) + + unless authorise? current_user, overseer_step.task_definition, :manage_overseer_steps + error!({ error: 'Not authorised to manage overseer for this task definition' }, 403) + end + + overseer_step.destroy! + + error!({ error: overseer_step.errors.full_messages.last }, 403) unless overseer_step.destroyed? + + present overseer_step.destroyed?, with: Grape::Presenters::Presenter + end + + desc 'Get test results for an overseer assessment' + get '/projects/:project_id/task_definitions/:task_def_id/overseer_assessments_results/:id' do + project = Project.find(params[:project_id]) + + unless authorise? current_user, project, :get_submission + error!({ error: 'Not authorised to view this project' }, 403) + end + + unit = project.unit + + overseer_assessment = OverseerAssessment.find(params[:id]) + present overseer_assessment.overseer_step_results, with: Entities::OverseerStepResultEntity, my_role: unit.role_for(current_user) + end +end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index c5e031279..b4fd9a127 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -335,17 +335,17 @@ class TaskDefinitionsApi < Grape::API upload_reqs = task.upload_requirements # Copy files to be PDFed - task.accept_submission(current_user, scoop_files(params, upload_reqs), self, nil, 'ready_for_feedback', nil, accepted_tii_eula: false) + task.accept_submission(current_user, scoop_files(params, upload_reqs), self, nil, 'ready_for_feedback', nil, accepted_tii_eula: false, test_submission: true) - logger.info '********* - about to perform overseer submission' - overseer_assessment = OverseerAssessment.create_for(task) - if overseer_assessment.present? - overseer_assessment.send_to_overseer + # logger.info '********* - about to perform overseer submission' + # overseer_assessment = OverseerAssessment.create_for(task) + # if overseer_assessment.present? + # overseer_assessment.send_to_overseer - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" - else - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" - end + # logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" + # else + # logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" + # end # todo: Do we need to return additional details here? e.g. the comment, and project? present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true @@ -880,45 +880,45 @@ class TaskDefinitionsApi < Grape::API present job, with: Entities::SidekiqJobEntity 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' - requires :task_def_id, type: Integer, desc: 'The task definition to download submissions for' - end - get '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to edit task details of unit' }, 403) - 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' + # requires :task_def_id, type: Integer, desc: 'The task definition to download submissions for' + # end + # get '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do + # unit = Unit.find(params[:unit_id]) + # unless authorise? current_user, unit, :add_task_def + # error!({ error: 'Not authorised to edit task details of unit' }, 403) + # end - td = unit.task_definitions.find(params[:task_def_id]) + # td = unit.task_definitions.find(params[:task_def_id]) - script_path = td.task_assessment_script + # script_path = td.task_assessment_script - content = File.read(script_path) - content - end + # content = File.read(script_path) + # content + # end - desc 'Update the contents of the overseer execution script' - 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 download submissions for' - requires :script_content, type: String, desc: 'Content of the overseer execution script' - end - put '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to edit task details of unit' }, 403) - end + # desc 'Update the contents of the overseer execution script' + # 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 download submissions for' + # requires :script_content, type: String, desc: 'Content of the overseer execution script' + # end + # put '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do + # unit = Unit.find(params[:unit_id]) + # unless authorise? current_user, unit, :add_task_def + # error!({ error: 'Not authorised to edit task details of unit' }, 403) + # end - td = unit.task_definitions.find(params[:task_def_id]) + # td = unit.task_definitions.find(params[:task_def_id]) - script_path = td.task_assessment_script + # script_path = td.task_assessment_script - decoded = Base64.urlsafe_decode64(params[:script_content]) + # decoded = Base64.urlsafe_decode64(params[:script_content]) - File.write(script_path, decoded) - status 200 - end + # File.write(script_path, decoded) + # status 200 + # end end diff --git a/app/models/comments/assessment_comment.rb b/app/models/comments/assessment_comment.rb index 5c281d32c..1b614b41f 100644 --- a/app/models/comments/assessment_comment.rb +++ b/app/models/comments/assessment_comment.rb @@ -6,6 +6,9 @@ class AssessmentComment < TaskComment def serialize(user) json = super(user) json[:overseer_assessment_id] = self.commentable_id + json[:overseer_total_steps] = self.commentable.total_steps + json[:overseer_passed_steps] = self.commentable.passed_steps + json[:overseer_status] = self.commentable.status json end end diff --git a/app/models/overseer_assessment.rb b/app/models/overseer_assessment.rb index f15063505..5fd5fe80c 100644 --- a/app/models/overseer_assessment.rb +++ b/app/models/overseer_assessment.rb @@ -4,6 +4,7 @@ class OverseerAssessment < ApplicationRecord has_one :project, through: :task has_many :assessment_comments, as: :commentable, dependent: :destroy + has_many :overseer_step_results, dependent: :destroy validates :status, presence: true validates :task_id, presence: true @@ -11,12 +12,15 @@ class OverseerAssessment < ApplicationRecord validates :submission_timestamp, uniqueness: { scope: :task_id } - enum :status, { pre_queued: 0, queued: 1, queue_failed: 2, done: 3 } + enum :status, { pre_queued: 0, passed: 1, failed: 2 } after_destroy :delete_associated_files + # TODO: track how many tests ran, and how many tests total at the time + # TODO: we might not have an overseerStepResult because a new test was added later + # Creates an OverseerAssessment object for a new submission - def self.create_for(task) + def self.create_for(task, test_submission) # Create only if: # unit's assessment is enabled && # task's assessment is enabled && @@ -26,7 +30,7 @@ def self.create_for(task) task_definition = task.task_definition unit = task_definition.unit - return nil unless task.overseer_enabled? + return nil unless task.overseer_enabled? || test_submission docker_image_name_tag = task_definition.docker_image_name_tag || unit.docker_image_name_tag # assessment_resources_path = task_definition.task_assessment_resources @@ -118,7 +122,7 @@ def update_assessment_comment(text) add_assessment_comment text end - def send_to_overseer() + def send_to_overseer(test_submission: false) return { error: "Your task is already queued for processing. Pleasse wait until you receive a response before queueing your task again." } if self.status == :queued # TODO: Check status and do not queue if already queued @@ -140,10 +144,10 @@ def send_to_overseer() assessment_resources_path = task_definition.task_assessment_resources - unless unit.assessment_enabled && - task_definition.assessment_enabled && - task_definition.has_task_assessment_script? && - (task.has_new_files? || task.has_done_file?) + unless unit.assessment_enabled && + (task_definition.assessment_enabled || test_submission) && + # task_definition.has_task_assessment_script? && + (task.has_new_files? || task.has_done_file?) puts "ERROR: Assessment is no longer configured for overseer assessment. Unable to send - OverseerAssessment #{id}" return { error: "This assessment is no longer setup for automated feedback. Automated feedback is turned off at either the unit or task level, or the task does not have the scripts needed to automate assessment." } @@ -259,5 +263,10 @@ def delete_associated_files def base64?(value) value.is_a?(String) && Base64.strict_encode64(Base64.decode64(value)) == value end + + + def passed_steps + overseer_step_results.select(&:pass).size + end end # rubocop:enable Rails/Output diff --git a/app/models/overseer_step.rb b/app/models/overseer_step.rb new file mode 100644 index 000000000..6273f0db0 --- /dev/null +++ b/app/models/overseer_step.rb @@ -0,0 +1,6 @@ +class OverseerStep < ApplicationRecord + belongs_to :task_definition, optional: false + + validates :timeout, presence: true, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 300, message: 'must be between 1 and 300' } + validates :sort_order, presence: true, numericality: { greater_than_or_equal_to: 0 } +end diff --git a/app/models/overseer_step_result.rb b/app/models/overseer_step_result.rb new file mode 100644 index 000000000..21e482158 --- /dev/null +++ b/app/models/overseer_step_result.rb @@ -0,0 +1,5 @@ +class OverseerStepResult < ApplicationRecord + belongs_to :overseer_assessment, optional: false + belongs_to :overseer_step, optional: false + +end diff --git a/app/models/task.rb b/app/models/task.rb index 92644d42e..e6a166fc6 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1403,7 +1403,7 @@ def create_submission_and_trigger_state_change(user, propagate = true, contribut # # Checks to make sure that the files match what we expect # - def accept_submission(current_user, files, ui, contributions, trigger, alignments, accepted_tii_eula: false) + def accept_submission(current_user, files, ui, contributions, trigger, alignments, accepted_tii_eula: false, test_submission: false) # Ensure there is not a submission already in process if processing_pdf? ui.error!({ 'error' => 'A submission is already being processed. Please wait for the current submission process to complete.' }, 403) @@ -1502,7 +1502,7 @@ def accept_submission(current_user, files, ui, contributions, trigger, alignment logger.info "Submission accepted! Status for task #{id} is now #{trigger}" # Trigger processing of new submission - async - AcceptSubmissionJob.perform_async(id, current_user.id, accepted_tii_eula) + AcceptSubmissionJob.perform_async(id, current_user.id, accepted_tii_eula, test_submission) end # The name that should be used for the uploaded file (based on index of upload requirements) @@ -1573,7 +1573,7 @@ def archive_submission def overseer_enabled? return unit.assessment_enabled && task_definition.assessment_enabled && - task_definition.has_task_assessment_script? && + # task_definition.has_task_assessment_script? && (has_new_files? || has_done_file?) end diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 652d1b4ce..7902ca942 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -15,7 +15,8 @@ def self.permissions :get_los, :create_task_prerequisite, :get_discussion_prompt, - :create_discussion_prompt + :create_discussion_prompt, + :manage_overseer_steps ] admin_role_permissions = [ @@ -26,7 +27,8 @@ def self.permissions :get_los, :create_task_prerequisite, :get_discussion_prompt, - :create_discussion_prompt + :create_discussion_prompt, + :manage_overseer_steps ] tutor_role_permissions = [ @@ -71,6 +73,7 @@ def self.permissions has_many :tasks, dependent: :destroy # Destroying a task definition will also nuke any instances has_many :group_submissions, dependent: :destroy # Destroying a task definition will also nuke any group submissions has_many :learning_outcomes, as: :context, dependent: :destroy + has_many :overseer_steps, -> { order(:sort_order) }, inverse_of: :task_definition, dependent: :destroy has_many :tii_group_attachments, dependent: :destroy # destroy uploaded files to tii - after the tasks has_many :tii_actions, as: :entity, dependent: :destroy @@ -302,7 +305,7 @@ def check_upload_requirements_format end # Check the name matches a valid filename format - unless req['name'].match?(/^[a-zA-Z0-9_\- \.]+$/) + unless req['name'].match?(/^[a-zA-Z0-9_\- .]+$/) errors.add(:upload_requirements, "the name for item #{i + 1} does not seem to be a valid filename --> #{req['name']}.") end @@ -733,6 +736,24 @@ def task_assessment_resources(create = true) task_assessment_resources_with_abbreviation(abbreviation, create) end + def overseer_resource_files + return [] unless File.exist?(task_assessment_resources) + + files = [] + Zip::File.open(task_assessment_resources) do |zip_file| + zip_file.each do |entry| + next if entry.directory? + # skip macOS metadata files and hidden files + next if File.basename(entry.name).start_with?('._', '.') + + # remove top-level folder + parts = entry.name.split('/', 2) + files << "/#{parts.last}" unless parts.empty? + end + end + files + end + def task_assessment_script(create = true) task_assessment_script_with_abbreviation(abbreviation, create) end diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index f812efa5f..c48800868 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -1,4 +1,5 @@ require 'yaml' +require 'open3' class AcceptOverseerJob include Sidekiq::Job @@ -20,96 +21,238 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment total(1) task = Task.find(task_id) + task_definition = task.task_definition + + raise "PDF is still compiling" if task.processing_pdf? || !task.has_done_file? + + raise "Submission file not found: #{submission}" unless File.exist?(submission) + + active_overseer_steps = task_definition.overseer_steps.select(&:enabled) + + raise "Task definition has no enabled overseer steps #{task.unit.detailed_name} #{task_definition.abbreviation}" if active_overseer_steps.empty? + + oa = OverseerAssessment.find(overseer_assessment_id) + oa.update!( + total_steps: active_overseer_steps.size + ) work_dir_name = "#{task.id}-#{overseer_assessment_id}" work_dir = Rails.root.join("tmp", "overseer", work_dir_name) FileUtils.mkdir_p(work_dir) - raise "PDF is still compiling" if task.processing_pdf? || !task.has_done_file? + extract_student_submission_files(task, submission, work_dir) + extract_overseer_resource_files(assessment, work_dir) - raise "Submission file not found: #{submission}" unless File.exist?(submission) + success_status = nil + failure_status = nil - # Extract submission files, removing any parent folders - Zip::File.open(submission) do |zip_file| - zip_file.each do |entry| - next if entry.name_is_directory? + oa.add_assessment_comment("Tests in progress") - parts = entry.name.split('/')[1..] - next unless parts.length >= 1 + steps_attempted = 0 + steps_passed = 0 - file_name = parts.first - index = file_name.to_i + assessment_pass = true - file = task.upload_requirements[index] - final_name = file['name'] + active_overseer_steps.each do |step| + result = run_overseer_step( + step: step, + work_dir: work_dir, + work_dir_name: work_dir_name, + task_id: task_id, + timestamp: timestamp, + docker_image_name_tag: docker_image_name_tag, + overseer_assessment_id: overseer_assessment_id + ) - dest_path = File.join(work_dir, final_name) - FileUtils.mkdir_p(File.dirname(dest_path)) - zip_file.extract(entry, dest_path) { true } + steps_attempted += 1 + + if result.valid? && result.pass + steps_passed += 1 + if step.halt_on_success && step.status_on_success_id + success_status = TaskStatus.find(step.status_on_success_id) + break + end + elsif step.halt_on_failure + failure_status = TaskStatus.find(step.status_on_failure_id) if step.status_on_failure_id + assessment_pass = false + break end end - # Extract optional assessment resources - if File.exist?(assessment) - Zip::File.open(assessment) do |zip_file| - zip_file.each do |entry| - dest_path = File.join(work_dir, entry.name) - FileUtils.mkdir_p(File.dirname(dest_path)) - zip_file.extract(entry, dest_path) { true } # overwrite if exists - end + oa.update_assessment_comment("Tests complete: #{steps_passed} / #{active_overseer_steps.count}") + + if steps_attempted == steps_passed && assessment_pass + oa.update!(status: :passed) + unless success_status.nil? + # TODO: have an override status setting for the step? eg. if the task is overdue, let it remain overdue, otherwise use this task status + task.update!(task_status: success_status) + task.add_status_comment(task.project.tutor_for(task.task_definition), success_status) + + oa.update!(result_task_status: success_status.status_key.to_s) end + else + oa.update!(status: :failed) + unless failure_status.nil? + # TODO: have an override status setting for the step? eg. if the task is overdue, let it remain overdue, otherwise use this task status + task.update!(task_status: failure_status) + task.add_status_comment(task.project.tutor_for(task.task_definition), failure_status) + oa.update!(result_task_status: failure_status.status_key.to_s) + end + task.add_text_comment(task.project.tutor_for(task.task_definition), "**Automated comment**: Some tests did not pass for this submission. Please review the Overseer report, verify your output, and resubmit.") end - # Extract execution script - script_path = task.task_definition.task_assessment_script - raise "No execution script found" unless File.exist?(script_path) + logger.info "Completed overseer job" + rescue StandardError => e + logger.error e + raise e + end - script_contents = File.read(script_path) + def run_overseer_step(step:, work_dir:, work_dir_name:, task_id:, timestamp:, docker_image_name_tag:, overseer_assessment_id:) + script_contents = step.run_command raise "Execution script is empty" if script_contents.blank? + # Create script run_sh_path = File.join(work_dir, 'run.sh') File.write(run_sh_path, script_contents) - system("chmod +x #{work_dir}/run.sh") + + # Ensure script is executable + system("chmod +x #{run_sh_path}") mount = Doubtfire::Application.config.overseer_workdir_volume_mount - volume_mount = if mount.nil? - # Fallback for development only — mounts the entire overseer container volume, - # allowing all task work directories to be accessible. This should never be - # used in production, as it breaks isolation between tasks. - "--volumes-from #{Doubtfire::Application.config.overseer_fallback_volume_container}" - else - "-v #{mount}/#{work_dir_name}:/overseer/work-dir/#{work_dir_name}" - end + volume_mount = + if mount.nil? + # Fallback for development only — mounts the entire overseer container volume, + # allowing all task work directories to be accessible. This should never be + # used in production, as it breaks isolation between tasks. + "--volumes-from #{Doubtfire::Application.config.overseer_fallback_volume_container}" + else + # Absolute path on the hosting server to the shared mount + "-v #{mount}/#{work_dir_name}:/overseer/work-dir/#{work_dir_name}" + end + + # Max runtime (seconds) before force-killing the step (exit status 124) + timeout = step.timeout + timeout = 30 if timeout.nil? || timeout.negative? container_name = "overseer-#{task_id}-#{timestamp}" command = %( - timeout 300 docker run --rm \ + timeout #{timeout} docker run --rm -i \ --cpus 1 \ --network none \ #{volume_mount} \ --name #{container_name} \ #{docker_image_name_tag} \ - bash -c "cd /overseer/work-dir/#{work_dir_name} && ./run.sh" + bash -c "cd /overseer/work-dir/#{work_dir_name} && timeout #{timeout} ./run.sh" ) - system(command) + stdin_input_file = nil + expected_output_file = nil - yaml_path = File.join(work_dir, 'output.yaml') + # Retrieve names of input/output files + if step.step_type == 'output_diff' + stdin_input_file = step.stdin_input_file.present? ? File.join(work_dir, step.stdin_input_file) : nil + expected_output_file = step.expected_output_file.present? ? File.join(work_dir, step.expected_output_file) : nil + end - oa = OverseerAssessment.find(overseer_assessment_id) + output = "" + status = nil + stdin_contents = nil + + # Execute script and capture output + Open3.popen2e(command) do |stdin, stdout_err, wait_thr| + # If input file exists, pass it as standard input + if stdin_input_file && File.exist?(stdin_input_file) + File.open(stdin_input_file, 'rb') { |f| IO.copy_stream(f, stdin) } + stdin_contents = File.read(stdin_input_file) + stdin.close + end - oa.update_from_output(work_dir) - if File.exist?(yaml_path) - path = FileHelper.task_submission_identifier_path_with_timestamp(:done, task, timestamp) - FileUtils.cp(yaml_path, path) - FileUtils.rm_rf(work_dir) + stdout_err.each { |line| output << line } + status = wait_thr.value end - logger.info "Completed overseer job" - rescue StandardError => e - logger.error e - raise e + output = output.chomp + pass = status.exitstatus == 0 + + expected_output_contents = nil + + # If step type is comparing output, retrieve expected output file contents + if step.step_type == 'output_diff' + expected_output_contents = + if expected_output_file && File.exist?(expected_output_file) + File.read(expected_output_file) + else + '' + end + matches_output = if step.partial_output_diff + output.include?(expected_output_contents) + else + output == expected_output_contents + end + + pass = false unless matches_output + end + + feedback_message = + if step.feedback_message.blank? + if step.step_type == 'output_diff' + "Your output did not match the expected result." + else + "This test did not complete successfully. Check the output for any errors." + end + else + step.feedback_message + end + + OverseerStepResult.create!( + overseer_assessment_id: overseer_assessment_id, + overseer_step: step, + exit_status: status.exitstatus, + pass: pass, + feedback_message: feedback_message, + stdout: output, + stdin: stdin_contents, + expected_output: expected_output_contents, + stdout_sha256: Digest::SHA256.hexdigest(output), + stdin_sha256: stdin_contents && Digest::SHA256.hexdigest(stdin_contents), + expected_output_sha256: expected_output_contents && Digest::SHA256.hexdigest(expected_output_contents) + ) + end + + def extract_student_submission_files(task, submission, work_dir) + # Extract submission files, removing any parent folders + Zip::File.open(submission) do |zip_file| + zip_file.each do |entry| + next if entry.name_is_directory? + + parts = entry.name.split('/')[1..] + next unless parts.length >= 1 + + file_name = parts.first + index = file_name.to_i + + file = task.upload_requirements[index] + final_name = file['name'] + + dest_path = File.join(work_dir, final_name) + FileUtils.mkdir_p(File.dirname(dest_path)) + zip_file.extract(entry, dest_path) { true } + end + end + end + + def extract_overseer_resource_files(assessment, work_dir) + # Extract optional assessment resources + if File.exist?(assessment) + Zip::File.open(assessment) do |zip_file| + zip_file.each do |entry| + dest_path = File.join(work_dir, entry.name) + FileUtils.mkdir_p(File.dirname(dest_path)) + zip_file.extract(entry, dest_path) { true } # overwrite if exists + end + end + end end end diff --git a/app/sidekiq/accept_submission_job.rb b/app/sidekiq/accept_submission_job.rb index 90c20026d..a8a44bf7f 100644 --- a/app/sidekiq/accept_submission_job.rb +++ b/app/sidekiq/accept_submission_job.rb @@ -2,7 +2,7 @@ class AcceptSubmissionJob include Sidekiq::Job include LogHelper - def perform(task_id, user_id, accepted_tii_eula) + def perform(task_id, user_id, accepted_tii_eula, test_submission) begin # Ensure cwd is valid... FileUtils.cd(Rails.root) @@ -50,12 +50,12 @@ def perform(task_id, user_id, accepted_tii_eula) task.send_documents_to_tii(user, accepted_tii_eula: accepted_tii_eula) end - if task.overseer_enabled? - overseer_assessment = OverseerAssessment.create_for(task) + if task.overseer_enabled? || test_submission + overseer_assessment = OverseerAssessment.create_for(task, test_submission) if overseer_assessment.present? logger.info "Launching Overseer assessment for task_def_id: #{task.task_definition.id} task_id: #{task.id}" - overseer_assessment.send_to_overseer + overseer_assessment.send_to_overseer(test_submission: test_submission) else logger.info "Overseer assessment for task_def_id: #{task.task_definition.id} task_id: #{task.id} was not performed #{overseer_assessment.inspect}" diff --git a/db/migrate/20251218031455_create_overseer_steps.rb b/db/migrate/20251218031455_create_overseer_steps.rb new file mode 100644 index 000000000..eab2bafe4 --- /dev/null +++ b/db/migrate/20251218031455_create_overseer_steps.rb @@ -0,0 +1,70 @@ +class CreateOverseerSteps < ActiveRecord::Migration[8.0] + def change + create_table :overseer_steps do |t| + t.references :task_definition, null: false + + # Staff only + t.string :name, null: false + t.text :description + + # Shown to the student + t.string :display_name, null: false + t.string :display_description + + t.text :run_command + + t.integer :timeout, default: 30, null: false + t.integer :sort_order, default: 0, null: false + + t.string :step_type, null: false # "status_check", "output_diff", etc. + t.boolean :partial_output_diff + + t.string :stdin_input_file # Name of file (or path) in assessment resources + t.string :expected_output_file # Name of file in (or path) assessment resources + + t.text :feedback_message + + t.references :status_on_success + t.references :status_on_failure + + t.boolean :halt_on_success + t.boolean :halt_on_failure + + t.boolean :show_expected_output + t.boolean :show_stdin + t.boolean :show_stdout + + t.boolean :enabled, default: true + + t.timestamps + end + + create_table :overseer_step_results do |t| + t.references :overseer_assessment, null: false + t.references :overseer_step, null: false + + t.integer :exit_status, null: false, default: -1 + t.boolean :pass, null: false, default: false + + t.text :feedback_message + + # The output from the overseer script and student's submission + t.text :stdout + + # The original input/output files, in case they have since been changed + t.text :stdin + t.text :expected_output + + # We may want to discard the original_stdin, expected_output, and stdout when archiving a unit. + # Storing hashes will allow us to confirm if the original outputs matched + t.string :stdout_sha256 + t.string :stdin_sha256 + t.string :expected_output_sha256 + + t.timestamps + end + + # Track the number of available steps at the time of assessment + add_column :overseer_assessments, :total_steps, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index d2a328042..689f92ad0 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: 2025_12_12_010033) do +ActiveRecord::Schema[8.0].define(version: 2025_12_18_031455) 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 @@ -215,6 +215,7 @@ t.integer "status", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "total_steps" t.index ["task_id", "submission_timestamp"], name: "index_overseer_assessments_on_task_id_and_submission_timestamp", unique: true t.index ["task_id"], name: "index_overseer_assessments_on_task_id" end @@ -231,6 +232,53 @@ t.index ["tag"], name: "index_overseer_images_on_tag", unique: true end + create_table "overseer_step_results", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "overseer_assessment_id", null: false + t.bigint "overseer_step_id", null: false + t.integer "exit_status", default: -1, null: false + t.boolean "pass", default: false, null: false + t.text "feedback_message" + t.text "stdout" + t.text "stdin" + t.text "expected_output" + t.string "stdout_sha256" + t.string "stdin_sha256" + t.string "expected_output_sha256" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["overseer_assessment_id"], name: "index_overseer_step_results_on_overseer_assessment_id" + t.index ["overseer_step_id"], name: "index_overseer_step_results_on_overseer_step_id" + end + + create_table "overseer_steps", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "task_definition_id", null: false + t.string "name", null: false + t.text "description" + t.string "display_name", null: false + t.string "display_description" + t.text "run_command" + t.integer "timeout", default: 30, null: false + t.integer "sort_order", default: 0, null: false + t.string "step_type", null: false + t.boolean "partial_output_diff" + t.string "stdin_input_file" + t.string "expected_output_file" + t.text "feedback_message" + t.bigint "status_on_success_id" + t.bigint "status_on_failure_id" + t.boolean "halt_on_success" + t.boolean "halt_on_failure" + t.boolean "show_expected_output" + t.boolean "show_stdin" + t.boolean "show_stdout" + t.boolean "enabled", default: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status_on_failure_id"], name: "index_overseer_steps_on_status_on_failure_id" + t.index ["status_on_success_id"], name: "index_overseer_steps_on_status_on_success_id" + t.index ["task_definition_id"], name: "index_overseer_steps_on_task_definition_id" + end + create_table "projects", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.bigint "unit_id" t.string "project_role" diff --git a/test/factories/overseer_steps.rb b/test/factories/overseer_steps.rb new file mode 100644 index 000000000..284b2e6b1 --- /dev/null +++ b/test/factories/overseer_steps.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :overseer_step do + + end +end diff --git a/test/models/overseer_step_test.rb b/test/models/overseer_step_test.rb new file mode 100644 index 000000000..6cce7d6fa --- /dev/null +++ b/test/models/overseer_step_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +describe OverseerStep do + # it "does a thing" do + # value(1+1).must_equal 2 + # end +end From 2a5063848d9cde0b0277f6e2463613945d720b99 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:41:56 +1100 Subject: [PATCH 27/46] feat: custom project task deadlines (#550) * feat: get all task prerequisites for a unit * feat: target start and due dates for a task * feat: endpoint to save project task dates * chore: update schema * chore: fix schema formatting * chore: prevent duplicate target start and end date updates * feat: endpoint to reset all target dates for a project * test: ensure target dates endpoint permissions * test: reset flexible dates * chore: require target start and end date --- app/api/entities/task_entity.rb | 2 + app/api/task_prerequisites_api.rb | 15 +++ app/api/tasks_api.rb | 94 +++++++++++++++++++ app/models/project.rb | 4 +- ...251124015104_add_project_task_deadlines.rb | 6 ++ db/schema.rb | 2 + test/api/tasks_api_test.rb | 62 ++++++++++++ 7 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20251124015104_add_project_task_deadlines.rb diff --git a/app/api/entities/task_entity.rb b/app/api/entities/task_entity.rb index ffb53bd86..1de8fb6b3 100644 --- a/app/api/entities/task_entity.rb +++ b/app/api/entities/task_entity.rb @@ -14,6 +14,8 @@ class TaskEntity < Grape::Entity expose :due_date expose :submission_date, expose_nil: false expose :completion_date, expose_nil: false + expose :target_due_date, expose_nil: false + expose :target_start_date, expose_nil: false end expose :extensions diff --git a/app/api/task_prerequisites_api.rb b/app/api/task_prerequisites_api.rb index 04e190fd6..3cca5d2ff 100644 --- a/app/api/task_prerequisites_api.rb +++ b/app/api/task_prerequisites_api.rb @@ -26,4 +26,19 @@ class TaskPrerequisitesApi < Grape::API present prerequisites, with: Entities::TaskPrerequisiteEntity end + desc 'Get task prerequisites for a unit' + params do + requires :unit_id, type: Integer, desc: 'The unit to get the task definition from' + end + get '/units/:unit_id/task_prerequisites' do + unit = Unit.find(params[:unit_id]) + + unless authorise? current_user, unit, :get_unit + error!({ error: 'Not authorised to get unit' }, 403) + end + + prerequisites = unit.task_definitions.flat_map(&:task_prerequisites) + + present prerequisites, with: Entities::TaskPrerequisiteEntity + end end diff --git a/app/api/tasks_api.rb b/app/api/tasks_api.rb index 4929e3903..47a41e64b 100644 --- a/app/api/tasks_api.rb +++ b/app/api/tasks_api.rb @@ -330,4 +330,98 @@ class TasksApi < Grape::API # Return the file data stream_file file_loc end + + desc 'Update the target dates for a task - when date flexibility is allowed' + params do + requires :id, type: Integer, desc: 'The project id to locate' + requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to update in this project' + requires :target_start_date, type: Date, desc: 'Target date to start the task' + requires :target_due_date, type: Date, desc: 'Target date to submit the task' + end + put '/projects/:id/task_def_id/:task_definition_id/target_dates' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + + # check the user can put this task + if authorise? current_user, project, :make_submission + # Check unit allows planned date changes + unless project.unit.allow_flexible_dates + error!({ error: 'This unit does not allow you to adjust due dates.' }, 403) + end + + task = project.task_for_task_definition(task_definition) + + if task.target_start_date == params[:target_start_date] && task.target_due_date == params[:target_due_date] + present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true + return + end + + task.update!( + target_start_date: params[:target_start_date], + target_due_date: params[:target_due_date] + ) + + comment_text = if params[:target_start_date].present? && params[:target_due_date].present? + "Planned date adjusted: #{task.target_start_date.strftime('%d %b')} - #{task.target_due_date.strftime('%d %b')}." + else + "Planned date reset: #{task_definition.start_date.strftime('%d %b')} - #{task_definition.target_date.strftime('%d %b')}." + end + + comment = TaskComment.create( + task: task, + user: current_user, + comment: comment_text, + content_type: :plan, + recipient: project.student + ) + + comment.mark_as_read(project.tutor_for(task_definition)) + + present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true + else + error!({ error: "You are not permitted to adjust the plan." }, 403) + end + end + + desc 'Update the target dates for a task - when date flexibility is allowed' + params do + requires :id, type: Integer, desc: 'The project id to locate' + end + put '/projects/:id/reset_target_dates' do + project = Project.find(params[:id]) + + # check the user can put this task + if authorise? current_user, project, :make_submission + # Check unit allows planned date changes + unless project.unit.allow_flexible_dates + error!({ error: 'This unit does not allow you to adjust due dates.' }, 403) + end + + project.tasks.each do |task| + next if task.target_start_date.nil? && task.target_due_date.nil? + + task.update!( + target_start_date: nil, + target_due_date: nil + ) + + comment_text = "Planned date reset: #{task.task_definition.start_date.strftime('%d %b')} - #{task.task_definition.target_date.strftime('%d %b')}." + comment = TaskComment.create( + task: task, + user: current_user, + comment: comment_text, + content_type: :plan, + recipient: project.student + ) + + comment.mark_as_read(project.tutor_for(task.task_definition)) + end + + present project, with: Entities::ProjectEntity, user: current_user, for_student: true, in_project: true + + else + error!({ error: "You are not permitted to adjust the plan." }, 403) + end + end + end diff --git a/app/models/project.rb b/app/models/project.rb index 030b11b52..1e5782484 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -310,7 +310,9 @@ def task_details_for_shallow_serializer(user) scorm_extensions: t.scorm_extensions, due_date: t.due_date, submission_date: t.submission_date, - completion_date: t.completion_date + completion_date: t.completion_date, + target_start_date: t.target_start_date, + target_due_date: t.target_due_date } end end diff --git a/db/migrate/20251124015104_add_project_task_deadlines.rb b/db/migrate/20251124015104_add_project_task_deadlines.rb new file mode 100644 index 000000000..0caf2a82a --- /dev/null +++ b/db/migrate/20251124015104_add_project_task_deadlines.rb @@ -0,0 +1,6 @@ +class AddProjectTaskDeadlines < ActiveRecord::Migration[8.0] + def change + add_column :tasks, :target_start_date, :datetime + add_column :tasks, :target_due_date, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 689f92ad0..3b78acfba 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -504,6 +504,8 @@ t.integer "quality_pts", default: -1 t.integer "extensions", default: 0, null: false t.integer "scorm_extensions", default: 0, null: false + t.datetime "target_start_date" + t.datetime "target_due_date" t.index ["group_submission_id"], name: "index_tasks_on_group_submission_id" t.index ["project_id", "task_definition_id"], name: "tasks_uniq_proj_task_def", unique: true t.index ["project_id"], name: "index_tasks_on_project_id" diff --git a/test/api/tasks_api_test.rb b/test/api/tasks_api_test.rb index 3ab4e7851..7848aaec8 100644 --- a/test/api/tasks_api_test.rb +++ b/test/api/tasks_api_test.rb @@ -1012,4 +1012,66 @@ def test_resubmission_doesnt_change_submission_date assert task1.submission_date > task2.submission_date end end + + def test_task_target_date_permissions + unit = FactoryBot.create(:unit, task_count: 2, student_count: 1) + # tutor = FactoryBot.create(:user, :tutor) + # convenor = FactoryBot.create(:user, :convenor) + + # convenor_role = unit.employ_staff(convenor, Role.convenor) + # tutor_role = unit.employ_staff(tutor, Role.tutor) + + td = unit.task_definitions.first + + original_start_date = Time.zone.today + original_end_date = Time.zone.today + 1.day + td.update!(target_date: original_end_date, start_date: original_start_date) + assert_not td.nil? + + student1 = FactoryBot.create(:user, :student) + + project1 = unit.enrol_student(student1, nil) + task = project1.task_for_task_definition(td) + + assert_equal original_end_date, task.due_date + + unit.update!(allow_flexible_dates: false) + + new_start_date = Time.zone.today + 2.days + new_end_date = Time.zone.today + 4.days + + add_auth_header_for(user: student1) + + put "/api/projects/#{project1.id}/task_def_id/#{td.id}/target_dates", { + target_start_date: new_start_date, + target_due_date: new_end_date + } + + assert_equal 403, last_response.status + assert_equal original_end_date, task.due_date + + unit.update!(allow_flexible_dates: true) + + put "/api/projects/#{project1.id}/task_def_id/#{td.id}/target_dates" + assert_equal 400, last_response.status # target_start_date and target_due_date are required + + put "/api/projects/#{project1.id}/task_def_id/#{td.id}/target_dates", { + target_start_date: new_start_date, + target_due_date: new_end_date + } + assert_equal 200, last_response.status + + task.reload + assert_equal new_start_date, task.target_start_date + assert_equal new_end_date, task.target_due_date + + put "/api/projects/#{project1.id}/reset_target_dates" + assert_equal 200, last_response.status + task.reload + assert_nil task.target_start_date + assert_nil task.target_due_date + + unit.update!(allow_flexible_dates: false) + end + end From c32d4621d6614d95d688c9b2406b9e8fd3443d2c Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:47:04 +1100 Subject: [PATCH 28/46] chore(release): 10.0.0-65 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f618415..cedbfbd4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-65](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-64...v10.0.0-65) (2026-01-05) + + +### Features + +* attention required task status ([#557](https://github.com/b0ink/doubtfire-deploy/issues/557)) ([88e0c7a](https://github.com/b0ink/doubtfire-deploy/commit/88e0c7a9e65a71aed0b9a8453d75af0aa52a09f4)) +* custom project task deadlines ([#550](https://github.com/b0ink/doubtfire-deploy/issues/550)) ([2a50638](https://github.com/b0ink/doubtfire-deploy/commit/2a5063848d9cde0b0277f6e2463613945d720b99)) +* overseer pipeline ([#559](https://github.com/b0ink/doubtfire-deploy/issues/559)) ([d2e49bf](https://github.com/b0ink/doubtfire-deploy/commit/d2e49bfd57e69281923458b8853226a85c9da390)) + ## [10.0.0-64](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-63...v10.0.0-64) (2025-12-03) ## [10.0.0-63](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-62...v10.0.0-63) (2025-12-03) From 201f8f6699925839ece80183e90cc24c53bc609c Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:49:28 +1100 Subject: [PATCH 29/46] chore: remove overseer work dir on completion --- app/sidekiq/accept_overseer_job.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index c48800868..3ab197c12 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -102,6 +102,8 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment task.add_text_comment(task.project.tutor_for(task.task_definition), "**Automated comment**: Some tests did not pass for this submission. Please review the Overseer report, verify your output, and resubmit.") end + FileUtils.rm_rf(work_dir) + logger.info "Completed overseer job" rescue StandardError => e logger.error e From 339b6493ec73432ef74e96c9983fb17d0659de92 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:22:57 +1100 Subject: [PATCH 30/46] fix: check correct user permissions when toggling observer only (#560) --- app/api/unit_roles_api.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/api/unit_roles_api.rb b/app/api/unit_roles_api.rb index 85ead4295..57f8ba3f2 100644 --- a/app/api/unit_roles_api.rb +++ b/app/api/unit_roles_api.rb @@ -87,7 +87,11 @@ class UnitRolesApi < Grape::API # Once they're an observer, they'll no longer have access to this route to remove the observer status from themselves # But let's double check just in case this route gets whitelisted... - if unit_role.observer_only + + unit = unit_role.unit + current_unit_role = unit.unit_role_for(current_user) + + if current_unit_role.observer_only error!({ error: "You are not authorised to update this staff member." }, 403) end From 1312537c3463deec57b92034a77e39806d072a3f Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 17 Jan 2026 16:49:53 +1000 Subject: [PATCH 31/46] chore: update gems Also correct the pdf reading test --- Gemfile.lock | 525 +++++++++++++++++++++++++-------------------------- db/schema.rb | 474 +++++++++++++++++++++++----------------------- 2 files changed, 495 insertions(+), 504 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1149766c4..b473c814a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,30 +1,32 @@ GEM remote: https://rubygems.org/ specs: - Ascii85 (2.0.1) - actioncable (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + Ascii85 (1.1.1) + action_text-trix (2.1.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) - actionmailer (8.0.2) - actionpack (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activesupport (= 8.0.2) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.2) - actionview (= 8.0.2) - activesupport (= 8.0.2) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -32,72 +34,73 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.2) - actionpack (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actiontext (8.1.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2) - activesupport (= 8.0.2) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.2) - activesupport (= 8.0.2) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (8.0.2) - activesupport (= 8.0.2) - activerecord (8.0.2) - activemodel (= 8.0.2) - activesupport (= 8.0.2) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) timeout (>= 0.4.0) - activestorage (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activesupport (= 8.0.2) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - activesupport (8.0.2) + activesupport (8.1.2) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) afm (0.2.2) - amq-protocol (2.3.3) - ast (2.4.3) + amq-protocol (2.3.2) + ast (2.4.2) backport (1.2.0) base64 (0.2.0) bcrypt (3.1.20) - benchmark (0.4.0) + benchmark (0.3.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.9) + bigdecimal (3.1.8) bindata (2.5.0) - bootsnap (1.18.4) + bootsnap (1.18.3) msgpack (~> 1.2) builder (3.3.0) - bunny (2.24.0) - amq-protocol (~> 2.3) + bunny (2.22.0) + amq-protocol (~> 2.3, >= 2.3.1) sorted_set (~> 1, >= 1.0.2) bunny-pub-sub (0.5.2) bunny (~> 2.14) - byebug (12.0.0) + byebug (11.1.3) chronic_duration (0.10.6) numerizer (~> 0.1.1) ci_reporter (2.1.0) @@ -106,21 +109,18 @@ GEM code_analyzer (0.5.5) sexp_processor coderay (1.1.3) - concurrent-ruby (1.3.5) - connection_pool (2.5.0) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) crack (1.0.0) bigdecimal rexml crass (1.0.6) - cronex (0.15.0) - tzinfo - unicode (>= 0.4.4.5) - csv (3.3.3) - database_cleaner-active_record (2.2.0) + csv (3.3.0) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.1) + date (3.5.1) devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -130,144 +130,139 @@ GEM devise_ldap_authenticatable (0.8.7) devise (>= 3.4.1) net-ldap (>= 0.16.0) - diff-lcs (1.6.1) - docile (1.4.1) + diff-lcs (1.5.1) + docile (1.4.0) domain_name (0.6.20240107) - dotenv (3.1.7) + dotenv (3.1.2) drb (2.2.1) - dry-core (1.1.0) + dry-core (1.0.1) concurrent-ruby (~> 1.0) - logger zeitwerk (~> 2.6) - dry-inflector (1.2.0) - dry-logic (1.6.0) - bigdecimal + dry-inflector (1.0.0) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) - dry-core (~> 1.1) + dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-types (1.8.2) + dry-types (1.7.2) bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) - erubi (1.13.1) + e2mmap (0.1.0) + erubi (1.12.0) erubis (2.7.0) et-orbi (1.2.11) tzinfo ethon (0.16.0) ffi (>= 1.15.0) - factory_bot (6.5.1) - activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) - factory_bot (~> 6.5) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.5.1) + faker (3.4.1) i18n (>= 1.8.11, < 2) - faraday (2.12.2) - faraday-net_http (>= 2.0, < 3.5) - json - logger + faraday (2.9.1) + faraday-net_http (>= 2.0, < 3.2) faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) - faraday-net_http (3.4.0) - net-http (>= 0.5.0) - ffi (1.17.1-aarch64-linux-gnu) - ffi (1.17.1-x86_64-linux-gnu) - fugit (1.11.1) + faraday-net_http (3.1.0) + net-http + ffi (1.17.0) + fugit (1.11.0) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - grape (2.3.0) - activesupport (>= 6) + grape (2.0.0) + activesupport (>= 5) + builder dry-types (>= 1.1) - mustermann-grape (~> 1.1.0) - rack (>= 2) - zeitwerk + mustermann-grape (~> 1.0.0) + rack (>= 1.3.0) + rack-accept grape-entity (1.0.1) activesupport (>= 3.0.0) multi_json (>= 1.3.2) - grape-swagger (2.1.2) + grape-swagger (2.1.0) grape (>= 1.7, < 3.0) rack-test (~> 2) - grape-swagger-rails (0.6.0) - ostruct + grape-swagger-rails (0.5.0) railties (>= 6.0.6.1) - hashdiff (1.1.2) + hashdiff (1.1.0) hashery (2.1.2) - hashie (5.0.0) + hashie (5.1.0) + logger hirb (0.7.3) http-accept (1.7.0) - http-cookie (1.0.8) + http-cookie (1.0.6) domain_name (~> 0.5) - i18n (1.14.7) + i18n (1.14.5) concurrent-ruby (~> 1.0) - icalendar (2.10.3) + icalendar (2.10.1) ice_cube (~> 0.16) - ostruct - ice_cube (0.17.0) - io-console (0.8.0) - irb (1.15.1) - pp (>= 0.6.0) + ice_cube (0.16.4) + io-console (0.7.2) + irb (1.13.1) rdoc (>= 4.0.0) reline (>= 0.4.2) jaro_winkler (1.6.0) - json (2.10.2) - json-jwt (1.16.7) + json (2.7.2) + json-jwt (1.16.6) activesupport (>= 4.2) aes_key_wrap base64 bindata faraday (~> 2.0) faraday-follow_redirects - jwt (2.10.1) + jwt (3.1.2) base64 - kramdown (2.5.1) - rexml (>= 3.3.9) + kramdown (2.4.0) + rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - language_server-protocol (3.17.0.4) - lint_roller (1.1.0) + language_server-protocol (3.17.0.3) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.24.0) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.0.4) - mime-types (3.6.2) - logger + marcel (1.1.0) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2025.0325) + mime-types-data (3.2024.0604) mini_mime (1.1.5) - minitest (5.25.5) + mini_portile2 (2.8.9) + minitest (5.23.1) minitest-around (0.5.0) minitest (~> 5.0) - minitest-rails (8.0.0) + minitest-rails (8.1.0) minitest (~> 5.20) - railties (>= 8.0.0, < 8.1.0) + railties (>= 8.1.0, < 8.2.0) moss_ruby (1.1.4) tcp_timeout (~> 0.1.1) - msgpack (1.8.0) + msgpack (1.7.2) multi_json (1.15.0) - multi_xml (0.7.1) - bigdecimal (~> 3.1) - mustermann (3.0.3) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) + mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - mustermann-grape (1.1.0) + mustermann-grape (1.0.2) mustermann (>= 1.0.0) mysql2 (0.5.6) - net-http (0.6.0) + net-http (0.4.1) uri - net-imap (0.5.6) + net-imap (0.6.2) date net-protocol net-ldap (0.19.0) @@ -275,79 +270,76 @@ GEM net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.1) + net-smtp (0.5.0) net-protocol netrc (0.11.0) - nio4r (2.7.4) - nokogiri (1.18.7-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-gnu) + nio4r (2.7.3) + nokogiri (1.16.5) + mini_portile2 (~> 2.8.2) racc (~> 1.4) numerizer (0.1.1) - oauth2 (2.0.9) - faraday (>= 0.17.3, < 3.0) - jwt (>= 1.0, < 3.0) + oauth2 (2.0.18) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) rack (>= 1.2, < 4) - snaky_hash (~> 2.0) - version_gem (~> 1.1) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (~> 1.1, >= 1.1.9) observer (0.1.2) orm_adapter (0.5.0) - ostruct (0.6.1) - parallel (1.26.3) - parser (3.3.7.4) + parallel (1.25.1) + parser (3.3.2.0) ast (~> 2.4.1) racc - pdf-reader (2.14.1) - Ascii85 (>= 1.0, < 3.0, != 2.0.0) + pdf-reader (2.12.0) + Ascii85 (~> 1.0) afm (~> 0.2.1) hashery (~> 2.0) ruby-rc4 ttfunk - pkg-config (1.6.0) - pp (0.6.2) - prettyprint - prettyprint (0.2.0) - prism (1.4.0) - psych (5.2.3) - date + pkg-config (1.5.6) + prism (0.29.0) + psych (5.1.2) stringio - public_suffix (6.0.1) - puma (6.6.0) + public_suffix (5.0.5) + puma (6.4.2) nio4r (~> 2.0) raabro (1.4.0) - racc (1.8.1) - rack (3.1.12) + racc (1.8.0) + rack (3.0.11) + rack-accept (0.4.5) + rack (>= 0.4) rack-cors (2.0.2) rack (>= 2.0.0) - rack-session (2.1.0) - base64 (>= 0.1.0) + rack-session (2.0.0) rack (>= 3.0.0) - rack-test (2.2.0) + rack-test (2.1.0) rack (>= 1.3) - rackup (2.2.1) + rackup (2.1.0) rack (>= 3) - rails (8.0.2) - actioncable (= 8.0.2) - actionmailbox (= 8.0.2) - actionmailer (= 8.0.2) - actionpack (= 8.0.2) - actiontext (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activemodel (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + webrick (~> 1.8) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 8.0.2) + railties (= 8.1.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) + rails-html-sanitizer (1.6.0) loofah (~> 2.21) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + nokogiri (~> 1.14) rails-latex (2.3.5) rails (>= 3.0.0, < 9) rails_best_practices (1.23.2) @@ -358,30 +350,30 @@ GEM json require_all (~> 3.0) ruby-progressbar - railties (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (3.9.2) - logger + rbs (2.8.4) rbtree (0.4.6) - rdoc (6.13.1) + rdoc (6.7.0) psych (>= 4.0.0) - redis (5.4.0) + redis (5.2.0) redis-client (>= 0.22.0) - redis-client (0.24.0) + redis-client (0.22.2) connection_pool - regexp_parser (2.10.0) - reline (0.6.0) + regexp_parser (2.9.2) + reline (0.5.8) io-console (~> 0.5) require_all (3.0.0) responders (3.1.1) @@ -392,172 +384,171 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - reverse_markdown (3.0.0) + reverse_markdown (2.1.1) nokogiri - rexml (3.4.1) - rmagick (6.1.1) + rexml (3.2.9) + strscan + rmagick (6.0.1) observer (~> 0.1) pkg-config (~> 1.4) - roo (2.10.1) + roo (2.7.1) nokogiri (~> 1) - rubyzip (>= 1.3.0, < 3.0.0) + rubyzip (~> 1.1, < 2.0.0) roo-xls (1.2.0) nokogiri roo (>= 2.0.0, < 3) spreadsheet (> 0.9.0) - rouge (4.5.1) - rubocop (1.75.1) + rouge (4.2.1) + rubocop (1.64.1) json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.43.0, < 2.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.43.0) - parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-factory_bot (2.27.1) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-faker (1.3.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) + rubocop-factory_bot (2.26.1) + rubocop (~> 1.61) + rubocop-faker (1.1.0) faker (>= 2.12.0) - lint_roller (~> 1.1) - rubocop (>= 1.72.1) - rubocop-minitest (0.37.1) - lint_roller (~> 1.1) - rubocop (>= 1.72.1, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-performance (1.24.0) - lint_roller (~> 1.1) - rubocop (>= 1.72.1, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.30.3) + rubocop (>= 0.82.0) + rubocop-minitest (0.36.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-performance (1.23.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.25.0) activesupport (>= 4.2.0) - lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.72.1, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) ruby-filemagic (0.7.3) - ruby-lsp (0.23.13) + ruby-lsp (0.17.2) language_server-protocol (~> 3.17.0) - prism (>= 1.2, < 2.0) - rbs (>= 3, < 4) + prism (>= 0.29.0, < 0.30) sorbet-runtime (>= 0.5.10782) ruby-ole (1.2.13.1) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby-saml (1.18.0) - nokogiri (>= 1.13.10) + ruby-saml (1.13.0) + nokogiri (>= 1.10.5) rexml ruby2_keywords (0.0.5) - rubyzip (2.4.1) + rubyzip (1.3.0) securerandom (0.4.1) - set (1.1.1) - sexp_processor (4.17.3) - shellwords (0.2.2) - sidekiq (7.3.9) - base64 + set (1.1.0) + sexp_processor (4.17.1) + shellwords (0.2.0) + sidekiq (7.2.4) + concurrent-ruby (< 2) connection_pool (>= 2.3.0) - logger rack (>= 2.2.4) - redis-client (>= 0.22.2) - sidekiq-cron (2.2.0) - cronex (>= 0.13.0) - fugit (~> 1.8, >= 1.11.1) + redis-client (>= 0.19.0) + sidekiq-cron (1.12.0) + fugit (~> 1.8) globalid (>= 1.0.1) - sidekiq (>= 6.5.0) - sidekiq-status (3.0.3) + sidekiq (>= 6) + sidekiq-status (4.0.0) + base64 chronic_duration - sidekiq (>= 6.0, < 8) - sidekiq-unique-jobs (8.0.10) + logger + sidekiq (>= 7, < 9) + sidekiq-unique-jobs (8.0.13) concurrent-ruby (~> 1.0, >= 1.0.5) - sidekiq (>= 7.0.0, < 8.0.0) + sidekiq (>= 7.0.0, < 9.0.0) thor (>= 1.0, < 3.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.1) + simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - snaky_hash (2.0.1) - hashie - version_gem (~> 1.1, >= 1.1.1) - solargraph (0.53.4) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) + solargraph (0.50.0) backport (~> 1.2) benchmark bundler (~> 2.0) diff-lcs (~> 1.4) - jaro_winkler (~> 1.6) + e2mmap + jaro_winkler (~> 1.5) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.1) - logger (~> 1.6) - observer (~> 0.1) - ostruct (~> 0.6) parser (~> 3.0) - rbs (~> 3.3) - reverse_markdown (>= 2.0, < 4) + rbs (~> 2.0) + reverse_markdown (~> 2.0) rubocop (~> 1.38) thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - yard-solargraph (~> 0.1) - sorbet-runtime (0.5.11966) + sorbet-runtime (0.5.11422) sorted_set (1.0.3) rbtree set (~> 1.0) - spreadsheet (1.3.4) + spreadsheet (1.3.1) bigdecimal - logger ruby-ole sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.5.2) + sprockets-rails (3.5.1) actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - stringio (3.1.6) + stringio (3.1.0) + strscan (3.1.0) tca_client (1.0.4) typhoeus (~> 1.0, >= 1.0.1) tcp_timeout (0.1.1) - thor (1.3.2) - tilt (2.6.0) - timeout (0.4.3) + thor (1.3.1) + tilt (2.3.0) + timeout (0.4.1) + tsort (0.2.0) ttfunk (1.8.0) bigdecimal (~> 3.1) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode (0.4.4.5) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) - uri (1.0.3) + unicode-display_width (2.5.0) + uri (1.1.1) useragent (0.16.11) - version_gem (1.1.6) + version_gem (1.1.9) warden (1.2.9) rack (>= 2.0.9) - webmock (3.25.1) + webmock (3.23.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.7) + webrick (1.8.1) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - yard (0.9.37) - yard-solargraph (0.1.0) - yard (~> 0.9) - zeitwerk (2.7.2) + yard (0.9.36) + zeitwerk (2.6.15) PLATFORMS - aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES better_errors @@ -623,7 +614,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.2p28 + ruby 3.4.7p58 BUNDLED WITH - 2.6.6 + 2.5.11 diff --git a/db/schema.rb b/db/schema.rb index 0e65657d5..0f1e6f5f9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,11 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_02_221253) do +ActiveRecord::Schema[8.1].define(version: 2025_11_02_221253) 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 t.datetime "created_at", null: false + t.string "name", null: false t.datetime "updated_at", null: false t.index ["abbreviation"], name: "index_activity_types_on_abbreviation", unique: true t.index ["name"], name: "index_activity_types_on_name", unique: true @@ -22,143 +22,143 @@ create_table "auth_tokens", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.datetime "auth_token_expiry", null: false - t.bigint "user_id" t.string "authentication_token", null: false - t.integer "token_type", default: 0, null: false t.datetime "created_at", null: false + t.integer "token_type", default: 0, null: false t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["token_type"], name: "index_auth_tokens_on_token_type" t.index ["user_id"], name: "index_auth_tokens_on_user_id" end create_table "breaks", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "start_date", null: false + t.datetime "created_at", null: false t.integer "number_of_weeks", null: false + t.datetime "start_date", null: false t.bigint "teaching_period_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["teaching_period_id"], name: "index_breaks_on_teaching_period_id" end create_table "campuses", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name", null: false - t.integer "mode", null: false t.string "abbreviation", null: false t.boolean "active", null: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "mode", null: false + t.string "name", null: false t.string "timezone" + t.datetime "updated_at", null: false t.index ["abbreviation"], name: "index_campuses_on_abbreviation", unique: true t.index ["active"], name: "index_campuses_on_active" t.index ["name"], name: "index_campuses_on_name", unique: true end create_table "chip_usages", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.bigint "feedback_chip_id", null: false t.bigint "tutor_id", null: false - t.integer "usage_count", default: 0, null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "usage_count", default: 0, null: false t.index ["feedback_chip_id"], name: "index_chip_usages_on_feedback_chip_id" t.index ["tutor_id"], name: "index_chip_usages_on_tutor_id" end create_table "comments_read_receipts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_comment_id", null: false - t.bigint "user_id", null: false t.datetime "created_at", null: false + t.bigint "task_comment_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["task_comment_id", "user_id"], name: "index_comments_read_receipts_on_task_comment_id_and_user_id", unique: true t.index ["task_comment_id"], name: "index_comments_read_receipts_on_task_comment_id" t.index ["user_id"], name: "index_comments_read_receipts_on_user_id" end create_table "d2l_assessment_mappings", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id", null: false - t.string "org_unit_id" - t.integer "grade_object_id" t.datetime "created_at", null: false + t.integer "grade_object_id" + t.string "org_unit_id" + t.bigint "unit_id", null: false t.datetime "updated_at", null: false t.index ["unit_id"], name: "index_d2l_assessment_mappings_on_unit_id", unique: true end create_table "discussion_comments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "time_started" - t.datetime "time_completed" - t.integer "number_of_prompts" t.datetime "created_at", null: false + t.integer "number_of_prompts" + t.datetime "time_completed" + t.datetime "time_started" t.datetime "updated_at", null: false end create_table "feedback_chips", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "type" t.text "chip_text" - t.text "description" t.text "comment_text" - t.text "summary_text" + t.datetime "created_at", null: false + t.text "description" t.bigint "learning_outcome_id", null: false t.bigint "parent_chip_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.text "summary_text" t.string "task_status" + t.string "type" + t.datetime "updated_at", null: false t.index ["learning_outcome_id"], name: "index_feedback_chips_on_learning_outcome_id" t.index ["parent_chip_id"], name: "index_feedback_chips_on_parent_chip_id" end create_table "group_memberships", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "group_id" - t.bigint "project_id" t.boolean "active", default: true t.datetime "created_at" + t.bigint "group_id" + t.bigint "project_id" t.datetime "updated_at" t.index ["group_id"], name: "index_group_memberships_on_group_id" t.index ["project_id"], name: "index_group_memberships_on_project_id" end create_table "group_sets", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "name" t.boolean "allow_students_to_create_groups", default: true t.boolean "allow_students_to_manage_groups", default: true - t.boolean "keep_groups_in_same_class", default: false - t.datetime "created_at" - t.datetime "updated_at" t.integer "capacity" + t.datetime "created_at" + t.boolean "keep_groups_in_same_class", default: false t.boolean "locked", default: false, null: false + t.string "name" + t.bigint "unit_id" + t.datetime "updated_at" t.index ["name", "unit_id"], name: "index_group_sets_on_name_and_unit_id", unique: true t.index ["unit_id"], name: "index_group_sets_on_unit_id" end create_table "group_submissions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at" t.bigint "group_id" t.string "notes" t.bigint "submitted_by_project_id" - t.datetime "created_at" - t.datetime "updated_at" t.bigint "task_definition_id" + t.datetime "updated_at" t.index ["group_id"], name: "index_group_submissions_on_group_id" t.index ["submitted_by_project_id"], name: "index_group_submissions_on_submitted_by_project_id" t.index ["task_definition_id"], name: "index_group_submissions_on_task_definition_id" end create_table "groups", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.integer "capacity_adjustment", default: 0, null: false + t.datetime "created_at" t.bigint "group_set_id" - t.bigint "tutorial_id" + t.boolean "locked", default: false, null: false t.string "name" - t.datetime "created_at" + t.bigint "tutorial_id" t.datetime "updated_at" - t.integer "capacity_adjustment", default: 0, null: false - t.boolean "locked", default: false, null: false t.index ["group_set_id"], name: "index_groups_on_group_set_id" t.index ["name", "group_set_id"], name: "index_groups_on_name_and_group_set_id", unique: true t.index ["tutorial_id"], name: "index_groups_on_tutorial_id" end create_table "learning_outcome_links", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "link_type" t.bigint "source_id", null: false t.bigint "target_id", null: false - t.string "link_type" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["source_id", "target_id"], name: "index_learning_outcome_links_on_source_id_and_target_id", unique: true t.index ["source_id"], name: "index_learning_outcome_links_on_source_id" @@ -166,85 +166,85 @@ end create_table "learning_outcomes", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "short_description" - t.string "full_outcome_description", limit: 4096 t.string "abbreviation" t.bigint "context_id" t.string "context_type" t.datetime "created_at", null: false + t.string "full_outcome_description", limit: 4096 + t.string "short_description" t.datetime "updated_at", null: false t.index ["abbreviation", "context_type", "context_id"], name: "index_learning_outcomes_on_abbreviation_and_context", unique: true t.index ["context_id", "context_type"], name: "index_learning_outcomes_on_context_id_and_context_type" end create_table "logins", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "timestamp" - t.bigint "user_id" t.datetime "created_at", null: false + t.datetime "timestamp" t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["user_id"], name: "index_logins_on_user_id" end create_table "marking_sessions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.bigint "unit_id", null: false + t.datetime "created_at", null: false + t.boolean "during_tutorial" + t.datetime "end_time" t.string "ip_address" t.datetime "start_time" - t.datetime "end_time" - t.boolean "during_tutorial" - t.datetime "created_at", null: false + t.bigint "unit_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["unit_id"], name: "index_marking_sessions_on_unit_id" t.index ["user_id", "unit_id", "ip_address", "updated_at"], name: "index_marking_sessions_on_user_unit_ip_and_time" t.index ["user_id"], name: "index_marking_sessions_on_user_id" end create_table "overseer_assessments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.string "submission_timestamp", null: false + t.datetime "created_at", null: false t.string "result_task_status" t.integer "status", default: 0, null: false - t.datetime "created_at", null: false + t.string "submission_timestamp", null: false + t.bigint "task_id", null: false t.datetime "updated_at", null: false t.index ["task_id", "submission_timestamp"], name: "index_overseer_assessments_on_task_id_and_submission_timestamp", unique: true t.index ["task_id"], name: "index_overseer_assessments_on_task_id" end create_table "overseer_images", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "last_pulled_date" t.string "name", null: false + t.integer "pulled_image_status" + t.text "pulled_image_text" t.string "tag", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "pulled_image_text" - t.integer "pulled_image_status" - t.datetime "last_pulled_date" t.index ["name"], name: "index_overseer_images_on_name", unique: true t.index ["tag"], name: "index_overseer_images_on_tag", unique: true end create_table "projects", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "project_role" + t.bigint "assessor_id" + t.bigint "campus_id" + t.boolean "compile_portfolio", default: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "started" - t.string "progress" - t.string "status" - t.string "task_stats" t.boolean "enrolled", default: true - t.integer "target_grade", default: 0 - t.boolean "compile_portfolio", default: false - t.date "portfolio_production_date" - t.bigint "user_id" t.integer "grade", default: 0 t.string "grade_rationale", limit: 4096 - t.bigint "campus_id" - t.integer "submitted_grade" - t.boolean "uses_draft_learning_summary", default: false, null: false t.boolean "portfolio_auto_generated", default: false, null: false t.integer "portfolio_generation_pid" + t.date "portfolio_production_date" + t.string "progress" + t.string "project_role" t.integer "spec_con_days", default: 0, null: false - t.bigint "assessor_id" + t.boolean "started" + t.string "status" + t.integer "submitted_grade" + t.integer "target_grade", default: 0 + t.string "task_stats" + t.bigint "unit_id" + t.datetime "updated_at", null: false + t.bigint "user_id" + t.boolean "uses_draft_learning_summary", default: false, null: false t.index ["assessor_id"], name: "index_projects_on_assessor_id" t.index ["campus_id"], name: "index_projects_on_campus_id" t.index ["enrolled"], name: "index_projects_on_enrolled" @@ -254,19 +254,19 @@ end create_table "roles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name" - t.text "description" t.datetime "created_at", null: false + t.text "description" + t.string "name" t.datetime "updated_at", null: false end create_table "session_activities", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "marking_session_id", null: false t.string "action" + t.datetime "created_at", null: false + t.bigint "marking_session_id", null: false t.bigint "project_id" - t.bigint "task_id" t.bigint "task_definition_id" - t.datetime "created_at", null: false + t.bigint "task_id" t.datetime "updated_at", null: false t.index ["action", "task_id", "created_at"], name: "index_session_activities_on_action_task_created_at" t.index ["marking_session_id"], name: "index_session_activities_on_marking_session_id" @@ -276,13 +276,13 @@ end create_table "staff_notes", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.text "note" t.bigint "project_id", null: false - t.bigint "user_id", null: false - t.bigint "staff_notes_id" t.bigint "reply_to_id" - t.datetime "created_at", null: false + t.bigint "staff_notes_id" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["project_id"], name: "index_staff_notes_on_project_id" t.index ["reply_to_id"], name: "index_staff_notes_on_reply_to_id" t.index ["staff_notes_id"], name: "index_staff_notes_on_staff_notes_id" @@ -290,27 +290,27 @@ end create_table "task_comments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.bigint "user_id", null: false + t.bigint "assessor_id" + t.string "attachment_extension" t.string "comment", limit: 4096 - t.datetime "created_at", null: false - t.bigint "recipient_id" + t.bigint "commentable_id" + t.string "commentable_type" t.string "content_type" - t.string "attachment_extension" - t.bigint "discussion_comment_id" - t.string "type" - t.datetime "time_discussion_started" - t.datetime "time_discussion_completed" - t.integer "number_of_prompts" + t.datetime "created_at", null: false t.datetime "date_extension_assessed" + t.bigint "discussion_comment_id" t.boolean "extension_granted" - t.bigint "assessor_id" - t.bigint "task_status_id" - t.integer "extension_weeks" t.string "extension_response" + t.integer "extension_weeks" + t.integer "number_of_prompts" + t.bigint "recipient_id" t.bigint "reply_to_id" - t.bigint "commentable_id" - t.string "commentable_type" + t.bigint "task_id", null: false + t.bigint "task_status_id" + t.datetime "time_discussion_completed" + t.datetime "time_discussion_started" + t.string "type" + t.bigint "user_id", null: false t.index ["assessor_id"], name: "index_task_comments_on_assessor_id" t.index ["commentable_type", "commentable_id"], name: "index_task_comments_on_commentable_type_and_commentable_id" t.index ["discussion_comment_id"], name: "index_task_comments_on_discussion_comment_id" @@ -322,38 +322,38 @@ end create_table "task_definitions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "name" - t.string "description", limit: 4096 - t.decimal "weighting", precision: 10 - t.datetime "target_date", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.string "abbreviation" - t.string "upload_requirements", limit: 4096 - t.integer "target_grade", default: 0 - t.boolean "restrict_status_updates", default: false - t.string "plagiarism_report_url" - t.boolean "plagiarism_updated", default: false - t.integer "plagiarism_warn_pct", default: 50 - t.bigint "group_set_id" + t.boolean "assess_in_portfolio_only", default: false, null: false + t.boolean "assessment_enabled", default: false + t.datetime "created_at", null: false + t.string "description", limit: 4096 t.datetime "due_date" - t.datetime "start_date", null: false + t.bigint "group_set_id" t.boolean "is_graded", default: false + t.boolean "lock_assessments_to_tutorial_stream", default: false, null: false t.integer "max_quality_pts", default: 0 - t.bigint "tutorial_stream_id" - t.boolean "assessment_enabled", default: false + t.string "name" t.bigint "overseer_image_id" - t.string "tii_group_id" - t.string "similarity_language" - t.boolean "scorm_enabled", default: false + t.string "plagiarism_report_url" + t.boolean "plagiarism_updated", default: false + t.integer "plagiarism_warn_pct", default: 50 + t.boolean "restrict_status_updates", default: false t.boolean "scorm_allow_review", default: false + t.integer "scorm_attempt_limit", default: 0 t.boolean "scorm_bypass_test", default: false + t.boolean "scorm_enabled", default: false t.boolean "scorm_time_delay_enabled", default: false - t.integer "scorm_attempt_limit", default: 0 - t.boolean "assess_in_portfolio_only", default: false, null: false + t.string "similarity_language" + t.datetime "start_date", null: false + t.datetime "target_date", null: false + t.integer "target_grade", default: 0 + t.string "tii_group_id" + t.bigint "tutorial_stream_id" + t.bigint "unit_id" + t.datetime "updated_at", null: false + t.string "upload_requirements", limit: 4096 t.boolean "use_resources_for_jplag_base_code", default: false, null: false - t.boolean "lock_assessments_to_tutorial_stream", default: false, null: false + t.decimal "weighting", precision: 10 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 @@ -363,29 +363,29 @@ end create_table "task_engagements", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "engagement_time" + t.datetime "created_at", null: false t.string "engagement" + t.datetime "engagement_time" t.bigint "task_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["task_id"], name: "index_task_engagements_on_task_id" end create_table "task_pins", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.bigint "user_id", null: false t.datetime "created_at", null: false + t.bigint "task_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["task_id", "user_id"], name: "index_task_pins_on_task_id_and_user_id", unique: true t.index ["task_id"], name: "index_task_pins_on_task_id" t.index ["user_id"], name: "fk_rails_915df186ed" end create_table "task_prerequisites", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_definition_id", null: false + t.datetime "created_at", null: false t.bigint "prerequisite_id", null: false + t.bigint "task_definition_id", null: false t.bigint "task_status_id", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["prerequisite_id"], name: "index_task_prerequisites_on_prerequisite_id" t.index ["task_definition_id", "prerequisite_id"], name: "idx_on_task_definition_id_prerequisite_id_90b47ca126", unique: true @@ -394,59 +394,59 @@ end create_table "task_similarities", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id" + t.datetime "created_at" + t.boolean "flagged", default: false t.bigint "other_task_id" t.integer "pct" - t.datetime "created_at" - t.datetime "updated_at" t.string "plagiarism_report_url" - t.boolean "flagged", default: false - t.string "type" + t.bigint "task_id" t.bigint "tii_submission_id" + t.string "type" + t.datetime "updated_at" t.index ["other_task_id"], name: "index_task_similarities_on_other_task_id" t.index ["task_id"], name: "index_task_similarities_on_task_id" t.index ["tii_submission_id"], name: "index_task_similarities_on_tii_submission_id" end create_table "task_statuses", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name" - t.string "description" t.datetime "created_at", null: false + t.string "description" + t.string "name" t.datetime "updated_at", null: false end create_table "task_submissions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "submission_time" t.datetime "assessment_time" + t.bigint "assessor_id" + t.datetime "created_at", null: false t.string "outcome" + t.datetime "submission_time" t.bigint "task_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "assessor_id" t.index ["assessor_id"], name: "index_task_submissions_on_assessor_id" t.index ["task_id"], name: "index_task_submissions_on_task_id" end create_table "tasks", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_definition_id" - t.bigint "project_id" - t.bigint "task_status_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "assessment_date" t.date "completion_date" - t.string "portfolio_evidence" - t.boolean "include_in_portfolio", default: true - t.datetime "file_uploaded_at" - t.bigint "group_submission_id" t.integer "contribution_pct", default: 100 - t.integer "times_assessed", default: 0 - t.datetime "submission_date" - t.datetime "assessment_date" - t.integer "grade" t.integer "contribution_pts", default: 3 - t.integer "quality_pts", default: -1 + t.datetime "created_at", null: false t.integer "extensions", default: 0, null: false + t.datetime "file_uploaded_at" + t.integer "grade" + t.bigint "group_submission_id" + t.boolean "include_in_portfolio", default: true + t.string "portfolio_evidence" + t.bigint "project_id" + t.integer "quality_pts", default: -1 t.integer "scorm_extensions", default: 0, null: false + t.datetime "submission_date" + t.bigint "task_definition_id" + t.bigint "task_status_id" + t.integer "times_assessed", default: 0 + t.datetime "updated_at", null: false t.index ["group_submission_id"], name: "index_tasks_on_group_submission_id" t.index ["project_id", "task_definition_id"], name: "tasks_uniq_proj_task_def", unique: true t.index ["project_id"], name: "index_tasks_on_project_id" @@ -455,43 +455,43 @@ end create_table "teaching_periods", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "period", null: false - t.datetime "start_date", null: false - t.datetime "end_date", null: false - t.integer "year", null: false t.datetime "active_until", null: false t.datetime "created_at", null: false + t.datetime "end_date", null: false + t.string "period", null: false + t.datetime "start_date", null: false t.datetime "updated_at", null: false + t.integer "year", null: false t.index ["period", "year"], name: "index_teaching_periods_on_period_and_year", unique: true end create_table "test_attempts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id" t.datetime "attempted_time", null: false - t.boolean "terminated", default: false - t.boolean "completion_status", default: false - t.boolean "success_status", default: false - t.float "score_scaled", default: 0.0 t.text "cmi_datamodel" + t.boolean "completion_status", default: false t.datetime "created_at", null: false + t.float "score_scaled", default: 0.0 + t.boolean "success_status", default: false + t.bigint "task_id" + t.boolean "terminated", default: false t.datetime "updated_at", null: false t.index ["task_id"], name: "index_test_attempts_on_task_id" end create_table "tii_actions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "entity_type" - t.bigint "entity_id" - t.string "type" t.boolean "complete", default: false, null: false - t.integer "retries", default: 0, null: false - t.datetime "last_run" t.datetime "complete_at" - t.boolean "retry", default: true, null: false - t.integer "error_code" + t.datetime "created_at", null: false t.text "custom_error_message" + t.bigint "entity_id" + t.string "entity_type" + t.integer "error_code" + t.datetime "last_run" t.text "log" t.string "params", limit: 1024, default: "{}" - t.datetime "created_at", null: false + t.integer "retries", default: 0, null: false + t.boolean "retry", default: true, null: false + t.string "type" t.datetime "updated_at", null: false t.index ["complete"], name: "index_tii_actions_on_complete" t.index ["entity_type", "entity_id"], name: "index_tii_actions_on_entity" @@ -499,29 +499,29 @@ end create_table "tii_group_attachments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_definition_id", null: false + t.datetime "created_at", null: false + t.string "file_sha1_digest" t.string "filename", null: false t.string "group_attachment_id" - t.string "file_sha1_digest" t.integer "status", default: 0, null: false - t.datetime "created_at", null: false + t.bigint "task_definition_id", null: false t.datetime "updated_at", null: false t.index ["task_definition_id"], name: "index_tii_group_attachments_on_task_definition_id" end create_table "tii_submissions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.bigint "tii_task_similarity_id" - t.bigint "submitted_by_user_id", null: false + t.datetime "created_at", null: false t.string "filename", null: false t.integer "idx", null: false - t.string "submission_id" + t.integer "overall_match_percentage" t.string "similarity_pdf_id" - t.datetime "submitted_at" t.datetime "similarity_request_at" t.integer "status", default: 0, null: false - t.integer "overall_match_percentage" - t.datetime "created_at", null: false + t.string "submission_id" + t.datetime "submitted_at" + t.bigint "submitted_by_user_id", null: false + t.bigint "task_id", null: false + t.bigint "tii_task_similarity_id" t.datetime "updated_at", null: false t.index ["submitted_by_user_id"], name: "index_tii_submissions_on_submitted_by_user_id" t.index ["task_id"], name: "index_tii_submissions_on_task_id" @@ -530,21 +530,21 @@ create_table "tutorial_enrolments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.bigint "project_id", null: false t.bigint "tutorial_id", null: false + t.datetime "updated_at", null: false t.index ["project_id"], name: "index_tutorial_enrolments_on_project_id" t.index ["tutorial_id", "project_id"], name: "index_tutorial_enrolments_on_tutorial_id_and_project_id", unique: true t.index ["tutorial_id"], name: "index_tutorial_enrolments_on_tutorial_id" end create_table "tutorial_streams", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name", null: false t.string "abbreviation", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.bigint "activity_type_id", null: false + t.datetime "created_at", null: false + t.string "name", null: false t.bigint "unit_id", null: false + t.datetime "updated_at", null: false t.index ["abbreviation", "unit_id"], name: "index_tutorial_streams_on_abbreviation_and_unit_id", unique: true t.index ["abbreviation"], name: "index_tutorial_streams_on_abbreviation" t.index ["activity_type_id"], name: "fk_rails_14ef80da76" @@ -553,18 +553,18 @@ end create_table "tutorials", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "meeting_day" - t.string "meeting_time" - t.string "meeting_location" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "code" - t.bigint "unit_role_id" t.string "abbreviation" - t.integer "capacity", default: -1 t.bigint "campus_id" + t.integer "capacity", default: -1 + t.string "code" + t.datetime "created_at", null: false + t.string "meeting_day" + t.string "meeting_location" + t.string "meeting_time" t.bigint "tutorial_stream_id" + t.bigint "unit_id" + t.bigint "unit_role_id" + t.datetime "updated_at", null: false t.index ["abbreviation", "unit_id"], name: "index_tutorials_on_abbreviation_and_unit_id", unique: true t.index ["campus_id"], name: "index_tutorials_on_campus_id" t.index ["tutorial_stream_id"], name: "index_tutorials_on_tutorial_stream_id" @@ -573,13 +573,13 @@ end create_table "unit_roles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id" - t.bigint "tutorial_id" t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.boolean "observer_only", default: false t.bigint "role_id" + t.bigint "tutorial_id" t.bigint "unit_id" - t.boolean "observer_only", default: false + t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["role_id"], name: "index_unit_roles_on_role_id" t.index ["tutorial_id"], name: "index_unit_roles_on_tutorial_id" t.index ["unit_id"], name: "index_unit_roles_on_unit_id" @@ -587,33 +587,33 @@ end create_table "units", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name" - t.string "description", limit: 4096 - t.datetime "start_date" - t.datetime "end_date" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "code" t.boolean "active", default: true - t.datetime "last_plagarism_scan" - t.bigint "teaching_period_id" - t.bigint "main_convenor_id" + t.boolean "allow_flexible_dates", default: false, null: false + t.boolean "allow_student_change_tutorial", default: true, null: false + t.boolean "allow_student_extension_requests", default: true, null: false + t.boolean "archived", default: false + t.boolean "assessment_enabled", default: true t.boolean "auto_apply_extension_before_deadline", default: true, null: false - t.boolean "send_notifications", default: true, null: false - t.boolean "enable_sync_timetable", default: true, null: false - t.boolean "enable_sync_enrolments", default: true, null: false + t.string "code" + t.datetime "created_at", null: false + t.string "description", limit: 4096 t.bigint "draft_task_definition_id" - t.boolean "allow_student_extension_requests", default: true, null: false + t.boolean "enable_sync_enrolments", default: true, null: false + t.boolean "enable_sync_timetable", default: true, null: false + t.datetime "end_date" t.integer "extension_weeks_on_resubmit_request", default: 1, null: false - t.boolean "allow_student_change_tutorial", default: true, null: false - t.boolean "assessment_enabled", default: true + t.datetime "last_plagarism_scan" + t.bigint "main_convenor_id" + t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false + t.string "name" t.bigint "overseer_image_id" t.datetime "portfolio_auto_generation_date" - t.string "tii_group_context_id" - t.boolean "archived", default: false - t.boolean "allow_flexible_dates", default: false, null: false t.datetime "portfolio_due_date" - t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false + t.boolean "send_notifications", default: true, null: false + t.datetime "start_date" + t.bigint "teaching_period_id" + t.string "tii_group_context_id" + t.datetime "updated_at", 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" @@ -621,53 +621,53 @@ end create_table "user_oauth_states", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.string "state" t.datetime "created_at", null: false + t.string "state" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["state"], name: "index_user_oauth_states_on_state", unique: true t.index ["user_id"], name: "index_user_oauth_states_on_user_id" end create_table "user_oauth_tokens", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "expires_at" t.integer "provider", default: 0, null: false t.text "token" - t.datetime "expires_at" - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["user_id"], name: "index_user_oauth_tokens_on_user_id" end create_table "users", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.datetime "created_at", null: false t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "first_name" + t.boolean "has_run_first_time_setup", default: false t.string "last_name" - t.string "username" + t.datetime "last_sign_in_at" + t.string "last_sign_in_ip" + t.string "login_id" t.string "nickname" - t.string "unlock_token" - t.bigint "role_id", default: 0 - t.boolean "receive_task_notifications", default: true + t.boolean "opt_in_to_research" t.boolean "receive_feedback_notifications", default: true t.boolean "receive_portfolio_notifications", default: true - t.boolean "opt_in_to_research" - t.boolean "has_run_first_time_setup", default: false - t.string "login_id" + t.boolean "receive_task_notifications", default: true + t.datetime "remember_created_at" + t.datetime "reset_password_sent_at" + t.string "reset_password_token" + t.bigint "role_id", default: 0 + t.integer "sign_in_count", default: 0 t.string "student_id" - t.string "tii_eula_version" t.datetime "tii_eula_date" + t.string "tii_eula_version" t.boolean "tii_eula_version_confirmed", default: false, null: false + t.string "unlock_token" + t.datetime "updated_at", null: false + t.string "username" t.index ["email"], name: "index_users_on_email", unique: true t.index ["login_id"], name: "index_users_on_login_id", unique: true t.index ["role_id"], name: "index_users_on_role_id" @@ -676,23 +676,23 @@ end create_table "webcal_unit_exclusions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "webcal_id", null: false - t.bigint "unit_id", null: false t.datetime "created_at", null: false + t.bigint "unit_id", null: false t.datetime "updated_at", null: false + t.bigint "webcal_id", null: false t.index ["unit_id", "webcal_id"], name: "index_webcal_unit_exclusions_on_unit_id_and_webcal_id", unique: true t.index ["unit_id"], name: "index_webcal_unit_exclusions_on_unit_id" t.index ["webcal_id"], name: "fk_rails_d5fab02cb7" end create_table "webcals", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.string "guid", limit: 36, null: false t.boolean "include_start_dates", default: false, null: false - t.bigint "user_id" t.integer "reminder_time" t.string "reminder_unit" - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["guid"], name: "index_webcals_on_guid", unique: true t.index ["user_id"], name: "index_webcals_on_user_id", unique: true end From a90072df86b8a976fe2727c3a7d85c365a55fda0 Mon Sep 17 00:00:00 2001 From: Sahiru Withanage Date: Sun, 6 Apr 2025 18:54:24 +1000 Subject: [PATCH 32/46] feat(api): add staff grant extension endpoint Enable staff to grant extensions to multiple students without formal requests. Reuse existing student extension logic through a new service for consistency. Supports flexible academic support and streamlines staff workflows. Relates to the OnTrack Staff Grant Extension design documentation. --- app/api/api_root.rb | 2 + app/api/staff_grant_extension_api.rb | 126 +++++++++++++ app/models/unit.rb | 1 + app/services/extension_service.rb | 52 ++++++ test/api/staff_grant_extension_test.rb | 249 +++++++++++++++++++++++++ 5 files changed, 430 insertions(+) create mode 100644 app/api/staff_grant_extension_api.rb create mode 100644 app/services/extension_service.rb create mode 100644 test/api/staff_grant_extension_test.rb diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 983749c55..39e5cfabc 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -65,6 +65,7 @@ class ApiRoot < Grape::API mount LearningOutcomesApi mount ProjectsApi mount SettingsApi + mount StaffGrantExtensionApi mount StudentsApi mount Submission::PortfolioApi mount Submission::PortfolioEvidenceApi @@ -118,6 +119,7 @@ class ApiRoot < Grape::API AuthenticationHelpers.add_auth_to GroupSetsApi AuthenticationHelpers.add_auth_to LearningOutcomesApi AuthenticationHelpers.add_auth_to ProjectsApi + AuthenticationHelpers.add_auth_to StaffGrantExtensionApi AuthenticationHelpers.add_auth_to StudentsApi AuthenticationHelpers.add_auth_to Submission::PortfolioApi AuthenticationHelpers.add_auth_to Submission::PortfolioEvidenceApi diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb new file mode 100644 index 000000000..9c50aa48b --- /dev/null +++ b/app/api/staff_grant_extension_api.rb @@ -0,0 +1,126 @@ +require 'grape' + +# +# API endpoint for staff to grant extensions to multiple students at once +# +class StaffGrantExtensionApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers DbHelpers + + before do + authenticated? + error!({ + error: 'Not authorized to grant extensions', + code: 'UNAUTHORIZED', + details: {} + }, 403) unless current_user.has_tutor_capability? + end + + desc 'Grant extensions to multiple students', + detail: 'This endpoint allows staff to grant extensions to multiple students at once for a specific task. The operation is atomic - either all extensions are granted or none are. Students not found in the unit are automatically skipped without affecting the transaction.', + success: [ + { code: 201, message: 'Extensions granted successfully' } + ], + failure: [ + { code: 400, message: 'Some extensions failed to be granted' }, + { code: 403, message: 'Not authorized to grant extensions for this unit' }, + { code: 404, message: 'Unit or task definition not found' }, + { code: 500, message: 'Internal server error' } + ], + response: { + successful: [ + { + student_id: 'Integer - ID of the student', + project_id: 'Integer - ID of the project', + weeks_requested: 'Integer - Number of weeks extension granted', + extension_response: 'String - Human readable message with new due date', + task_status: 'String - Updated status of the task' + } + ], + failed: [ + { + student_id: 'Integer - ID of the student', + project_id: 'Integer - ID of the project', + error: 'String - Error message explaining why extension failed' + } + ], + skipped: [ + { + student_id: 'Integer - ID of the student', + reason: 'String - Reason why the student was skipped' + } + ] + } + params do + requires :student_ids, type: Array[Integer], desc: 'List of student IDs to grant extensions to' + requires :task_definition_id, type: Integer, desc: 'Task definition ID' + requires :weeks_requested, type: Integer, desc: 'Number of weeks to extend by' + requires :comment, type: String, desc: 'Reason for extension' + end + post '/units/:unit_id/staff-grant-extension' do + unit = Unit.find(params[:unit_id]) + task_definition = unit.task_definitions.find(params[:task_definition_id]) + + # Use transaction to ensure atomic operation + ActiveRecord::Base.transaction do + results = { + successful: [], + failed: [], + skipped: [] + } + + params[:student_ids].each do |student_id| + # Find project for this student in the unit + project = unit.projects.find_by(user_id: student_id) + if project.nil? + results[:skipped] << { + student_id: student_id, + reason: 'Student not found in unit' + } + next + end + + result = ExtensionService.grant_extension( + project.id, + task_definition.id, + current_user, + params[:weeks_requested], + params[:comment], + true # is_staff_grant = true + ) + + if result[:success] + extension_comment = result[:result] + results[:successful] << { + student_id: student_id, + project_id: project.id, + weeks_requested: extension_comment.extension_weeks, + extension_response: extension_comment.extension_response, + task_status: extension_comment.task.status + } + else + results[:failed] << { + student_id: student_id, + project_id: project.id, + error: result[:error] + } + # If it's a validation error (403), raise it immediately + error!({ error: result[:error] }, result[:status]) if result[:status] == 403 + end + end + + # If any extensions failed (but not due to validation), rollback the entire transaction + if results[:failed].any? + error!({ error: 'Some extensions failed to be granted', results: results }, 400) + end + + status 201 + present results, with: Grape::Presenters::Presenter + end + rescue ActiveRecord::RecordNotFound + error!({ error: 'Unit or task definition not found' }, 404) + rescue StandardError + error!({ error: 'An unexpected error occurred' }, 500) + end +end diff --git a/app/models/unit.rb b/app/models/unit.rb index 085e3968b..6d44ecbd9 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -36,6 +36,7 @@ def self.permissions :download_jplag_report, :get_marking_sessions, :get_tutor_times, + :grant_extensions ] # What can convenors do with units? diff --git a/app/services/extension_service.rb b/app/services/extension_service.rb new file mode 100644 index 000000000..59cc7c4a2 --- /dev/null +++ b/app/services/extension_service.rb @@ -0,0 +1,52 @@ +class ExtensionService + def self.grant_extension(project_id, task_definition_id, user, weeks_requested, comment, is_staff_grant = false) + # Find project and task + project = Project.find(project_id) + task_definition = project.unit.task_definitions.find(task_definition_id) + task = project.task_for_task_definition(task_definition) + + # ===== Common Validation Logic (used by both endpoints) ===== + # Validate extension weeks + return { success: false, error: 'Extension weeks cannot be 0', status: 403 } if weeks_requested == 0 + + # Calculate max duration + max_duration = task.weeks_can_extend + duration = weeks_requested + duration = max_duration unless weeks_requested <= max_duration + + # Check if extension would exceed deadline + return { success: false, error: 'Extensions cannot be granted beyond task deadline', status: 403 } if duration <= 0 + + # ===== Student Request Logic (current endpoint) ===== + unless is_staff_grant + # Check task-level authorization for student requests + unless AuthorisationHelpers.authorise?(user, task, :request_extension) + return { success: false, error: 'Not authorised to request an extension for this task', status: 403 } + end + end + + # ===== Staff Grant Logic (new endpoint) ===== + if is_staff_grant + # Check unit-level authorization for staff grants + unless AuthorisationHelpers.authorise?(user, project.unit, :grant_extensions) + return { success: false, error: 'Not authorised to grant extensions for this unit', status: 403 } + end + end + + # ===== Common Extension Logic ===== + # Apply the extension + result = task.apply_for_extension(user, comment, duration) + + # Auto-approve if it's a staff grant + if is_staff_grant + extension_comment = result.becomes(ExtensionComment) + extension_comment.assess_extension(user, true, true) + end + + { success: true, result: result, status: 201 } + rescue ActiveRecord::RecordNotFound => e + { success: false, error: 'Task or project not found', status: 404 } + rescue StandardError => e + { success: false, error: e.message, status: 500 } + end +end diff --git a/test/api/staff_grant_extension_test.rb b/test/api/staff_grant_extension_test.rb new file mode 100644 index 000000000..ad48c474f --- /dev/null +++ b/test/api/staff_grant_extension_test.rb @@ -0,0 +1,249 @@ +require 'test_helper' + +class StaffGrantExtensionTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + + def app + Rails.application + end + + def test_staff_grant_extension_success + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + + td = TaskDefinition.new({ + unit_id: unit.id, + tutorial_stream: unit.tutorial_streams.first, + name: 'Staff Grant Extension Test', + description: 'Test task for staff grant extension', + weighting: 4, + target_grade: 0, + start_date: Time.zone.now - 1.week, + target_date: Time.zone.now + 1.week, + due_date: Time.zone.now + 2.weeks, + abbreviation: 'STAFFGRANTTEST', + restrict_status_updates: false, + upload_requirements: [], + plagiarism_warn_pct: 0.8, + is_graded: false, + max_quality_pts: 0 + }) + td.save! + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: "Staff granted extension" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 201, last_response.status + + response = last_response_body + assert response["successful"].length == 1, "Should have one successful extension" + assert response["failed"].empty?, "Should have no failed extensions" + assert response["successful"][0]["student_id"] == project.student.id, "Should match the student ID" + assert response["successful"][0]["weeks_requested"] == 1, "Should have requested 1 week" + assert response["successful"][0]["extension_response"].present?, "Should have extension response" + assert response["successful"][0]["task_status"].present?, "Should have task status" + + td.destroy! + unit.destroy! + end + + def test_staff_grant_extension_unauthorized + unit = FactoryBot.create(:unit) + project = unit.projects.first + student = project.student # Using student instead of staff + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: "Unauthorized attempt" + } + + add_auth_header_for user: student + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, "Should not allow non-staff to grant extensions" + end + + def test_staff_grant_extension_invalid_weeks + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 0, + comment: "Invalid weeks" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, "Should not allow 0 weeks extension" + end + + def test_staff_grant_extension_negative_weeks + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: -1, + comment: "Negative weeks" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, "Should not allow negative weeks extension" + end + + def test_staff_grant_extension_missing_params + unit = FactoryBot.create(:unit) + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + + data_to_post = { + student_ids: [1], + # Missing task_definition_id and weeks_requested + comment: "Missing params" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 400, last_response.status, "Should require all parameters" + end + + def test_staff_grant_extension_transaction_rollback + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + # Test case 1: One valid student, one skipped student + data_to_post = { + student_ids: [project.student.id, 999999], # One valid, one invalid + task_definition_id: td.id, + weeks_requested: 1, + comment: "Transaction test with skipped student" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 201, last_response.status, "Should succeed for valid student" + + response = last_response_body + assert response["successful"].length == 1, "Should have one successful extension" + assert response["skipped"].length == 1, "Should have one skipped student" + assert response["failed"].empty?, "Should have no failed extensions" + assert response["skipped"][0]["student_id"] == 999999, "Should have skipped the invalid student ID" + assert response["skipped"][0]["reason"] == "Student not found in unit", "Should have correct skip reason" + + # Verify only the valid student got an extension + task = project.task_for_task_definition(td) + assert task.extensions == 1, "Should have one extension for the valid student" + + # Test case 2: Test actual transaction rollback + # Create a second project to test with + project2 = unit.projects.create!( + user: FactoryBot.create(:user, role: Role.student), + enrolled: true + ) + + # First, grant extensions to both students + data_to_post = { + student_ids: [project.student.id, project2.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: "Initial extensions" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 201, last_response.status, "Should succeed for both students" + + # Verify both students got extensions + task1 = project.task_for_task_definition(td) + task2 = project2.task_for_task_definition(td) + assert task1.extensions == 2, "First student should have two extensions" + assert task2.extensions == 1, "Second student should have one extension" + + # Now try to grant extensions with a task that would cause a failure + # Use a task that's past its deadline to force a failure + td.due_date = Time.zone.now - 1.day + td.save! + + data_to_post = { + student_ids: [project.student.id, project2.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: "Transaction rollback test" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, "Should fail with 403 when task is past deadline" + + # Verify neither student got a new extension (transaction rolled back) + task1.reload + task2.reload + assert task1.extensions == 2, "First student should still have two extensions" + assert task2.extensions == 1, "Second student should still have one extension" + + td.destroy! + unit.destroy! + end + + def test_staff_grant_extension_invalid_unit + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: "Invalid unit" + } + + add_auth_header_for user: staff + post_json "/api/units/999999/staff-grant-extension", data_to_post + assert_equal 404, last_response.status, "Should return 404 for invalid unit" + end + + def test_staff_grant_extension_invalid_task + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: 999999, + weeks_requested: 1, + comment: "Invalid task" + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 404, last_response.status, "Should return 404 for invalid task definition" + end +end From d27fd3227df426dff2cd5518ee42ae0c66ed4db3 Mon Sep 17 00:00:00 2001 From: SahiruWithanage Date: Thu, 10 Apr 2025 02:43:54 +1000 Subject: [PATCH 33/46] refactor(tests): replace double quotes with single quotes in non-interpolated strings This aligns the test file with the string formatting convention used in the rest of the codebase. Single quotes are preferred when string interpolation is not needed, improving consistency. Reviewed as part of peer feedback. --- test/api/staff_grant_extension_test.rb | 70 +++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/test/api/staff_grant_extension_test.rb b/test/api/staff_grant_extension_test.rb index ad48c474f..f03b9a62e 100644 --- a/test/api/staff_grant_extension_test.rb +++ b/test/api/staff_grant_extension_test.rb @@ -38,7 +38,7 @@ def test_staff_grant_extension_success student_ids: [project.student.id], task_definition_id: td.id, weeks_requested: 1, - comment: "Staff granted extension" + comment: 'Staff granted extension' } add_auth_header_for user: staff @@ -46,12 +46,12 @@ def test_staff_grant_extension_success assert_equal 201, last_response.status response = last_response_body - assert response["successful"].length == 1, "Should have one successful extension" - assert response["failed"].empty?, "Should have no failed extensions" - assert response["successful"][0]["student_id"] == project.student.id, "Should match the student ID" - assert response["successful"][0]["weeks_requested"] == 1, "Should have requested 1 week" - assert response["successful"][0]["extension_response"].present?, "Should have extension response" - assert response["successful"][0]["task_status"].present?, "Should have task status" + assert response["successful"].length == 1, 'Should have one successful extension' + assert response["failed"].empty?, 'Should have no failed extensions' + assert response["successful"][0]["student_id"] == project.student.id, 'Should match the student ID' + assert response["successful"][0]["weeks_requested"] == 1, 'Should have requested 1 week' + assert response["successful"][0]["extension_response"].present?, 'Should have extension response' + assert response["successful"][0]["task_status"].present?, 'Should have task status' td.destroy! unit.destroy! @@ -67,12 +67,12 @@ def test_staff_grant_extension_unauthorized student_ids: [project.student.id], task_definition_id: td.id, weeks_requested: 1, - comment: "Unauthorized attempt" + comment: 'Unauthorized attempt' } add_auth_header_for user: student post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 403, last_response.status, "Should not allow non-staff to grant extensions" + assert_equal 403, last_response.status, 'Should not allow non-staff to grant extensions' end def test_staff_grant_extension_invalid_weeks @@ -86,12 +86,12 @@ def test_staff_grant_extension_invalid_weeks student_ids: [project.student.id], task_definition_id: td.id, weeks_requested: 0, - comment: "Invalid weeks" + comment: 'Invalid weeks' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 403, last_response.status, "Should not allow 0 weeks extension" + assert_equal 403, last_response.status, 'Should not allow 0 weeks extension' end def test_staff_grant_extension_negative_weeks @@ -105,12 +105,12 @@ def test_staff_grant_extension_negative_weeks student_ids: [project.student.id], task_definition_id: td.id, weeks_requested: -1, - comment: "Negative weeks" + comment: 'Negative weeks' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 403, last_response.status, "Should not allow negative weeks extension" + assert_equal 403, last_response.status, 'Should not allow negative weeks extension' end def test_staff_grant_extension_missing_params @@ -121,12 +121,12 @@ def test_staff_grant_extension_missing_params data_to_post = { student_ids: [1], # Missing task_definition_id and weeks_requested - comment: "Missing params" + comment: 'Missing params' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 400, last_response.status, "Should require all parameters" + assert_equal 400, last_response.status, 'Should require all parameters' end def test_staff_grant_extension_transaction_rollback @@ -141,23 +141,23 @@ def test_staff_grant_extension_transaction_rollback student_ids: [project.student.id, 999999], # One valid, one invalid task_definition_id: td.id, weeks_requested: 1, - comment: "Transaction test with skipped student" + comment: 'Transaction test with skipped student' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 201, last_response.status, "Should succeed for valid student" + assert_equal 201, last_response.status, 'Should succeed for valid student' response = last_response_body - assert response["successful"].length == 1, "Should have one successful extension" - assert response["skipped"].length == 1, "Should have one skipped student" - assert response["failed"].empty?, "Should have no failed extensions" - assert response["skipped"][0]["student_id"] == 999999, "Should have skipped the invalid student ID" - assert response["skipped"][0]["reason"] == "Student not found in unit", "Should have correct skip reason" + assert response["successful"].length == 1, 'Should have one successful extension' + assert response["skipped"].length == 1, 'Should have one skipped student' + assert response["failed"].empty?, 'Should have no failed extensions' + assert response["skipped"][0]["student_id"] == 999999, 'Should have skipped the invalid student ID' + assert response["skipped"][0]["reason"] == 'Student not found in unit', 'Should have correct skip reason' # Verify only the valid student got an extension task = project.task_for_task_definition(td) - assert task.extensions == 1, "Should have one extension for the valid student" + assert task.extensions == 1, 'Should have one extension for the valid student' # Test case 2: Test actual transaction rollback # Create a second project to test with @@ -171,18 +171,18 @@ def test_staff_grant_extension_transaction_rollback student_ids: [project.student.id, project2.student.id], task_definition_id: td.id, weeks_requested: 1, - comment: "Initial extensions" + comment: 'Initial extensions' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 201, last_response.status, "Should succeed for both students" + assert_equal 201, last_response.status, 'Should succeed for both students' # Verify both students got extensions task1 = project.task_for_task_definition(td) task2 = project2.task_for_task_definition(td) - assert task1.extensions == 2, "First student should have two extensions" - assert task2.extensions == 1, "Second student should have one extension" + assert task1.extensions == 2, 'First student should have two extensions' + assert task2.extensions == 1, 'Second student should have one extension' # Now try to grant extensions with a task that would cause a failure # Use a task that's past its deadline to force a failure @@ -193,18 +193,18 @@ def test_staff_grant_extension_transaction_rollback student_ids: [project.student.id, project2.student.id], task_definition_id: td.id, weeks_requested: 1, - comment: "Transaction rollback test" + comment: 'Transaction rollback test' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 403, last_response.status, "Should fail with 403 when task is past deadline" + assert_equal 403, last_response.status, 'Should fail with 403 when task is past deadline' # Verify neither student got a new extension (transaction rolled back) task1.reload task2.reload - assert task1.extensions == 2, "First student should still have two extensions" - assert task2.extensions == 1, "Second student should still have one extension" + assert task1.extensions == 2, 'First student should still have two extensions' + assert task2.extensions == 1, 'Second student should still have one extension' td.destroy! unit.destroy! @@ -221,12 +221,12 @@ def test_staff_grant_extension_invalid_unit student_ids: [project.student.id], task_definition_id: td.id, weeks_requested: 1, - comment: "Invalid unit" + comment: 'Invalid unit' } add_auth_header_for user: staff post_json "/api/units/999999/staff-grant-extension", data_to_post - assert_equal 404, last_response.status, "Should return 404 for invalid unit" + assert_equal 404, last_response.status, 'Should return 404 for invalid unit' end def test_staff_grant_extension_invalid_task @@ -239,11 +239,11 @@ def test_staff_grant_extension_invalid_task student_ids: [project.student.id], task_definition_id: 999999, weeks_requested: 1, - comment: "Invalid task" + comment: 'Invalid task' } add_auth_header_for user: staff post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post - assert_equal 404, last_response.status, "Should return 404 for invalid task definition" + assert_equal 404, last_response.status, 'Should return 404 for invalid task definition' end end From fb80917432003cac243404835769eb80e840a41a Mon Sep 17 00:00:00 2001 From: SahiruWithanage Date: Fri, 11 Apr 2025 18:47:06 +1000 Subject: [PATCH 34/46] refactor(api): unify extension handling via shared service Linked extension_comments_api (student-requested extensions) to use the shared ExtensionService, previously set up for staff-granted extensions. This refactor ensures both student and staff extension flows use the same logic, improving consistency and reducing duplication. --- app/api/extension_comments_api.rb | 19 +++++++++++++------ app/services/extension_service.rb | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/api/extension_comments_api.rb b/app/api/extension_comments_api.rb index 1d510f079..86acd33f2 100644 --- a/app/api/extension_comments_api.rb +++ b/app/api/extension_comments_api.rb @@ -10,13 +10,20 @@ class ExtensionCommentsApi < Grape::API requires :weeks_requested, type: Integer, desc: 'The details of the request' end post '/projects/:project_id/task_def_id/:task_definition_id/request_extension' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) + # Use the ExtensionService to handle the extension request + result = ExtensionService.grant_extension( + params[:project_id], + params[:task_definition_id], + current_user, + params[:weeks_requested], + params[:comment] + ) - # check permissions using specific permission has with addition of request extension if allowed in unit - unless authorise? current_user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to request an extension for this task' }, 403) + # Handle the service response + if result[:success] + present result[:result].serialize(current_user), Grape::Presenters::Presenter + else + error!({ error: result[:error] }, result[:status]) end if project.unit.allow_flexible_dates diff --git a/app/services/extension_service.rb b/app/services/extension_service.rb index 59cc7c4a2..323b430b5 100644 --- a/app/services/extension_service.rb +++ b/app/services/extension_service.rb @@ -19,8 +19,8 @@ def self.grant_extension(project_id, task_definition_id, user, weeks_requested, # ===== Student Request Logic (current endpoint) ===== unless is_staff_grant - # Check task-level authorization for student requests - unless AuthorisationHelpers.authorise?(user, task, :request_extension) + # Check task-level authorization for student requests with specific permission hash + unless AuthorisationHelpers.authorise?(user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) }) return { success: false, error: 'Not authorised to request an extension for this task', status: 403 } end end From ed7ae3def4d6fa39dc5656101e2abdc193d3db95 Mon Sep 17 00:00:00 2001 From: SahiruWithanage Date: Sun, 27 Apr 2025 15:51:11 +1000 Subject: [PATCH 35/46] feat(notifications): send extension notifications Implemented backend logic to send emails to tutor and student when extensions are granted. Also enable it so the front end can use the returned information from the api to display notifications. --- app/api/staff_grant_extension_api.rb | 25 +++ app/mailers/notifications_mailer.rb | 79 +++++++- .../extension_granted.html.erb | 124 +++++++++++++ .../extension_granted.text.erb | 38 ++++ config/environments/test.rb | 3 + test/mailers/notifications_mailer_test.rb | 168 ++++++++++++++++++ 6 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 app/views/notifications_mailer/extension_granted.html.erb create mode 100644 app/views/notifications_mailer/extension_granted.text.erb create mode 100644 test/mailers/notifications_mailer_test.rb diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb index 9c50aa48b..e3d113b13 100644 --- a/app/api/staff_grant_extension_api.rb +++ b/app/api/staff_grant_extension_api.rb @@ -115,9 +115,34 @@ class StaffGrantExtensionApi < Grape::API error!({ error: 'Some extensions failed to be granted', results: results }, 400) end + # Send notifications only if successful and after processing all students + if results[:successful].any? + successful_extensions = results[:successful].map do |result| + # Re-fetch project within the transaction to ensure consistency + project = Project.find(result[:project_id]) + task = project.task_for_task_definition(task_definition) + # Ensure we get the latest extension comment created within this transaction + task.all_comments.where(content_type: :extension).order(created_at: :desc).first + end + + # Filter out any nil results in case a comment wasn't found (shouldn't happen ideally) + successful_extensions.compact! + + if successful_extensions.any? + NotificationsMailer.extension_granted( + successful_extensions, + current_user, + params[:student_ids].count, + results[:failed], + true # is_staff_grant = true + ).deliver_later + end + end + status 201 present results, with: Grape::Presenters::Presenter end + rescue ActiveRecord::RecordNotFound error!({ error: 'Unit or task definition not found' }, 404) rescue StandardError diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 433f2e7eb..34c98badf 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -1,7 +1,20 @@ class NotificationsMailer < ApplicationMailer + + # Load configuration values at class level + def self.doubtfire_host + Doubtfire::Application.config.institution[:host] || 'doubtfire.deakin.edu.au' + end + + def self.doubtfire_product_name + Doubtfire::Application.config.institution[:product_name] || 'Doubtfire' + end + + # Set default from address using class methods + default from: -> { "#{self.class.doubtfire_product_name} <#{@granted_by&.email}>" } + def add_general - @doubtfire_host = Doubtfire::Application.config.institution[:host] - @doubtfire_product_name = Doubtfire::Application.config.institution[:product_name] + @doubtfire_host = self.class.doubtfire_host + @doubtfire_product_name = self.class.doubtfire_product_name @unsubscribe_url = "#{@doubtfire_host}/edit_profile" end @@ -108,6 +121,68 @@ def this_these(num) end end + # Sends a summary email to the staff member who granted the extensions + def extension_granted_summary(extensions, granted_by, total_selected, failed_extensions = []) + @granted_by = granted_by + @extensions = extensions + @total_selected = total_selected + @failed_extensions = failed_extensions + @unit = extensions.any? ? extensions.first.task.unit : nil + @is_tutor = true + + add_general + + email_with_name = %("#{@granted_by.name}" <#{@granted_by.email}>) + mail( + to: email_with_name, + subject: @unit ? "#{@unit.name}: Staff Grant Extensions" : "Staff Grant Extensions", + template_name: 'extension_granted' + ) + end + + # Sends a notification to a student about their granted extension + def extension_granted_notification(extension, granted_by) + @granted_by = granted_by + @extension = extension + @task = extension.task + @student = extension.project.student + @is_tutor = false + + add_general + + email_with_name = %("#{@student.name}" <#{@student.email}>) + tutor_email = %("#{@granted_by.name}" <#{@granted_by.email}>) + + mail( + to: email_with_name, + from: tutor_email, + subject: "#{@task.unit.name}: Extension granted for #{@task.task_definition.name}", + template_name: 'extension_granted' + ) + end + + # Main method to handle extension notifications from staff + def extension_granted(extensions, granted_by, total_selected, failed_extensions = [], is_staff_grant = false) + # Only send notifications for staff-granted bulk extensions + return unless is_staff_grant && (extensions.any? || failed_extensions.any?) + + begin + # Send summary to staff member who granted the extensions + extension_granted_summary(extensions, granted_by, total_selected, failed_extensions).deliver_now + + # Send individual notifications only to students who have enabled email notifications + extensions.each do |extension| + student = extension.project.student + if student.receive_task_notifications + extension_granted_notification(extension, granted_by).deliver_now + end + end + rescue => e + Rails.logger.error "Failed to send extension notifications: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + end + end + helper_method :top_task_desc helper_method :were_was helper_method :are_is diff --git a/app/views/notifications_mailer/extension_granted.html.erb b/app/views/notifications_mailer/extension_granted.html.erb new file mode 100644 index 000000000..d3eb9a397 --- /dev/null +++ b/app/views/notifications_mailer/extension_granted.html.erb @@ -0,0 +1,124 @@ + + + + + + + +
+

Extension Granted

+
+ +
+ <% if @is_tutor %> +

You have granted extensions for the following students:

+ + + + + + + + + + + <% @extensions.each do |extension| %> + + + + + + <% end %> + +
StudentTaskNew Due Date
<%= extension.project.student.name %><%= extension.task.task_definition.name %><%= extension.task.due_date.strftime("%d %b %Y") %>
+ + <% if @failed_extensions.any? %> +

Failed Extensions

+ + + + + + + + + <% @failed_extensions.each do |failed| %> + + + + + <% end %> + +
Student IDError
<%= failed[:student_id] %><%= failed[:error] %>
+ <% end %> + +

Total students selected: <%= @total_selected %>

+

Successfully granted: <%= @extensions.count %>

+ <% if @failed_extensions.any? %> +

Failed: <%= @failed_extensions.count %>

+ <% end %> + <% else %> +

Dear <%= @student.name %>,

+ +

An extension has been granted for your task: <%= @task.task_definition.name %>

+ +

Details:

+
    +
  • New due date: <%= @task.due_date.strftime("%d %b %Y") %>
  • +
  • Granted by: <%= @granted_by.name %>
  • + <% if @extension.comment.present? %> +
  • Comment: <%= @extension.comment %>
  • + <% end %> +
+ <% end %> +
+ + + + diff --git a/app/views/notifications_mailer/extension_granted.text.erb b/app/views/notifications_mailer/extension_granted.text.erb new file mode 100644 index 000000000..f1d986465 --- /dev/null +++ b/app/views/notifications_mailer/extension_granted.text.erb @@ -0,0 +1,38 @@ +<% if @is_tutor %> +You have granted extensions for the following students: + +Extensions granted: +<% @extensions.each do |extension| %> +- <%= extension.project.student.name %>: <%= extension.task.task_definition.name %> + New due date: <%= extension.task.due_date.strftime("%B %d, %Y") %> +<% end %> + +Summary: +- Total selected for extension: <%= @total_selected %> +- Successfully granted: <%= @extensions.count %> +<% if @failed_extensions.present? %> +- Failed to grant: <%= @failed_extensions.count %> + +Failed extensions: +<% @failed_extensions.each do |failed| %> +- Student ID <%= failed[:student_id] %>: <%= failed[:error] %> +<% end %> +<% end %> +<% else %> +Dear <%= @student.name %>, + +An extension has been granted for your task: <%= @task.task_definition.name %> + +Details: +- New due date: <%= @task.due_date.strftime("%B %d, %Y") %> +- Granted by: <%= @granted_by.name %> +<% if @extension.comment.present? %> +- Comment: <%= @extension.comment %> +<% end %> +<% end %> + +Cheers, +The <%= @doubtfire_product_name %> Team + +--- +To unsubscribe from these notifications, visit: <%= @unsubscribe_url %> diff --git a/config/environments/test.rb b/config/environments/test.rb index 24fcb3d4c..b49259719 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -27,6 +27,9 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = true + config.action_mailer.default_url_options = { host: 'test.host' } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/test/mailers/notifications_mailer_test.rb b/test/mailers/notifications_mailer_test.rb new file mode 100644 index 000000000..d353839fb --- /dev/null +++ b/test/mailers/notifications_mailer_test.rb @@ -0,0 +1,168 @@ +require 'test_helper' + +class NotificationsMailerTest < ActionMailer::TestCase + include TestHelpers::AuthHelper + + def setup + # Mock Doubtfire configuration + Doubtfire::Application.config.institution = { + host: 'doubtfire.deakin.edu.au', + product_name: 'Doubtfire' + } + + # Create unit and staff + @unit = FactoryBot.create(:unit) + @staff = FactoryBot.create(:user, role: Role.tutor) + @unit.employ_staff(@staff, Role.tutor) + + # Create a task definition + @task_definition = @unit.task_definitions.create!({ + tutorial_stream: @unit.tutorial_streams.first, + name: 'Test Task', + description: 'Test task for notifications', + weighting: 4, + target_grade: 0, + start_date: Time.zone.now - 1.week, + target_date: Time.zone.now + 1.week, + due_date: Time.zone.now + 2.weeks, + abbreviation: 'TESTTASK', + restrict_status_updates: false, + upload_requirements: [], + plagiarism_warn_pct: 0.8, + is_graded: false, + max_quality_pts: 0 + }) + + # Create students and projects with notification preferences + @students = [] + @projects = [] + + # Create one student with notifications enabled + student_with_notifications = FactoryBot.create(:user, role: Role.student) + student_with_notifications.update(receive_task_notifications: true) + project = @unit.projects.create!(user: student_with_notifications, enrolled: true) + @students << student_with_notifications + @projects << project + + # Create two students without notifications + 2.times do + student = FactoryBot.create(:user, role: Role.student) + student.update(receive_task_notifications: false) + project = @unit.projects.create!(user: student, enrolled: true) + @students << student + @projects << project + end + + # Clear any existing emails before each test + ActionMailer::Base.deliveries.clear + end + + def teardown + @task_definition.destroy! + @unit.destroy! + ActionMailer::Base.deliveries.clear + end + + test 'creates correct extension summary email' do + # Create extensions + extensions = [] + @projects.each do |project| + task = project.task_for_task_definition(@task_definition) + extension = task.apply_for_extension(@staff, 'Test comment', 1) + extension.assess_extension(@staff, true, true) + extensions << extension + end + + # Get the mail object + mail = NotificationsMailer.extension_granted_summary(extensions, @staff, extensions.count) + + # Verify email properties + assert_equal [@staff.email], mail.to + assert_equal "#{@unit.name}: Staff Grant Extensions", mail.subject + assert_match /You have granted extensions for the following students/, mail.html_part.body.to_s + end + + test 'creates correct extension notification email' do + # Create extension + project = @projects.first + task = project.task_for_task_definition(@task_definition) + extension = task.apply_for_extension(@staff, 'Test comment', 1) + extension.assess_extension(@staff, true, true) + + # Get the mail object + mail = NotificationsMailer.extension_granted_notification(extension, @staff) + + # Verify email properties + assert_equal [@students.first.email], mail.to + assert_equal "#{@unit.name}: Extension granted for #{@task_definition.name}", mail.subject + assert_match /Dear #{@students.first.name}/, mail.html_part.body.to_s + end + + test 'creates correct extension summary with failed extensions' do + # Create successful extensions + successful_extensions = [] + @projects.each do |project| + task = project.task_for_task_definition(@task_definition) + extension = task.apply_for_extension(@staff, 'Test comment', 1) + extension.assess_extension(@staff, true, true) + successful_extensions << extension + end + + # Create failed extensions + failed_extensions = [ + { student_id: 999, error: 'Student not found in unit' }, + { student_id: 1000, error: 'Extension cannot be granted beyond task deadline' } + ] + + # Get the mail object + mail = NotificationsMailer.extension_granted_summary( + successful_extensions, + @staff, + successful_extensions.count, + failed_extensions + ) + + # Verify email includes failed extensions + assert_equal [@staff.email], mail.to + assert_match /Failed Extensions/, mail.html_part.body.to_s + assert_match /999/, mail.html_part.body.to_s + assert_match /1000/, mail.html_part.body.to_s + end + + test 'creates correct extension notification with special characters' do + # Create task with special characters + special_task = @unit.task_definitions.create!({ + tutorial_stream: @unit.tutorial_streams.first, + name: 'Test Task with !@#$%^&*()', + description: 'Test task with special characters', + weighting: 4, + target_grade: 0, + start_date: Time.zone.now - 1.week, + target_date: Time.zone.now + 1.week, + due_date: Time.zone.now + 2.weeks, + abbreviation: 'SPECIAL', + restrict_status_updates: false, + upload_requirements: [], + plagiarism_warn_pct: 0.8, + is_graded: false, + max_quality_pts: 0 + }) + + # Create extension + project = @projects.first + task = project.task_for_task_definition(special_task) + extension = task.apply_for_extension(@staff, 'Special characters test', 1) + extension.assess_extension(@staff, true, true) + + # Get the mail object + mail = NotificationsMailer.extension_granted_notification(extension, @staff) + + # Verify email handles special characters + assert_equal [@students.first.email], mail.to + assert_equal "#{@unit.name}: Extension granted for #{special_task.name}", mail.subject + assert_match /Dear #{@students.first.name}/, mail.html_part.body.to_s + + # Clean up + special_task.destroy! + end +end From c4162a1222332ce63d931826dc1e46e8a3ee6fa7 Mon Sep 17 00:00:00 2001 From: Sahiru Withanage <165999067+SahiruWithanage@users.noreply.github.com> Date: Sun, 11 May 2025 22:40:43 +1000 Subject: [PATCH 36/46] Make a comment line change A comment line change made in the staff grant extension feature branch that hasn't been updated here. Changing to keep the consistency. --- app/services/extension_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/extension_service.rb b/app/services/extension_service.rb index 323b430b5..01af8cc4c 100644 --- a/app/services/extension_service.rb +++ b/app/services/extension_service.rb @@ -17,7 +17,7 @@ def self.grant_extension(project_id, task_definition_id, user, weeks_requested, # Check if extension would exceed deadline return { success: false, error: 'Extensions cannot be granted beyond task deadline', status: 403 } if duration <= 0 - # ===== Student Request Logic (current endpoint) ===== + # ===== Student-Initiated Extension Logic (current endpoint) ===== unless is_staff_grant # Check task-level authorization for student requests with specific permission hash unless AuthorisationHelpers.authorise?(user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) }) From b885a34a95c10dbf883b73f85e893e02a5bbdac0 Mon Sep 17 00:00:00 2001 From: SahiruWithanage Date: Mon, 19 May 2025 23:04:53 +1000 Subject: [PATCH 37/46] fix(mailer): fix email sender and add error handling --- app/api/staff_grant_extension_api.rb | 20 +++++--- app/mailers/notifications_mailer.rb | 7 +-- test/mailers/notifications_mailer_test.rb | 58 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb index e3d113b13..52ae1d310 100644 --- a/app/api/staff_grant_extension_api.rb +++ b/app/api/staff_grant_extension_api.rb @@ -129,13 +129,19 @@ class StaffGrantExtensionApi < Grape::API successful_extensions.compact! if successful_extensions.any? - NotificationsMailer.extension_granted( - successful_extensions, - current_user, - params[:student_ids].count, - results[:failed], - true # is_staff_grant = true - ).deliver_later + begin + NotificationsMailer.extension_granted( + successful_extensions, + current_user, + params[:student_ids].count, + results[:failed], + true # is_staff_grant = true + ).deliver_later + rescue => e + # Log error but don't fail the API call, as extensions were successfully granted + Rails.logger.error "Failed to queue extension notification emails: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + end end end diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 34c98badf..389124d48 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -9,9 +9,6 @@ def self.doubtfire_product_name Doubtfire::Application.config.institution[:product_name] || 'Doubtfire' end - # Set default from address using class methods - default from: -> { "#{self.class.doubtfire_product_name} <#{@granted_by&.email}>" } - def add_general @doubtfire_host = self.class.doubtfire_host @doubtfire_product_name = self.class.doubtfire_product_name @@ -133,8 +130,12 @@ def extension_granted_summary(extensions, granted_by, total_selected, failed_ext add_general email_with_name = %("#{@granted_by.name}" <#{@granted_by.email}>) + # Set explicit from address using product name and a default sender + from_address = %("#{self.class.doubtfire_product_name}" ) + mail( to: email_with_name, + from: from_address, subject: @unit ? "#{@unit.name}: Staff Grant Extensions" : "Staff Grant Extensions", template_name: 'extension_granted' ) diff --git a/test/mailers/notifications_mailer_test.rb b/test/mailers/notifications_mailer_test.rb index d353839fb..393b38029 100644 --- a/test/mailers/notifications_mailer_test.rb +++ b/test/mailers/notifications_mailer_test.rb @@ -80,6 +80,10 @@ def teardown assert_equal [@staff.email], mail.to assert_equal "#{@unit.name}: Staff Grant Extensions", mail.subject assert_match /You have granted extensions for the following students/, mail.html_part.body.to_s + + # Verify from address contains no-reply + assert_includes mail.from.first, "no-reply@" + assert_includes mail.from.first, NotificationsMailer.doubtfire_host end test 'creates correct extension notification email' do @@ -96,6 +100,9 @@ def teardown assert_equal [@students.first.email], mail.to assert_equal "#{@unit.name}: Extension granted for #{@task_definition.name}", mail.subject assert_match /Dear #{@students.first.name}/, mail.html_part.body.to_s + + # Verify from address contains staff email + assert_includes mail.from.first, @staff.email end test 'creates correct extension summary with failed extensions' do @@ -127,6 +134,10 @@ def teardown assert_match /Failed Extensions/, mail.html_part.body.to_s assert_match /999/, mail.html_part.body.to_s assert_match /1000/, mail.html_part.body.to_s + + # Verify from address contains no-reply + assert_includes mail.from.first, "no-reply@" + assert_includes mail.from.first, NotificationsMailer.doubtfire_host end test 'creates correct extension notification with special characters' do @@ -162,7 +173,54 @@ def teardown assert_equal "#{@unit.name}: Extension granted for #{special_task.name}", mail.subject assert_match /Dear #{@students.first.name}/, mail.html_part.body.to_s + # Verify from address contains staff email + assert_includes mail.from.first, @staff.email + # Clean up special_task.destroy! end + + test 'creates correct weekly staff summary email' do + # Create data for summary stats + summary_stats = { + unit: @unit, + week_start: Time.zone.now - 1.week, + week_end: Time.zone.now, + staff: {} + } + + unit_role = @unit.unit_roles.find_by(user: @staff) + summary_stats[:staff][unit_role] = { data: "test data" } + + # Get the mail object + mail = NotificationsMailer.weekly_staff_summary(unit_role, summary_stats) + + # Verify email properties + assert_equal [@staff.email], mail.to + assert_equal "#{@unit.name}: Weekly Summary", mail.subject + + # Verify from address contains convenor email + assert_includes mail.from.first, @unit.main_convenor_user.email + end + + test 'creates correct weekly student summary email' do + # Create data for summary stats + summary_stats = { + unit: @unit, + week_start: Time.zone.now - 1.week, + week_end: Time.zone.now + } + + project = @projects.first + + # Get the mail object + mail = NotificationsMailer.weekly_student_summary(project, summary_stats, false) + + # Verify email properties + assert_equal [@students.first.email], mail.to + assert_equal "#{@unit.name}: Weekly Summary", mail.subject + + # Verify from address contains tutor email + assert_includes mail.from.first, project.main_convenor_user.email + end end From 82ea701dc79dbd9fb2120a30d96d7fdeeb3c6794 Mon Sep 17 00:00:00 2001 From: samindiii Date: Sun, 18 May 2025 16:56:39 +1000 Subject: [PATCH 38/46] feat: add notification table and model Also, define relation with user --- app/models/notification.rb | 3 +++ app/models/user.rb | 1 + db/migrate/20250518011250_create_notifications.rb | 10 ++++++++++ 3 files changed, 14 insertions(+) create mode 100644 app/models/notification.rb create mode 100644 db/migrate/20250518011250_create_notifications.rb diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 000000000..c99183b19 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,3 @@ +class Notification < ApplicationRecord + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb index c360c140f..07c8b62ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -150,6 +150,7 @@ def token_for_text?(a_token, token_type) has_many :chip_usage, dependent: :destroy, inverse_of: :tutor, class_name: 'Feedback::ChipUsage' has_many :marking_sessions, dependent: :destroy + has_many :notifications, dependent: :destroy # Model validations/constraints validates :first_name, presence: true diff --git a/db/migrate/20250518011250_create_notifications.rb b/db/migrate/20250518011250_create_notifications.rb new file mode 100644 index 000000000..0f8326726 --- /dev/null +++ b/db/migrate/20250518011250_create_notifications.rb @@ -0,0 +1,10 @@ +class CreateNotifications < ActiveRecord::Migration[7.1] + def change + create_table :notifications do |t| + t.integer :user_id + t.string :message + + t.timestamps + end + end +end From 6a310ba764af07d6ec7cab7c3aa8f1120234af4f Mon Sep 17 00:00:00 2001 From: samindiii Date: Sun, 18 May 2025 16:57:45 +1000 Subject: [PATCH 39/46] feat: define notification api to GET and DELETE --- app/api/api_root.rb | 5 +++++ app/api/notifications_api.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 app/api/notifications_api.rb diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 39e5cfabc..605ec9a6e 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -63,6 +63,11 @@ class ApiRoot < Grape::API mount ScormExtensionCommentsApi mount GroupSetsApi mount LearningOutcomesApi + mount LearningOutcomesApi + # mount LearningAlignmentApi + # the mount above is available in 9.x but has not been ported to `10.0.x` + mount NotificationsApi + mount ProjectsApi mount ProjectsApi mount SettingsApi mount StaffGrantExtensionApi diff --git a/app/api/notifications_api.rb b/app/api/notifications_api.rb new file mode 100644 index 000000000..371b08c5b --- /dev/null +++ b/app/api/notifications_api.rb @@ -0,0 +1,29 @@ +class NotificationsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + before do + authenticated? + end + + desc 'Get current user notifications' + get '/notifications' do + notifications = current_user.notifications.order(created_at: :desc) + # Return array of notifications as JSON (id and message only) + notifications.as_json(only: [:id, :message]) + end + + desc 'Delete user notification by id' + delete '/notifications/:id' do + notification = current_user.notifications.find_by(id: params[:id]) + error!({ error: 'Notification not found' }, 404) unless notification + notification.destroy + status 204 + end + + desc 'Delete all user notifications' + delete '/notifications' do + current_user.notifications.delete_all + status 204 + end +end From 3c996710f9f2278f7806044c92c38600695f0c02 Mon Sep 17 00:00:00 2001 From: samindiii Date: Sun, 18 May 2025 16:58:59 +1000 Subject: [PATCH 40/46] Create in-system notifications for students with successfull extensions --- app/api/staff_grant_extension_api.rb | 29 +++++++++++++++----------- test/api/staff_grant_extension_test.rb | 7 +++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb index 52ae1d310..254f8b5fb 100644 --- a/app/api/staff_grant_extension_api.rb +++ b/app/api/staff_grant_extension_api.rb @@ -129,18 +129,23 @@ class StaffGrantExtensionApi < Grape::API successful_extensions.compact! if successful_extensions.any? - begin - NotificationsMailer.extension_granted( - successful_extensions, - current_user, - params[:student_ids].count, - results[:failed], - true # is_staff_grant = true - ).deliver_later - rescue => e - # Log error but don't fail the API call, as extensions were successfully granted - Rails.logger.error "Failed to queue extension notification emails: #{e.message}" - Rails.logger.error e.backtrace.join("\n") + NotificationsMailer.extension_granted( + successful_extensions, + current_user, + params[:student_ids].count, + results[:failed], + true # is_staff_grant = true + ).deliver_later + + # Create in-system notifications for successful extensions + results[:successful].each do |result| + student = User.find_by(id: result[:student_id]) + next unless student + + Notification.create!( + user_id: student.id, + message: "#{unit.name}: You were granted an extension for task '#{task_definition.name}'." + ) end end end diff --git a/test/api/staff_grant_extension_test.rb b/test/api/staff_grant_extension_test.rb index f03b9a62e..a90d74dcd 100644 --- a/test/api/staff_grant_extension_test.rb +++ b/test/api/staff_grant_extension_test.rb @@ -53,6 +53,13 @@ def test_staff_grant_extension_success assert response["successful"][0]["extension_response"].present?, 'Should have extension response' assert response["successful"][0]["task_status"].present?, 'Should have task status' + notifications = Notification.where(user_id: project.student.id) + assert_equal 1, notifications.count, 'Should create one notification for the student' + notification = notifications.first + assert_match /You were granted an extension for task/, notification.message + assert_match /#{td.name}/, notification.message + assert_match /#{unit.name}/, notification.message + td.destroy! unit.destroy! end From dca5e00266699749979ff1952ab67b874dda9db1 Mon Sep 17 00:00:00 2001 From: SahiruWithanage Date: Sun, 14 Sep 2025 15:56:37 +1000 Subject: [PATCH 41/46] feat: fix email notifications for SGE feature - Fix delivery method consistency (deliver_now vs deliver_later) - Fix ExtensionComment retrieval logic to prevent race conditions - Add proper error handling for email failures - Fix mailer method calls (class methods vs instance methods) - Add input validation for API parameters - Improve error logging and debugging - Ensure thread-safe extension processing The email notification system now works correctly: - Staff receive summary emails for all granted extensions - Students receive individual notification emails - Proper error handling prevents API failures on email issues - All email delivery methods are consistent across the project --- app/api/staff_grant_extension_api.rb | 46 ++++++++++++++++++---------- app/mailers/notifications_mailer.rb | 4 +-- app/models/unit.rb | 3 +- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb index 254f8b5fb..fe93b3bb3 100644 --- a/app/api/staff_grant_extension_api.rb +++ b/app/api/staff_grant_extension_api.rb @@ -55,8 +55,8 @@ class StaffGrantExtensionApi < Grape::API params do requires :student_ids, type: Array[Integer], desc: 'List of student IDs to grant extensions to' requires :task_definition_id, type: Integer, desc: 'Task definition ID' - requires :weeks_requested, type: Integer, desc: 'Number of weeks to extend by' - requires :comment, type: String, desc: 'Reason for extension' + requires :weeks_requested, type: Integer, desc: 'Number of weeks to extend by (1-4)' + requires :comment, type: String, desc: 'Reason for extension (max 300 characters)' end post '/units/:unit_id/staff-grant-extension' do unit = Unit.find(params[:unit_id]) @@ -97,7 +97,8 @@ class StaffGrantExtensionApi < Grape::API project_id: project.id, weeks_requested: extension_comment.extension_weeks, extension_response: extension_comment.extension_response, - task_status: extension_comment.task.status + task_status: extension_comment.task.status, + extension_comment: extension_comment # Store internally for notifications } else results[:failed] << { @@ -117,25 +118,38 @@ class StaffGrantExtensionApi < Grape::API # Send notifications only if successful and after processing all students if results[:successful].any? + # Use the extension comments directly from the service results (thread-safe) successful_extensions = results[:successful].map do |result| - # Re-fetch project within the transaction to ensure consistency - project = Project.find(result[:project_id]) - task = project.task_for_task_definition(task_definition) - # Ensure we get the latest extension comment created within this transaction - task.all_comments.where(content_type: :extension).order(created_at: :desc).first + extension_comment = result[:extension_comment] + if extension_comment.nil? + Rails.logger.warn "No extension comment found for project #{result[:project_id]}" + nil + else + Rails.logger.debug "Using extension comment: #{extension_comment.id} for project #{result[:project_id]}" + extension_comment + end end - # Filter out any nil results in case a comment wasn't found (shouldn't happen ideally) + # Filter out any nil results in case a comment wasn't found successful_extensions.compact! + Rails.logger.info "Processing #{successful_extensions.count} successful extensions for notifications" if successful_extensions.any? - NotificationsMailer.extension_granted( - successful_extensions, - current_user, - params[:student_ids].count, - results[:failed], - true # is_staff_grant = true - ).deliver_later + begin + Rails.logger.info "Sending extension notifications for #{successful_extensions.count} extensions" + NotificationsMailer.extension_granted( + successful_extensions, + current_user, + params[:student_ids].count, + results[:failed], + true # is_staff_grant = true + ).deliver_now + Rails.logger.info "Extension notifications sent successfully" + rescue => e + Rails.logger.error "Failed to send extension notifications: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + # Don't fail the entire request if email fails, but log the error + end # Create in-system notifications for successful extensions results[:successful].each do |result| diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 389124d48..cc42bc341 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -169,13 +169,13 @@ def extension_granted(extensions, granted_by, total_selected, failed_extensions begin # Send summary to staff member who granted the extensions - extension_granted_summary(extensions, granted_by, total_selected, failed_extensions).deliver_now + NotificationsMailer.extension_granted_summary(extensions, granted_by, total_selected, failed_extensions).deliver_now # Send individual notifications only to students who have enabled email notifications extensions.each do |extension| student = extension.project.student if student.receive_task_notifications - extension_granted_notification(extension, granted_by).deliver_now + NotificationsMailer.extension_granted_notification(extension, granted_by).deliver_now end end rescue => e diff --git a/app/models/unit.rb b/app/models/unit.rb index 6d44ecbd9..c0a4e9bea 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -65,7 +65,8 @@ def self.permissions :get_tutor_times, :get_tutor_times_summary, :get_marking_sessions, - :upload_grades_csv + :upload_grades_csv, + :grant_extensions ] # What can admin do with units? From 6bee233c71ee392a620d617ea2e6a3aa8b3c8aaa Mon Sep 17 00:00:00 2001 From: SahiruWithanage Date: Sun, 14 Sep 2025 22:09:11 +1000 Subject: [PATCH 42/46] refactor: use unit code instead of unit name in extension notifications - Change notification message from unit.name to unit.code for better consistency - Unit codes are more concise and standardized (e.g., 'SITXXX') - Improves readability of extension grant notifications --- app/api/staff_grant_extension_api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb index fe93b3bb3..e63c3360d 100644 --- a/app/api/staff_grant_extension_api.rb +++ b/app/api/staff_grant_extension_api.rb @@ -158,7 +158,7 @@ class StaffGrantExtensionApi < Grape::API Notification.create!( user_id: student.id, - message: "#{unit.name}: You were granted an extension for task '#{task_definition.name}'." + message: "#{unit.code}: You were granted an extension for task '#{task_definition.name}'." ) end end From 755f467a4b3d6eeb3aa93cf55839bb5d54033fd6 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 17 Jan 2026 22:52:54 +1000 Subject: [PATCH 43/46] chore: update gems, regen lockfile, schema version --- Gemfile.lock | 523 ++++++++++++++++++++++---------------------- app/api/api_root.rb | 1 - db/schema.rb | 18 +- 3 files changed, 283 insertions(+), 259 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b473c814a..68cf502eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,32 +1,30 @@ GEM remote: https://rubygems.org/ specs: - Ascii85 (1.1.1) - action_text-trix (2.1.16) - railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + Ascii85 (2.0.1) + actioncable (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.0.2) + actionpack (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activesupport (= 8.0.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.0.2) + actionview (= 8.0.2) + activesupport (= 8.0.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -34,73 +32,73 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) - action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actiontext (8.0.2) + actionpack (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.0.2) + activesupport (= 8.0.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.0.2) + activesupport (= 8.0.2) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.0.2) + activesupport (= 8.0.2) + activerecord (8.0.2) + activemodel (= 8.0.2) + activesupport (= 8.0.2) timeout (>= 0.4.0) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activesupport (= 8.0.2) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.0.2) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) - json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) afm (0.2.2) - amq-protocol (2.3.2) - ast (2.4.2) + amq-protocol (2.3.3) + ast (2.4.3) backport (1.2.0) base64 (0.2.0) bcrypt (3.1.20) - benchmark (0.3.0) + benchmark (0.4.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.8) + bigdecimal (3.1.9) bindata (2.5.0) - bootsnap (1.18.3) + bootsnap (1.18.4) msgpack (~> 1.2) builder (3.3.0) - bunny (2.22.0) - amq-protocol (~> 2.3, >= 2.3.1) + bunny (2.24.0) + amq-protocol (~> 2.3) sorted_set (~> 1, >= 1.0.2) bunny-pub-sub (0.5.2) bunny (~> 2.14) - byebug (11.1.3) + byebug (12.0.0) + cgi (0.4.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) ci_reporter (2.1.0) @@ -109,18 +107,21 @@ GEM code_analyzer (0.5.5) sexp_processor coderay (1.1.3) - concurrent-ruby (1.3.3) - connection_pool (2.4.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) crack (1.0.0) bigdecimal rexml crass (1.0.6) - csv (3.3.0) - database_cleaner-active_record (2.1.0) + cronex (0.15.0) + tzinfo + unicode (>= 0.4.4.5) + csv (3.3.3) + database_cleaner-active_record (2.2.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.5.1) + date (3.4.1) devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -130,139 +131,145 @@ GEM devise_ldap_authenticatable (0.8.7) devise (>= 3.4.1) net-ldap (>= 0.16.0) - diff-lcs (1.5.1) - docile (1.4.0) + diff-lcs (1.6.1) + docile (1.4.1) domain_name (0.6.20240107) - dotenv (3.1.2) + dotenv (3.1.7) drb (2.2.1) - dry-core (1.0.1) + dry-core (1.1.0) concurrent-ruby (~> 1.0) + logger zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-logic (1.5.0) + dry-inflector (1.2.0) + dry-logic (1.6.0) + bigdecimal concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) + dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-types (1.7.2) + dry-types (1.8.2) bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) - e2mmap (0.1.0) - erubi (1.12.0) + erb (4.0.4) + cgi (>= 0.3.3) + erubi (1.13.1) erubis (2.7.0) et-orbi (1.2.11) tzinfo ethon (0.16.0) ffi (>= 1.15.0) - factory_bot (6.4.6) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) + factory_bot (6.5.1) + activesupport (>= 6.1.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) railties (>= 5.0.0) - faker (3.4.1) + faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (2.9.1) - faraday-net_http (>= 2.0, < 3.2) + faraday (2.12.2) + faraday-net_http (>= 2.0, < 3.5) + json + logger faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) - faraday-net_http (3.1.0) - net-http - ffi (1.17.0) - fugit (1.11.0) + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + ffi (1.17.1-x86_64-linux-gnu) + fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - grape (2.0.0) - activesupport (>= 5) - builder + grape (2.3.0) + activesupport (>= 6) dry-types (>= 1.1) - mustermann-grape (~> 1.0.0) - rack (>= 1.3.0) - rack-accept + mustermann-grape (~> 1.1.0) + rack (>= 2) + zeitwerk grape-entity (1.0.1) activesupport (>= 3.0.0) multi_json (>= 1.3.2) - grape-swagger (2.1.0) + grape-swagger (2.1.2) grape (>= 1.7, < 3.0) rack-test (~> 2) - grape-swagger-rails (0.5.0) + grape-swagger-rails (0.6.0) + ostruct railties (>= 6.0.6.1) - hashdiff (1.1.0) + hashdiff (1.1.2) hashery (2.1.2) - hashie (5.1.0) - logger + hashie (5.0.0) hirb (0.7.3) http-accept (1.7.0) - http-cookie (1.0.6) + http-cookie (1.0.8) domain_name (~> 0.5) - i18n (1.14.5) + i18n (1.14.7) concurrent-ruby (~> 1.0) - icalendar (2.10.1) + icalendar (2.10.3) ice_cube (~> 0.16) - ice_cube (0.16.4) - io-console (0.7.2) - irb (1.13.1) + ostruct + ice_cube (0.17.0) + io-console (0.8.1) + irb (1.15.1) + pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jaro_winkler (1.6.0) - json (2.7.2) - json-jwt (1.16.6) + json (2.10.2) + json-jwt (1.16.7) activesupport (>= 4.2) aes_key_wrap base64 bindata faraday (~> 2.0) faraday-follow_redirects - jwt (3.1.2) + jwt (2.10.1) base64 - kramdown (2.4.0) - rexml + kramdown (2.5.1) + rexml (>= 3.3.9) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - language_server-protocol (3.17.0.3) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.22.0) + loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.9.0) - logger + mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.1.0) - mime-types (3.5.2) + marcel (1.0.4) + mime-types (3.6.2) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.0604) + mime-types-data (3.2025.0325) mini_mime (1.1.5) - mini_portile2 (2.8.9) - minitest (5.23.1) + minitest (5.25.5) minitest-around (0.5.0) minitest (~> 5.0) - minitest-rails (8.1.0) + minitest-rails (8.0.0) minitest (~> 5.20) - railties (>= 8.1.0, < 8.2.0) + railties (>= 8.0.0, < 8.1.0) moss_ruby (1.1.4) tcp_timeout (~> 0.1.1) - msgpack (1.7.2) + msgpack (1.8.0) multi_json (1.15.0) - multi_xml (0.8.1) - bigdecimal (>= 3.1, < 5) - mustermann (3.0.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + mustermann (3.0.3) ruby2_keywords (~> 0.0.1) - mustermann-grape (1.0.2) + mustermann-grape (1.1.0) mustermann (>= 1.0.0) mysql2 (0.5.6) - net-http (0.4.1) + net-http (0.6.0) uri - net-imap (0.6.2) + net-imap (0.5.6) date net-protocol net-ldap (0.19.0) @@ -270,76 +277,77 @@ GEM net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.0) + net-smtp (0.5.1) net-protocol netrc (0.11.0) - nio4r (2.7.3) - nokogiri (1.16.5) - mini_portile2 (~> 2.8.2) + nio4r (2.7.4) + nokogiri (1.18.7-x86_64-linux-gnu) racc (~> 1.4) numerizer (0.1.1) - oauth2 (2.0.18) - faraday (>= 0.17.3, < 4.0) - jwt (>= 1.0, < 4.0) - logger (~> 1.2) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) multi_xml (~> 0.5) rack (>= 1.2, < 4) - snaky_hash (~> 2.0, >= 2.0.3) - version_gem (~> 1.1, >= 1.1.9) + snaky_hash (~> 2.0) + version_gem (~> 1.1) observer (0.1.2) orm_adapter (0.5.0) - parallel (1.25.1) - parser (3.3.2.0) + ostruct (0.6.1) + parallel (1.26.3) + parser (3.3.7.4) ast (~> 2.4.1) racc - pdf-reader (2.12.0) - Ascii85 (~> 1.0) + pdf-reader (2.14.1) + Ascii85 (>= 1.0, < 3.0, != 2.0.0) afm (~> 0.2.1) hashery (~> 2.0) ruby-rc4 ttfunk - pkg-config (1.5.6) - prism (0.29.0) - psych (5.1.2) + pkg-config (1.6.0) + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + prism (1.5.1) + psych (5.2.3) + date stringio - public_suffix (5.0.5) - puma (6.4.2) + public_suffix (6.0.1) + puma (6.6.0) nio4r (~> 2.0) raabro (1.4.0) - racc (1.8.0) - rack (3.0.11) - rack-accept (0.4.5) - rack (>= 0.4) + racc (1.8.1) + rack (3.1.12) rack-cors (2.0.2) rack (>= 2.0.0) - rack-session (2.0.0) + rack-session (2.1.0) + base64 (>= 0.1.0) rack (>= 3.0.0) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rackup (2.1.0) + rackup (2.2.1) rack (>= 3) - webrick (~> 1.8) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.0.2) + actioncable (= 8.0.2) + actionmailbox (= 8.0.2) + actionmailer (= 8.0.2) + actionpack (= 8.0.2) + actiontext (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activemodel (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.0.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-latex (2.3.5) rails (>= 3.0.0, < 9) rails_best_practices (1.23.2) @@ -350,30 +358,31 @@ GEM json require_all (~> 3.0) ruby-progressbar - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) - tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (2.8.4) + rbs (3.9.2) + logger rbtree (0.4.6) - rdoc (6.7.0) + rdoc (6.14.0) + erb psych (>= 4.0.0) - redis (5.2.0) + redis (5.4.0) redis-client (>= 0.22.0) - redis-client (0.22.2) + redis-client (0.24.0) connection_pool - regexp_parser (2.9.2) - reline (0.5.8) + regexp_parser (2.10.0) + reline (0.6.0) io-console (~> 0.5) require_all (3.0.0) responders (3.1.1) @@ -384,171 +393,171 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - reverse_markdown (2.1.1) + reverse_markdown (3.0.0) nokogiri - rexml (3.2.9) - strscan - rmagick (6.0.1) + rexml (3.4.1) + rmagick (6.1.1) observer (~> 0.1) pkg-config (~> 1.4) - roo (2.7.1) + roo (2.10.1) nokogiri (~> 1) - rubyzip (~> 1.1, < 2.0.0) + rubyzip (>= 1.3.0, < 3.0.0) roo-xls (1.2.0) nokogiri roo (>= 2.0.0, < 3) spreadsheet (> 0.9.0) - rouge (4.2.1) - rubocop (1.64.1) + rouge (4.5.1) + rubocop (1.75.1) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.43.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) - rubocop-factory_bot (2.26.1) - rubocop (~> 1.61) - rubocop-faker (1.1.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.43.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-factory_bot (2.27.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-faker (1.3.0) faker (>= 2.12.0) - rubocop (>= 0.82.0) - rubocop-minitest (0.36.0) - rubocop (>= 1.61, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-performance (1.23.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-minitest (0.37.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-performance (1.24.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.30.3) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) ruby-filemagic (0.7.3) - ruby-lsp (0.17.2) + ruby-lsp (0.23.13) language_server-protocol (~> 3.17.0) - prism (>= 0.29.0, < 0.30) + prism (>= 1.2, < 2.0) + rbs (>= 3, < 4) sorbet-runtime (>= 0.5.10782) ruby-ole (1.2.13.1) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby-saml (1.13.0) - nokogiri (>= 1.10.5) + ruby-saml (1.18.0) + nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) - rubyzip (1.3.0) + rubyzip (2.4.1) securerandom (0.4.1) - set (1.1.0) - sexp_processor (4.17.1) - shellwords (0.2.0) - sidekiq (7.2.4) - concurrent-ruby (< 2) + set (1.1.1) + sexp_processor (4.17.3) + shellwords (0.2.2) + sidekiq (7.3.9) + base64 connection_pool (>= 2.3.0) + logger rack (>= 2.2.4) - redis-client (>= 0.19.0) - sidekiq-cron (1.12.0) - fugit (~> 1.8) + redis-client (>= 0.22.2) + sidekiq-cron (2.2.0) + cronex (>= 0.13.0) + fugit (~> 1.8, >= 1.11.1) globalid (>= 1.0.1) - sidekiq (>= 6) - sidekiq-status (4.0.0) - base64 + sidekiq (>= 6.5.0) + sidekiq-status (3.0.3) chronic_duration - logger - sidekiq (>= 7, < 9) - sidekiq-unique-jobs (8.0.13) + sidekiq (>= 6.0, < 8) + sidekiq-unique-jobs (8.0.10) concurrent-ruby (~> 1.0, >= 1.0.5) - sidekiq (>= 7.0.0, < 9.0.0) + sidekiq (>= 7.0.0, < 8.0.0) thor (>= 1.0, < 3.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) - snaky_hash (2.0.3) - hashie (>= 0.1.0, < 6) - version_gem (>= 1.1.8, < 3) - solargraph (0.50.0) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) + solargraph (0.53.4) backport (~> 1.2) benchmark bundler (~> 2.0) diff-lcs (~> 1.4) - e2mmap - jaro_winkler (~> 1.5) + jaro_winkler (~> 1.6) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.1) + logger (~> 1.6) + observer (~> 0.1) + ostruct (~> 0.6) parser (~> 3.0) - rbs (~> 2.0) - reverse_markdown (~> 2.0) + rbs (~> 3.3) + reverse_markdown (>= 2.0, < 4) rubocop (~> 1.38) thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - sorbet-runtime (0.5.11422) + yard-solargraph (~> 0.1) + sorbet-runtime (0.5.11966) sorted_set (1.0.3) rbtree set (~> 1.0) - spreadsheet (1.3.1) + spreadsheet (1.3.4) bigdecimal + logger ruby-ole sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.5.1) + sprockets-rails (3.5.2) actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - stringio (3.1.0) - strscan (3.1.0) + stringio (3.1.6) tca_client (1.0.4) typhoeus (~> 1.0, >= 1.0.1) tcp_timeout (0.1.1) - thor (1.3.1) - tilt (2.3.0) - timeout (0.4.1) - tsort (0.2.0) + thor (1.3.2) + tilt (2.6.0) + timeout (0.4.3) ttfunk (1.8.0) bigdecimal (~> 3.1) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) - uri (1.1.1) + unicode (0.4.4.5) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.4) useragent (0.16.11) - version_gem (1.1.9) + version_gem (1.1.6) warden (1.2.9) rack (>= 2.0.9) - webmock (3.23.1) + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) - websocket-driver (0.8.0) + websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - yard (0.9.36) - zeitwerk (2.6.15) + yard (0.9.37) + yard-solargraph (0.1.0) + yard (~> 0.9) + zeitwerk (2.7.2) PLATFORMS - aarch64-linux-gnu - aarch64-linux-musl - arm-linux - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86-linux - x86-linux-gnu - x86-linux-musl - x86_64-darwin - x86_64-linux x86_64-linux-gnu - x86_64-linux-musl DEPENDENCIES better_errors @@ -617,4 +626,4 @@ RUBY VERSION ruby 3.4.7p58 BUNDLED WITH - 2.5.11 + 2.6.9 diff --git a/app/api/api_root.rb b/app/api/api_root.rb index f262edba2..46c609479 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -68,7 +68,6 @@ class ApiRoot < Grape::API # the mount above is available in 9.x but has not been ported to `10.0.x` mount NotificationsApi mount ProjectsApi - mount ProjectsApi mount SettingsApi mount StaffGrantExtensionApi mount StudentsApi diff --git a/db/schema.rb b/db/schema.rb index 0f1e6f5f9..91184060a 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.1].define(version: 2025_11_02_221253) do +ActiveRecord::Schema[8.0].define(version: 2025_12_12_010033) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "abbreviation", null: false t.datetime "created_at", null: false @@ -90,6 +90,15 @@ t.datetime "updated_at", null: false end + create_table "discussion_prompts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.text "content", null: false + t.datetime "created_at", null: false + t.integer "priority", default: 0 + t.bigint "task_definition_id", null: false + t.datetime "updated_at", null: false + t.index ["task_definition_id"], name: "index_discussion_prompts_on_task_definition_id" + end + create_table "feedback_chips", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.text "chip_text" t.text "comment_text" @@ -199,6 +208,13 @@ t.index ["user_id"], name: "index_marking_sessions_on_user_id" end + create_table "notifications", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "message" + t.datetime "updated_at", null: false + t.integer "user_id" + end + create_table "overseer_assessments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.datetime "created_at", null: false t.string "result_task_status" From 1b8c7241cfff389fb6bd418c66b07854a9a2a753 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 17 Jan 2026 22:59:51 +1000 Subject: [PATCH 44/46] fix: remove duplicate api endpoints --- app/api/api_root.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 46c609479..e039e0b6c 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -63,7 +63,6 @@ class ApiRoot < Grape::API mount ScormExtensionCommentsApi mount GroupSetsApi mount LearningOutcomesApi - mount LearningOutcomesApi # mount LearningAlignmentApi # the mount above is available in 9.x but has not been ported to `10.0.x` mount NotificationsApi From c7f9b669101cceaf88a06a80da4d49173ef31678 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 18 Jan 2026 01:20:12 +1000 Subject: [PATCH 45/46] chore: address rubocop nits --- .rubocop_todo.yml | 931 ++++----------------------- app/api/staff_grant_extension_api.rb | 21 +- app/mailers/notifications_mailer.rb | 4 +- app/services/extension_service.rb | 31 +- 4 files changed, 176 insertions(+), 811 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6268621a0..0fc47dee4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,29 +1,11 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2022-12-29 09:30:33 UTC using RuboCop version 1.41.1. +# on 2026-01-17 13:44:49 UTC using RuboCop version 1.75.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: outdent, indent -Layout/AccessModifierIndentation: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: with_first_argument, with_fixed_indentation -Layout/ArgumentAlignment: - Exclude: - - 'app/api/activity_types_authenticated_api.rb' - - 'app/api/authentication_api.rb' - - 'app/api/campuses_authenticated_api.rb' - # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -32,90 +14,25 @@ Layout/ArrayAlignment: Exclude: - 'app/helpers/file_helper.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: IndentationWidth. -Layout/AssignmentIndentation: - Exclude: - - 'lib/tasks/init.rake' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith. -# SupportedStylesAlignWith: either, start_of_block, start_of_line -Layout/BlockAlignment: - Exclude: - - 'app/models/project.rb' - - 'config/deakin.rb' - -# Offense count: 19 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth. -# SupportedStyles: case, end -Layout/CaseIndentation: - Exclude: - - 'app/models/task_status.rb' - - 'app/models/webcal.rb' - - 'config/deakin.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Layout/ClosingParenthesisIndentation: - Exclude: - - 'app/api/units_api.rb' - - 'app/models/unit.rb' - - 'config/deakin.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment. -Layout/CommentIndentation: - Exclude: - - 'config/initializers/inflections.rb' - -# Offense count: 76 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: leading, trailing Layout/DotPosition: Exclude: - - 'app/api/group_sets_api.rb' - - 'app/api/task_definitions_api.rb' - - 'app/api/tasks_api.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/tutorial_enrolment.rb' - 'app/models/unit.rb' - - 'app/models/unit_role.rb' - - 'config/deakin.rb' - - 'lib/tasks/maintenance.rake' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Layout/ElseAlignment: - Exclude: - - 'app/api/students_api.rb' - - 'app/models/task_definition.rb' -# Offense count: 67 +# Offense count: 28 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLineAfterGuardClause: Enabled: false -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'app/api/discussion_comment_api.rb' - - 'app/models/comments/task_comment.rb' - # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines. +# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Exclude: - 'app/models/overseer_assessment.rb' - - 'app/models/role.rb' - 'app/models/unit.rb' # Offense count: 13 @@ -123,101 +40,35 @@ Layout/EmptyLineBetweenDefs: Layout/EmptyLines: Exclude: - 'app/api/submission/portfolio_evidence_api.rb' - - 'app/helpers/csv_helper.rb' - - 'app/models/auth_token.rb' - 'app/models/overseer_assessment.rb' - - 'app/models/role.rb' - 'app/models/unit.rb' - - 'config/environments/development.rb' - - 'lib/tasks/init.rake' -# Offense count: 9 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: around, only_before -Layout/EmptyLinesAroundAccessModifier: - Exclude: - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/tutorial.rb' - - 'app/models/tutorial_enrolment.rb' - - 'app/models/tutorial_stream.rb' - - 'app/models/unit.rb' - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'app/api/submission/portfolio_evidence_api.rb' - - 'app/api/webcal_public_api.rb' - - 'app/models/tutorial_enrolment.rb' - - 'config/environments/production.rb' - - 'lib/tasks/init.rake' - - 'lib/tasks/send_status_emails.rake' - -# Offense count: 22 +# Offense count: 23 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only Layout/EmptyLinesAroundClassBody: Enabled: false -# Offense count: 5 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLinesAroundExceptionHandlingKeywords: Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/overseer_assessment.rb' + - 'app/api/staff_grant_extension_api.rb' - 'app/models/task.rb' - - 'app/models/unit.rb' - - 'lib/assets/ontrack_receive_action.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLinesAroundMethodBody: Exclude: - - 'app/models/portfolio_evidence.rb' - 'app/models/unit_role.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines -Layout/EmptyLinesAroundModuleBody: - Exclude: - - 'app/api/admin/overseer_admin_api.rb' - - 'app/helpers/authentication_helpers.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith, Severity. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Layout/EndAlignment: - Exclude: - - 'app/api/students_api.rb' - - 'app/channels/application_cable/channel.rb' - - 'app/models/task_definition.rb' - - 'config/application.rb' - -# Offense count: 21 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. Layout/ExtraSpacing: Exclude: - - 'app/api/entities/project_entity.rb' - - 'app/api/entities/unit_entity.rb' - - 'app/api/projects_api.rb' - - 'app/api/tutorial_streams_api.rb' - - 'app/helpers/mime_check_helpers.rb' - - 'app/mailers/notifications_mailer.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'config/initializers/swagger.rb' - - 'lib/helpers/database_populator.rb' + - 'app/api/staff_grant_extension_api.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). @@ -227,251 +78,54 @@ Layout/FirstArrayElementIndentation: Exclude: - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Layout/FirstHashElementIndentation: Exclude: + - 'app/api/staff_grant_extension_api.rb' - 'app/models/unit.rb' - - 'config/no_institution_setting.rb' - - 'lib/helpers/database_populator.rb' - -# Offense count: 114 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. -# SupportedHashRocketStyles: key, separator, table -# SupportedColonStyles: key, separator, table -# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/HashAlignment: - Exclude: - - 'app/api/authentication_api.rb' - - 'app/api/settings_api.rb' - - 'app/models/task.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'config/environments/production.rb' - - 'config/no_institution_setting.rb' - - 'lib/helpers/database_populator.rb' - - 'lib/helpers/find_or_create_students.rb' # Offense count: 9 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: normal, indented_internal_methods -Layout/IndentationConsistency: - Exclude: - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/tutorial_enrolment.rb' - - 'config/deakin.rb' - -# Offense count: 36 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: Width, AllowedPatterns. Layout/IndentationWidth: Exclude: - - 'app/api/students_api.rb' - - 'app/channels/application_cable/channel.rb' - - 'app/mailers/notifications_mailer.rb' - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/tutorial_enrolment.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - - 'config/application.rb' - - 'config/deakin.rb' - - 'config/initializers/inflections.rb' - - 'config/no_institution_setting.rb' - - 'lib/tasks/send_status_emails.rake' - - 'lib/tasks/skip_prod.rake' - -# Offense count: 38 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. -Layout/LeadingCommentSpace: - Exclude: - - 'app/api/entities/group_entity.rb' - - 'app/api/entities/tutorial_entity.rb' - - 'app/api/entities/tutorial_stream_entity.rb' - - 'app/api/task_definitions_api.rb' - - 'app/models/overseer_assessment.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - 'app/models/task_definition.rb' - - 'app/models/task_status.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -Layout/LeadingEmptyLines: - Exclude: - - 'app/api/entities/minimal/minimal_unit_entity.rb' - - 'app/api/entities/minimal/minimal_user_entity.rb' - - 'app/api/entities/user_entity.rb' - - 'app/api/entities/webcal_entity.rb' - - 'config/no_institution_setting.rb' - -# Offense count: 16 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: leading, trailing -Layout/LineContinuationLeadingSpace: - Exclude: - - 'config/application.rb' - -# Offense count: 16 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: space, no_space -Layout/LineContinuationSpacing: - Exclude: - - 'config/application.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/LineEndStringConcatenationIndentation: - Exclude: - - 'config/application.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineMethodCallBraceLayout: - Exclude: - - 'app/api/units_api.rb' - 'app/models/unit.rb' - - 'app/models/unit_role.rb' -# Offense count: 82 +# Offense count: 45 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver Layout/MultilineMethodCallIndentation: Exclude: - - 'app/api/activity_types_authenticated_api.rb' - - 'app/api/admin/overseer_admin_api.rb' - - 'app/api/campuses_authenticated_api.rb' - - 'app/api/group_sets_api.rb' - - 'app/api/learning_outcomes_api.rb' - - 'app/api/task_definitions_api.rb' - - 'app/api/tasks_api.rb' - - 'app/api/teaching_periods_authenticated_api.rb' - - 'app/api/unit_roles_api.rb' - - 'app/api/webcal_api.rb' - - 'app/models/project.rb' - - 'app/models/tutorial_enrolment.rb' - 'app/models/unit.rb' - 'app/models/unit_role.rb' - - 'config/deakin.rb' - -# Offense count: 11 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/MultilineOperationIndentation: - Exclude: - - 'app/api/authentication_api.rb' - - 'app/models/unit.rb' - - 'config/application.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAfterColon: - Exclude: - - 'app/api/extension_comments_api.rb' - - 'config/deakin.rb' -# Offense count: 46 +# Offense count: 35 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAfterComma: Exclude: - - 'app/api/api_root.rb' - - 'app/api/tutorial_streams_api.rb' - - 'app/api/units_api.rb' - - 'app/helpers/application_helper.rb' - 'app/helpers/file_helper.rb' - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/task.rb' - - 'app/models/teaching_period.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAfterMethodName: - Exclude: - - 'app/models/project.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'config/no_institution_setting.rb' - -# Offense count: 14 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAfterNot: Exclude: - - 'app/api/group_sets_api.rb' - - 'app/api/tutorial_enrolments_api.rb' - - 'app/models/comments/extension_comment.rb' - - 'app/models/comments/task_comment.rb' - - 'app/models/group.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/tutorial.rb' - - 'app/models/tutorial_enrolment.rb' - 'app/models/unit.rb' - - 'config/deakin.rb' - -# Offense count: 22 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleInsidePipes. -# SupportedStylesInsidePipes: space, no_space -Layout/SpaceAroundBlockParameters: - Exclude: - - 'app/api/entities/project_entity.rb' - - 'app/models/task.rb' - - 'app/models/webcal.rb' - - 'lib/helpers/database_populator.rb' - - 'lib/tasks/init.rake' - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceAroundEqualsInParameterDefault: - Exclude: - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'lib/helpers/faker_randomiser.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAroundMethodCallOperator: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 7 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator. +# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. # SupportedStylesForExponentOperator: space, no_space +# SupportedStylesForRationalLiterals: space, no_space Layout/SpaceAroundOperators: Exclude: - - 'app/api/submission/portfolio_evidence_api.rb' - - 'app/models/task.rb' - - 'config/deakin.rb' - - 'config/initializers/swagger.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 12 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space @@ -479,170 +133,39 @@ Layout/SpaceAroundOperators: Layout/SpaceBeforeBlockBraces: Exclude: - 'app/helpers/file_helper.rb' - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/project.rb' - 'app/models/task_definition.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'app/models/webcal.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceBeforeBrackets: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 118 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. # SupportedStyles: space, no_space, compact # SupportedStylesForEmptyBrackets: space, no_space Layout/SpaceInsideArrayLiteralBrackets: Exclude: - - 'app/api/projects_api.rb' - - 'app/api/units_api.rb' - - 'app/api/users_api.rb' - 'app/helpers/file_helper.rb' - - 'app/models/group.rb' - - 'app/models/group_set.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/unit.rb' - - 'app/models/unit_role.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'config/initializers/devise.rb' - - 'lib/helpers/database_populator.rb' -# Offense count: 34 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: Exclude: - - 'app/api/entities/project_entity.rb' - - 'app/helpers/file_helper.rb' - - 'app/mailers/notifications_mailer.rb' - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/portfolio_evidence.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'app/models/webcal.rb' - -# Offense count: 69 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space, compact -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideHashLiteralBraces: - Exclude: - - 'app/api/api_root.rb' - - 'app/api/extension_comments_api.rb' - - 'app/api/group_sets_api.rb' - - 'app/api/projects_api.rb' - - 'app/api/task_comments_api.rb' - - 'app/api/teaching_periods_authenticated_api.rb' - - 'app/api/units_api.rb' - - 'app/models/overseer_assessment.rb' - - 'app/models/project.rb' - - 'app/models/tutorial_stream.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' - -# Offense count: 27 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, compact, no_space -Layout/SpaceInsideParens: - Exclude: - - 'app/api/api_root.rb' - - 'app/api/units_api.rb' - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' - - 'app/models/task_definition.rb' - - 'app/models/tutorial_enrolment.rb' - - 'app/models/unit.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' - - 'lib/tasks/generate_pdfs.rake' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceInsideRangeLiteral: - Exclude: - - 'lib/helpers/database_populator.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBrackets: space, no_space -Layout/SpaceInsideReferenceBrackets: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/tutorial_enrolment.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: final_newline, final_blank_line -Layout/TrailingEmptyLines: - Exclude: - - 'app/channels/application_cable/channel.rb' - - 'config/initializers/swagger.rb' - - 'lib/helpers/faker_randomiser.rb' - - 'lib/tasks/send_status_emails.rake' - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'app/mailers/convenor_contact_mailer.rb' - - 'app/mailers/portfolio_evidence_mailer.rb' - - 'app/models/portfolio_evidence.rb' - - 'config/deakin.rb' - - 'lib/tasks/send_status_emails.rake' + - 'app/models/task_definition.rb' # Offense count: 1 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - 'app/models/task.rb' -# Offense count: 3 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperator: Exclude: - - 'app/helpers/file_helper.rb' - 'app/models/portfolio_evidence.rb' - - 'app/models/task.rb' - -# Offense count: 14 -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: - Exclude: - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/unit.rb' - - 'lib/tasks/populate.rake' # Offense count: 7 # This cop supports safe autocorrection (--autocorrect). @@ -655,28 +178,12 @@ Lint/AmbiguousRegexpLiteral: - 'app/helpers/file_helper.rb' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Exclude: - 'app/channels/application_cable/connection.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Lint/DeprecatedClassMethods: - Exclude: - - 'app/models/task.rb' - -# Offense count: 24 -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: - Exclude: - - 'app/api/api_root.rb' - - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' - - 'app/models/task_status.rb' - - 'lib/tasks/populate.rake' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Lint/ElseLayout: @@ -684,21 +191,15 @@ Lint/ElseLayout: - 'app/models/project.rb' - 'app/models/task.rb' -# Offense count: 1 -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'app/helpers/file_helper.rb' - # Offense count: 1 Lint/FloatComparison: Exclude: - 'app/models/project.rb' -# Offense count: 8 +# Offense count: 12 +# This cop supports safe autocorrection (--autocorrect). Lint/ImplicitStringConcatenation: Exclude: - - 'app/api/learning_alignment_api.rb' - 'app/api/learning_outcomes_api.rb' # Offense count: 2 @@ -709,6 +210,7 @@ Lint/Loop: - 'config/no_institution_setting.rb' # Offense count: 4 +# Configuration parameters: AllowedParentClasses. Lint/MissingSuper: Exclude: - 'app/controllers/lecture_resource_downloads_controller.rb' @@ -716,15 +218,6 @@ Lint/MissingSuper: - 'app/controllers/task_downloads_controller.rb' - 'app/controllers/task_submission_pdfs_controller.rb' -# Offense count: 18 -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/NonAtomicFileOperation: - Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/comments/task_comment.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - # Offense count: 1 Lint/NonLocalExitFromIterator: Exclude: @@ -736,18 +229,13 @@ Lint/ParenthesesAsGroupedExpression: Exclude: - 'app/models/unit.rb' -# Offense count: 5 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). Lint/RedundantStringCoercion: Exclude: - 'app/helpers/file_helper.rb' -# Offense count: 2 -Lint/RequireParentheses: - Exclude: - - 'config/application.rb' - -# Offense count: 15 +# Offense count: 16 Lint/RescueException: Exclude: - 'app/models/portfolio_evidence.rb' @@ -756,34 +244,16 @@ Lint/RescueException: - 'config/deakin.rb' - 'lib/tasks/generate_pdfs.rake' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Lint/ScriptPermission: - Exclude: - - 'Rakefile' - -# Offense count: 8 +# Offense count: 4 Lint/ShadowingOuterLocalVariable: Exclude: - - 'app/models/learning_outcome.rb' - - 'app/models/teaching_period.rb' - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 1 # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Exclude: - - 'app/models/project.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, consistent -Lint/SymbolConversion: - Exclude: - - 'lib/tasks/init.rake' # Offense count: 2 # Configuration parameters: AllowKeywordBlockArguments. @@ -791,9 +261,9 @@ Lint/UnderscorePrefixedVariableName: Exclude: - 'app/models/comments/discussion_comment.rb' -# Offense count: 33 +# Offense count: 45 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'app/api/entities/comment_entity.rb' @@ -811,9 +281,10 @@ Lint/UnusedBlockArgument: - 'lib/helpers/database_populator.rb' - 'lib/tasks/checks.rake' -# Offense count: 7 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. +# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: - 'app/models/project.rb' @@ -821,51 +292,53 @@ Lint/UnusedMethodArgument: - 'config/deakin.rb' - 'config/no_institution_setting.rb' -# Offense count: 55 +# Offense count: 61 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Enabled: false -# Offense count: 145 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. +# Offense count: 225 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 153 -# Offense count: 65 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 96 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 200 + Max: 157 -# Offense count: 12 -# Configuration parameters: CountBlocks. +# Offense count: 10 +# Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: Max: 5 -# Offense count: 65 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 96 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 39 + Max: 36 -# Offense count: 173 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 273 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 140 + Max: 115 -# Offense count: 1 +# Offense count: 4 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Enabled: false + Max: 580 -# Offense count: 4 +# Offense count: 7 # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: MaxOptionalParameters: 4 Max: 8 -# Offense count: 62 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 84 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 50 + Max: 39 # Offense count: 2 Naming/AccessorMethodName: @@ -874,13 +347,14 @@ Naming/AccessorMethodName: - 'app/models/user.rb' # Offense count: 1 -# Configuration parameters: EnforcedStyle, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: EnforcedStyle, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. # SupportedStyles: snake_case, camelCase +# ForbiddenIdentifiers: __id__, __send__ Naming/MethodName: Exclude: - 'app/models/comments/discussion_comment.rb' -# Offense count: 16 +# Offense count: 13 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: @@ -892,15 +366,14 @@ Naming/MethodParameterName: - 'app/models/task.rb' - 'app/models/unit.rb' -# Offense count: 36 -# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. -# NamePrefix: is_, has_, have_ -# ForbiddenPrefixes: is_, has_, have_ +# Offense count: 37 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. +# NamePrefix: is_, has_, have_, does_ +# ForbiddenPrefixes: is_, has_, have_, does_ # AllowedMethods: is_a? # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - - 'spec/**/*' - 'app/api/entities/unit_entity.rb' - 'app/models/group.rb' - 'app/models/overseer_assessment.rb' @@ -914,7 +387,7 @@ Naming/PredicateName: - 'lib/tasks/generate_pdfs.rake' # Offense count: 27 -# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns. +# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. # SupportedStyles: snake_case, camelCase Naming/VariableName: Exclude: @@ -922,28 +395,12 @@ Naming/VariableName: - 'app/models/comments/task_comment.rb' - 'config/deakin.rb' -# Offense count: 2 -# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. -# SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 -Naming/VariableNumber: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -Security/IoMethods: - Exclude: - - 'app/api/discussion_comment_api.rb' - - 'app/api/task_comments_api.rb' - -# Offense count: 13 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: separated, grouped Style/AccessorGrouping: Exclude: - - 'app/models/project.rb' - 'app/models/task.rb' # Offense count: 2 @@ -970,9 +427,9 @@ Style/AndOr: - 'app/models/tutorial_enrolment.rb' - 'app/models/tutorial_stream.rb' -# Offense count: 7 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. +# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object # FunctionalMethods: let, let!, subject, watch @@ -986,28 +443,26 @@ Style/BlockDelimiters: - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 6 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: MinBranchesCount. Style/CaseLikeIf: Exclude: - 'app/helpers/csv_helper.rb' - - 'app/helpers/file_helper.rb' - - 'app/models/comments/task_comment.rb' - - 'app/models/user.rb' -# Offense count: 3 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. +# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. # SupportedStyles: nested, compact +# SupportedStylesForClasses: , nested, compact +# SupportedStylesForModules: , nested, compact Style/ClassAndModuleChildren: Exclude: - - 'app/api/entities/minimal/minimal_unit_entity.rb' - - 'app/api/entities/minimal/minimal_user_entity.rb' - 'app/api/submission/generate_helpers.rb' -# Offense count: 12 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? Style/ClassEqualityComparison: Exclude: @@ -1022,6 +477,7 @@ Style/ColonMethodCall: - 'app/helpers/timeout_helper.rb' # Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'app/models/unit.rb' @@ -1034,18 +490,17 @@ Style/CommentAnnotation: Exclude: - 'app/api/task_definitions_api.rb' -# Offense count: 24 +# Offense count: 20 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CommentedKeyword: Exclude: - 'app/api/projects_api.rb' - - 'app/api/submission/batch_task_api.rb' - 'app/api/submission/portfolio_api.rb' - 'app/api/submission/portfolio_evidence_api.rb' - 'app/models/unit.rb' - 'config/deakin.rb' -# Offense count: 13 +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition @@ -1053,30 +508,28 @@ Style/ConditionalAssignment: Exclude: - 'app/api/submission/portfolio_evidence_api.rb' - 'app/models/comments/extension_comment.rb' - - 'app/models/learning_outcome_task_link.rb' - 'app/models/task.rb' - 'app/models/unit.rb' - 'lib/helpers/database_populator.rb' - 'lib/tasks/maintenance.rake' -# Offense count: 11 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). Style/DefWithParentheses: Exclude: - 'app/helpers/file_helper.rb' - - 'app/models/overseer_assessment.rb' - 'app/models/task_definition.rb' - 'app/models/user.rb' - 'config/deakin.rb' -# Offense count: 120 +# Offense count: 197 # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowComments. +# Configuration parameters: AutoCorrect, EnforcedStyle, AllowComments. # SupportedStyles: empty, nil, both Style/EmptyElse: Exclude: @@ -1105,30 +558,7 @@ Style/ExplicitBlockArgument: Exclude: - 'app/helpers/timeout_helper.rb' -# Offense count: 31 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedVars. -Style/FetchEnvVar: - Exclude: - - 'config/application.rb' - - 'config/deakin.rb' - - 'config/environments/production.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Style/FileRead: - Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/unit.rb' - - 'lib/tasks/checks.rake' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/FileWrite: - Exclude: - - 'lib/tasks/generate_pdfs.rake' - -# Offense count: 19 +# Offense count: 18 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: each, for @@ -1153,27 +583,28 @@ Style/FormatString: Exclude: - 'app/models/project.rb' -# Offense count: 8 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated +# AllowedMethods: redirect Style/FormatStringToken: EnforcedStyle: template -# Offense count: 152 +# Offense count: 251 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: Enabled: false -# Offense count: 2 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/GlobalStdStream: Exclude: - 'lib/tasks/skip_prod.rake' -# Offense count: 46 +# Offense count: 70 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: @@ -1183,7 +614,7 @@ Style/GuardClause: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -# SupportedShorthandSyntax: always, never, either, consistent +# SupportedShorthandSyntax: always, never, either, consistent, either_consistent Style/HashSyntax: Exclude: - 'app/models/campus.rb' @@ -1197,30 +628,20 @@ Style/HashSyntax: Style/IfInsideElse: Exclude: - 'app/api/units_api.rb' - - 'app/models/learning_outcome_task_link.rb' - 'app/models/task.rb' -# Offense count: 316 +# Offense count: 500 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Enabled: false -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods. -# AllowedMethods: nonzero? -Style/IfWithBooleanLiteralBranches: - Exclude: - - 'config/application.rb' - -# Offense count: 5 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: - 'app/api/entities/task_entity.rb' - 'app/api/submission/generate_helpers.rb' - - 'app/helpers/file_helper.rb' - 'app/models/unit.rb' # Offense count: 15 @@ -1229,29 +650,26 @@ Style/InverseMethods: # SupportedStyles: line_count_dependent, lambda, literal Style/Lambda: Exclude: - - 'app/api/entities/project_entity.rb' - 'app/api/entities/unit_entity.rb' - 'app/models/project.rb' - 'app/models/unit.rb' - 'config/deakin.rb' -# Offense count: 25 +# Offense count: 27 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: AllowedMethods, AllowedPatterns. Style/MethodCallWithoutArgsParentheses: Exclude: - 'app/api/task_definitions_api.rb' - 'app/models/comments/discussion_comment.rb' - - 'app/models/overseer_assessment.rb' - 'app/models/project.rb' - - 'app/models/task.rb' - 'app/models/task_definition.rb' - 'app/models/unit.rb' - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' - 'lib/tasks/checks.rake' -# Offense count: 12 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline @@ -1262,7 +680,6 @@ Style/MethodDefParentheses: - 'app/models/group_submission.rb' - 'app/models/task_definition.rb' - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' # Offense count: 1 Style/MultilineBlockChain: @@ -1270,14 +687,12 @@ Style/MultilineBlockChain: - 'app/models/project.rb' # Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: literals, strict -Style/MutableConstant: +# This cop supports safe autocorrection (--autocorrect). +Style/MultilineIfModifier: Exclude: - - 'app/helpers/grade_helper.rb' + - 'app/api/staff_grant_extension_api.rb' -# Offense count: 6 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: both, prefix, postfix @@ -1285,16 +700,7 @@ Style/NegatedIf: Exclude: - 'app/api/task_definitions_api.rb' - 'app/api/units_api.rb' - - 'app/helpers/file_helper.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Style/NegatedIfElseCondition: - Exclude: - - 'app/models/project.rb' - - 'app/models/unit.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -1302,14 +708,13 @@ Style/NestedTernaryOperator: Exclude: - 'app/models/project.rb' -# Offense count: 7 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinBodyLength. +# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals. # SupportedStyles: skip_modifier_ifs, always Style/Next: Exclude: - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - 'config/deakin.rb' # Offense count: 2 @@ -1335,28 +740,27 @@ Style/Not: Style/NumericLiterals: MinDigits: 6 -# Offense count: 90 +# Offense count: 97 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false -# Offense count: 23 +# Offense count: 27 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - 'app/helpers/file_helper.rb' + - 'app/mailers/notifications_mailer.rb' - 'app/models/auth_token.rb' - 'app/models/comments/extension_comment.rb' - 'app/models/portfolio_evidence.rb' - - 'app/models/project.rb' - 'app/models/task.rb' - 'app/models/task_definition.rb' - - 'app/models/teaching_period.rb' - 'app/models/unit.rb' - - 'app/models/user.rb' + - 'app/services/extension_service.rb' - 'lib/helpers/faker_randomiser.rb' # Offense count: 2 @@ -1365,16 +769,14 @@ Style/OrAssignment: Exclude: - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. Style/ParenthesesAroundCondition: Exclude: - - 'app/models/group.rb' - - 'app/models/task_definition.rb' - 'config/application.rb' -# Offense count: 29 +# Offense count: 34 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -1382,7 +784,6 @@ Style/PercentLiteralDelimiters: - 'app/api/task_comments_api.rb' - 'app/helpers/file_helper.rb' - 'app/models/learning_outcome.rb' - - 'app/models/learning_outcome_task_link.rb' - 'app/models/task.rb' - 'app/models/task_definition.rb' - 'app/models/unit.rb' @@ -1390,36 +791,20 @@ Style/PercentLiteralDelimiters: - 'app/models/webcal.rb' - 'config/application.rb' -# Offense count: 12 +# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: . # SupportedStyles: same_as_string_literals, single_quotes, double_quotes Style/QuotedSymbols: EnforcedStyle: double_quotes -# Offense count: 5 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Methods. -Style/RedundantArgument: - Exclude: - - 'app/helpers/csv_helper.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - -# Offense count: 5 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Style/RedundantBegin: Exclude: - 'app/helpers/timeout_helper.rb' - - 'app/models/task_definition.rb' - 'app/models/unit.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantConstantBase: - Exclude: - - 'config.ru' - # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SafeForConstants. @@ -1427,13 +812,7 @@ Style/RedundantFetchBlock: Exclude: - 'config/puma.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantFileExtensionInRequire: - Exclude: - - 'lib/tasks/register_q_assessment_results_subscriber.rake' - -# Offense count: 11 +# Offense count: 16 # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: @@ -1441,26 +820,21 @@ Style/RedundantInterpolation: - 'app/models/task_definition.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 6 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Style/RedundantParentheses: Exclude: - 'app/models/project.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - 'config/application.rb' -# Offense count: 17 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: - - 'app/api/discussion_comment_api.rb' - - 'app/api/task_comments_api.rb' - 'app/helpers/csv_helper.rb' - - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' -# Offense count: 20 +# Offense count: 21 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: @@ -1474,7 +848,7 @@ Style/RedundantReturn: - 'app/models/user.rb' - 'app/models/webcal.rb' -# Offense count: 88 +# Offense count: 174 # This cop supports safe autocorrection (--autocorrect). Style/RedundantSelf: Enabled: false @@ -1485,13 +859,7 @@ Style/RedundantSort: Exclude: - 'app/models/project.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantStringEscape: - Exclude: - - 'app/api/authentication_api.rb' - -# Offense count: 13 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed @@ -1503,10 +871,7 @@ Style/RegexpLiteral: - 'app/controllers/task_submission_pdfs_controller.rb' - 'app/helpers/csv_helper.rb' - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/unit.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -1514,14 +879,16 @@ Style/RescueModifier: Exclude: - 'app/models/unit.rb' -# Offense count: 27 +# Offense count: 22 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, explicit Style/RescueStandardError: Exclude: + - 'app/api/staff_grant_extension_api.rb' - 'app/helpers/file_helper.rb' - 'app/helpers/timeout_helper.rb' + - 'app/mailers/notifications_mailer.rb' - 'app/models/group_submission.rb' - 'app/models/portfolio_evidence.rb' - 'app/models/project.rb' @@ -1532,13 +899,12 @@ Style/RescueStandardError: - 'lib/tasks/checks.rake' - 'lib/tasks/maintenance.rake' -# Offense count: 29 +# Offense count: 41 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Exclude: - - 'app/api/entities/minimal/minimal_unit_entity.rb' - 'app/api/entities/task_definition_entity.rb' - 'app/api/entities/tutorial_entity.rb' - 'app/api/entities/unit_entity.rb' @@ -1548,13 +914,6 @@ Style/SafeNavigation: - 'app/models/tutorial.rb' - 'app/models/unit.rb' - 'app/models/user.rb' - - 'lib/assets/ontrack_receive_action.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/SelectByRegexp: - Exclude: - - 'app/helpers/file_helper.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). @@ -1562,30 +921,29 @@ Style/SelfAssignment: Exclude: - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowAsExpressionSeparator. Style/Semicolon: Exclude: - - 'app/models/unit.rb' - 'lib/helpers/database_populator.rb' - 'lib/tasks/generate_pdfs.rake' -# Offense count: 2 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: - 'app/models/task.rb' -# Offense count: 8 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. Style/SoleNestedConditional: Exclude: - 'app/api/group_sets_api.rb' - - 'app/api/task_definitions_api.rb' - 'app/models/group.rb' - 'app/models/task.rb' + - 'app/services/extension_service.rb' - 'config/deakin.rb' # Offense count: 10 @@ -1597,7 +955,7 @@ Style/StringConcatenation: - 'app/models/task.rb' - 'app/models/unit.rb' -# Offense count: 349 +# Offense count: 627 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes @@ -1615,20 +973,19 @@ Style/StringLiteralsInInterpolation: - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 41 +# Offense count: 72 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MinSize. # SupportedStyles: percent, brackets Style/SymbolArray: Enabled: false -# Offense count: 5 +# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, IgnoredMethods, AllowComments. -# AllowedMethods: define_method +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to Style/SymbolProc: Exclude: - - 'app/models/teaching_period.rb' - 'app/models/unit.rb' # Offense count: 2 @@ -1640,7 +997,7 @@ Style/TernaryParentheses: - 'app/models/project.rb' - 'app/models/tutorial_stream.rb' -# Offense count: 5 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma @@ -1650,20 +1007,20 @@ Style/TrailingCommaInArguments: - 'app/api/units_api.rb' - 'app/models/unit.rb' -# Offense count: 6 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma +# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInArrayLiteral: Exclude: - 'app/models/task.rb' - 'app/models/unit.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 7 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma +# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInHashLiteral: Exclude: - 'app/models/comments/task_comment.rb' @@ -1684,15 +1041,9 @@ Style/WordArray: - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ZeroLengthPredicate: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 583 +# Offense count: 594 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: Max: 369 diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb index e63c3360d..a54ac214a 100644 --- a/app/api/staff_grant_extension_api.rb +++ b/app/api/staff_grant_extension_api.rb @@ -10,11 +10,17 @@ class StaffGrantExtensionApi < Grape::API before do authenticated? - error!({ - error: 'Not authorized to grant extensions', - code: 'UNAUTHORIZED', - details: {} - }, 403) unless current_user.has_tutor_capability? + + unless current_user.has_tutor_capability? + error!( + { + error: 'Not authorized to grant extensions', + code: 'UNAUTHORIZED', + details: {} + }, + 403 + ) + end end desc 'Grant extensions to multiple students', @@ -98,7 +104,7 @@ class StaffGrantExtensionApi < Grape::API weeks_requested: extension_comment.extension_weeks, extension_response: extension_comment.extension_response, task_status: extension_comment.task.status, - extension_comment: extension_comment # Store internally for notifications + extension_comment: extension_comment # Store internally for notifications } else results[:failed] << { @@ -145,7 +151,7 @@ class StaffGrantExtensionApi < Grape::API true # is_staff_grant = true ).deliver_now Rails.logger.info "Extension notifications sent successfully" - rescue => e + rescue StandardError => e Rails.logger.error "Failed to send extension notifications: #{e.message}" Rails.logger.error e.backtrace.join("\n") # Don't fail the entire request if email fails, but log the error @@ -167,7 +173,6 @@ class StaffGrantExtensionApi < Grape::API status 201 present results, with: Grape::Presenters::Presenter end - rescue ActiveRecord::RecordNotFound error!({ error: 'Unit or task definition not found' }, 404) rescue StandardError diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 5aa251caf..5c810fd80 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -163,7 +163,7 @@ def extension_granted_notification(extension, granted_by) end # Main method to handle extension notifications from staff - def extension_granted(extensions, granted_by, total_selected, failed_extensions = [], is_staff_grant = false) + def extension_granted(extensions, granted_by, total_selected, failed_extensions = [], is_staff_grant: false) # Only send notifications for staff-granted bulk extensions return unless is_staff_grant && (extensions.any? || failed_extensions.any?) @@ -178,7 +178,7 @@ def extension_granted(extensions, granted_by, total_selected, failed_extensions NotificationsMailer.extension_granted_notification(extension, granted_by).deliver_now end end - rescue => e + rescue StandardError => e Rails.logger.error "Failed to send extension notifications: #{e.message}" Rails.logger.error e.backtrace.join("\n") end diff --git a/app/services/extension_service.rb b/app/services/extension_service.rb index 01af8cc4c..0730b421b 100644 --- a/app/services/extension_service.rb +++ b/app/services/extension_service.rb @@ -1,5 +1,5 @@ class ExtensionService - def self.grant_extension(project_id, task_definition_id, user, weeks_requested, comment, is_staff_grant = false) + def self.grant_extension(project_id, task_definition_id, user, weeks_requested, comment, is_staff_grant: false) # Find project and task project = Project.find(project_id) task_definition = project.unit.task_definitions.find(task_definition_id) @@ -18,19 +18,28 @@ def self.grant_extension(project_id, task_definition_id, user, weeks_requested, return { success: false, error: 'Extensions cannot be granted beyond task deadline', status: 403 } if duration <= 0 # ===== Student-Initiated Extension Logic (current endpoint) ===== - unless is_staff_grant - # Check task-level authorization for student requests with specific permission hash - unless AuthorisationHelpers.authorise?(user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) }) - return { success: false, error: 'Not authorised to request an extension for this task', status: 403 } - end + unless is_staff_grant || + AuthorisationHelpers.authorise?( + user, + task, + :request_extension, + ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) } + ) + return { + success: false, + error: 'Not authorised to request an extension for this task', + status: 403 + } end # ===== Staff Grant Logic (new endpoint) ===== - if is_staff_grant - # Check unit-level authorization for staff grants - unless AuthorisationHelpers.authorise?(user, project.unit, :grant_extensions) - return { success: false, error: 'Not authorised to grant extensions for this unit', status: 403 } - end + if is_staff_grant && + !AuthorisationHelpers.authorise?(user, project.unit, :grant_extensions) + return { + success: false, + error: 'Not authorised to grant extensions for this unit', + status: 403 + } end # ===== Common Extension Logic ===== From 7a98ff2c77b67e98baab1df80e6021e279b798da Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 18 Jan 2026 01:30:16 +1000 Subject: [PATCH 46/46] chore: Remove shebang from Rakefile (rubocop) --- Rakefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Rakefile b/Rakefile index 350ebd498..f36b80f28 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.