From 4e337234607497a7410d432e3365108be12eb255 Mon Sep 17 00:00:00 2001 From: Vineesha Paul Date: Fri, 5 Oct 2018 10:07:18 +1000 Subject: [PATCH 01/30] NEW: Unit test cases for units API --- test/api/units_test.rb | 252 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 234 insertions(+), 18 deletions(-) diff --git a/test/api/units_test.rb b/test/api/units_test.rb index 15c3ad6f7..f02de79b6 100644 --- a/test/api/units_test.rb +++ b/test/api/units_test.rb @@ -43,6 +43,112 @@ def test_units_post assert_equal unit_count + 1, Unit.all.count assert_equal expected_unit[:name], Unit.last.name end + + def create_unit + { + name:'Intro to Social Skills', + code:'JRRW40003', + start_date:'2016-05-14T00:00:00.000Z', + end_date:'2017-05-14T00:00:00.000Z' + } + end + + def test_post_create_unit_custom_token(token='abcdef') + count = Unit.all.length + unit = create_unit + + data_to_post = { + unit: unit, + auth_token: token + } + + post_json '/api/units', data_to_post + # Successful assertion of same length again means no record was created + assert_equal count, Unit.all.length + assert_equal 419, last_response.status + end + + def test_post_create_unit_empty_token + test_post_create_unit_custom_token '' + end + + def create_same_unit_again + count = Unit.all.length + + data_to_post = { + unit: create_unit, + auth_token: auth_token + } + + post_json '/api/units', data_to_post + assert_equal count + 1, Unit.all.length + + assert_equal 201, last_response.status + + post_json '/api/units', data_to_post + # Successful assertion of same length again means no record was created + assert_equal count + 1, Unit.all.length + assert_equal 500, last_response.status + end + + def post_create_same_unit_different_name + count = Unit.all.length + unit = create_unit + + data_to_post = { + unit: unit, + auth_token: auth_token + } + + post_json '/api/units', data_to_post + assert_equal count + 1, Unit.all.length + + # Changes name of unit in data_to_post automatically + unit[:name] = 'Intro to Python' + + post_json '/api/units', data_to_post + # Successful assertion of same length again means no record was created + assert_equal count + 1, Unit.all.length + assert_equal 500, last_response.status + end + + + def assert_tutorial_model_response(response, expected) + expected = expected.as_json + + # Can't use assert_json_matches_model as keys differ + assert_equal response[:meeting_day], expected[:day] + assert_equal response[:meeting_time], expected[:time] + assert_equal response[:location], expected[:location] + assert_equal response[:abbrev], expected[:abbrev] + end + + + def test_addtutorial_to_unit + count_tutorials = Tutorial.all.length + + tutorial = { + day: 'Wednesday', + time: '2:30', + location: 'HE12', + tutor_username: 'acain', + abbrev: 'BC43' + } + + data_to_post = { + tutorial: tutorial, + id: '1', + auth_token: auth_token + } + + # perform the post + post_json '/api/units/1/tutorials', data_to_post + + # Check there is a new tutorial + assert_equal Tutorial.all.length, count_tutorials + 1 + assert_tutorial_model_response last_response_body, tutorial + end + # End POST tests # --------------------------------------------------------------------------- # @@ -50,26 +156,26 @@ def test_units_post # GET tests # Test GET for getting all units - def test_units_get + #def test_units_get # The GET we are testing - get with_auth_token '/api/units' + # get with_auth_token '/api/units' - actual_unit = last_response_body[0] - expected_unit = Unit.first - assert_equal expected_unit.name, actual_unit['name'] - assert_equal expected_unit.code, actual_unit['code'] - assert_equal expected_unit.start_date.to_date, actual_unit['start_date'].to_date - assert_equal expected_unit.end_date.to_date, actual_unit['end_date'].to_date.to_date + # actual_unit = last_response_body[0] + #expected_unit = Unit.first + # assert_equal expected_unit.name, actual_unit['name'] + # assert_equal expected_unit.code, actual_unit['code'] + # assert_equal expected_unit.start_date.to_date, actual_unit['start_date'].to_date + #assert_equal expected_unit.end_date.to_date, actual_unit['end_date'].to_date.to_date # Check last unit in Units (created in seed.db) - actual_unit = last_response_body[1] - expected_unit = Unit.find(2) + # actual_unit = last_response_body[1] + #expected_unit = Unit.find(2) - assert_equal expected_unit.name, actual_unit['name'] - assert_equal expected_unit.code, actual_unit['code'] - assert_equal expected_unit.start_date.to_date, actual_unit['start_date'].to_date - assert_equal expected_unit.end_date.to_date, actual_unit['end_date'].to_date.to_date - end + #assert_equal expected_unit.name, actual_unit['name'] + #assert_equal expected_unit.code, actual_unit['code'] + #assert_equal expected_unit.start_date.to_date, actual_unit['start_date'].to_date + #assert_equal expected_unit.end_date.to_date, actual_unit['end_date'].to_date.to_date + #end # Test GET for getting a specific unit by id def test_units_get_by_id @@ -98,13 +204,119 @@ def test_units_get_by_id assert_equal actual_unit['start_date'].to_date, expected_unit.start_date.to_date assert_equal actual_unit['end_date'].to_date, expected_unit.end_date.to_date end + + #Test GET for getting the unit details of current user + def test_units_current + get with_auth_token '/api/units' + assert_equal 200, last_response.status + end + + + #Test PUT for updating unit details with valid id + def test_units_put + unit=Unit.first + unit[:name] = 'Intro to python' + unit[:code] = 'JRSW40004' + unit[:description] = 'new language' + unit[:start_date] = '2018-12-14T00:00:00.000Z' + unit[:end_date]='2019-05-14T00:00:00.000Z' + unit[:active]='true' + data_to_put = { + unit:unit, + auth_token: auth_token + } + put_json '/api/units/1', data_to_put + assert_equal 200, last_response.status + end + + #Test PUT for updating unit details with empty name + def test_put_update_unit_empty_name + unit = Unit.first + unit[:name] = '' + + data_to_put = { + unit: unit, + auth_token: auth_token + } + + put_json '/api/units/1', data_to_put + assert_equal 500, last_response.status + end + +#Test PUT for updating unit details with invalid id +def test_put_update_unit_invalid_id + unit= Unit.first + data_to_put = { + unit:unit, + auth_token: auth_token + } + + put_json '/api/units/12', data_to_put + assert_equal 500, last_response.status +end + + # Test GET for getting a specific unit by invalid id + def test_fail_units_get_by_id + get with_auth_token '/api/units/12' + assert_equal 500, last_response.status + end + + def test_put_update_unit_custom_token(token='abcdef') + unit= Unit.first + data_to_put = { + unit: unit, + auth_token:token + } + + put_json '/api/units/1', data_to_put + assert_equal 419, last_response.status + end + + def test_put_update_unit_empty_token + test_put_update_unit_custom_token '' + end + + def test_tasks_for_task_inbox + user = User.first + unit = Unit.first + + expected_response = unit.tasks_for_task_inbox(user) + + get with_auth_token "/api/units/#{unit.id}/tasks/inbox", user + + assert_equal 200, last_response.status + + # check each is the same + last_response_body.zip(expected_response).each do |response, expected| + assert_json_matches_model response, expected, ['id'] + end + end + + def test_get_awaiting_feedback + user = User.first + unit = Unit.first + + expected_response = unit.tasks_awaiting_feedback(user) + + get with_auth_token "/api/units/#{unit.id}/feedback", user + + assert_equal 200, last_response.status + + # check each is the same + last_response_body.zip(expected_response).each do |response, expected| + assert_json_matches_model response, expected, ['id'] + end + end + + end + # End GET tests # --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- # # PUT tests - def test_units_put + #def test_units_put # users = { # acain: {first_name: "Andrew", last_name: "Cain", nickname: "Macite", role_id: Role.admin_id}, # jrenzella: {first_name: "Jake", last_name: "Renzella", nickname: "FactoryBoy<3", role_id: Role.convenor_id}, @@ -176,7 +388,11 @@ def test_units_put # assert_equal expected_unit.code, actual_unit['code'] # assert_equal expected_unit['start_date'].to_date, actual_unit['start_date'].to_date # assert_equal expected_unit['end_date'].to_date, actual_unit['end_date'].to_date - end + #end # End PUT tests # --------------------------------------------------------------------------- # -end + #end + + + + \ No newline at end of file From 574c486382303ff82d9e70ca177571ad075d876c Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Tue, 16 Oct 2018 20:04:56 +1100 Subject: [PATCH 02/30] TEST: Update tests with correct error codes --- test/api/units_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/units_test.rb b/test/api/units_test.rb index f02de79b6..d2a772451 100644 --- a/test/api/units_test.rb +++ b/test/api/units_test.rb @@ -240,7 +240,7 @@ def test_put_update_unit_empty_name } put_json '/api/units/1', data_to_put - assert_equal 500, last_response.status + assert_equal 400, last_response.status end #Test PUT for updating unit details with invalid id @@ -252,13 +252,13 @@ def test_put_update_unit_invalid_id } put_json '/api/units/12', data_to_put - assert_equal 500, last_response.status + assert_equal 404, last_response.status end # Test GET for getting a specific unit by invalid id def test_fail_units_get_by_id get with_auth_token '/api/units/12' - assert_equal 500, last_response.status + assert_equal 404, last_response.status end def test_put_update_unit_custom_token(token='abcdef') From 5e9af5a975ecc70a43acb6c9c0479379916e63c8 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 20:51:31 +1100 Subject: [PATCH 03/30] FIX: Correct missing end in settings --- app/api/settings.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/settings.rb b/app/api/settings.rb index c7601ace9..83432bf68 100644 --- a/app/api/settings.rb +++ b/app/api/settings.rb @@ -29,6 +29,7 @@ class Settings < Grape::API } end result + end desc 'Return privacy policy details' get '/settings/privacy' do From d1961cc28007760a7a7da3271973a72c9ea0d315 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 20:52:05 +1100 Subject: [PATCH 04/30] ENHANCE: Add teaching periods to populator --- lib/helpers/database_populator.rb | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index c6efff7af..631dd6208 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -58,6 +58,43 @@ def initialize(scale = :small) # Fixed data contains all fixed units and users created generate_fixed_data() + + generate_teaching_periods() + end + + def generate_teaching_periods + data = { + period: 'T1', + year: 2018, + start_date: Date.parse('2018-03-05'), + end_date: Date.parse('2018-05-25'), + active_until: Date.parse('2018-06-15') + } + tp = TeachingPeriod.create!(data) + + tp.add_break Date.parse('2018-03-30'), 1 + + data = { + period: 'T2', + year: 2018, + start_date: Date.parse('2018-07-09'), + end_date: Date.parse('2018-09-28'), + active_until: Date.parse('2018-10-19') + } + tp = TeachingPeriod.create! data + + tp.add_break Date.parse('2018-08-13'), 1 + + data = { + period: 'T3', + year: 2018, + start_date: Date.parse('2018-11-05'), + end_date: Date.parse('2019-02-01'), + active_until: Date.parse('2019-02-15') + } + tp = TeachingPeriod.create! data + + tp.add_break Date.parse('2018-12-24'), 2 end def generate_admin From 253efab815bf8f3b477b62bc7a3cc6b29d71cbee Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 20:52:50 +1100 Subject: [PATCH 05/30] CONFIG: Remove requst to simulate signoff on populate --- lib/tasks/populate.rake | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/tasks/populate.rake b/lib/tasks/populate.rake index 80cb0bb84..99ee14f64 100644 --- a/lib/tasks/populate.rake +++ b/lib/tasks/populate.rake @@ -221,14 +221,6 @@ namespace :db do dbpop.generate_users dbpop.generate_units - # Run simulate signoff? - unless extended - puts '-> Would you like to simulate student progress? This may take a while... [y/n]' - end - if extended || STDIN.gets.chomp.casecmp('y').zero? - puts '-> Simulating signoff...' - Rake::Task['db:simulate_signoff'].execute - end puts '-> Done.' end end From 3644f75fb0e58f562b7bf38e59b3332926f7392c Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 20:54:29 +1100 Subject: [PATCH 06/30] TEST: Add tests for teaching periods --- app/models/teaching_period.rb | 4 +- test/api/teaching_period_test.rb | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 test/api/teaching_period_test.rb diff --git a/app/models/teaching_period.rb b/app/models/teaching_period.rb index 34c113966..55a1d6c15 100644 --- a/app/models/teaching_period.rb +++ b/app/models/teaching_period.rb @@ -13,13 +13,13 @@ class TeachingPeriod < ActiveRecord::Base validate :validate_end_date_after_start_date, :validate_active_until_after_end_date def validate_end_date_after_start_date - if end_date < start_date + if end_date.present? && start_date.present? && end_date < start_date errors.add(:end_date, "should be after the Start date") end end def validate_active_until_after_end_date - if active_until < end_date + if end_date.present? && active_until.present? && active_until < end_date errors.add(:active_until, "date should be after the End date") end end diff --git a/test/api/teaching_period_test.rb b/test/api/teaching_period_test.rb new file mode 100644 index 000000000..b2f814a3f --- /dev/null +++ b/test/api/teaching_period_test.rb @@ -0,0 +1,95 @@ +require 'test_helper' + +class TeachingPeriodTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + + def app + Rails.application + end + + def test_check_periods_are_created + # Ensure that at the start there are 3 teaching periods + assert_equal 3, TeachingPeriod.count, 'There are 3 teaching periods initially' + end + + # Check that units cannot be created with both TP and custom dates + def test_create_unit_with_tp_and_dates + tp = TeachingPeriod.first + + data = { + name: 'Unit with error', + code: 'TEST111', + teaching_period_id: tp.id, + description: 'Unit with both TP and start date', + start_date: Date.parse('2018-01-01'), + end_date: Date.parse('2018-02-01') + } + + unit = Unit.create(data) + refute unit.valid? + end + + # Check that you can create a teaching period + def test_create_teaching_period + data = { + year: 2019, + period: 'T1', + start_date: Date.parse('2018-01-01'), + end_date: Date.parse('2018-02-01'), + active_until: Date.parse('2018-03-01') + } + + tp = TeachingPeriod.create(data) + assert tp.valid? + end + + # Test invalid dates + def test_create_teaching_period_with_invalid_dates + data = { + year: 2019, + period: 'T1', + start_date: Date.parse('2018-01-01'), + end_date: Date.parse('2018-02-01'), + active_until: Date.parse('2017-03-01') + } + + tp = TeachingPeriod.create(data) + refute tp.valid? + + data = { + year: 2019, + period: 'T1', + start_date: Date.parse('2018-01-01'), + end_date: Date.parse('2017-02-01'), + active_until: Date.parse('2018-03-01') + } + + tp = TeachingPeriod.create(data) + refute tp.valid? + + # Check that unit requires both start and end dates + data = { + year: 2019, + period: 'T1', + start_date: Date.parse('2018-01-01'), + active_until: Date.parse('2018-03-01') + } + + tp = TeachingPeriod.create(data) + refute tp.valid? + + data = { + year: 2019, + period: 'T1', + end_date: Date.parse('2018-01-01'), + active_until: Date.parse('2018-03-01') + } + + tp = TeachingPeriod.create(data) + refute tp.valid? + + end + +end From c003bed66bdcd75f91f9a5b7a61d41f6d13bede8 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 22:47:12 +1100 Subject: [PATCH 07/30] ENHANCE: Hard code lookup of task status key This will improve performance, and values are set elsewhere. --- app/api/task_definitions.rb | 2 +- app/api/tasks.rb | 2 +- app/models/project.rb | 2 +- app/models/task_status.rb | 18 ++++++++++++++++++ test/models/task_status_test.rb | 9 +++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 test/models/task_status_test.rb diff --git a/app/api/task_definitions.rb b/app/api/task_definitions.rb index cd854e22b..085170f3f 100644 --- a/app/api/task_definitions.rb +++ b/app/api/task_definitions.rb @@ -321,7 +321,7 @@ class TaskDefinitions < Grape::API id: t.id, task_definition_id: t.task_definition_id, tutorial_id: t.tutorial_id, - status: TaskStatus.find(t.status_id).status_key, + status: TaskStatus.id_to_key(t.status_id), completion_date: t.completion_date, submission_date: t.submission_date, times_assessed: t.times_assessed, diff --git a/app/api/tasks.rb b/app/api/tasks.rb index d1d43ef29..9bd8149ab 100644 --- a/app/api/tasks.rb +++ b/app/api/tasks.rb @@ -38,7 +38,7 @@ class Tasks < Grape::API id: r.id, tutorial_id: r.tutorial_id, task_definition_id: r.task_definition_id, - status: TaskStatus.find(r.status_id).status_key + status: TaskStatus.id_to_key(r.status_id) } end end diff --git a/app/models/project.rb b/app/models/project.rb index 49426a17d..2896db34c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -223,7 +223,7 @@ def task_details_for_shallow_serializer(user) t = Task.find(r.id) { id: r.id, - status: TaskStatus.find(r.status_id).status_key, + status: TaskStatus.id_to_key(r.status_id), task_definition_id: r.task_definition_id, include_in_portfolio: r.include_in_portfolio, pct_similar: t.pct_similar, diff --git a/app/models/task_status.rb b/app/models/task_status.rb index b1f01e540..181e4e2e6 100644 --- a/app/models/task_status.rb +++ b/app/models/task_status.rb @@ -61,6 +61,24 @@ def self.staff_assigned_statuses TaskStatus.where('id > 4') end + def self.id_to_key(id) + case id + when 1 then :not_started + when 2 then :complete + when 3 then :need_help + when 4 then :working_on_it + when 5 then :fix_and_resubmit + when 6 then :do_not_resubmit + when 7 then :redo + when 8 then :discuss + when 9 then :ready_to_mark + when 10 then :demonstrate + when 11 then :fail + when 12 then :time_exceeded + else :not_started + end + end + def status_key return :complete if self == TaskStatus.complete return :not_started if self == TaskStatus.not_started diff --git a/test/models/task_status_test.rb b/test/models/task_status_test.rb new file mode 100644 index 000000000..db9be3386 --- /dev/null +++ b/test/models/task_status_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class TaskStatusTest < ActiveSupport::TestCase + test 'ensure status matches id' do + TaskStatus.all.each do |ts| + assert_equal TaskStatus.id_to_key(ts.id), ts.status_key + end + end +end From 87a8d09f90e3e727226540a84bc5b1339bbbe0ba Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 22:48:09 +1100 Subject: [PATCH 08/30] FIX: Ensure date checks work in invalid cases --- app/models/unit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index f83ebb30e..531475fff 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -132,7 +132,7 @@ def ensure_teaching_period_dates_match end def validate_end_date_after_start_date - if end_date < start_date + if end_date.present? && start_date.present? && end_date < start_date errors.add(:end_date, "should be after the Start date") end end From 46500d53fea7f26068b5782d5248e93333036493 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 22:48:53 +1100 Subject: [PATCH 09/30] FIX: Make use of new id to status in task --- app/models/unit.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 531475fff..5cd7b15a3 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -1446,7 +1446,7 @@ def tasks_as_hash(data) project_id: t.project_id, task_definition_id: t.task_definition_id, tutorial_id: t.tutorial_id, - status: TaskStatus.find(t.status_id).status_key, + status: TaskStatus.id_to_key(t.status_id), completion_date: t.completion_date, submission_date: t.submission_date, times_assessed: t.times_assessed, @@ -1523,7 +1523,7 @@ def task_status_stats { tutorial_id: r.tutorial_id, task_definition_id: r.task_definition_id, - status: TaskStatus.find(r.status_id).status_key, + status: TaskStatus.id_to_key(r.status_id), num: r.num_tasks } end From 91082ba0a949902c5996c8a561b01a223a26cdc7 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 22:49:25 +1100 Subject: [PATCH 10/30] FIX: Add time exceeded to ilo scale calculations --- app/models/unit.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 5cd7b15a3..4a3847fe9 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -1683,7 +1683,8 @@ def student_ilo_progress_stats redo: 0.1, do_not_resubmit: 0.1, fix_and_resubmit: 0.3, - ready_to_mark: 0.5, + time_exceeded: 0.5, + ready_to_mark: 0.7, discuss: 0.8, demonstrate: 0.8, complete: 1.0 From 37c2b0bebd6b48af31757061e1b1b2efc40b78d8 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 22:50:09 +1100 Subject: [PATCH 11/30] FIX: Use id lookup for another case of status lookup --- app/models/unit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 4a3847fe9..475134c30 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -1669,7 +1669,7 @@ def student_ilo_progress_stats learning_outcome_id: r.learning_outcome_id, rating: r.rating, grade: r.target_grade, - status: TaskStatus.find(r.status_id).status_key, + status: TaskStatus.id_to_key(r.status_id), num: r.num } end From fef286da39072e8b09e69710727312de6255bdb5 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 18 Oct 2018 22:50:58 +1100 Subject: [PATCH 12/30] ENHANCE: Fix populator to import unit to make consistent and meaningful --- lib/helpers/database_populator.rb | 42 +++++++++++++---- test_files/COS10001-Tasks.csv | 74 +++++++++++++++--------------- test_files/COS10001-tasks.zip | Bin 0 -> 838674 bytes 3 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 test_files/COS10001-tasks.zip diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index 631dd6208..cc13962a2 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -17,7 +17,6 @@ def initialize(scale = :small) scale ||= :small @user_cache = {} @unit_cache = {} - @task_def_cache = {} # Set up the scale scale_data = { small: { @@ -166,12 +165,26 @@ def generate_units # Run through the unit_details and initialise their data @unit_data.each do | unit_key, unit_details | puts "---> Generating unit #{unit_details[:code]}" - unit = Unit.create!( + + if unit_details[:teaching_period].present? + data = { code: unit_details[:code], name: unit_details[:name], description: faker_random_sentence(10, 15), + teaching_period_id: unit_details[:teaching_period].id + } + else + data = { + code: unit_details[:code], + name: unit_details[:name], + description: faker_random_sentence(10, 15), start_date: Time.zone.now - 6.weeks, end_date: 13.weeks.since(Time.zone.now - 6.weeks) + } + end + + unit = Unit.create!( + data ) # Assign the convenors for this unit unit_details[:convenors].each do | user_key | @@ -230,11 +243,13 @@ def generate_fixed_data some_tasks = @scale[:some_tasks] many_tasks = @scale[:many_tasks] few_tasks = @scale[:few_tasks] + no_tasks = @scale[:no_tasks] @unit_data = { intro_prog: { code: "COS10001", name: "Introduction to Programming", convenors: [ :acain, :aconvenor ], + teaching_period: TeachingPeriod.first, tutors: [ { user: :acain, num: many_tutorials }, { user: :aconvenor, num: many_tutorials }, @@ -246,8 +261,6 @@ def generate_fixed_data { user: :angusmorton, num: some_tutorials }, { user: :cliff, num: some_tutorials }, ], - num_tasks: some_tasks, - ilos: Faker::Number.between(0,3), students: [ ] }, oop: { @@ -343,6 +356,13 @@ def generate_task_statuses # def generate_tasks_for_unit(unit, unit_details) print "----> Generating #{unit_details[:num_tasks]} tasks" + + if File.exists? Rails.root.join('test_files',"#{unit.code}-Tasks.csv") + unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{unit.code}-Tasks.csv")) + unit.import_task_files_from_zip Rails.root.join('test_files',"#{unit.code}-Tasks.zip") + return + end + unit_details[:num_tasks].times do |count| up_reqs = [] Faker::Number.between(1,4).times.each_with_index do | file, idx | @@ -351,7 +371,7 @@ def generate_tasks_for_unit(unit, unit_details) target_date = unit.start_date + ((count + 1) % 12).weeks # Assignment 6 due week 6, etc. start_date = target_date - Faker::Number.between(1.0,2.0).weeks # Make sure at least 30% of the tasks are pass - target_grade = @task_def_cache.length > (unit_details[:num_tasks] / 3) ? Faker::Number.between(0,3) : 0 + target_grade = Faker::Number.between(0,3) task_def = TaskDefinition.create( name: "Assignment #{count + 1}", abbreviation: "A#{count + 1}", @@ -363,7 +383,6 @@ def generate_tasks_for_unit(unit, unit_details) start_date: start_date, target_grade: target_grade ) - @task_def_cache[task_def.id] = task_def print "." end puts "!" @@ -373,12 +392,15 @@ def generate_tasks_for_unit(unit, unit_details) # Generates ILOs and aligns ILOs to tasks for unit # def generate_and_align_ilos_for_unit(unit, unit_details) - if @task_def_cache.empty? - throw "Task definition cache is empty. Call generate_tasks_for_unit unit_key, first before calling generate_and_align_ilos_for_unit" - end - # Create the ILOs print "----> Adding #{unit_details[:ilos]} ILOs" + + if File.exists? Rails.root.join('test_files',"#{unit.code}-Outcomes.csv") + unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{unit.code}-Outcomes.csv")) + unit.import_task_alignment_from_csv File.open(Rails.root.join('test_files',"#{unit.code}-Alignment.csv")), nil + return + end + ilo_cache = {} unit_details[:ilos].times do |index| ilo_number = index + 1 diff --git a/test_files/COS10001-Tasks.csv b/test_files/COS10001-Tasks.csv index 2c5155063..b926b18bc 100644 --- a/test_files/COS10001-Tasks.csv +++ b/test_files/COS10001-Tasks.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 -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,,,0,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,,,0,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,,,0,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 -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,,,0,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,,,0,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,,,0,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,,,0,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 -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,,,0,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,,,0,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,,,0,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 -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 -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 -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,,,0,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 -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 -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,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,,,0,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,,,0,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 -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 -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 -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,,,0,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,,,0,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 -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,,,0,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,,,0,FALSE -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,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 -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 -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 -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 -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 -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 \ No newline at end of file +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 +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +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,, +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,, +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,, +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,, +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,, +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,, +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,, +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,, \ No newline at end of file diff --git a/test_files/COS10001-tasks.zip b/test_files/COS10001-tasks.zip new file mode 100644 index 0000000000000000000000000000000000000000..b91091011836871b417048465764b7685d89b162 GIT binary patch literal 838674 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 JdHTms{{xd`xy}Fp literal 0 HcmV?d00001 From 202b9a67eee42eaa2d93352e9a61d057b448a012 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 08:52:28 +1100 Subject: [PATCH 13/30] TEST: Add test for unit ilo progress --- test/models/unit_test.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/models/unit_test.rb diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb new file mode 100644 index 000000000..4ec38bf48 --- /dev/null +++ b/test/models/unit_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class UnitTest < ActiveSupport::TestCase + test 'ensure valid response from unit ilo data' do + unit = Unit.first + details = unit.ilo_progress_class_details + + assert details.key?('all'), 'contains all key' + + unit.tutorials.each do |tute| + assert details.key?(tute.id), 'contains tutorial keys' + end + + end +end From 0be129fa3f52250dbb442b7ace28d5516d2ed34d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 08:54:54 +1100 Subject: [PATCH 14/30] FIX: Remove unused 'notasks' --- lib/helpers/database_populator.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index cc13962a2..26e276bf3 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -243,7 +243,6 @@ def generate_fixed_data some_tasks = @scale[:some_tasks] many_tasks = @scale[:many_tasks] few_tasks = @scale[:few_tasks] - no_tasks = @scale[:no_tasks] @unit_data = { intro_prog: { code: "COS10001", From b9107f1a8553f572ad34a845d37bc6e5eec7ba91 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 08:55:15 +1100 Subject: [PATCH 15/30] CONFIG: Add editor config --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..0963e31d9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +indent_size = 2 +indent_style = space From d03b094c9ab09f648f5e4162685a34f11b2adc7c Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 08:55:37 +1100 Subject: [PATCH 16/30] TEST: Add start of unit test with ilo data --- test/models/unit_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb index 4ec38bf48..72999f746 100644 --- a/test/models/unit_test.rb +++ b/test/models/unit_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class UnitTest < ActiveSupport::TestCase + test 'ensure valid response from unit ilo data' do unit = Unit.first details = unit.ilo_progress_class_details From de3f351e27ee45100264ae3b89e71b16e2bc1c55 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 11:26:18 +1100 Subject: [PATCH 17/30] TEST: Include unit tests with students from populator --- lib/helpers/database_populator.rb | 182 ++++++++++++++----------- lib/helpers/find_or_create_students.rb | 4 +- test/models/unit_test.rb | 40 +++++- 3 files changed, 140 insertions(+), 86 deletions(-) diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index 26e276bf3..ca50cacde 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -15,7 +15,8 @@ class DatabasePopulator def initialize(scale = :small) # Set up our caches scale ||= :small - @user_cache = {} + @user_cache = nil + @echo = false @unit_cache = {} # Set up the scale scale_data = { @@ -47,11 +48,16 @@ def initialize(scale = :small) accepted_scale_types = scale_data.keys unless accepted_scale_types.include?(scale) throw "Invalid scale value '#{scale}'. Acceptable values are: #{accepted_scale_types.join(", ")}" - else - puts "-> Scale is set to #{scale}" end @scale = scale_data[scale] + return if Role.count > 0 + + echo_line "-> Scale is set to #{scale}" + + @user_cache = {} + @echo = true + generate_user_roles generate_task_statuses @@ -118,12 +124,12 @@ def generate_users(filter = nil) throw "Unaccepted filter for generate_users, should be one of #{accepted_to_str}" end - print "--> Generating users with role(s) #{filter.pluck(:name).join(', ')}" + echo "--> Generating users with role(s) #{filter.pluck(:name).join(', ')}" users_to_generate = @user_data.select { | user_key, profile | filter.pluck(:id).include? profile[:role_id] } # Create each user users_to_generate.each do |user_key, profile| - print '.' + echo '.' username = user_key.to_s profile[:email] ||= "#{username}@doubtfire.com" @@ -141,18 +147,18 @@ def generate_users(filter = nil) @user_cache[user_key] = user end - puts '!' + echo_line '!' end # # Generates some units # def generate_units - puts "--> Generating units" + echo_line "--> Generating units" if @user_cache.empty? # Must generate users first! - puts "---> No users generated. Generating users first..." + echo_line "---> No users generated. Generating users first..." generate_users() end @@ -164,7 +170,7 @@ def generate_units # Run through the unit_details and initialise their data @unit_data.each do | unit_key, unit_details | - puts "---> Generating unit #{unit_details[:code]}" + echo_line "---> Generating unit #{unit_details[:code]}" if unit_details[:teaching_period].present? data = { @@ -188,7 +194,7 @@ def generate_units ) # Assign the convenors for this unit unit_details[:convenors].each do | user_key | - puts "----> Adding convenor #{user_key}" + echo_line "----> Adding convenor #{user_key}" unit.employ_staff(@user_cache[user_key], Role.convenor) end # Cache what we have @@ -300,16 +306,85 @@ def generate_fixed_data students: [ :acain, :aadmin ] }, } - puts "-> Defined #{@user_data.length} fixed users and #{@unit_data.length} units" + echo_line "-> Defined #{@user_data.length} fixed users and #{@unit_data.length} units" + end + + # + # Generates tutorials for unit and enrols some students in them + # + def generate_tutorials_and_enrol_students_for_unit(unit, unit_details) + student_count = 0 + tutorial_count = 0 + + # Grab stuff from scale + max_tutorials = @scale[:max_tutorials] + min_students = @scale[:min_students] + delta_students = @scale[:delta_students] + + # Collection of weekdays to be used + weekdays = %w[Monday Tuesday Wednesday Thursday Friday] + + # Create tutorials and enrol students + unit_details[:tutors].each do | user_details | + # only up to 4 tutorials for small scale + if tutorial_count > max_tutorials then break end + + if @user_cache.present? + tutor = @user_cache[user_details[:user]] + else + tutor = User.find_by_username(user_details[:user]) + end + + echo_line "----> Enrolling tutor #{tutor.name} with #{user_details[:num]} tutorials" + tutor_unit_role = unit.employ_staff(tutor, Role.tutor) + + user_details[:num].times do | count | + tutorial_count += 1 + #day, time, location, tutor_username, abbrev + tutorial = unit.add_tutorial( + "#{weekdays.sample}", + "#{8 + Faker::Number.between(0,11)}:#{['00', '30'].sample}", # Mon-Fri 8am-7:30pm + "#{['EN', 'BA'].sample}#{Faker::Number.between(0,6)}0#{Faker::Number.between(0,8)}", # EN###/BA### + tutor, + "LA1-#{tutorial_count.to_s.rjust(2, '0')}" + ) + + # Add a random number of students to the tutorial + num_students_in_tutorial = (min_students + Faker::Number.between(0,delta_students - 1)) + echo "-----> Creating #{num_students_in_tutorial} projects under tutorial #{tutorial.abbreviation}" + num_students_in_tutorial.times do + student = find_or_create_student("student_#{student_count}") + project = unit.enrol_student(student, tutorial.id) + student_count += 1 + echo '.' + end + # Add fixed students to first tutorial + if count == 0 + unit_details[:students].each do | student_key | + unit.enrol_student(@user_cache[student_key], tutorial.id) + end + end + echo_line "!" + end + end end private + # Output + def echo *args + print(*args) if @echo + end + + def echo_line *args + puts(*args) if @echo + end + # # Generate roles # def generate_user_roles - print "-> Generating user roles" + echo "-> Generating user roles" roles = [ { name: 'Student', description: "Students are able to be enrolled into units, and to submit progress for their unit projects." }, { name: 'Tutor', description: "Tutors are able to supervise tutorial classes and provide feedback to students, they may also be students in other units" }, @@ -319,16 +394,16 @@ def generate_user_roles roles.each do |role| Role.create!(name: role[:name], description: role[:description]) - print "." + echo "." end - puts "!" + echo_line "!" end # # Generate tasks statuses # def generate_task_statuses - print "-> Generating task statuses" + echo "-> Generating task statuses" statuses = { "Not Started": "You have not yet started this task.", "Complete": "This task has been signed off by your tutor.", @@ -344,17 +419,17 @@ def generate_task_statuses "Time Exceeded": "You did not submit or complete the task before the appropriate deadline." } statuses.each do | name, desc | - print "." + echo "." TaskStatus.create(name: name, description: desc) end - puts "!" + echo_line "!" end # # Generates tasks for the given unit # def generate_tasks_for_unit(unit, unit_details) - print "----> Generating #{unit_details[:num_tasks]} tasks" + echo "----> Generating #{unit_details[:num_tasks]} tasks" if File.exists? Rails.root.join('test_files',"#{unit.code}-Tasks.csv") unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{unit.code}-Tasks.csv")) @@ -382,9 +457,9 @@ def generate_tasks_for_unit(unit, unit_details) start_date: start_date, target_grade: target_grade ) - print "." + echo "." end - puts "!" + echo_line "!" end # @@ -392,7 +467,7 @@ def generate_tasks_for_unit(unit, unit_details) # def generate_and_align_ilos_for_unit(unit, unit_details) # Create the ILOs - print "----> Adding #{unit_details[:ilos]} ILOs" + echo "----> Adding #{unit_details[:ilos]} ILOs" if File.exists? Rails.root.join('test_files',"#{unit.code}-Outcomes.csv") unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{unit.code}-Outcomes.csv")) @@ -411,13 +486,13 @@ def generate_and_align_ilos_for_unit(unit, unit_details) description: faker_random_sentence(10, 15) ) ilo_cache[ilo.id] = ilo - print "." + echo "." end - puts "!" + echo_line "!" # Align each of the ILOs to a task if unit_details[:ilos] > 0 - print "----> Aligning tasks to ILOs" + echo "----> Aligning tasks to ILOs" 20.times do ilo_id = unit.learning_outcomes.pluck('id').sample task_def_id = unit.task_definition_ids.sample @@ -429,64 +504,9 @@ def generate_and_align_ilos_for_unit(unit, unit_details) link.rating = Faker::Number.between(1,4) link.description = faker_random_sentence(5, 10) link.save! - print '.' - end - puts '!' - end - end - - # - # Generates tutorials for unit and enrols some students in them - # - def generate_tutorials_and_enrol_students_for_unit(unit, unit_details) - student_count = 0 - tutorial_count = 0 - - # Grab stuff from scale - max_tutorials = @scale[:max_tutorials] - min_students = @scale[:min_students] - delta_students = @scale[:delta_students] - - # Collection of weekdays to be used - weekdays = %w[Monday Tuesday Wednesday Thursday Friday] - - # Create tutorials and enrol students - unit_details[:tutors].each do | user_details | - # only up to 4 tutorials for small scale - if tutorial_count > max_tutorials then break end - - tutor = @user_cache[user_details[:user]] - puts "----> Enrolling tutor #{tutor.name} with #{user_details[:num]} tutorials" - tutor_unit_role = unit.employ_staff(tutor, Role.tutor) - - user_details[:num].times do | count | - tutorial_count += 1 - #day, time, location, tutor_username, abbrev - tutorial = unit.add_tutorial( - "#{weekdays.sample}", - "#{8 + Faker::Number.between(0,11)}:#{['00', '30'].sample}", # Mon-Fri 8am-7:30pm - "#{['EN', 'BA'].sample}#{Faker::Number.between(0,6)}0#{Faker::Number.between(0,8)}", # EN###/BA### - tutor, - "LA1-#{tutorial_count.to_s.rjust(2, '0')}" - ) - - # Add a random number of students to the tutorial - num_students_in_tutorial = (min_students + Faker::Number.between(0,delta_students - 1)) - print "-----> Creating #{num_students_in_tutorial} projects under tutorial #{tutorial.abbreviation}" - num_students_in_tutorial.times do - student = find_or_create_student("student_#{student_count}") - project = unit.enrol_student(student, tutorial.id) - student_count += 1 - print '.' - end - # Add fixed students to first tutorial - if count == 0 - unit_details[:students].each do | student_key | - unit.enrol_student(@user_cache[student_key], tutorial.id) - end - end - puts "!" + echo '.' end + echo_line '!' end end end diff --git a/lib/helpers/find_or_create_students.rb b/lib/helpers/find_or_create_students.rb index d00ddb59c..54a4f104d 100644 --- a/lib/helpers/find_or_create_students.rb +++ b/lib/helpers/find_or_create_students.rb @@ -4,7 +4,7 @@ def find_or_create_student(username) user_created = nil using_cache = !@user_cache.nil? - if !using_cache || !@user_cache.key?(username) + if using_cache && !@user_cache.key?(username) profile = { first_name: Faker::Name.first_name, last_name: Faker::Name.last_name, @@ -19,6 +19,8 @@ def find_or_create_student(username) end user_created = User.create!(profile) @user_cache[username] = user_created if using_cache + else + user_created = User.find_by_username(username) end user_created || @user_cache[username] end diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb index 72999f746..55987b274 100644 --- a/test/models/unit_test.rb +++ b/test/models/unit_test.rb @@ -2,15 +2,47 @@ class UnitTest < ActiveSupport::TestCase + def setup + data = { + code: 'COS10001', + name: 'Testing in Unit Tests', + description: faker_random_sentence(10, 15), + teaching_period_id: TeachingPeriod.find(3).id + } + @unit = Unit.create(data) + + # @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) + # @unit.import_task_alignment_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Alignment.csv")), nil + end + + test 'import tasks worked' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + assert_equal 36, @unit.task_definitions.count, 'imported all task definitions' + end + + test 'import task files' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + @unit.import_task_files_from_zip Rails.root.join('test_files',"#{@unit.code}-Tasks.zip") + + assert File.exists? @unit.task_definitions.first.task_sheet + assert File.exists? @unit.task_definitions.first.task_resources + end + test 'ensure valid response from unit ilo data' do - unit = Unit.first - details = unit.ilo_progress_class_details + @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) + DatabasePopulator.new.generate_tutorials_and_enrol_students_for_unit @unit, { + tutors: [ + { user: :acain, num: 2 }, + { user: :aconvenor, num: 2 }, + ], + students: [ ] + } + details = @unit.ilo_progress_class_details assert details.key?('all'), 'contains all key' - unit.tutorials.each do |tute| + @unit.tutorials.each do |tute| assert details.key?(tute.id), 'contains tutorial keys' end - end end From 1480b890a14c2706a91c0cf413877d284d342f44 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 12:56:17 +1100 Subject: [PATCH 18/30] TEST: Shift task assessment to enable explicit use in tests --- lib/helpers/database_populator.rb | 33 +++++++++++++++- lib/tasks/populate.rake | 66 ++++++++----------------------- test/models/unit_test.rb | 34 +++++++++++++++- 3 files changed, 81 insertions(+), 52 deletions(-) diff --git a/lib/helpers/database_populator.rb b/lib/helpers/database_populator.rb index ca50cacde..760c2a1b4 100644 --- a/lib/helpers/database_populator.rb +++ b/lib/helpers/database_populator.rb @@ -327,7 +327,7 @@ def generate_tutorials_and_enrol_students_for_unit(unit, unit_details) # Create tutorials and enrol students unit_details[:tutors].each do | user_details | # only up to 4 tutorials for small scale - if tutorial_count > max_tutorials then break end + break if tutorial_count > max_tutorials if @user_cache.present? tutor = @user_cache[user_details[:user]] @@ -369,6 +369,37 @@ def generate_tutorials_and_enrol_students_for_unit(unit, unit_details) end end + def self.assess_task(proj, task, tutor, status, complete_date) + alignments = [] + sum_ratings = 0 + task.unit.learning_outcomes.each do |lo| + data = { + ilo_id: lo.id, + rating: rand(0..5), + rationale: "Simulated rationale text..." + } + sum_ratings += data[:rating] + alignments << data + end + + if task.group_task? + raise "Cant support group tasks yet in simulation :(" + end + contributions = nil + + task.create_alignments_from_submission(alignments) unless alignments.nil? + task.create_submission_and_trigger_state_change(proj.student) #, propagate = true, contributions = contributions, trigger = trigger) + task.assess status, tutor, complete_date + + pdf_path = task.final_pdf_path + if pdf_path + FileUtils.ln_s(Rails.root.join('test_files', 'unit_files', 'sample-student-submission.pdf'), pdf_path) + end + + task.portfolio_evidence = pdf_path + task.save + end + private # Output diff --git a/lib/tasks/populate.rake b/lib/tasks/populate.rake index 99ee14f64..b8bd95f47 100644 --- a/lib/tasks/populate.rake +++ b/lib/tasks/populate.rake @@ -11,38 +11,6 @@ namespace :db do end end - def assess_task(proj, task, tutor, status, complete_date) - alignments = [] - sum_ratings = 0 - task.unit.learning_outcomes.each do |lo| - data = { - ilo_id: lo.id, - rating: rand(0..5), - rationale: "Simulated rationale text..." - } - sum_ratings += data[:rating] - alignments << data - end - - if task.group_task? - raise "Cant support group tasks yet in simulation :(" - end - contributions = nil - trigger = - - task.create_alignments_from_submission(alignments) unless alignments.nil? - task.create_submission_and_trigger_state_change(proj.student) #, propagate = true, contributions = contributions, trigger = trigger) - task.assess status, tutor, complete_date - - pdf_path = task.final_pdf_path - if pdf_path - FileUtils.ln_s(Rails.root.join('test_files', 'unit_files', 'sample-student-submission.pdf'), pdf_path) - end - - task.portfolio_evidence = pdf_path - task.save - end - desc 'Mark off some of the due tasks' task simulate_signoff: [:skip_prod, :environment] do Unit.all.each do |unit| @@ -106,7 +74,7 @@ namespace :db do elsif complete_date > Time.zone.now complete_date = Time.zone.now end - assess_task(proj, task, tutor, TaskStatus.complete, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.complete, complete_date) elsif kept_up_to_date >= task.target_date + 1.week complete_date = unit.start_date + i * time_to_complete_task + rand(7..14).days if complete_date < unit.start_date + 1.week @@ -118,21 +86,21 @@ namespace :db do # 1 to 3 case rand(1..100) when 0..50 - assess_task(proj, task, tutor, TaskStatus.complete, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.complete, complete_date) when 51..75 - assess_task(proj, task, tutor, TaskStatus.discuss, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.discuss, complete_date) when 76..90 - assess_task(proj, task, tutor, TaskStatus.demonstrate, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.demonstrate, complete_date) when 91..95 - assess_task(proj, task, tutor, TaskStatus.fix_and_resubmit, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.fix_and_resubmit, complete_date) when 96..97 - assess_task(proj, task, tutor, TaskStatus.working_on_it, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.working_on_it, complete_date) when 97 - assess_task(proj, task, tutor, TaskStatus.do_not_resubmit, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.do_not_resubmit, complete_date) when 98..99 - assess_task(proj, task, tutor, TaskStatus.redo, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.redo, complete_date) else - assess_task(proj, task, tutor, TaskStatus.ready_to_mark, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.ready_to_mark, complete_date) end else complete_date = unit.start_date + i * time_to_complete_task + rand(7..10).days @@ -145,21 +113,21 @@ namespace :db do # 1 to 3 case rand(1..100) when 0..3 - assess_task(proj, task, tutor, TaskStatus.complete, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.complete, complete_date) when 4..60 - assess_task(proj, task, tutor, TaskStatus.ready_to_mark, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.ready_to_mark, complete_date) when 61..70 - assess_task(proj, task, tutor, TaskStatus.discuss, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.discuss, complete_date) when 71..80 - assess_task(proj, task, tutor, TaskStatus.demonstrate, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.demonstrate, complete_date) when 81..90 - assess_task(proj, task, tutor, TaskStatus.fix_and_resubmit, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.fix_and_resubmit, complete_date) when 91..98 - assess_task(proj, task, tutor, TaskStatus.working_on_it, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.working_on_it, complete_date) when 99 - assess_task(proj, task, tutor, TaskStatus.redo, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.redo, complete_date) else - assess_task(proj, task, tutor, TaskStatus.ready_to_mark, complete_date) + DatabasePopulator.assess_task(proj, task, tutor, TaskStatus.ready_to_mark, complete_date) end end diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb index 55987b274..3d9085552 100644 --- a/test/models/unit_test.rb +++ b/test/models/unit_test.rb @@ -12,7 +12,6 @@ def setup @unit = Unit.create(data) # @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) - # @unit.import_task_alignment_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Alignment.csv")), nil end test 'import tasks worked' do @@ -29,14 +28,45 @@ def setup end test 'ensure valid response from unit ilo data' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) + @unit.import_task_alignment_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Alignment.csv")), nil + DatabasePopulator.new.generate_tutorials_and_enrol_students_for_unit @unit, { tutors: [ - { user: :acain, num: 2 }, + { user: :acain, num: 1 }, { user: :aconvenor, num: 2 }, ], students: [ ] } + + assert_equal 3, @unit.tutorials.count + + @unit.students.each do |student| + @unit.task_definitions.each do |td| + task = student.task_for_task_definition(td) + + case rand(1..100) + when 1..20 + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.complete, td.due_date + 1.week) + when 21..40 + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.ready_to_mark, td.due_date + 1.week) + when 41..50 + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.time_exceeded, td.due_date + 1.week) + when 51..60 + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.not_started, td.due_date + 1.week) + when 61..70 + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.working_on_it, td.due_date + 1.week) + when 71..80 + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.discuss, td.due_date + 1.week) + else + DatabasePopulator.assess_task(student, task, student.main_tutor, TaskStatus.fix_and_resubmit, td.due_date + 1.week) + end + + break if rand(1..100) > 80 + end + end + details = @unit.ilo_progress_class_details assert details.key?('all'), 'contains all key' From 5b87cb48904f7f108da2906322ad3e58dd5fa327 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 19 Oct 2018 13:44:00 +1100 Subject: [PATCH 19/30] CONFIG: Remove hyper --- Gemfile | 1 - Gemfile.lock | 4 +--- README.md | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 89e611674..fd18db234 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,6 @@ group :development, :test do gem 'database_cleaner' gem 'factory_girl_rails' gem 'minitest-around' - gem 'minitest-hyper' gem 'minitest-osx' gem 'minitest-rails' gem 'byebug' diff --git a/Gemfile.lock b/Gemfile.lock index 512d711c0..ee2dde335 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,7 +132,6 @@ GEM minitest (5.9.0) minitest-around (0.4.0) minitest (~> 5.0) - minitest-hyper (0.2.0) minitest-osx (0.1.0) minitest (~> 5.4) terminal-notifier (~> 1.6) @@ -280,7 +279,6 @@ DEPENDENCIES hirb json-jwt (= 1.7.0) minitest-around - minitest-hyper minitest-osx minitest-rails moss_ruby (= 1.1.2) @@ -306,4 +304,4 @@ RUBY VERSION ruby 2.3.1p112 BUNDLED WITH - 1.16.3 + 1.16.6 diff --git a/README.md b/README.md index 385f70dd8..c7e2b8024 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,6 @@ To run unit tests, execute: $ rake test ``` -A report will be generated under `spec/reports/hyper/index.html`. - Unit tests are located in the `test` directory, where **model** tests are under the `model` subdirectory and **API** tests are under the `api` subdirectory. From 9435e31b3ddd839a0fad3a1c51c4773773400ce9 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Sat, 20 Oct 2018 06:00:43 +1100 Subject: [PATCH 20/30] TEST: Test week numbers and start rollover tests --- app/models/break.rb | 8 +++++ app/models/teaching_period.rb | 38 ++++++++++++++++++++++++ test/models/teaching_period_test.rb | 45 +++++++++++++++++++++++++++++ test/models/unit_test.rb | 25 ++++++++++++++-- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 test/models/teaching_period_test.rb diff --git a/app/models/break.rb b/app/models/break.rb index 4fc4af565..1eb75773f 100644 --- a/app/models/break.rb +++ b/app/models/break.rb @@ -18,4 +18,12 @@ def ensure_break_end_is_within_teaching_period errors.add(:number_of_weeks, "is exceeding Teaching Period end date") end end + + def duration + number_of_weeks.weeks + end + + def end_date + start_date + duration + end end \ No newline at end of file diff --git a/app/models/teaching_period.rb b/app/models/teaching_period.rb index 55a1d6c15..48fac0248 100644 --- a/app/models/teaching_period.rb +++ b/app/models/teaching_period.rb @@ -33,6 +33,44 @@ def add_break(start_date, number_of_weeks) break_in_teaching_period end + def week_no(date) + # Calcualte date offset, add 2 so 0-week offset is week 1 not week 0 + result = ((date - start_date) / 1.week).floor + 1 + + for a_break in breaks.all do + if date >= a_break.start_date + # we are in or after the break, so calculated week needs to + # be reduced by this break + if date >= a_break.end_date + result -= a_break.number_of_weeks + elsif date == a_break.start_date + # cant use standard calculation as this give 0 for this exact moment... + result -= 1 + else + # in break so partial reduction + result -= ((date - a_break.start_date) / 1.week).ceil + end + end + end + + result + end + + def date_for_week(num) + # start by switching from 1 based to 0 based + # week 1 is offset 0 weeks from the start + num -= 1 + for a_break in breaks do + if num >= week_no(a_break.start_date) + # we are in or after the break, so calculated date is + # extended by the break period + num += a_break.number_of_weeks + end + end + + result = start_date + num.weeks + end + def rollover(rollover_to) rollover_to.add_associations(self) rollover_to.save! diff --git a/test/models/teaching_period_test.rb b/test/models/teaching_period_test.rb new file mode 100644 index 000000000..f286ccbc5 --- /dev/null +++ b/test/models/teaching_period_test.rb @@ -0,0 +1,45 @@ +require 'test_helper' + +class TeachingPeriodTest < ActiveSupport::TestCase + + test 'week 1 is first week of teaching period' do + tp = TeachingPeriod.first + + assert_equal 1, tp.week_no(tp.start_date) + end + + test 'weeks advance with calendar weeks' do + tp = TeachingPeriod.first + + assert_equal 2, tp.week_no(tp.start_date + 1.week) + end + + test 'weeks advance with breaks' do + tp = TeachingPeriod.find(2) + + assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.duration) + assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.duration) + end + + test 'can map week number to date' do + tp = TeachingPeriod.first + + assert_equal tp.start_date, tp.date_for_week(1) + assert_equal tp.start_date + 1.week, tp.date_for_week(2) + end + + test 'can map week number to date across breaks' do + tp = TeachingPeriod.first + + break_in_cal_week = (tp.breaks.first.start_date - tp.start_date) / 1.week + 1 + assert_equal tp.breaks.first.start_date + tp.breaks.first.number_of_weeks.week, tp.date_for_week(break_in_cal_week) + end + + test 'week number works with mult-week breaks' do + tp = TeachingPeriod.find(3) + + assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.number_of_weeks.weeks) + assert_equal 2, tp.breaks.first.number_of_weeks + end + +end diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb index 3d9085552..693e3a5b5 100644 --- a/test/models/unit_test.rb +++ b/test/models/unit_test.rb @@ -10,8 +10,6 @@ def setup teaching_period_id: TeachingPeriod.find(3).id } @unit = Unit.create(data) - - # @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) end test 'import tasks worked' do @@ -27,6 +25,29 @@ def setup assert File.exists? @unit.task_definitions.first.task_resources end + test 'rollover of task files' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + @unit.import_task_files_from_zip Rails.root.join('test_files',"#{@unit.code}-Tasks.zip") + + unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil + + assert File.exists?(unit2.task_definitions.first.task_sheet), 'task sheet is absent' + assert File.exists?(unit2.task_definitions.first.task_resources), 'task resource is absent' + end + + test 'rollover of tasks' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + @unit.import_task_files_from_zip Rails.root.join('test_files',"#{@unit.code}-Tasks.zip") + + unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil + + @unit.task_definitions.each do |td| + td2 = unit2.task_definitions.find_by_abbreviation(td.abbreviation) + assert_equal td.start_day, td2.start_day, "#{td.abbreviation} not on same day" + assert_equal td.start_week, td2.start_week, "#{td.abbreviation} not in same week" + end + end + test 'ensure valid response from unit ilo data' do @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) From e41ccaaa8610c2aea6075bd7ddc058cb17d41da2 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Sat, 20 Oct 2018 14:00:56 +1100 Subject: [PATCH 21/30] TEST: Check date from day and week in teaching period --- app/models/teaching_period.rb | 13 +++++++++++++ test/models/teaching_period_test.rb | 28 ++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/models/teaching_period.rb b/app/models/teaching_period.rb index 48fac0248..2f933b9a6 100644 --- a/app/models/teaching_period.rb +++ b/app/models/teaching_period.rb @@ -71,6 +71,19 @@ def date_for_week(num) result = start_date + num.weeks end + def date_for_week_and_day(week, day) + return nil if week.nil? || day.nil? + + week_start = date_for_week(week) + + day_num = Date::ABBR_DAYNAMES.index day.titlecase + return nil if day_num.nil? + + start_day_num = start_date.wday + + week_start + (day_num - start_day_num).days + end + def rollover(rollover_to) rollover_to.add_associations(self) rollover_to.save! diff --git a/test/models/teaching_period_test.rb b/test/models/teaching_period_test.rb index f286ccbc5..752007e00 100644 --- a/test/models/teaching_period_test.rb +++ b/test/models/teaching_period_test.rb @@ -15,10 +15,10 @@ class TeachingPeriodTest < ActiveSupport::TestCase end test 'weeks advance with breaks' do - tp = TeachingPeriod.find(2) + tp = TeachingPeriod.find(1) assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.duration) - assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.duration) + assert_equal tp.week_no(tp.breaks.first.start_date), tp.week_no(tp.breaks.first.start_date + 1.day) end test 'can map week number to date' do @@ -28,6 +28,27 @@ class TeachingPeriodTest < ActiveSupport::TestCase assert_equal tp.start_date + 1.week, tp.date_for_week(2) end + test 'can map week and day to date' do + tp = TeachingPeriod.first + + assert_equal tp.start_date + 1.day, tp.date_for_week_and_day(1, 'Tue') + assert_equal tp.start_date + 2.day + 1.week, tp.date_for_week_and_day(2, 'Wed') + assert_equal tp.start_date + 3.day + 2.week, tp.date_for_week_and_day(3, 'Thu') + assert_equal tp.start_date + 4.day + 2.week, tp.date_for_week_and_day(3, 'Fri') + assert_equal tp.start_date + 5.day, tp.date_for_week_and_day(1, 'Sat') + assert_equal tp.start_date - 1.day, tp.date_for_week_and_day(1, 'Sun') + end + + test 'can map week and day to date after break' do + tp = TeachingPeriod.find(2) + + start_of_break = tp.breaks.first.start_date + end_of_break = tp.breaks.first.end_date + break_in_cal_week = (start_of_break - tp.start_date) / 1.week + 1 + + assert_equal end_of_break + 1.day, tp.date_for_week_and_day(break_in_cal_week, 'Tue') + end + test 'can map week number to date across breaks' do tp = TeachingPeriod.first @@ -42,4 +63,7 @@ class TeachingPeriodTest < ActiveSupport::TestCase assert_equal 2, tp.breaks.first.number_of_weeks end + test 'week date works for initial weeks' do + end + end From 46dd5253c285627464acbed00665613d2063443c Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Sun, 21 Oct 2018 07:53:43 +1100 Subject: [PATCH 22/30] TEST: Check breaks mid week for week and day --- app/models/break.rb | 10 +++++++ app/models/teaching_period.rb | 38 +++++++++++++++++++++----- test/models/teaching_period_test.rb | 41 +++++++++++++++++++++++++---- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/app/models/break.rb b/app/models/break.rb index 1eb75773f..9b4b4a772 100644 --- a/app/models/break.rb +++ b/app/models/break.rb @@ -23,6 +23,16 @@ def duration number_of_weeks.weeks end + def first_monday + return start_date if start_date.wday == 1 + return start_date + 1.day if start_date.wday == 0 + return start_date + (8 - start_date.wday).days + end + + def monday_after_break + first_monday + number_of_weeks.weeks + end + def end_date start_date + duration end diff --git a/app/models/teaching_period.rb b/app/models/teaching_period.rb index 2f933b9a6..1edff0f33 100644 --- a/app/models/teaching_period.rb +++ b/app/models/teaching_period.rb @@ -41,14 +41,22 @@ def week_no(date) if date >= a_break.start_date # we are in or after the break, so calculated week needs to # be reduced by this break + if date >= a_break.end_date + # past the end of the break... result -= a_break.number_of_weeks elsif date == a_break.start_date # cant use standard calculation as this give 0 for this exact moment... - result -= 1 - else + result -= 1 if date >= a_break.first_monday + elsif date >= a_break.first_monday # in break so partial reduction - result -= ((date - a_break.start_date) / 1.week).ceil + result -= ((date - a_break.first_monday) / 1.week).ceil + end + + # for times just past the break but before start of next week... + if date >= a_break.end_date && date < a_break.monday_after_break + # Need to add 1 as we are now in a new week! + result += 1 end end end @@ -57,18 +65,24 @@ def week_no(date) end def date_for_week(num) + num = num.floor + # start by switching from 1 based to 0 based # week 1 is offset 0 weeks from the start num -= 1 + + result = start_date + num.weeks + + # check breaks for a_break in breaks do - if num >= week_no(a_break.start_date) + if result >= a_break.start_date # we are in or after the break, so calculated date is # extended by the break period - num += a_break.number_of_weeks + result += a_break.number_of_weeks.weeks end end - result = start_date + num.weeks + result end def date_for_week_and_day(week, day) @@ -81,7 +95,17 @@ def date_for_week_and_day(week, day) start_day_num = start_date.wday - week_start + (day_num - start_day_num).days + result = week_start + (day_num - start_day_num).days + + for a_break in breaks do + if result >= a_break.start_date && result < a_break.end_date + # we are in or after the break, so calculated date is + # extended by the break period + result += a_break.number_of_weeks.weeks + end + end + + result end def rollover(rollover_to) diff --git a/test/models/teaching_period_test.rb b/test/models/teaching_period_test.rb index 752007e00..07b416f61 100644 --- a/test/models/teaching_period_test.rb +++ b/test/models/teaching_period_test.rb @@ -17,7 +17,7 @@ class TeachingPeriodTest < ActiveSupport::TestCase test 'weeks advance with breaks' do tp = TeachingPeriod.find(1) - assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.duration) + assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.end_date) assert_equal tp.week_no(tp.breaks.first.start_date), tp.week_no(tp.breaks.first.start_date + 1.day) end @@ -44,25 +44,56 @@ class TeachingPeriodTest < ActiveSupport::TestCase start_of_break = tp.breaks.first.start_date end_of_break = tp.breaks.first.end_date - break_in_cal_week = (start_of_break - tp.start_date) / 1.week + 1 + break_in_cal_week = ((start_of_break - tp.start_date) / 1.week).ceil + 1 # as mon break assert_equal end_of_break + 1.day, tp.date_for_week_and_day(break_in_cal_week, 'Tue') end + test 'Test date for week and day where day ends on break' do + tp = TeachingPeriod.find(1) + + start_of_break = tp.breaks.first.start_date + end_of_break = tp.breaks.first.end_date + break_in_cal_week = ((start_of_break - tp.start_date) / 1.week).ceil # no + 1 as fri + + assert_equal end_of_break, tp.date_for_week_and_day(break_in_cal_week, 'Fri') + end + + test 'can map week number to date across breaks' do - tp = TeachingPeriod.first + tp = TeachingPeriod.find(2) - break_in_cal_week = (tp.breaks.first.start_date - tp.start_date) / 1.week + 1 + break_in_cal_week = ((tp.breaks.first.start_date - tp.start_date) / 1.week).ceil + 1 # + 1 as mon break assert_equal tp.breaks.first.start_date + tp.breaks.first.number_of_weeks.week, tp.date_for_week(break_in_cal_week) end + test 'can map week number to date across breaks starting friday' do + tp = TeachingPeriod.find(1) + + break_in_cal_week = ((tp.breaks.first.start_date - tp.start_date) / 1.week).ceil # no +1 as Fri + assert_equal tp.breaks.first.start_date - 4.days, tp.date_for_week(break_in_cal_week) + assert_equal tp.breaks.first.start_date + tp.breaks.first.number_of_weeks.week + 3.days, tp.date_for_week(break_in_cal_week + 1) + end + test 'week number works with mult-week breaks' do tp = TeachingPeriod.find(3) - assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.start_date + tp.breaks.first.number_of_weeks.weeks) + assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.end_date) assert_equal 2, tp.breaks.first.number_of_weeks end + test 'check end date' do + TeachingPeriod.all.each do |tp| + assert_equal tp.breaks.first.start_date + tp.breaks.first.number_of_weeks.weeks, tp.breaks.first.end_date + end + end + + test 'check break next monday' do + tp = TeachingPeriod.first + + assert_equal tp.date_for_week(5), tp.breaks.first.monday_after_break + end + test 'week date works for initial weeks' do end From f921271402fc7415ee4228c1379809648c82942c Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 22 Oct 2018 20:24:29 +1100 Subject: [PATCH 23/30] FIX: Correct importing of task definitions with groups --- app/models/task_definition.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 652f38cac..9539c282b 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -382,11 +382,11 @@ def self.task_def_for_csv_row(unit, row) result.plagiarism_warn_pct = row[:plagiarism_warn_pct] result.plagiarism_checks = row[:plagiarism_checks] - unless row[:group_set].present? - result.group_set = unit.group_sets.where(name: row[:group_set]).first + if row[:group_set].present? + result.group_set = unit.group_sets.where(name: row[:group_set]).first end - if result.valid? && (row[:group_set].nil? || !result.group_set.nil?) + if result.valid? && (row[:group_set].blank? || result.group_set.present?) begin result.save rescue @@ -394,7 +394,7 @@ def self.task_def_for_csv_row(unit, row) return [nil, false, 'Failed to save definition due to data error.'] end else - if result.group_set.nil? && !row[:group_set].nil? + if result.group_set.nil? && row[:group_set].present? return [nil, false, "Unable to find groupset with name #{row[:group_set]} in unit."] else return [nil, false, result.errors.full_messages.join('. ')] From 3bef39bbfbf73a0e36c12839a84f245ac9c05ffd Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 22 Oct 2018 20:43:34 +1100 Subject: [PATCH 24/30] TEST: Check that group imports pass and link to group sets --- app/models/group_set.rb | 1 + app/models/task_definition.rb | 2 ++ test/models/task_definition_test.rb | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/app/models/group_set.rb b/app/models/group_set.rb index 485386790..4deebb70f 100644 --- a/app/models/group_set.rb +++ b/app/models/group_set.rb @@ -1,5 +1,6 @@ class GroupSet < ActiveRecord::Base belongs_to :unit + has_many :task_definitions has_many :groups, dependent: :destroy validates_associated :groups diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 9539c282b..06a84e035 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -394,6 +394,8 @@ def self.task_def_for_csv_row(unit, row) return [nil, false, 'Failed to save definition due to data error.'] end else + # delete the task if it was new + result.destroy if new_task if result.group_set.nil? && row[:group_set].present? return [nil, false, "Unable to find groupset with name #{row[:group_set]} in unit."] else diff --git a/test/models/task_definition_test.rb b/test/models/task_definition_test.rb index 8e9503772..ee9d1ba0e 100644 --- a/test/models/task_definition_test.rb +++ b/test/models/task_definition_test.rb @@ -36,4 +36,27 @@ def test_default_quality_points td.destroy end + + def test_default_quality_points + u = Unit.first + + group_params = { + name: 'Group Work', + allow_students_to_create_groups: true, + allow_students_to_manage_groups: true, + keep_groups_in_same_class: true + } + + initial_count = u.task_definitions.count + + group_set = GroupSet.create!(group_params) + group_set.unit = u + group_set.save! + + path = Rails.root.join('test_files', 'unit_csv_imports', 'import_group_tasks.csv') + u.import_tasks_from_csv File.new(path) + + assert_equal 1, group_set.task_definitions.count + assert_equal initial_count + 1, u.task_definitions.count + end end From 38cb5408476ee76e690b5642369f27c385086469 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 31 Oct 2018 16:56:49 +1100 Subject: [PATCH 25/30] FIX: Switch rollover to use day/week for task defsSimplify the logic to make use of the date for week and daycode. Going forward this should be the way oftracking task dates. --- app/models/task_definition.rb | 44 +++++++++++++++++++-- app/models/teaching_period.rb | 2 +- app/models/unit.rb | 58 ++++++++++++++-------------- test/models/teaching_period_test.rb | 10 ++--- test/models/unit_test.rb | 44 ++++++++++++++++++--- test_files/COS10001-tasks.zip | Bin 838674 -> 862615 bytes 6 files changed, 115 insertions(+), 43 deletions(-) diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index e9f873e09..7227a7f19 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -33,6 +33,27 @@ class TaskDefinition < ActiveRecord::Base validate :ensure_no_submissions, if: :has_change_group_status? + def copy_to(other_unit) + new_td = self.dup + + # change the unit... + new_td.unit_id = other_unit.id # for database + new_td.unit = other_unit # for other operations + other_unit.task_definitions << new_td # so we can see it in unit elsewhere + + # Adjust dates + new_td.start_week_and_day = start_week, start_day + new_td.target_week_and_day = target_week, target_day + + if self['due_date'].present? + new_td.due_week_and_day = due_week, due_day + end + + new_td.save + + new_td + end + def has_change_group_status? group_set_id != group_set_id_was end @@ -277,21 +298,31 @@ def self.to_csv(task_definitions, options = {}) end def start_week - ((start_date - unit.start_date) / 1.week).floor + unit.week_number(start_date) end def start_day Date::ABBR_DAYNAMES[start_date.wday] end + def start_week_and_day= value + week, day = value + self.start_date = unit.date_for_week_and_day(week, day) + end + def target_week - ((target_date - unit.start_date) / 1.week).floor + unit.week_number(target_date) end def target_day Date::ABBR_DAYNAMES[target_date.wday] end + def target_week_and_day= value + week, day = value + self.target_date = unit.date_for_week_and_day(week, day) + end + # Override due date to return either the final date of the unit, or the set due date def due_date return self['due_date'] if self['due_date'].present? @@ -299,13 +330,18 @@ def due_date end def due_week - if due_date - ((due_date - unit.start_date) / 1.week).floor + if due_date.present? + unit.week_number(due_date) else '' end end + def due_week_and_day= value + week, day = value + self.due_date = unit.date_for_week_and_day(week, day) + end + def due_day if due_date Date::ABBR_DAYNAMES[due_date.wday] diff --git a/app/models/teaching_period.rb b/app/models/teaching_period.rb index 1edff0f33..d14008095 100644 --- a/app/models/teaching_period.rb +++ b/app/models/teaching_period.rb @@ -33,7 +33,7 @@ def add_break(start_date, number_of_weeks) break_in_teaching_period end - def week_no(date) + def week_number(date) # Calcualte date offset, add 2 so 0-week offset is week 1 not week 0 result = ((date - start_date) / 1.week).floor + 1 diff --git a/app/models/unit.rb b/app/models/unit.rb index 475134c30..801a80478 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -139,21 +139,22 @@ def validate_end_date_after_start_date def rollover(teaching_period_id, start_date, end_date) new_unit = self.dup + if teaching_period_id.present? - new_unit.set_teaching_period(teaching_period_id) + new_unit.teaching_period_id = teaching_period_id + new_unit.teaching_period = TeachingPeriod.find(teaching_period_id) else - new_unit.set_custom_dates(start_date, end_date) + new_unit.start_date = start_date + new_unit.end_date = end_date end - new_unit.add_associations(self) - new_unit - end + + new_unit.save! - def set_teaching_period(teaching_period_id) - self.teaching_period_id = teaching_period_id - self.save! + new_unit.duplicate_details(self) + new_unit end - def add_associations(unit) + def duplicate_details(unit) self.duplicate_task_definitions_from_existing_unit(unit) self.duplicate_learning_outcomes_from_existing_unit(unit) self.duplicate_group_sets_from_existing_unit(unit) @@ -161,15 +162,8 @@ def add_associations(unit) end def duplicate_task_definitions_from_existing_unit(unit) - diff_in_sec = (self.start_date - unit.start_date).to_i - unit.task_definitions.each do |task_definitions| - new_task_definitions = task_definitions.dup - new_task_definitions.adjust_dates(diff_in_sec) - self.task_definitions << new_task_definitions - end - self.task_definitions.each do |task_definitions| - task_definitions.adjust_dates_for_breaks_in_current_teaching_period - task_definitions.save! + unit.task_definitions.each do |td| + td.copy_to(self) end end @@ -191,12 +185,6 @@ def duplicate_convenors_from_existing_unit(unit) end end - def set_custom_dates(start_date, end_date) - self.start_date = start_date - self.end_date = end_date - self.save! - end - def ordered_ilos learning_outcomes.order(:ilo_number) end @@ -933,13 +921,27 @@ def add_tutorial(day, time, location, tutor, abbrev) end end + # First day of the week is sunday... def date_for_week_and_day(week, day) return nil if week.nil? || day.nil? - day_num = Date::ABBR_DAYNAMES.index day.titlecase - return nil if day_num.nil? - start_day_num = start_date.wday - start_date + week.weeks + (day_num - start_day_num).days + if teaching_period.present? + teaching_period.date_for_week_and_day(week, day) + else + day_num = Date::ABBR_DAYNAMES.index day.titlecase + return nil if day_num.nil? + start_day_num = start_date.wday + + start_date + week.weeks + (day_num - start_day_num).days + end + end + + def week_number(date) + if teaching_period.present? + teaching_period.week_number(date) + else + ((date - start_date) / 1.week).floor + end end def import_tasks_from_csv(file) diff --git a/test/models/teaching_period_test.rb b/test/models/teaching_period_test.rb index 07b416f61..2a286e82d 100644 --- a/test/models/teaching_period_test.rb +++ b/test/models/teaching_period_test.rb @@ -5,20 +5,20 @@ class TeachingPeriodTest < ActiveSupport::TestCase test 'week 1 is first week of teaching period' do tp = TeachingPeriod.first - assert_equal 1, tp.week_no(tp.start_date) + assert_equal 1, tp.week_number(tp.start_date) end test 'weeks advance with calendar weeks' do tp = TeachingPeriod.first - assert_equal 2, tp.week_no(tp.start_date + 1.week) + assert_equal 2, tp.week_number(tp.start_date + 1.week) end test 'weeks advance with breaks' do tp = TeachingPeriod.find(1) - assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.end_date) - assert_equal tp.week_no(tp.breaks.first.start_date), tp.week_no(tp.breaks.first.start_date + 1.day) + assert_equal tp.week_number(tp.breaks.first.start_date) + 1, tp.week_number(tp.breaks.first.end_date) + assert_equal tp.week_number(tp.breaks.first.start_date), tp.week_number(tp.breaks.first.start_date + 1.day) end test 'can map week number to date' do @@ -78,7 +78,7 @@ class TeachingPeriodTest < ActiveSupport::TestCase test 'week number works with mult-week breaks' do tp = TeachingPeriod.find(3) - assert_equal tp.week_no(tp.breaks.first.start_date) + 1, tp.week_no(tp.breaks.first.end_date) + assert_equal tp.week_number(tp.breaks.first.start_date) + 1, tp.week_number(tp.breaks.first.end_date) assert_equal 2, tp.breaks.first.number_of_weeks end diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb index 693e3a5b5..f55757a2b 100644 --- a/test/models/unit_test.rb +++ b/test/models/unit_test.rb @@ -6,7 +6,7 @@ def setup data = { code: 'COS10001', name: 'Testing in Unit Tests', - description: faker_random_sentence(10, 15), + description: 'Test unit', teaching_period_id: TeachingPeriod.find(3).id } @unit = Unit.create(data) @@ -21,7 +21,10 @@ def setup @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) @unit.import_task_files_from_zip Rails.root.join('test_files',"#{@unit.code}-Tasks.zip") - assert File.exists? @unit.task_definitions.first.task_sheet + @unit.task_definitions.each do |td| + assert File.exists?(td.task_sheet), "#{td.abbreviation} task sheet missing" + end + assert File.exists? @unit.task_definitions.first.task_resources end @@ -31,23 +34,54 @@ def setup unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil - assert File.exists?(unit2.task_definitions.first.task_sheet), 'task sheet is absent' + unit2.task_definitions.each do |td| + assert File.exists?(td.task_sheet), 'task sheet is absent' + end + assert File.exists?(unit2.task_definitions.first.task_resources), 'task resource is absent' end - test 'rollover of tasks' do + test 'rollover of tasks have same start week and day' do @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) - @unit.import_task_files_from_zip Rails.root.join('test_files',"#{@unit.code}-Tasks.zip") unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil + assert_equal 3, @unit.teaching_period_id + assert_equal 2, unit2.teaching_period_id + @unit.task_definitions.each do |td| td2 = unit2.task_definitions.find_by_abbreviation(td.abbreviation) + assert_equal td.start_day, td2.start_day, "#{td.abbreviation} not on same day" assert_equal td.start_week, td2.start_week, "#{td.abbreviation} not in same week" end end + test 'rollover of tasks have same target week and day' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + + unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil + + @unit.task_definitions.each do |td| + td2 = unit2.task_definitions.find_by_abbreviation(td.abbreviation) + assert_equal td.target_day, td2.target_day, "#{td.abbreviation} not on same day" + assert_equal td.target_week, td2.target_week, "#{td.abbreviation} not targetting same week" + end + end + + test 'rollover of tasks have same due week and day' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + + unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil + + @unit.task_definitions.each do |td| + td2 = unit2.task_definitions.find_by_abbreviation(td.abbreviation) + assert_equal td.due_day, td2.due_day, "#{td.abbreviation} not on same day" + assert_equal td.due_week, td2.due_week, "#{td.abbreviation} not due same week" + end + end + + test 'ensure valid response from unit ilo data' do @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) diff --git a/test_files/COS10001-tasks.zip b/test_files/COS10001-tasks.zip index b91091011836871b417048465764b7685d89b162..7478f02c8a9f7b30911741705e3054b1cae2106e 100644 GIT binary patch delta 4156 zcmbuDT}TvB6vt=X+}X_)C3oA(C|B2f$#G|9XE(u!F#0laEv4x}%s$B;G7O>!S}6!B zBYWu9LunB7P+=A{QBbZR1O`zGK@@5qL?A^y7(rh%J8jL}b9QWUmSuMK?)lyS|D1C# zmpyY%B*T&Nc#U(0&&jA)>`-&9c28e<>WAvO4SYBp=F9iBwC`@OAjt`Ko-Y6gcx7yY z^~oj4zV)@eFF!S%kQwH@`bK_wYn6Om#u@dl9G^^bJ&ybWBv1)@9M@!Y>T$<-BMePZSYA=*wD0w71 zgc=aINMCT4OyX}``Ite0lXQbvMn$bE_>`iq2F+&wk!(DhVmh0c5;3hLfYd?cqAQfH zDmIAvkLWs4F`ZQ`E+)c>x^pyabV3coaw1t6=P$VNxumpl%$vkX7T%(|H!Lv92^N8| z%z-h>z$F&hjJtC1v{dRrSjH-qQ&nmg`TcMOHSl^-(J?_A^T_1;eXqL_+g?H{P`mOI z{mZpGwb{&MinMPT2cxdRy=&h;Z5weU*w?AM{!Ci>y$cQpm^7#;ak-;hji5k0$H%$0 zZ97cBuB$yAH-{aJCpoHtnZ|F=wR^nAk+saJZHR6LZf3@bw#)iNl3{;L&2(^An`x#4 zXRZzJP-igBl#*U-mR{NirIWq^Z^3Nlx7tMarm??j39H(#F*BMx4rpJ9bF`@O!rLiP z(a%Ppmd~IWIDZQOlCFNQ@2{pu$HxttGz8_V;VwY%r2e7N7i6rPV=C*(Sy{lBx_z(C zSl8e6)a#nccF;^5Yz0Kpll1!k7)!T=0YQoSF#r&}WwSw1RQY0sPTHnm)xBFwUQxNH z%D1N-0LhI3L-`nppfp_>0tkTm&Bh#whv!yI;GD3nwwpuFhEM#zE_5ZtE^#K5-X}LVS(oMUR z?DnW~5i;z#r26aQBmk25LtFh-R_6h})Kl93XTJFg07mU)o@!X~69Ed-Q22P`Oxkj6 zS_;`42r#okedqPq88zL$1MiiWJaca7h4 q)>*gHL3n1aAIz*`t%D79k}lR delta 3024 zcmbuBT}TvB6vxlFpQBFNYSM?6E?Sh!EfAM|1$w&YTfwLaG4+Dji$&{Om!@7+=Fy|c42>@Lf&XU_SZ|2^l-y<=vz z@d-6GX;ehgbw5fPBUgru%0LhRqH@$66NMeA7e>NJPm980bI!v&E((=i?2^793R8yV zt)^ZY--(MB$=yvp6PgTB#HmyfMuj%|h-d1#v0!=ma1JFkYy7kfB$Fa5 zR3^(rC$~%*AQAng_Mu*{wXGCosO%q6G9@>Enlbv%*k45UCnX@ZU^SLth?EmohM81 z4x)5BrnMugS;WCPgvd=vp~T#1jc)XSLPl#P$i70Z%2TINP@#^mN_g`M6N>56zpxh( zG&h|NvD3EgtQXAW7OA?>%DHmIvtCf$mTRQh2WZoEBb6)otx-%Urd+SzVS=l%<~00L z{fMBuc`!Ia-6c$jbve$seQdav?fvWr^GLXG%ebsV9Ra}SkBh7vVd5$#sBQ?{*kry2 zKzBPfU(h~_&c7nYrs@VJxDw7Sy<-s*iW!?@Jt89LZko;&@A6#{VyhO9o0Rll5zMt) z3r`A{=wn|gw62jbaFZ$Q;NR~&`X44Yx5AewEc(6pxtfS^D`)@z@v~S x?Jhraw%uP-j^1J%A0_)C)yu}u52AR*F`n4~@Jo#kzcxV=`U(=jpV@Ns??01_P$>Wa From 6399f368631d5670cc84fbe385d4f317a7505a8a Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 31 Oct 2018 17:01:45 +1100 Subject: [PATCH 26/30] TEST: Add missing group tasks test csv --- test_files/unit_csv_imports/import_group_tasks.csv | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test_files/unit_csv_imports/import_group_tasks.csv diff --git a/test_files/unit_csv_imports/import_group_tasks.csv b/test_files/unit_csv_imports/import_group_tasks.csv new file mode 100644 index 000000000..a20a46a52 --- /dev/null +++ b/test_files/unit_csv_imports/import_group_tasks.csv @@ -0,0 +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 +Group Import 1,1GI,"Test Description - Import",16,0,false,0,false,80,[],Group Work,[],0,Mon,1,Sun,2,Wed +Missing Group,2GI,"Test Description - Import FAIL",16,0,false,0,false,80,[],Group Work1,[],0,Mon,1,Sun,2,Wed From 40f72c7f53589b27e197229fc87fbead395538c6 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 31 Oct 2018 17:05:35 +1100 Subject: [PATCH 27/30] QUALITY: Merge duplication code in unitSimplify the structure, move into rollover --- app/models/unit.rb | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 801a80478..19d7040f4 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -150,39 +150,27 @@ def rollover(teaching_period_id, start_date, end_date) new_unit.save! - new_unit.duplicate_details(self) - new_unit - end - - def duplicate_details(unit) - self.duplicate_task_definitions_from_existing_unit(unit) - self.duplicate_learning_outcomes_from_existing_unit(unit) - self.duplicate_group_sets_from_existing_unit(unit) - self.duplicate_convenors_from_existing_unit(unit) - end - - def duplicate_task_definitions_from_existing_unit(unit) - unit.task_definitions.each do |td| - td.copy_to(self) + # Duplicate task definitions + task_definitions.each do |td| + td.copy_to(new_unit) end - end - def duplicate_learning_outcomes_from_existing_unit(unit) - unit.learning_outcomes.each do |learning_outcomes| - self.learning_outcomes << learning_outcomes.dup + # Duplicate unit learning outcomes + learning_outcomes.each do |learning_outcomes| + new_unit.learning_outcomes << learning_outcomes.dup end - end - def duplicate_group_sets_from_existing_unit(unit) - unit.group_sets.each do |group_sets| - self.group_sets << group_sets.dup + # Duplicate group sets + group_sets.each do |group_sets| + new_unit.group_sets << group_sets.dup end - end - def duplicate_convenors_from_existing_unit(unit) - unit.convenors.each do |convenors| - self.convenors << convenors.dup + # Duplicate convenors + convenors.each do |convenors| + new_unit.convenors << convenors.dup end + + new_unit end def ordered_ilos From f863e7ecc927ba0d0626786851f8d2ef3fbfabd8 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 31 Oct 2018 17:22:20 +1100 Subject: [PATCH 28/30] FIX: Ensure task sheets are copied on definition copy --- app/models/task_definition.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index caa0e523a..83fd7e6a2 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -49,6 +49,17 @@ def copy_to(other_unit) new_td.due_week_and_day = due_week, due_day end + # Ensure we have the dir for the destination task sheet + FileHelper.task_file_dir_for_unit(other_unit, create = true) + + if has_task_sheet? + FileUtils.cp(task_sheet, new_td.task_sheet()) + end + + if has_task_resources? + FileUtils.cp(task_resources, new_td.task_resources) + end + new_td.save new_td From 1baca840a4c2fd603c8718e4e238c60a6f804143 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 31 Oct 2018 17:22:49 +1100 Subject: [PATCH 29/30] TEST: Ensure unit tests succeed with new teaching period details --- test/api/units_test.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/api/units_test.rb b/test/api/units_test.rb index d2a772451..ebcab4184 100644 --- a/test/api/units_test.rb +++ b/test/api/units_test.rb @@ -214,7 +214,7 @@ def test_units_current #Test PUT for updating unit details with valid id def test_units_put - unit=Unit.first + unit={} unit[:name] = 'Intro to python' unit[:code] = 'JRSW40004' unit[:description] = 'new language' @@ -245,9 +245,8 @@ def test_put_update_unit_empty_name #Test PUT for updating unit details with invalid id def test_put_update_unit_invalid_id - unit= Unit.first data_to_put = { - unit:unit, + unit: { name: 'test'}, auth_token: auth_token } From a48fc1c99f8215723fd026519c7b768ac713d597 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 31 Oct 2018 20:24:20 +1100 Subject: [PATCH 30/30] TEST: Add test for alignment links on rollover --- test/models/unit_test.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/models/unit_test.rb b/test/models/unit_test.rb index f55757a2b..f14e436d2 100644 --- a/test/models/unit_test.rb +++ b/test/models/unit_test.rb @@ -41,6 +41,24 @@ def setup assert File.exists?(unit2.task_definitions.first.task_resources), 'task resource is absent' end + test 'rollover of task ilo links' do + @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv")) + @unit.import_outcomes_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Outcomes.csv")) + @unit.import_task_alignment_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Alignment.csv")), nil + + unit2 = @unit.rollover TeachingPeriod.find(2), nil, nil + + assert @unit.task_outcome_alignments.count > 0 + assert_equal @unit.task_outcome_alignments.count, unit2.task_outcome_alignments.count + + @unit.task_outcome_alignments.each do |link| + other = unit2.task_outcome_alignments.where(task_definition_id: link.task_definition_id, learning_outcome_id: link.learning_outcome.id).first + + assert other + assert_equal link.rating, other.rating, "rating does not match for #{link.task_definition.abbreviation} - #{link.learning_outcome.abbreviation}" + end + end + test 'rollover of tasks have same start week and day' do @unit.import_tasks_from_csv File.open(Rails.root.join('test_files',"#{@unit.code}-Tasks.csv"))