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;
Prev: 5.5. TestAdminister |
Up: 5. Formal Specifications |
Next: 5.7. TestGrading |