module TestGeneration; from StudentInterface import Student; from TestGrading import Mean,Median,Mode,GradedQuestion; from TestTaking import AnsweredQuestion; from TestAdminister import Roster; from QuestionModule import Question,Topic,Author,Difficulty,Time,ClassName,Type,Date,Year, ID; from QuestionBankModule import QuestionBank,LocalQuestionBank,SharedQuestionBank; export Test,Wizard,TestName,TotalPoints,TotalTime,PointsPossible,TestQuestion,TotalGrade,ClassTaken,Section,QuestionNumber; (*-------------------- BEGIN MATT DeVORE'S SECTION --------------------*) object Weight is integer description: (* Weight indicates the amount of representation a Topic or question Type receives in a test. If a Topic has a high weight, it will be touched upon more in the generated test than a Topic with a lower weight. *); end; (* enum type: GenerationType *) object GenerationType is components: manual:ManualGeneration or automatic:AutomaticGeneration; description: (* Represents one of the two generation methods available: ManualGeneration or AutomaticGeneration. This is a parameter entered by the user in the first step of generation. *); end GenerationType; object ManualGeneration is components: ; description: (* This is one of the two available methods that teachers can use to generate a test. ManualGeneration indicates that the generated test will initially have no questions, and that every question must be searched for manually by the instructor and inserted using the Edit Test interface. *); end ManualGeneration; object AutomaticGeneration is components: ; description: (* This is one of the two available methods that teachers can use to generate a test. AutomaticGeneration requires the teacher to enter various criteria which allow the software to automatically find a pool of questions that satisfies the teacher's needs. *); end AutomaticGeneration; (* enum type: Direction *) object Direction is components: for:Forward or back:Backward; description: (* A Direction is equal to one of the two directions the user may be moving while walking between steps in the Test Generation Wizard. Forward indicates a clicking of the Next button. Backward indicates a clicking of the Previous button. *); end Direction; object Forward; object Backward; object LowerLimit is components: integer; description: (* The lower limit of a Range. Ranges are entered in the fourth step of the Wizard. *); end LowerLimit; object UpperLimit is components: integer; description: (* The upper limit of a Range. Ranges are entered in the fourth step of the Wizard. *); end UpperLimit; object Range is components: lower:LowerLimit and upper:UpperLimit; description: (* Represents one of three ranges that the user enters in the fourth step of the Wizard. Each of the three Ranges are conveyed through three different object types that are defined simply "is Range": AllowedDifficulties, AverageDifficulty, and EstimatedTimeLengthOfTest. *); end Range; object AllowedDifficulties is Range; object AverageDifficulty is Range; object EstimatedTimeLengthOfTest is Range; obj Class is components: roster:Roster, classname:ClassName, Quarter, Year, Instructor; end Class; obj CourseNumber is integer; obj Section is integer; obj Quarter is string; obj Instructor is string; object Comments is string; object Grade is integer; object CreatedDate is Date; object ModifiedDate is Date; object LastUsedDate is Date; object Notes is string; object EstimatedTimeTaken is real; object SuccessRate is real; object DateTaken is Date; object ClassTaken is Class; object TotalTime is integer; object PointsPossible is integer; object TotalPoints is PointsPossible; object TotalGrade is integer; object TestName is string; object IsAvailable is boolean; object QuestionNumber is integer; object TestQuestion inherits from Question components: qt:Type, points:PointsPossible, num:QuestionNumber; end TestQuestion; object Test is components: name:TestName, totalTime:TotalTime, tp:TotalPoints, tg:TotalGrade,qs:TestQuestion*,aq:AnsweredQuestion*,gq:GradedQuestion*, Student, TotalGrade, DateTaken, Class, ClassTaken, Mean,Median,Mode, ia:IsAvailable; description: (* In this module, represents a generated test. However, in other modules, it may also represent a taken or graded test.*); operations: AddQuestion, RemoveQuestion, EditQuestion, MoveUp, MoveDown; end Test; object CriteriaRanges is components: allowedDiff:AllowedDifficulties, averageDiff:AverageDifficulty and estTimeLength:EstimatedTimeLengthOfTest; description: (* This object represents the three ranges that are entered during step four of the Wizard. Note that these ranges may not be blank. *); end CriteriaRanges; object WizardTopic is components: topic:Topic and weight:Weight; description: (* Contains both a Topic and that Topic's Weight. The Weight value is the number displayed in the Advanced Weight Specification dialog next to the Topic indicated in this object.*); end WizardTopic; object WizardType is components: qt:Type and weight:Weight; description: (* Contains both a Type and that Type's Weight. The Weight value is the number displayed in the Advanced Weight Specification dialog next to the Type indicated in this object. *); end WizardType; object CurrentStep is integer; object FinishedFlag is boolean; object IncludedClasses is ClassName*; object IncludedTypes is WizardType*; object IncludedTopics is WizardTopic*; object Wizard is components: step:CurrentStep, finished:FinishedFlag, name:TestName, genType:GenerationType, classes:IncludedClasses, topics:IncludedTopics, types:IncludedTypes and critRanges:CriteriaRanges; operations: Step1, Step2, Step3, Step4, Finish, SmallestMarginOfError; end Wizard; (* The sole purpose of the StepX operations is to plant the user-given values for various criteria into the Wizard object as the Wizard proceeds. *) operation Step1 is inputs: wiz:Wizard and testName:TestName and genType:GenerationType; outputs: wiz':Wizard; precondition: wiz.step = 1 and #testName > 0; postcondition: (wiz'.step = 2 and wiz'.name = testName); description: (* Plants the user-given values for generation criteria that were entered in Step 1. These values will be in the output Wizard object. *); end Step1; operation Step2 is inputs: dir:Direction and wiz:Wizard and classes:ClassName*; outputs: wiz':Wizard; precondition: wiz.step = 2; postcondition: if (dir?for) then (wiz'.step = 3 and wiz'.classes = classes) else wiz'.step = 1; description: (* Plants the user-given values for generation criteria that were entered in Step 2. These values will be in the output Wizard object. This is needed whenever the user leaves Step 2 to go to Step 3 or Step 1. *); end Step2; operation Step3 is inputs: dir:Direction and wiz:Wizard and wizTops:WizardTopic*; outputs: wiz':Wizard; precondition: wiz.step = 3; postcondition: if (dir?for) then (wiz'.step = 4 and wiz'.topics = wizTops) else wiz'.step = 2; description: (* Plants the user-given values for generation criteria that were entered in Step 3. These values will be in the output Wizard object. This is needed whenever the user leaves Step 3 to go to Step 4 or Step 2. *); end Step3; operation Step4 is inputs: dir:Direction and wiz:Wizard and types:WizardType* and critRanges:CriteriaRanges; outputs: wiz':Wizard; precondition: wiz.step = 4; postcondition: if (dir?for) then (wiz'.finished = true and wiz'.step = 5 and wiz'.types = types and wiz'.critRanges = critRanges) else wiz'.step = 3; description: (* Plants the user-given values for generation criteria that were entered in Step 4. These values will be in the output Wizard object. This is needed whenever the user leaves Step 4 to go to Step 3 or to Finish the generation. *); end Step4; operation Finish is inputs: wiz:Wizard, bank:TestQuestion*; outputs: test:Test; precondition: wiz.finished; postcondition: (* In case of manual generation, just return a blank test. *) (wiz.genType?manual and #(test.qs) = 0) or (let topicList = GetTopics (wiz.topics); let typeList = GetTypes (wiz.types); let error = SmallestMarginOfError (wiz, test); (* Margin of error cannot be so small so as to render the weights meaningless. *) error <= MinTypeWeight (wiz.types) and error <= MinTopicWeight (wiz.topics) and (* Only classes that have been specified for inclusion may be represented on the test. Same goes for topics and question types. *) HasValidClasses (test.qs, wiz.classes) and HasValidTopics (test.qs, topicList) and HasValidTypes (test.qs, typeList) and (* The user weights must correctly apply to the test questions. *) TopicWeightsMatch (wiz.topics, test.qs, error) and TypeWeightsMatch (wiz.types, test.qs, error) and (* The three ranges entered in step four must be adhered to. *) FitsRange (wiz.critRanges.allowedDiff, MinDifficulty (test.qs)) and FitsRange (wiz.critRanges.allowedDiff, MaxDifficulty (test.qs)) and FitsRange (wiz.critRanges.averageDiff, AverageDifficultyOfQuestions (test.qs)) and FitsRange (wiz.critRanges.estTimeLength, TotalTimeOfQuestions (test.qs)) and (* Test name must match the one entered exactly. *) test.name = wiz.name and (* Every question included in the test must be in the local question bank. *) forall (q in test.qs) (q in bank)) or (* If the above doesn't work, try making the test again, but of a shorter length. *) (test = Finish (LowerMinimumTime(wiz), bank)); description: (* This finishes the Test Generation Wizard by returning a new test that follows the given criteria that was entered in the Generation Wizard. These criteria are stored in the Wizard object. *); end Finish; operation SmallestMarginOfError is inputs: wiz:Wizard, test:Test; outputs: error:integer; precondition: wiz.finished; postcondition: TopicWeightsMatch (wiz.topics, test.qs, error) and TypeWeightsMatch (wiz.types, test.qs, error) and error >= 0 and not (exists (smallerError: integer) ((smallerError < error) and (TopicWeightsMatch (wiz.topics, test.qs, smallerError)) and (TypeWeightsMatch (wiz.types, test.qs, smallerError)))); description: (* This is not a user-visible operation, but can only be expressed through pre- and post-conditions. It returns the smallest margin of error required in order to make the given questions fit the user-entered weights. *); end SmallestMarginOfError; (* LowerMinimumTime: returns the same Wizard object, but the lower limit of the Estimated Length in Minutes range is decremented by one minute. This is used so that if the software cannot generate a test based on the given criteria, it reduces the amount of time required for the test to consume, and attempts generation again. See 2.2.1.2, subsection Weights Values Flexibility. *) function LowerMinimumTime (wiz:Wizard) -> (Wizard) = {wiz.step, wiz.finished, wiz.name, wiz.genType, wiz.classes, wiz.topics, {wiz.critRanges.averageDiff, wiz.critRanges.allowedDiff, {wiz.critRanges.estTimeLength.lower - 1, wiz.critRanges.estTimeLength.upper}}}; function Min (x:integer, y:integer) -> (integer) = if (x < y) then x else y; function Max (x:integer, y:integer) -> (integer) = if (x > y) then x else y; function MinReal (x:real, y:real) -> (real) = if (x < y) then x else y; function MaxReal (x:real, y:real) -> (real) = if (x > y) then x else y; function TotalTimeOfQuestions (coll:TestQuestion*) -> (real) = if (#coll = 0) then 0 else (coll[1].time + TotalTimeOfQuestions (coll - coll[1])) end TotalTime; (* Utility functions used for post/preconditions in the Finish operation *) (* PointTotal: adds up the point value of every single question in the list. *) function PointTotal (coll:TestQuestion*) -> (integer) = if (#coll = 0) then 0 else (coll[1].points + PointTotal (coll - coll[1])) end PointTotal; (* Difficulty Total: adds up the difficulty rating of every single question in the list, for the purposes of finding the mean. *) function DifficultyTotal (coll:TestQuestion*) -> (integer) = if (#coll = 0) then 0 else (coll[1].difficulty + DifficultyTotal (coll - coll[1])) end DifficultyTotal; (* AverageDifficultyOfQuestions: finds the mean difficulty rating of the questions in the list. *) function AverageDifficultyOfQuestions (coll:TestQuestion*) -> (real) = DifficultyTotal(coll) / #coll end AverageDifficultyOfQuestions; (* Returns the lowest difficulty rating out of all the given questions. *) function MinDifficulty (coll:TestQuestion*) -> (integer) = if (1 = #coll) then coll[1].difficulty else Min(coll[1].difficulty, MinDifficulty (coll - coll[1])) end MinDifficulty; (* Returns the highest difficulty rating out of all the given questions. *) function MaxDifficulty (coll:TestQuestion*) -> (integer) = if (1 = #coll) then coll[1].difficulty else Max(coll[1].difficulty, MaxDifficulty (coll - coll[1])) end MaxDifficulty; (* Indicates the number of times that a particular topic is touched upon by any question, given a list of questions. *) function TopicRepresentation (coll:TestQuestion*, what:Topic) -> (integer) = (let thisCount = if (what in coll[1].topics) then 1 else 0; if (1 = #coll) then thisCount else (thisCount + TopicRepresentation (coll - coll[1], what))) end TopicRepresentation; (* Finds all the questions of a given Type, then adds up their point values. *) function TypeRepresentation (coll:TestQuestion*, what:Type) -> (integer) = (let thisCount = if (coll[1].qt = what) then coll[1].points else 0; if (1 = #coll) then thisCount else (thisCount + TypeRepresentation (coll - coll[1], what))) end TypeRepresentation; (* Returns true iff the question list has questions from a particular set of classes. If validClasses has CSC101 and CSC102, then the Question list can only have questions pertaining to those classes. *) function HasValidClasses (coll:TestQuestion*, validClasses:ClassName*) -> (boolean) = (forall (q in coll) q.cn in validClasses) end HasValidClasses; (* Similar to the HasValidClasses function, but works on topics. *) function HasValidTopics (coll:TestQuestion*, validTopics:Topic*) -> (boolean) = forall (q in coll) forall (t in q.topics) t in validTopics end HasValidTopics; function HasValidTypes (coll:TestQuestion*, validTypes:Type*) -> (boolean) = forall (q in coll) q.qt in validTypes end HasValidTypes; (* Given a list of WizardTopics, returns a list of Topics. This has the effect of cutting away the weight values (see the WizardTopic object). *) function GetTopics (coll:WizardTopic*) -> (Topic*) = if (0 = #coll) then empty else (GetTopics (coll - coll[1]) + coll[1].topic) end GetTopics; (* Given a list of WizardTypes, returns a list of Types. This has the effect of cutting away the weight values. (see the WizardType object).*) function GetTypes (coll:WizardType*) -> (Type*) = if (0 = #coll) then empty else (GetTypes (coll - coll[1]) + coll[1].qt) end GetTypes; function FitsRange (r:Range, v:integer) -> (boolean) = ((v >= r.lower) and (v <= r.upper)); (* The ...WeightsMatch functions make sure the given weights match the representation of authors and topics in the given question list. A don't care weight is given by zero, so if any weight is non-positive, then that means the weights don't matter. *) function TopicWeightsMatch (tops:WizardTopic*, qs:TestQuestion*, error:integer) -> (boolean) = exists (top in tops) top.weight <= 0 or exists (factor:real) forall (top in tops) FitsRange ({-error, +error}, top.weight * factor - TopicRepresentation (qs, top.topic)) end TopicWeightsMatch; function TypeWeightsMatch (types:WizardType*, qs:TestQuestion*, error:integer) -> (boolean) = exists (type in types) type.weight <= 0 or exists (factor:real) forall (type in types) FitsRange ({-error, +error}, type.weight * factor - TypeRepresentation (qs, type.qt)) end TypeWeightsMatch; (* Returns the weight of the lowest-weighted Type. *) function MinTypeWeight (types:WizardType*) -> (real) = if (1 = #types) then types[1].weight else MinReal (types[1].weight, MinTypeWeight(types - types[1])) end MinTypeWeight; (* Returns the weight of the lowest-weighted Topic. *) function MinTopicWeight (topics:WizardTopic*) -> (real) = if (1 = #topics) then topics[1].weight else MinReal (topics[1].weight, MinTopicWeight(topics - topics[1])) end MinTopicWeight; (*-------------------- END MATT DeVORE'S SECTION --------------------*) (*-------------------- BEGIN Ben Williams's section------------------*) operation AddQuestion is inputs: t:Test, q:Question, p:PointsPossible; outputs: t':Test; precondition: (* The given question is not in the given input test. *) (not (exists (q' in t.qs) q'.id = q.id)); postcondition: (* The given question is in the output test. *) (exists (newq in t'.qs) q.id = newq.id and newq.points = p) and (* * A question is in the output test if and only if it is the new * question or if it is in the input test. *) (forall (q':TestQuestion) (q' in t'.qs) iff ((q'.id = q.id) or (q' in t.qs))) and (* * There exists a test question in the output test that is the new * question with the same id, number of points, and is at the end * of the test. *) (exists (tq in t'.qs) (tq.id = q.id) and (tq.points = p) and (tq.num = #(t.qs) + 1)); description: (* AddQuestion adds the given question to the given test, which in turn produces an updated test containing that question with the given number of points. *); end AddQuestion; operation RemoveQuestion is inputs: id:ID, t:Test; outputs: t':Test; precondition: (* The given question is in the given input test. *) ((exists (q' in t.qs) q'.id = id)); postcondition: (* The given question is not in the output test. *) (not (exists (q' in t'.qs) (q'.id = id))) and (* * A question is in the output test if and only if it is not * the given question to be removed and if it is in the input *test. *) (forall (r':TestQuestion) (r' in t'.qs) iff ((r'.id != id) and (r' in t.qs))); description: (* RemoveQuestion removes the Question with the matching given ID from the given test, which in turn produces an updated Test without a question matching the given ID. *); end RemoveQuestion; operation EditQuestion inputs: t:Test, id:ID, tq:TestQuestion; outputs: t':Test; precondition: (* The question with the given id exists in the test. *) (exists (q in t.qs) (q.id = id) and (q.type = tq.type)); postcondition: (* The new question is in the test *) tq in t.qs and (* * The number of the editied question is the same as the * question was before it was edited(replaced) *) (exists (q' in t.qs) (q'.id = id) and (q'.num = tq.num)) and (* * A question is only in the test if it was in the input test * and is not the original question that will be edited or if * it is the edited version of the the question that was changed. *) (forall (s':TestQuestion) (s' in t'.qs) iff (((s'.id != id) and (s' in t.qs)) or (s'.id = tq.id))); description: (* EditQuestion finds the test question on the given test with an ID matching the given ID. This test question is replaced with the given updated or edited question and put back in the Test, which is then returned. *); end EditQuestion; operation MoveUp is inputs: t:Test, id:ID; outputs: t':Test; precondition: (* * The question to be moved up does not exist at the top * of the list (in the 1st question spot) *) (exists (q' in t.qs) (q'.id = id) and q'.num != 1); postcondition: (* * The number is moved up into the position 1 above it and the * number in that place is moved down to where the number to be * moved originally was. *) ((exists (q' in t.qs) (exists (q'' in t'.qs) (q'.id = id) and (q''.id = id) and (q''.num = q'.num - 1) and ((exists (r' in t.qs) (exists (r'' in t'.qs) (r'.id = r''.id) and (r'.num = q''.num) and (r''.num = q'.num))))))); description: (* * MoveUp moves the question matching the input id up one spot in the * tests order of questions. *); end MoveUp; operation MoveDown is inputs: t:Test, id:ID; outputs: t':Test; precondition: (* * The question to be moved up does not exist at the top * of the list (in the 1st question spot) *) (exists (q' in t.qs) ((q'.id = id) and (q'.num != #(t.qs)))); postcondition: (* * The number is moved up into the position 1 above it and the * number in that place is moved down to where the number to be * moved originally was. *) ((exists (q' in t.qs) (exists (q'' in t'.qs) (q'.id = id) and (q''.id = id) and (q''.num = q'.num + 1) and ((exists (r' in t.qs) (exists (r'' in t'.qs) (r'.id = r''.id) and (r'.num = q''.num) and (r''.num = q'.num))))))); description: (* * MoveDown moves the question matching the input id down one spot in the * tests order of questions. *); end MoveDown; (*-------------------- END Ben Williams's section--------------------*) end TestGeneration;