object TestWizard components: length:Time and class:ClassName and constraints:ConstraintSet*; description: (* This is the data gathered from the user to be used in the * generation of a new test. *); end TestWizard; object QuestionBlock components: constraints:ConstraintSet and questions:TestQuestion*; description: (* A test consists of one or more blocks of questions. (When using the Simple * test generation interface, only one block is created.) During the creation * phase, a QuestionBlock does not yet contain questions, only the * information needed to select questions; once a test is generated, the * questions are populated, but the specifications are retained in case the * questions need to be replaced. *); end QuestionBlock; object ConstraintSet components: number:(NumQuestions) and time:(TimeConstraint) and lastUsed:(LastUsedConstraint) and type:(QuestionType) and length:(Length) and week:(Week); description: (* A model of all the constraints that are in effect when questions are * selected. *); end ConstraintSet; object TimeConstraint components: direction:UpperOrLower and limit:Minutes; description: (* When a test is generated, questions may be limited by an * upper or a lower bound on their completion time. *); end TimeConstraint; object LastUsedConstraint components: (direction:UpperOrLower and limit:DateLastUsed); description: (* When a test is generated, recently used questions may be filtered out, or * questions that have not been recently used may be filtered out. *); end LastUsedConstraint; object UpperOrLower = "Upper" or "Lower" description: (* Describes whether a limit is an upper bound or a lower bound. *); end UpperOrLower; object EditableTest components: length:Time and blocks:QuestionBlock* and className:ClassName and numQuestions:integer; description: (* A test contains information about its length, what class it is for, and * what questions it consists of. *); end EditableTest; object Test components: length:Time and questions:TestQuestion* and className:ClassName; description: (* A test contains information about its length, what class it is for, and * what questions it consists of. *); end Test; object QuestionType = integer description: (* Enumeration of question types. *); end QuestionType; object TestQuestion extends Question components: Point and questionNumber:QuestionNumber and constraints:ConstraintSet; description: (* A question in a test knows its place in the test, the number of points it * is worth, and the constraints that generated it. *); end TestQuestion; object Point = integer description: (* The number of points that a question is worth on a test. *); end; object QuestionNumber = integer description: (* The question's place in a test. *); end; object NumQuestions = integer description: (* The number of questions on a test. *); end; object Time components: Hours and Minutes; description: (* The length of a test. *); end Time; object Hours = integer description: (* The hours place of a time. *); end; object Minutes = integer description: (* The minutes place of a time. *); end; function GetQuestionEditable(test:EditableTest, index:integer)->question:TestQuestion = (* Retrieves the question at the given index in the test. *) question.questionNumber = index and (exists (qb in test.blocks) question in qb.questions); function GetQuestion(test:Test, index:integer)->question:TestQuestion = (* Retrieves the question at the given index in the test. *) question.questionNumber = index and ((question in test.questions)); operation MakeEditableTest(wizard:TestWizard)->t:EditableTest pre: (* The TestWizard object is initialized. *); post: (* The EditableTest object contains a number of QuestionBlocks equal to the number of * ConstraintSets in the TestWizard. Each QuestionBlock has a reference to * one of the ConstraintSets, and no to QuestionBlocks refer to the same * ConstraintSet. *) wizard.class = t.className and wizard.length = t.length and forall (fs in wizard.constraints) exists (qb in t.blocks) qb.constraints = fs and not (exists (qb in t.blocks) exists (qb' in t.blocks) qb.constraints = qb'.constraints) and (* Each QuestionBlock has a number of TestQuestions equal to the number * specified by its ConstrainSet; these questions must meet the criteria * given by the ConstraintSet, and no question may be present in the test * more than once. A QuestionSet may contain null questions if there are * insufficient questions in the database. *) forall (qb in t.blocks) ( qb.constraints.number = #(qb.questions) and forall (q in qb.questions) ( (Passes(qb.constraints, q) and q.cl = t.className) or q = nil ) ); description: (* MakeTest extracts the data from the TestWizard to generate a EditableTest. The * QuestionBlocks are populated with questions and added to the EditableTest. *); end MakeTestEditable; operation MakeTest(et:EditableTest)->t:Test pre: ; post: et.className = t.className and et.length = t.length and forall (q in t.questions) exists (qb in et.blocks) q in qb.questions and forall (qb in et.blocks) forall (q in qb.questions) q in t.questions ; description: (* Converts an editable test object into one suitable for administering to * students. *); end; function Passes(filters:ConstraintSet, question:TestQuestion) -> result:boolean = ( (((filters.time)).direction = "Upper" and question.l <= filters.time.limit) or (filters.time.direction = "Lower" and question.l >= filters.time.limit) or filters.time = nil ) and (filters.lastUsed = nil or (filters.lastUsed.direction = "Upper" and question.dlu <= filters.lastUsed.limit) or (filters.lastUsed.direction = "Lower" and question.dlu >= filters.lastUsed.limit) ) and (filters.length = nil or (filters.length = question.l) ) and (filters.week = nil or filters.week = question.w ); function Equivalent(q1:TestQuestion, q2:TestQuestion) -> result:boolean = (* The two questions share the same class name, length, keywords, week, * question string, author, last used date, difficulty, answer, and database * number. *) q1.cl = q2.cl and q1.l = q2.l and q1.k = q2.k and q1.w = q2.w and q1.qs = q2.qs and q1.au = q2.au and q1.dlu = q2.dlu and q1.d = q2.d and q1.an = q2.an and q1.dbn = q2.dbn; operation AddQuestionEditable(test:EditableTest, question:TestQuestion, index:integer)->test':EditableTest (* The integer >= 1 and <= the number of questions in the test + 1. *) pre: index >= 1 and index <= test.numQuestions + 1 and question.questionNumber = index; post: (* The EditableTest contains the TestQuestion, at the index given by the integer. *) (exists (qb in test.blocks) question in qb.questions) and (* If a question already exists at that index, every question with an index * equal to or greater than the argument has its index incremented by one. *) (forall (q:TestQuestion | exists (qb in test.blocks) q in qb.questions) if (q.questionNumber < index) then (exists (qb' in test'.blocks) q in qb'.questions) else (exists (qb' in test'.blocks) (exists (q' in qb'.questions) q'.questionNumber = q.questionNumber + 1))) ; description: (* Adds a question to an EditableTest. *); end AddQuestionEditable; operation RemoveQuestionEditable(test:EditableTest, index:integer)->test':EditableTest pre: (* The EditableTest contains a TestQuestion with the given index. *) index >= 1 and index <= test.numQuestions ; post: (* The EditableTest no longer contains the TestQuestion that had the passed index. *) if (exists (qb in test.blocks) (exists (q in qb.questions) q.questionNumber = index)) then not (exists (qb' in test'.blocks) (exists (q' in qb'.questions) q' = GetQuestionEditable(test, index))) and (* All TestQuestions with an index greater than the argument have their * index decremented by one. *) (forall (q:TestQuestion | exists (qb in test.blocks) q in qb.questions) if (q.questionNumber < index) then (exists (qb' in test'.blocks) q in qb'.questions) else (exists (qb' in test'.blocks) (exists (q' in qb'.questions) q'.questionNumber = q.questionNumber - 1))); description: (* Removes a question from an editable test. *); end RemoveQuestionEditable; operation MoveQuestionUpEditable(test:EditableTest, index:integer)->test':EditableTest pre: (* The integer argument is > 1 and <= the number of questions in the test. *) index > 1 and index <= test.numQuestions; post: (* The question and the previous question have their indeces swapped. *) Equivalent(GetQuestionEditable(test, index), GetQuestionEditable(test', index - 1)) and Equivalent(GetQuestionEditable(test, index - 1), GetQuestionEditable(test', index)); description: (* Swaps the question at the given index with the question at the previous * index. *); end MoveQuestionUpEditable; operation MoveQuestionDownEditable(test:EditableTest, index:integer)->test':EditableTest pre: (* The integer argument is >= 1 and < the number of questions in the test. *) index >= 1 and index < test.numQuestions; post: (* The question and the next question have their indeces switched. *) Equivalent(GetQuestionEditable(test, index), GetQuestionEditable(test', index + 1)) and Equivalent(GetQuestionEditable(test, index + 1), GetQuestionEditable(test', index)); description: (* Swaps the question at the given index with the question at the next index. *); end MoveQuestionDownEditable; operation ReplaceQuestionEditable(test:EditableTest, index:integer)->test':EditableTest pre: (* The integer argument is >= 1 and <= the number of questions in the * test. *) index >= 1 and index <= test.numQuestions ; post: (* The question at the given index has been replaced with a different * question that meets the criteria given by the ConstraintSet of its * QuestionGroup. *) not (exists (qb in test'.blocks) GetQuestionEditable(test, index) in qb.questions) and forall (qb in test'.blocks) if (GetQuestionEditable(test', index) in qb.questions) then GetQuestionEditable(test', index) = nil or (Passes(qb.constraints, GetQuestionEditable(test', index)) and GetQuestionEditable(test',index).cl = test.className) else true ; description: (* The TestQuestion's QuestionBlock is found, a new question is generated * according to the ConstraintSet of the QuestionBlock, and this new * TestQuestion replaces the existing one. *); end ReplaceQuestionEditable; operation AddConstraintSet(w:TestWizard, constraints:ConstraintSet)->w':TestWizard pre: ; post: (* The TestWizard contains the QuestionBlock. *) constraints in w'.constraints and forall (constraints':ConstraintSet | constraints' != constraints) if (constraints' in w.constraints) then constraints' in w'.constraints else not (constraints' in w'.constraints); description: (* Adds a new block of questions to the test wizard. *); end AddConstraintSet; operation RemoveConstraintSet(w:TestWizard, cset:ConstraintSet)->w':TestWizard pre: (* The TestWizard contains the QuestionBlock. *) cset in w.constraints; post: (* The TestWizard no longer contains the QuestionBlock. *) not (cset in w'.constraints) and forall (cset':ConstraintSet | cset' != cset) if (cset' in w.constraints) then cset' in w'.constraints else not (cset' in w'.constraints); description: (* Removes a block of questions from the test wizard. *); end RemoveConstraintSet;