(*In case TestCreation.sl doesn't compile with the rest of the .sl files, this file is self-contained. I added the necessary code in order to model test creation. This includes objects like the InstructorWorkspace, Filespace, Repository, File, etc...*) object TestTitle; object NumberOfQuestions; object Class; object Filename; object Time = hours:integer and minutes:integer; object Tags = string*; object Column; object Points; object Bundle; object FileType; object FileData; object Keywords = string*; object InstructorWorkspace components: Bundle and tests:Test*; end InstructorWorkspace; object FileSpace = File*; object File components: name:Filename and permissions:FilePermissions and file_type:FileType and data:FileData; end File; object FilePermissions = is_readable:IsReadable and is_writable:IsWritable; object IsReadable = boolean; object IsWritable = boolean; object Repository components :questions:Question*; end Repository; object Question components: class:Class and time:Time and Difficulty:integer and keywords:Keywords; description: (* The Question class contains components that are common to all Questions in the database. *); end Question; object QuestionBank components: questions:Question*; operations: FilterByClass; end QuestionBank; object Test components: questions:Question* and class:Class and testtitle:TestTitle and filename:Filename and points:Points and time:Time and num_questions:NumberOfQuestions and difficulty:integer and requires_saving:boolean; operations: (*none*); end Test; (*These objects could also be set up as operations, but I chose to do it this way*) object Generator components: time:Time and num_questions:NumberOfQuestions and class:Class and difficulty:integer and qb:QuestionBank and testtitle: TestTitle and filename: Filename; operations: Generate; description: (*The Generator object is used to supply automatic generation with its necessary parameters. It seemed logical to model this as an object, but it can be done other ways.*); end Generator; object Creator components: class:Class and testtitle: TestTitle and filename:Filename; operations: Create; description: (*The Creator object is used to create a new test from scratch*); end Creator; operation ClickToAddQuestion inputs: q:Question, cur_test:Test, qb:QuestionBank; outputs: cur_test':Test; precondition: cur_test != nil and not(q in cur_test.questions); postcondition: (q in qb) and (cur_test'.questions = cur_test.questions + q); end ClickToAddQuestion; operation DragToAddQuestion inputs: q:Question, cur_test:Test; outputs: cur_test':Test; description: (*This operation adds a question from the question bank to the current test*); precondition: cur_test != nil and not(q in cur_test.questions); postcondition: (q in cur_test'.questions) and (cur_test'.questions = cur_test.questions + q); end DragToAddQuestion; operation SortQuestionByColumn inputs: qb:QuestionBank, cname:string, cur_test:Test; outputs: qb':QuestionBank; description: (*This operation is performed on the question bank table, the system makes changes to the question bank visible in the question bank tab of the main ui*); precondition: (cur_test != nil); postcondition: (*Assuming that there is going to be a comparator that will take in the column to sort by, this should be sufficient*) (forall (i:integer | (i >= 1) and (i < #qb.questions)) qb.questions[i] < qb.questions[i+1]); end SortQuestionByColumn; operation SearchQuestionByTag inputs: t:Tags, qb:QuestionBank, cur_test:Test; outputs: qb':QuestionBank; description: (*This operation uses the user-given tags to search the current test's question bank for questions containing these tags in the question body. Output is visible in the question bank tab*); precondition: cur_test != nil; postcondition: (*qb' only has questions that contain a tag specified by the user*) (forall(i:integer | (i >= 1) and (i < #qb.questions) ) (forall(j:integer | (j >=1) and (j < #qb.questions[i].keywords) ) (forall(k:integer | (k >= 1) and (k <= #t) ) qb.questions[i] in qb' iff (qb.questions[i].keywords[j] = t[k]) ) ) ); end SearchQuestionByTag; operation RemoveQuestion inputs: q:Question, cur_test:Test; outputs: cur_test':Test; precondition: cur_test != nil; postcondition: (cur_test'.questions = cur_test.questions - q); description: (*Remove a question from the current test. Changes are made visible in the current test window.*); end RemoveQuestion; operation MoveQuestion inputs: q:Question, cur_test:Test, newloc:integer; outputs: cur_test': Test; description: (*Move a question to rearrange the current test, cascading all questions below it. Changes are made visible in the current test window*); precondition: cur_test != nil; postcondition: (*make sure the move and cascading worked correctly*) (cur_test'.questions[newloc] = q) and (forall(quest in cur_test'.questions) if (quest in cur_test'.questions) then (quest in cur_test.questions)) and (forall(i:integer | (i > newloc) and (i < #cur_test'.questions)) cur_test'.questions[i + 1] = cur_test.questions[i]); end MoveQuestion; operation EditQuestion inputs: q:Question, cur_test:Test; outputs: cur_test':Test, q':Question; description: (*Edit a question on the current test. Only temporary changes are made to the current test, because the repository is shared among Instructors*); precondition: cur_test != nil; postcondition: (*a simple way to model an edit with an addition and a removal*) (cur_test'.questions = cur_test.questions - q + q'); end EditQuestion; operation Generate inputs: g:Generator, iws:InstructorWorkspace, fs:FileSpace; outputs: cur_test:Test, iws':InstructorWorkspace, fs':FileSpace, qb:QuestionBank; precondition : IsUniqueName(iws, g.filename, 0) and (g.time != nil) and (g.num_questions != nil) and (g.class != nil) and (g.difficulty != nil) and (g.qb != nil) and (g.testtitle != nil) and (g.filename != nil) and (#(g.qb) > 0); postcondition: (exists (file in fs')( (fs' = fs + file) and (file in fs) and (file.name = g.filename) and (file.permissions.is_writable) and (file.permissions.is_readable) and (not cur_test.requires_saving) )) and (exists (cur_test in iws'.tests)( (g.num_questions = cur_test.num_questions) and (g.testtitle = cur_test.testtitle) and (g.class = cur_test.class) and (qb = g.qb) and (g.filename = cur_test.filename) and (DifficultyIsValid(g, cur_test)) and (TimeIsValid(cur_test, g.time, 0)); )) and (forall (quest:Question) if (quest.class = g.class) then (quest in qb)); description: (*Generate a new Test given the user-specified criteria: test time, test difficulty, number of questions, class, test title, filename . The new test must match the user-specified difficulty and time attributes +/- a variance value. This operation creates a new test object and modifies the instructor workspace to contain this object This operation also modifies the FileSpace by adding a new file that represents the test); end Generate; operation Create inputs: c:Creator, iws:InstructorWorkspace, fs:FileSpace; outputs: cur_test:Test, iws':InstructorWorkspace, fs':FileSpace, qb:QuestionBank; precondition: IsUniqueName(iws, c.filename, 0) and (c.class != nil) and (c.testtitle != nil) and (c.filename != nil); postcondition: (cur_test != nil) and (*There is a new blank current test in the InstructorWorkspace*) (exists(cur_test in iws'.tests)( (cur_test.filename = c.filename) and (cur_test.testtitle = c.testtitle) and (cur_test.class = c.class) and (not cur_test.requires_saving) )) and (exists (file in fs')( (fs' = fs + file) and (file in fs) and (file.name = c.filename) and (file.permissions.is_writable) and (file.permissions.is_readable) )) and (forall (quest:Question) if (quest.class = c.class) then (quest in qb)) and (forall (quest in qb) (quest in qb iff quest in c.qb)); description: (*Create a new Test given the user-specified criteria. The new test is initially blank, and it is visible in the current test window. The Creator's question bank object is created by using the FilterByClass operation. This object remains accessible until a new test is generated or created, and it is located within the Creator*); end Create; operation FilterByClass inputs: c:Class, r:Repository; outputs: qb:QuestionBank; description: (*This operation takes the original repository containing the questions for all classes and returns a new questionbank with the questions from only one class*); precondition: (r != nil); postcondition: (forall (q in r) if (q.class = c) then (q in qb)) and (forall (q in qb) (q in qb) iff (q.class = c)); end FilterByClass; (*IsUniqueName looks through all tests within the instructor workspace and compares the desired filename with all pre-existing filenames. If the desired filename is unique, return true, otherwise false.*) function IsUniqueName(iws:InstructorWorkspace, fn:Filename, i:integer)->boolean = ( if (i > #(iws.tests)) then true else (if (iws.tests[i].filename = fn) then false else IsUniqueName(iws, fn, i + 1);) ); function totalDifficulty(cur_test:Test, i:integer, total:number)->number = ( if (i > #(cur_test.questions)) then total else totalDifficulty(cur_test, i+1, (cur_test.questions[i].Difficulty + total)) ); (*DifficultyIsValid returns true if the total question difficulty averages to the user-specified test difficulty +/- 1. The +/- value is customizable, but for example purposes I chos +/- 1.*) function DifficultyIsValid(g:Generator, cur_test:Test)->boolean = ( (((totalDifficulty(cur_test, 0, 0) - g.difficulty) > -1) and ((totalDifficulty(cur_test,0,0) - g.difficulty) < 1)) ); (*TimeIsValid returns true if the generated test has a time attribute that matches the user-specified time +/- a certain value. As an example, this value is initally 5 minutes.*) function TimeIsValid(cur_test:Test, time:Time, plusorminus:integer)->boolean = ( (((cur_test.time.minutes - time.minutes) > -5) and ((cur_test.time.minutes - time.minutes) < 5)) );