(*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))
	
);