mirror of
https://github.com/acanas/swad-core.git
synced 2024-09-23 00:00:50 +02:00
1197 lines
43 KiB
C
1197 lines
43 KiB
C
// swad_test_import.c: import and export self-assessment tests using XML files
|
|
|
|
/*
|
|
SWAD (Shared Workspace At a Distance),
|
|
is a web platform developed at the University of Granada (Spain),
|
|
and used to support university teaching.
|
|
|
|
This file is part of SWAD core.
|
|
Copyright (C) 1999-2019 Antonio Cañas Vargas
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
/*****************************************************************************/
|
|
/*********************************** Headers *********************************/
|
|
/*****************************************************************************/
|
|
|
|
#include <stdlib.h> // For exit, system, malloc, free, etc
|
|
#include <string.h> // For string functions
|
|
#include <sys/stat.h> // For mkdir
|
|
#include <sys/types.h> // For mkdir
|
|
|
|
#include "swad_box.h"
|
|
#include "swad_database.h"
|
|
#include "swad_form.h"
|
|
#include "swad_global.h"
|
|
#include "swad_parameter.h"
|
|
#include "swad_table.h"
|
|
#include "swad_test.h"
|
|
#include "swad_xml.h"
|
|
|
|
/*****************************************************************************/
|
|
/***************************** Public constants ******************************/
|
|
/*****************************************************************************/
|
|
|
|
/*****************************************************************************/
|
|
/**************************** Private constants ******************************/
|
|
/*****************************************************************************/
|
|
|
|
/*****************************************************************************/
|
|
/******************************* Internal types ******************************/
|
|
/*****************************************************************************/
|
|
|
|
/*****************************************************************************/
|
|
/************** External global variables from others modules ****************/
|
|
/*****************************************************************************/
|
|
|
|
extern struct Globals Gbl;
|
|
|
|
/*****************************************************************************/
|
|
/************************* Internal global variables *************************/
|
|
/*****************************************************************************/
|
|
|
|
/*****************************************************************************/
|
|
/***************************** Internal prototypes ***************************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_PutParamsExportQsts (void);
|
|
|
|
static void TsI_GetAndWriteTagsXML (long QstCod);
|
|
static void TsI_WriteAnswersOfAQstXML (long QstCod);
|
|
static void TsI_ReadQuestionsFromXMLFileAndStoreInDB (const char *FileNameXML);
|
|
static void TsI_ImportQuestionsFromXMLBuffer (const char *XMLBuffer);
|
|
static Tst_AnswerType_t TsI_ConvertFromStrAnsTypXMLToAnsTyp (const char *StrAnsTypeXML);
|
|
static bool TsI_CheckIfQuestionExistsInDB (void);
|
|
static void TsI_GetAnswerFromXML (struct XMLElement *AnswerElem);
|
|
static void TsI_WriteHeadingListImportedQst (void);
|
|
static void TsI_WriteRowImportedQst (struct XMLElement *StemElem,
|
|
struct XMLElement *FeedbackElem,
|
|
bool QuestionExists);
|
|
|
|
/*****************************************************************************/
|
|
/**************** Put a link (form) to export test questions *****************/
|
|
/*****************************************************************************/
|
|
|
|
void TsI_PutFormToExportQuestions (void)
|
|
{
|
|
extern const char *Txt_Export_questions;
|
|
|
|
/***** Put a link to create a file with questions *****/
|
|
Lay_PutContextualLinkIconText (ActLstTstQst,NULL,TsI_PutParamsExportQsts,
|
|
"file-import.svg",
|
|
Txt_Export_questions);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/****************** Put params to export test questions **********************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_PutParamsExportQsts (void)
|
|
{
|
|
Dat_WriteParamsIniEndDates ();
|
|
Tst_WriteParamEditQst ();
|
|
Par_PutHiddenParamChar ("OnlyThisQst",'N');
|
|
Par_PutHiddenParamUnsigned ("Order",(unsigned) Gbl.Test.SelectedOrder);
|
|
Par_PutHiddenParamChar ("CreateXML",'Y');
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/*************** Put a link (form) to import test questions ******************/
|
|
/*****************************************************************************/
|
|
|
|
void TsI_PutFormToImportQuestions (void)
|
|
{
|
|
extern const char *Txt_Import_questions;
|
|
|
|
/***** Put a link to create a file with questions *****/
|
|
Lay_PutContextualLinkIconText (ActReqImpTstQst,NULL,NULL,
|
|
"file-export.svg",
|
|
Txt_Import_questions);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/*********** Show form to import test questions from an XML file *************/
|
|
/*****************************************************************************/
|
|
|
|
void TsI_ShowFormImportQstsFromXML (void)
|
|
{
|
|
extern const char *Hlp_ASSESSMENT_Tests;
|
|
extern const char *The_ClassFormInBox[The_NUM_THEMES];
|
|
extern const char *Txt_Import_questions;
|
|
extern const char *Txt_You_need_an_XML_file_containing_a_list_of_questions;
|
|
extern const char *Txt_XML_file;
|
|
|
|
/***** Start box *****/
|
|
Box_StartBox (NULL,Txt_Import_questions,NULL,
|
|
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
|
|
|
|
/***** Write help message *****/
|
|
Ale_ShowAlert (Ale_INFO,Txt_You_need_an_XML_file_containing_a_list_of_questions);
|
|
|
|
/***** Write a form to import questions *****/
|
|
Frm_StartForm (ActImpTstQst);
|
|
fprintf (Gbl.F.Out,"<label class=\"%s\">"
|
|
"%s: "
|
|
"<input type=\"file\" name=\"%s\" accept=\".xml\""
|
|
" onchange=\"document.getElementById('%s').submit();\" />"
|
|
"</label>",
|
|
The_ClassFormInBox[Gbl.Prefs.Theme],
|
|
Txt_XML_file,
|
|
Fil_NAME_OF_PARAM_FILENAME_ORG,
|
|
Gbl.Form.Id);
|
|
Frm_EndForm ();
|
|
|
|
/***** End box *****/
|
|
Box_EndBox ();
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/*** Create the XML file with test questions and put a link to download it ***/
|
|
/*****************************************************************************/
|
|
|
|
void TsI_CreateXML (unsigned long NumRows,MYSQL_RES *mysql_res)
|
|
{
|
|
extern const char *The_ClassFormOutBoxBold[The_NUM_THEMES];
|
|
extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
|
|
extern const char *Txt_NEW_LINE;
|
|
extern const char *Txt_XML_file;
|
|
char PathPubFile[PATH_MAX + 1];
|
|
unsigned long NumRow;
|
|
MYSQL_ROW row;
|
|
long QstCod;
|
|
|
|
/***** Create a temporary public directory
|
|
used to download the XML file *****/
|
|
Brw_CreateDirDownloadTmp ();
|
|
|
|
/***** Create public XML file with the questions *****/
|
|
snprintf (PathPubFile,sizeof (PathPubFile),
|
|
"%s/%s/%s/test.xml",
|
|
Cfg_PATH_FILE_BROWSER_TMP_PUBLIC,
|
|
Gbl.FileBrowser.TmpPubDir.L,
|
|
Gbl.FileBrowser.TmpPubDir.R);
|
|
if ((Gbl.Test.XML.FileXML = fopen (PathPubFile,"wb")) == NULL)
|
|
Lay_ShowErrorAndExit ("Can not open target file.");
|
|
|
|
/***** Start XML file *****/
|
|
XML_WriteStartFile (Gbl.Test.XML.FileXML,"test",false);
|
|
fprintf (Gbl.Test.XML.FileXML,"%s",Txt_NEW_LINE);
|
|
|
|
/***** Write rows *****/
|
|
for (NumRow = 0;
|
|
NumRow < NumRows;
|
|
NumRow++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res);
|
|
/*
|
|
row[0] QstCod
|
|
row[1] UNIX_TIMESTAMP(EditTime)
|
|
row[2] AnsType
|
|
row[3] Shuffle
|
|
row[4] Stem
|
|
row[5] Feedback
|
|
row[6] MedCod
|
|
row[7] NumHits
|
|
row[8] NumHitsNotBlank
|
|
row[9] Score
|
|
*/
|
|
/* row[0] holds the code of the question */
|
|
if ((QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
|
|
Lay_ShowErrorAndExit ("Wrong code of question.");
|
|
|
|
/* Write the question type (row[2]) */
|
|
Gbl.Test.AnswerType = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[2]);
|
|
fprintf (Gbl.Test.XML.FileXML,"<question type=\"%s\">%s",
|
|
Tst_StrAnswerTypesXML[Gbl.Test.AnswerType],Txt_NEW_LINE);
|
|
|
|
/* Write the question tags */
|
|
fprintf (Gbl.Test.XML.FileXML,"<tags>%s",Txt_NEW_LINE);
|
|
TsI_GetAndWriteTagsXML (QstCod);
|
|
fprintf (Gbl.Test.XML.FileXML,"</tags>%s",Txt_NEW_LINE);
|
|
|
|
/* Write the stem (row[4]), that is in HTML format */
|
|
fprintf (Gbl.Test.XML.FileXML,"<stem>%s</stem>%s",
|
|
row[4],Txt_NEW_LINE);
|
|
|
|
/* Write the feedback (row[5]), that is in HTML format */
|
|
if (row[5])
|
|
if (row[5][0])
|
|
fprintf (Gbl.Test.XML.FileXML,"<feedback>%s</feedback>%s",
|
|
row[5],Txt_NEW_LINE);
|
|
|
|
/* Write the answers of this question.
|
|
Shuffle can be enabled or disabled (row[3]) */
|
|
fprintf (Gbl.Test.XML.FileXML,"<answer");
|
|
if (Gbl.Test.AnswerType == Tst_ANS_UNIQUE_CHOICE ||
|
|
Gbl.Test.AnswerType == Tst_ANS_MULTIPLE_CHOICE)
|
|
fprintf (Gbl.Test.XML.FileXML," shuffle=\"%s\"",
|
|
(row[3][0] == 'Y') ? "yes" :
|
|
"no");
|
|
fprintf (Gbl.Test.XML.FileXML,">");
|
|
TsI_WriteAnswersOfAQstXML (QstCod);
|
|
fprintf (Gbl.Test.XML.FileXML,"</answer>%s",Txt_NEW_LINE);
|
|
|
|
/* End question */
|
|
fprintf (Gbl.Test.XML.FileXML,"</question>%s%s",
|
|
Txt_NEW_LINE,Txt_NEW_LINE);
|
|
}
|
|
|
|
/***** End XML file *****/
|
|
XML_WriteEndFile (Gbl.Test.XML.FileXML,"test");
|
|
|
|
/***** Close the XML file *****/
|
|
fclose (Gbl.Test.XML.FileXML);
|
|
|
|
/***** Return to start of query result *****/
|
|
mysql_data_seek (mysql_res,0);
|
|
|
|
/***** Write the link to XML file *****/
|
|
fprintf (Gbl.F.Out,"<a href=\"%s/%s/%s/test.xml\""
|
|
" class=\"%s\" target=\"_blank\">",
|
|
Cfg_URL_FILE_BROWSER_TMP_PUBLIC,
|
|
Gbl.FileBrowser.TmpPubDir.L,
|
|
Gbl.FileBrowser.TmpPubDir.R,
|
|
The_ClassFormOutBoxBold[Gbl.Prefs.Theme]);
|
|
Ico_PutIconTextLink ("file.svg",
|
|
Txt_XML_file);
|
|
fprintf (Gbl.F.Out,"</a>");
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/************* Get and write tags of a question into the XML file ************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_GetAndWriteTagsXML (long QstCod)
|
|
{
|
|
extern const char *Txt_NEW_LINE;
|
|
unsigned long NumRow;
|
|
unsigned long NumRows;
|
|
MYSQL_RES *mysql_res;
|
|
MYSQL_ROW row;
|
|
|
|
if ((NumRows = Tst_GetTagsQst (QstCod,&mysql_res))) // Result: TagTxt
|
|
/***** Write the tags *****/
|
|
for (NumRow = 1;
|
|
NumRow <= NumRows;
|
|
NumRow++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res);
|
|
fprintf (Gbl.Test.XML.FileXML,"<tag>%s</tag>%s",
|
|
row[0],Txt_NEW_LINE);
|
|
}
|
|
|
|
/***** Free structure that stores the query result *****/
|
|
DB_FreeMySQLResult (&mysql_res);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/**************** Get and write the answers of a test question ***************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_WriteAnswersOfAQstXML (long QstCod)
|
|
{
|
|
extern const char *Txt_NEW_LINE;
|
|
unsigned NumOpt;
|
|
unsigned i;
|
|
MYSQL_RES *mysql_res;
|
|
MYSQL_ROW row;
|
|
double FloatNum[2];
|
|
|
|
Gbl.Test.Answer.NumOptions = Tst_GetAnswersQst (QstCod,&mysql_res,false); // Result: AnsInd,Answer,Correct
|
|
/*
|
|
row[0] AnsInd
|
|
row[1] Answer
|
|
row[2] Feedback
|
|
row[3] MedCod
|
|
row[4] Correct
|
|
*/
|
|
/***** Write the answers *****/
|
|
switch (Gbl.Test.AnswerType)
|
|
{
|
|
case Tst_ANS_INT:
|
|
Tst_CheckIfNumberOfAnswersIsOne ();
|
|
row = mysql_fetch_row (mysql_res);
|
|
fprintf (Gbl.Test.XML.FileXML,"%ld",
|
|
Tst_GetIntAnsFromStr (row[1]));
|
|
break;
|
|
case Tst_ANS_FLOAT:
|
|
if (Gbl.Test.Answer.NumOptions != 2)
|
|
Lay_ShowErrorAndExit ("Wrong float range.");
|
|
|
|
for (i = 0;
|
|
i < 2;
|
|
i++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res);
|
|
FloatNum[i] = Tst_GetFloatAnsFromStr (row[1]);
|
|
}
|
|
fprintf (Gbl.Test.XML.FileXML,"%s"
|
|
"<lower>%lg</lower>%s"
|
|
"<upper>%lg</upper>%s",
|
|
Txt_NEW_LINE,
|
|
FloatNum[0],Txt_NEW_LINE,
|
|
FloatNum[1],Txt_NEW_LINE);
|
|
break;
|
|
case Tst_ANS_TRUE_FALSE:
|
|
Tst_CheckIfNumberOfAnswersIsOne ();
|
|
row = mysql_fetch_row (mysql_res);
|
|
fprintf (Gbl.Test.XML.FileXML,"%s",
|
|
row[1][0] == 'T' ? "true" :
|
|
"false");
|
|
break;
|
|
case Tst_ANS_UNIQUE_CHOICE:
|
|
case Tst_ANS_MULTIPLE_CHOICE:
|
|
case Tst_ANS_TEXT:
|
|
fprintf (Gbl.Test.XML.FileXML,"%s",Txt_NEW_LINE);
|
|
for (NumOpt = 0;
|
|
NumOpt < Gbl.Test.Answer.NumOptions;
|
|
NumOpt++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res);
|
|
|
|
/* Start answer */
|
|
fprintf (Gbl.Test.XML.FileXML,"<option");
|
|
|
|
/* Write whether the answer is correct or not (row[4]) */
|
|
if (Gbl.Test.AnswerType != Tst_ANS_TEXT)
|
|
fprintf (Gbl.Test.XML.FileXML," correct=\"%s\"",
|
|
(row[4][0] == 'Y') ? "yes" :
|
|
"no");
|
|
|
|
fprintf (Gbl.Test.XML.FileXML,">%s",Txt_NEW_LINE);
|
|
|
|
/* Write the answer (row[1]), that is in HTML */
|
|
fprintf (Gbl.Test.XML.FileXML,"<text>%s</text>%s",
|
|
row[1],Txt_NEW_LINE);
|
|
|
|
/* Write the feedback (row[2]) */
|
|
if (row[2])
|
|
if (row[2][0])
|
|
fprintf (Gbl.Test.XML.FileXML,"<feedback>%s</feedback>%s",
|
|
row[2],Txt_NEW_LINE);
|
|
|
|
/* End answer */
|
|
fprintf (Gbl.Test.XML.FileXML,"</option>%s",
|
|
Txt_NEW_LINE);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/***** Free structure that stores the query result *****/
|
|
DB_FreeMySQLResult (&mysql_res);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/************ Get questions from XML and store them in database **************/
|
|
/*****************************************************************************/
|
|
|
|
void TsI_ImportQstsFromXML (void)
|
|
{
|
|
extern const char *Txt_The_file_is_not_X;
|
|
struct Param *Param;
|
|
char FileNameXMLSrc[PATH_MAX + 1];
|
|
char FileNameXMLTmp[PATH_MAX + 1]; // Full name (including path and .xml) of the destination temporary file
|
|
char MIMEType[Brw_MAX_BYTES_MIME_TYPE + 1];
|
|
bool WrongType = false;
|
|
|
|
/***** Creates directory if not exists *****/
|
|
Fil_CreateDirIfNotExists (Cfg_PATH_TEST_PRIVATE);
|
|
|
|
/***** First of all, copy in disk the file received from stdin (really from Gbl.F.Tmp) *****/
|
|
Param = Fil_StartReceptionOfFile (Fil_NAME_OF_PARAM_FILENAME_ORG,
|
|
FileNameXMLSrc,MIMEType);
|
|
|
|
/* Check if the file type is XML */
|
|
if (strcmp (MIMEType,"text/xml"))
|
|
if (strcmp (MIMEType,"application/xml"))
|
|
if (strcmp (MIMEType,"application/octet-stream"))
|
|
if (strcmp (MIMEType,"application/octetstream"))
|
|
if (strcmp (MIMEType,"application/octet"))
|
|
WrongType = true;
|
|
|
|
if (WrongType)
|
|
Ale_ShowAlert (Ale_WARNING,Txt_The_file_is_not_X,
|
|
"xml");
|
|
else
|
|
{
|
|
/* End the reception of XML in a temporary file */
|
|
snprintf (FileNameXMLTmp,sizeof (FileNameXMLTmp),
|
|
"%s/%s.xml",
|
|
Cfg_PATH_TEST_PRIVATE,Gbl.UniqueNameEncrypted);
|
|
if (Fil_EndReceptionOfFile (FileNameXMLTmp,Param))
|
|
/***** Get questions from XML file and store them in database *****/
|
|
TsI_ReadQuestionsFromXMLFileAndStoreInDB (FileNameXMLTmp);
|
|
else
|
|
Ale_ShowAlert (Ale_WARNING,"Error copying file.");
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/********** Get questions from XML file and store them in database ***********/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_ReadQuestionsFromXMLFileAndStoreInDB (const char *FileNameXML)
|
|
{
|
|
char *XMLBuffer;
|
|
unsigned long FileSize;
|
|
|
|
/***** Open file *****/
|
|
if ((Gbl.Test.XML.FileXML = fopen (FileNameXML,"rb")) == NULL)
|
|
Lay_ShowErrorAndExit ("Can not open XML file.");
|
|
|
|
/***** Compute file size *****/
|
|
fseek (Gbl.Test.XML.FileXML,0L,SEEK_END);
|
|
FileSize = (unsigned long) ftell (Gbl.Test.XML.FileXML);
|
|
fseek (Gbl.Test.XML.FileXML,0L,SEEK_SET);
|
|
|
|
/***** Allocate memory for XML buffer *****/
|
|
if ((XMLBuffer = (char *) malloc (FileSize + 1)) == NULL)
|
|
Lay_NotEnoughMemoryExit ();
|
|
else
|
|
{
|
|
/***** Read file contents into XML buffer *****/
|
|
if (fread ((void *) XMLBuffer,sizeof (char),(size_t) FileSize,Gbl.Test.XML.FileXML))
|
|
XMLBuffer[FileSize] = '\0';
|
|
else
|
|
XMLBuffer[0] = '\0';
|
|
|
|
/***** Import questions from XML buffer *****/
|
|
TsI_ImportQuestionsFromXMLBuffer (XMLBuffer);
|
|
|
|
free (XMLBuffer);
|
|
}
|
|
|
|
/***** Close file *****/
|
|
fclose (Gbl.Test.XML.FileXML);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/******************** Import questions from XML buffer ***********************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_ImportQuestionsFromXMLBuffer (const char *XMLBuffer)
|
|
{
|
|
extern const char *Hlp_ASSESSMENT_Tests;
|
|
extern const char *Txt_XML_file_content;
|
|
extern const char *Txt_Imported_questions;
|
|
struct XMLElement *RootElem;
|
|
struct XMLElement *TestElem = NULL;
|
|
struct XMLElement *QuestionElem;
|
|
struct XMLElement *TagsElem;
|
|
struct XMLElement *TagElem;
|
|
struct XMLElement *StemElem;
|
|
struct XMLElement *FeedbackElem;
|
|
struct XMLElement *AnswerElem;
|
|
struct XMLAttribute *Attribute;
|
|
bool AnswerTypeFound;
|
|
bool QuestionExists;
|
|
char Stem[Cns_MAX_BYTES_TEXT + 1];
|
|
char Feedback[Cns_MAX_BYTES_TEXT + 1];
|
|
|
|
/***** Allocate and get XML tree *****/
|
|
XML_GetTree (XMLBuffer,&RootElem);
|
|
|
|
/***** Start box *****/
|
|
Box_StartBox (NULL,Txt_Imported_questions,NULL,
|
|
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
|
|
|
|
/***** Print XML tree *****/
|
|
fprintf (Gbl.F.Out,"<div class=\"CENTER_MIDDLE\""
|
|
" style=\"padding-bottom:20px;\">"
|
|
"<textarea title=\"%s\" cols=\"60\" rows=\"5\""
|
|
" spellcheck=\"false\" readonly>",
|
|
Txt_XML_file_content);
|
|
XML_PrintTree (RootElem);
|
|
fprintf (Gbl.F.Out,"</textarea>"
|
|
"</div>");
|
|
|
|
/***** Get questions from XML tree and print them *****/
|
|
/* Go to <test> element */
|
|
if (RootElem->FirstChild)
|
|
{
|
|
TestElem = RootElem->FirstChild;
|
|
if (strcmp (TestElem->TagName,"test")) // <test> must be at level 1
|
|
TestElem = NULL;
|
|
}
|
|
if (TestElem)
|
|
{
|
|
/* Current element is <test> */
|
|
|
|
/***** Write heading of list of imported questions *****/
|
|
Tbl_StartTableWideMargin (2);
|
|
TsI_WriteHeadingListImportedQst ();
|
|
|
|
/***** For each question... *****/
|
|
for (QuestionElem = TestElem->FirstChild;
|
|
QuestionElem != NULL;
|
|
QuestionElem = QuestionElem->NextBrother)
|
|
{
|
|
if (!strcmp (QuestionElem->TagName,"question"))
|
|
{
|
|
/***** Create test question *****/
|
|
Tst_QstConstructor ();
|
|
|
|
/* Get type of questions (in mandatory attribute "type") */
|
|
AnswerTypeFound = false;
|
|
for (Attribute = QuestionElem->FirstAttribute;
|
|
Attribute != NULL;
|
|
Attribute = Attribute->Next)
|
|
if (!strcmp (Attribute->AttributeName,"type"))
|
|
{
|
|
Gbl.Test.AnswerType = TsI_ConvertFromStrAnsTypXMLToAnsTyp (Attribute->Content);
|
|
AnswerTypeFound = true;
|
|
break; // Only first attribute "type"
|
|
}
|
|
if (!AnswerTypeFound)
|
|
Lay_ShowErrorAndExit ("Wrong type of answer.");
|
|
|
|
/* Get tags */
|
|
for (TagsElem = QuestionElem->FirstChild, Gbl.Test.Tags.Num = 0;
|
|
TagsElem != NULL;
|
|
TagsElem = TagsElem->NextBrother)
|
|
if (!strcmp (TagsElem->TagName,"tags"))
|
|
{
|
|
for (TagElem = TagsElem->FirstChild;
|
|
TagElem != NULL && Gbl.Test.Tags.Num < Tst_MAX_TAGS_PER_QUESTION;
|
|
TagElem = TagElem->NextBrother)
|
|
if (!strcmp (TagElem->TagName,"tag"))
|
|
{
|
|
if (TagElem->Content)
|
|
{
|
|
Str_Copy (Gbl.Test.Tags.Txt[Gbl.Test.Tags.Num],
|
|
TagElem->Content,
|
|
Tst_MAX_BYTES_TAG);
|
|
Gbl.Test.Tags.Num++;
|
|
}
|
|
}
|
|
break; // Only first element "tags"
|
|
}
|
|
|
|
/* Get stem (mandatory) */
|
|
for (StemElem = QuestionElem->FirstChild;
|
|
StemElem != NULL;
|
|
StemElem = StemElem->NextBrother)
|
|
if (!strcmp (StemElem->TagName,"stem"))
|
|
{
|
|
if (StemElem->Content)
|
|
{
|
|
/* Convert stem from text to HTML (in database stem is stored in HTML) */
|
|
Str_Copy (Stem,StemElem->Content,
|
|
Cns_MAX_BYTES_TEXT);
|
|
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
|
|
Stem,Cns_MAX_BYTES_TEXT,true);
|
|
|
|
Gbl.Test.Stem.Text = Stem;
|
|
Gbl.Test.Stem.Length = strlen (Stem);
|
|
}
|
|
break; // Only first element "stem"
|
|
}
|
|
|
|
/* Get feedback (optional) */
|
|
for (FeedbackElem = QuestionElem->FirstChild;
|
|
FeedbackElem != NULL;
|
|
FeedbackElem = FeedbackElem->NextBrother)
|
|
if (!strcmp (FeedbackElem->TagName,"feedback"))
|
|
{
|
|
if (FeedbackElem->Content)
|
|
{
|
|
/* Convert feedback from text to HTML (in database feedback is stored in HTML) */
|
|
Str_Copy (Feedback,FeedbackElem->Content,
|
|
Cns_MAX_BYTES_TEXT);
|
|
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
|
|
Feedback,Cns_MAX_BYTES_TEXT,true);
|
|
|
|
Gbl.Test.Feedback.Text = Feedback;
|
|
Gbl.Test.Feedback.Length = strlen (Feedback);
|
|
}
|
|
break; // Only first element "feedback"
|
|
}
|
|
|
|
/* Get shuffle. By default, shuffle is false. */
|
|
Gbl.Test.Shuffle = false;
|
|
for (AnswerElem = QuestionElem->FirstChild;
|
|
AnswerElem != NULL;
|
|
AnswerElem = AnswerElem->NextBrother)
|
|
if (!strcmp (AnswerElem->TagName,"answer"))
|
|
{
|
|
if (Gbl.Test.AnswerType == Tst_ANS_UNIQUE_CHOICE ||
|
|
Gbl.Test.AnswerType == Tst_ANS_MULTIPLE_CHOICE)
|
|
/* Get whether shuffle answers (in attribute "shuffle") */
|
|
for (Attribute = AnswerElem->FirstAttribute;
|
|
Attribute != NULL;
|
|
Attribute = Attribute->Next)
|
|
if (!strcmp (Attribute->AttributeName,"shuffle"))
|
|
{
|
|
Gbl.Test.Shuffle = XML_GetAttributteYesNoFromXMLTree (Attribute);
|
|
break; // Only first attribute "shuffle"
|
|
}
|
|
break; // Only first element "answer"
|
|
}
|
|
|
|
/* Get answer (mandatory) */
|
|
TsI_GetAnswerFromXML (AnswerElem);
|
|
|
|
/* Make sure that tags, text and answer are not empty */
|
|
if (Tst_CheckIfQstFormatIsCorrectAndCountNumOptions ())
|
|
{
|
|
/* Check if question already exists in database */
|
|
QuestionExists = TsI_CheckIfQuestionExistsInDB ();
|
|
|
|
/* Write row with this imported question */
|
|
TsI_WriteRowImportedQst (StemElem,FeedbackElem,QuestionExists);
|
|
|
|
/***** If a new question ==> insert question, tags and answer in the database *****/
|
|
if (!QuestionExists)
|
|
{
|
|
Gbl.Test.QstCod = -1L;
|
|
Tst_InsertOrUpdateQstTagsAnsIntoDB ();
|
|
}
|
|
}
|
|
|
|
/***** Destroy test question *****/
|
|
Tst_QstDestructor ();
|
|
}
|
|
}
|
|
|
|
Tbl_EndTable ();
|
|
}
|
|
else // TestElem not found
|
|
Ale_ShowAlert (Ale_ERROR,"Root element <test> not found.");
|
|
|
|
/***** End table *****/
|
|
Box_EndBox ();
|
|
|
|
/***** Free XML tree *****/
|
|
XML_FreeTree (RootElem);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/***** Convert a string with the type of answer in XML to type of answer *****/
|
|
/*****************************************************************************/
|
|
|
|
static Tst_AnswerType_t TsI_ConvertFromStrAnsTypXMLToAnsTyp (const char *StrAnsTypeXML)
|
|
{
|
|
extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
|
|
Tst_AnswerType_t AnsType;
|
|
|
|
if (StrAnsTypeXML != NULL)
|
|
for (AnsType = (Tst_AnswerType_t) 0;
|
|
AnsType < Tst_NUM_ANS_TYPES;
|
|
AnsType++)
|
|
// comparison must be case insensitive, because users can edit XML
|
|
if (!strcasecmp (StrAnsTypeXML,Tst_StrAnswerTypesXML[AnsType]))
|
|
return AnsType;
|
|
|
|
Lay_ShowErrorAndExit ("Wrong type of answer.");
|
|
return (Tst_AnswerType_t) 0; // Not reached
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/**************** Get answer inside an XML question elements *****************/
|
|
/*****************************************************************************/
|
|
|
|
static bool TsI_CheckIfQuestionExistsInDB (void)
|
|
{
|
|
extern const char *Tst_StrAnswerTypesDB[Tst_NUM_ANS_TYPES];
|
|
MYSQL_RES *mysql_res_qst;
|
|
MYSQL_RES *mysql_res_ans;
|
|
MYSQL_ROW row;
|
|
bool IdenticalQuestionFound = false;
|
|
bool IdenticalAnswers;
|
|
unsigned NumQst;
|
|
unsigned NumQstsWithThisStem;
|
|
unsigned NumOpt;
|
|
unsigned NumOptsExistingQstInDB;
|
|
long QstCod;
|
|
unsigned i;
|
|
|
|
/***** Check if stem exists *****/
|
|
NumQstsWithThisStem =
|
|
(unsigned) DB_QuerySELECT (&mysql_res_qst,"can not check"
|
|
" if a question exists",
|
|
"SELECT QstCod FROM tst_questions"
|
|
" WHERE CrsCod=%ld AND AnsType='%s' AND Stem='%s'",
|
|
Gbl.Hierarchy.Crs.CrsCod,
|
|
Tst_StrAnswerTypesDB[Gbl.Test.AnswerType],
|
|
Gbl.Test.Stem.Text);
|
|
|
|
if (NumQstsWithThisStem) // There are questions in database with the same stem that the one of this question
|
|
{
|
|
/***** Check if the answer exists in any of the questions with the same stem *****/
|
|
/* For each question with the same stem */
|
|
for (NumQst = 0;
|
|
!IdenticalQuestionFound && NumQst < NumQstsWithThisStem;
|
|
NumQst++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res_qst);
|
|
if ((QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
|
|
Lay_ShowErrorAndExit ("Wrong code of question.");
|
|
|
|
/* Get answers from this question */
|
|
NumOptsExistingQstInDB =
|
|
(unsigned) DB_QuerySELECT (&mysql_res_ans,"can not get the answer"
|
|
" of a question",
|
|
"SELECT Answer FROM tst_answers"
|
|
" WHERE QstCod=%ld ORDER BY AnsInd",
|
|
QstCod);
|
|
|
|
switch (Gbl.Test.AnswerType)
|
|
{
|
|
case Tst_ANS_INT:
|
|
row = mysql_fetch_row (mysql_res_ans);
|
|
IdenticalQuestionFound = (Tst_GetIntAnsFromStr (row[0]) == Gbl.Test.Answer.Integer);
|
|
break;
|
|
case Tst_ANS_FLOAT:
|
|
for (IdenticalAnswers = true, i = 0;
|
|
IdenticalAnswers && i < 2;
|
|
i++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res_ans);
|
|
IdenticalAnswers = (Tst_GetFloatAnsFromStr (row[0]) == Gbl.Test.Answer.FloatingPoint[i]);
|
|
}
|
|
IdenticalQuestionFound = IdenticalAnswers;
|
|
break;
|
|
case Tst_ANS_TRUE_FALSE:
|
|
row = mysql_fetch_row (mysql_res_ans);
|
|
IdenticalQuestionFound = (Str_ConvertToUpperLetter (row[0][0]) == Gbl.Test.Answer.TF);
|
|
break;
|
|
case Tst_ANS_UNIQUE_CHOICE:
|
|
case Tst_ANS_MULTIPLE_CHOICE:
|
|
case Tst_ANS_TEXT:
|
|
if (NumOptsExistingQstInDB == Gbl.Test.Answer.NumOptions)
|
|
{
|
|
for (IdenticalAnswers = true, NumOpt = 0;
|
|
IdenticalAnswers && NumOpt < NumOptsExistingQstInDB;
|
|
NumOpt++)
|
|
{
|
|
row = mysql_fetch_row (mysql_res_ans);
|
|
|
|
if (strcasecmp (row[0],Gbl.Test.Answer.Options[NumOpt].Text))
|
|
IdenticalAnswers = false;
|
|
}
|
|
}
|
|
else // Different number of answers (options)
|
|
IdenticalAnswers = false;
|
|
IdenticalQuestionFound = IdenticalAnswers;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* Free structure that stores the query result for answers */
|
|
DB_FreeMySQLResult (&mysql_res_ans);
|
|
}
|
|
}
|
|
else // Stem does not exist
|
|
IdenticalQuestionFound = false;
|
|
|
|
/* Free structure that stores the query result for questions */
|
|
DB_FreeMySQLResult (&mysql_res_qst);
|
|
|
|
return IdenticalQuestionFound;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/**************** Get answer inside an XML question elements *****************/
|
|
/*****************************************************************************/
|
|
// Answer is mandatory
|
|
|
|
static void TsI_GetAnswerFromXML (struct XMLElement *AnswerElem)
|
|
{
|
|
struct XMLElement *OptionElem;
|
|
struct XMLElement *TextElem;
|
|
struct XMLElement *FeedbackElem;
|
|
struct XMLElement *LowerUpperElem;
|
|
struct XMLAttribute *Attribute;
|
|
unsigned NumOpt;
|
|
|
|
switch (Gbl.Test.AnswerType)
|
|
{
|
|
case Tst_ANS_INT:
|
|
if (!Tst_AllocateTextChoiceAnswer (0))
|
|
/* Abort on error */
|
|
Ale_ShowAlertsAndExit ();
|
|
|
|
if (AnswerElem->Content)
|
|
Str_Copy (Gbl.Test.Answer.Options[0].Text,AnswerElem->Content,
|
|
Tst_MAX_BYTES_ANSWER_OR_FEEDBACK);
|
|
break;
|
|
case Tst_ANS_FLOAT:
|
|
if (!Tst_AllocateTextChoiceAnswer (0))
|
|
/* Abort on error */
|
|
Ale_ShowAlertsAndExit ();
|
|
if (!Tst_AllocateTextChoiceAnswer (1))
|
|
/* Abort on error */
|
|
Ale_ShowAlertsAndExit ();
|
|
|
|
for (LowerUpperElem = AnswerElem->FirstChild;
|
|
LowerUpperElem != NULL;
|
|
LowerUpperElem = LowerUpperElem->NextBrother)
|
|
if (!strcmp (LowerUpperElem->TagName,"lower"))
|
|
{
|
|
if (LowerUpperElem->Content)
|
|
Str_Copy (Gbl.Test.Answer.Options[0].Text,
|
|
LowerUpperElem->Content,
|
|
Tst_MAX_BYTES_ANSWER_OR_FEEDBACK);
|
|
break; // Only first element "lower"
|
|
}
|
|
for (LowerUpperElem = AnswerElem->FirstChild;
|
|
LowerUpperElem != NULL;
|
|
LowerUpperElem = LowerUpperElem->NextBrother)
|
|
if (!strcmp (LowerUpperElem->TagName,"upper"))
|
|
{
|
|
if (LowerUpperElem->Content)
|
|
Str_Copy (Gbl.Test.Answer.Options[1].Text,
|
|
LowerUpperElem->Content,
|
|
Tst_MAX_BYTES_ANSWER_OR_FEEDBACK);
|
|
break; // Only first element "upper"
|
|
}
|
|
break;
|
|
case Tst_ANS_TRUE_FALSE:
|
|
// Comparisons must be case insensitive, because users can edit XML
|
|
if (!AnswerElem->Content)
|
|
Gbl.Test.Answer.TF = ' ';
|
|
else if (!strcasecmp (AnswerElem->Content,"true") ||
|
|
!strcasecmp (AnswerElem->Content,"T") ||
|
|
!strcasecmp (AnswerElem->Content,"yes") ||
|
|
!strcasecmp (AnswerElem->Content,"Y"))
|
|
Gbl.Test.Answer.TF = 'T';
|
|
else if (!strcasecmp (AnswerElem->Content,"false") ||
|
|
!strcasecmp (AnswerElem->Content,"F") ||
|
|
!strcasecmp (AnswerElem->Content,"no") ||
|
|
!strcasecmp (AnswerElem->Content,"N"))
|
|
Gbl.Test.Answer.TF = 'F';
|
|
else
|
|
Gbl.Test.Answer.TF = ' ';
|
|
break;
|
|
case Tst_ANS_UNIQUE_CHOICE:
|
|
case Tst_ANS_MULTIPLE_CHOICE:
|
|
case Tst_ANS_TEXT:
|
|
/* Get options */
|
|
for (OptionElem = AnswerElem->FirstChild, NumOpt = 0;
|
|
OptionElem != NULL && NumOpt < Tst_MAX_OPTIONS_PER_QUESTION;
|
|
OptionElem = OptionElem->NextBrother, NumOpt++)
|
|
if (!strcmp (OptionElem->TagName,"option"))
|
|
{
|
|
if (!Tst_AllocateTextChoiceAnswer (NumOpt))
|
|
/* Abort on error */
|
|
Ale_ShowAlertsAndExit ();
|
|
|
|
for (TextElem = OptionElem->FirstChild;
|
|
TextElem != NULL;
|
|
TextElem = TextElem->NextBrother)
|
|
if (!strcmp (TextElem->TagName,"text"))
|
|
{
|
|
if (TextElem->Content)
|
|
{
|
|
Str_Copy (Gbl.Test.Answer.Options[NumOpt].Text,
|
|
TextElem->Content,
|
|
Tst_MAX_BYTES_ANSWER_OR_FEEDBACK);
|
|
|
|
/* Convert answer from text to HTML (in database answer text is stored in HTML) */
|
|
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
|
|
Gbl.Test.Answer.Options[NumOpt].Text,Tst_MAX_BYTES_ANSWER_OR_FEEDBACK,true);
|
|
}
|
|
break; // Only first element "text"
|
|
}
|
|
|
|
for (FeedbackElem = OptionElem->FirstChild;
|
|
FeedbackElem != NULL;
|
|
FeedbackElem = FeedbackElem->NextBrother)
|
|
if (!strcmp (FeedbackElem->TagName,"feedback"))
|
|
{
|
|
if (FeedbackElem->Content)
|
|
{
|
|
Str_Copy (Gbl.Test.Answer.Options[NumOpt].Feedback,
|
|
FeedbackElem->Content,
|
|
Tst_MAX_BYTES_ANSWER_OR_FEEDBACK);
|
|
|
|
/* Convert feedback from text to HTML (in database answer feedback is stored in HTML) */
|
|
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
|
|
Gbl.Test.Answer.Options[NumOpt].Feedback,Tst_MAX_BYTES_ANSWER_OR_FEEDBACK,true);
|
|
}
|
|
break; // Only first element "feedback"
|
|
}
|
|
|
|
if (Gbl.Test.AnswerType == Tst_ANS_TEXT)
|
|
Gbl.Test.Answer.Options[NumOpt].Correct = true;
|
|
else
|
|
/* Check if option is correct or wrong */
|
|
for (Attribute = OptionElem->FirstAttribute;
|
|
Attribute != NULL;
|
|
Attribute = Attribute->Next)
|
|
if (!strcmp (Attribute->AttributeName,"correct"))
|
|
{
|
|
Gbl.Test.Answer.Options[NumOpt].Correct = XML_GetAttributteYesNoFromXMLTree (Attribute);
|
|
break; // Only first attribute "correct"
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/************* Write heading of list of imported test questions **************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_WriteHeadingListImportedQst (void)
|
|
{
|
|
extern const char *Txt_No_INDEX;
|
|
extern const char *Txt_Tags;
|
|
extern const char *Txt_Type;
|
|
extern const char *Txt_Shuffle;
|
|
extern const char *Txt_Question;
|
|
|
|
/***** Write the heading *****/
|
|
fprintf (Gbl.F.Out,"<tr>"
|
|
"<th></th>"
|
|
"<th class=\"CENTER_TOP\">"
|
|
"%s"
|
|
"</th>"
|
|
"<th class=\"CENTER_TOP\">"
|
|
"%s"
|
|
"</th>"
|
|
"<th class=\"CENTER_TOP\">"
|
|
"%s"
|
|
"</th>"
|
|
"<th class=\"CENTER_TOP\">"
|
|
"%s"
|
|
"</th>"
|
|
"<th class=\"LEFT_TOP\">"
|
|
"%s"
|
|
"</th>"
|
|
"</tr>",
|
|
Txt_No_INDEX,
|
|
Txt_Tags,
|
|
Txt_Type,
|
|
Txt_Shuffle,
|
|
Txt_Question);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/**************** Write a row with one imported test question ****************/
|
|
/*****************************************************************************/
|
|
|
|
static void TsI_WriteRowImportedQst (struct XMLElement *StemElem,
|
|
struct XMLElement *FeedbackElem,
|
|
bool QuestionExists)
|
|
{
|
|
extern const char *Txt_Existing_question;
|
|
extern const char *Txt_New_question;
|
|
extern const char *Txt_no_tags;
|
|
extern const char *Txt_TST_STR_ANSWER_TYPES[Tst_NUM_ANS_TYPES];
|
|
extern const char *Txt_TST_Answer_given_by_the_teachers;
|
|
static unsigned NumQst = 0;
|
|
static unsigned NumNonExistingQst = 0;
|
|
const char *Stem = (StemElem != NULL) ? StemElem->Content :
|
|
"";
|
|
const char *Feedback = (FeedbackElem != NULL) ? FeedbackElem->Content :
|
|
"";
|
|
unsigned NumTag;
|
|
unsigned NumOpt;
|
|
char *AnswerText;
|
|
size_t AnswerTextLength;
|
|
char *AnswerFeedback;
|
|
size_t AnswerFeedbackLength;
|
|
const char *ClassData = QuestionExists ? "DAT_SMALL_LIGHT" :
|
|
"DAT_SMALL";
|
|
const char *ClassStem = QuestionExists ? "TEST_EDI_LIGHT" :
|
|
"TEST_EDI";
|
|
|
|
Gbl.RowEvenOdd = NumQst % 2;
|
|
NumQst++;
|
|
|
|
/***** Put icon to indicate that a question does not exist in database *****/
|
|
fprintf (Gbl.F.Out,"<tr>"
|
|
"<td class=\"BT%u CENTER_TOP\">"
|
|
"<img src=\"%s/%s\""
|
|
" alt=\"%s\" title=\"%s\""
|
|
" class=\"CONTEXT_ICO_16x16\" />"
|
|
"</td>",
|
|
Gbl.RowEvenOdd,
|
|
Cfg_URL_ICON_PUBLIC,
|
|
QuestionExists ? "tr16x16.gif" :
|
|
"check-circle.svg",
|
|
QuestionExists ? Txt_Existing_question :
|
|
Txt_New_question,
|
|
QuestionExists ? Txt_Existing_question :
|
|
Txt_New_question);
|
|
|
|
/***** Write number of question *****/
|
|
fprintf (Gbl.F.Out,"<td class=\"%s CENTER_TOP COLOR%u\">",
|
|
ClassData,Gbl.RowEvenOdd);
|
|
if (!QuestionExists)
|
|
fprintf (Gbl.F.Out,"%u ",++NumNonExistingQst);
|
|
fprintf (Gbl.F.Out,"</td>");
|
|
|
|
/***** Write the question tags *****/
|
|
fprintf (Gbl.F.Out,"<td class=\"LEFT_TOP COLOR%u\">",
|
|
Gbl.RowEvenOdd);
|
|
if (Gbl.Test.Tags.Num)
|
|
{
|
|
/***** Write the tags *****/
|
|
fprintf (Gbl.F.Out,"<table>");
|
|
for (NumTag = 0;
|
|
NumTag < Gbl.Test.Tags.Num;
|
|
NumTag++)
|
|
fprintf (Gbl.F.Out,"<tr>"
|
|
"<td class=\"%s LEFT_TOP\">"
|
|
" • "
|
|
"</td>"
|
|
"<td class=\"%s LEFT_TOP\">"
|
|
"%s"
|
|
"</td>"
|
|
"</tr>",
|
|
ClassData,
|
|
ClassData,Gbl.Test.Tags.Txt[NumTag]);
|
|
fprintf (Gbl.F.Out,"</table>");
|
|
}
|
|
else // no tags for this question
|
|
fprintf (Gbl.F.Out,"<span class=\"%s\"> (%s) </span>",
|
|
ClassData,Txt_no_tags);
|
|
|
|
fprintf (Gbl.F.Out,"</td>");
|
|
|
|
/***** Write the question type *****/
|
|
fprintf (Gbl.F.Out,"<td class=\"%s CENTER_TOP COLOR%u\">"
|
|
"%s "
|
|
"</td>",
|
|
ClassData,Gbl.RowEvenOdd,
|
|
Txt_TST_STR_ANSWER_TYPES[Gbl.Test.AnswerType]);
|
|
|
|
/***** Write if shuffle is enabled *****/
|
|
fprintf (Gbl.F.Out,"<td class=\"CENTER_TOP COLOR%u\">",
|
|
Gbl.RowEvenOdd);
|
|
if (Gbl.Test.AnswerType == Tst_ANS_UNIQUE_CHOICE ||
|
|
Gbl.Test.AnswerType == Tst_ANS_MULTIPLE_CHOICE)
|
|
/* Put an icon that indicates whether shuffle is enabled or not */
|
|
if (Gbl.Test.Shuffle)
|
|
fprintf (Gbl.F.Out,"<img src=\"%s/check.svg\""
|
|
" alt=\"%s\" title=\"%s\""
|
|
" class=\"%sICO16x16\" />",
|
|
Cfg_URL_ICON_PUBLIC,
|
|
Txt_TST_Answer_given_by_the_teachers,
|
|
Txt_TST_Answer_given_by_the_teachers,
|
|
QuestionExists ? "ICO_HIDDEN " :
|
|
"");
|
|
fprintf (Gbl.F.Out,"</td>");
|
|
|
|
/***** Write the stem and the answers *****/
|
|
fprintf (Gbl.F.Out,"<td class=\"LEFT_TOP COLOR%u\">",
|
|
Gbl.RowEvenOdd);
|
|
Tst_WriteQstStem (Stem,ClassStem);
|
|
Tst_WriteQstFeedback (Feedback,"TEST_EDI_LIGHT");
|
|
switch (Gbl.Test.AnswerType)
|
|
{
|
|
case Tst_ANS_INT:
|
|
fprintf (Gbl.F.Out,"<span class=\"%s\">(%ld)</span>",
|
|
ClassStem,Gbl.Test.Answer.Integer);
|
|
break;
|
|
case Tst_ANS_FLOAT:
|
|
fprintf (Gbl.F.Out,"<span class=\"%s\">([%lg; %lg])</span>",
|
|
ClassStem,Gbl.Test.Answer.FloatingPoint[0],Gbl.Test.Answer.FloatingPoint[1]);
|
|
break;
|
|
case Tst_ANS_TRUE_FALSE:
|
|
fprintf (Gbl.F.Out,"<span class=\"%s\">(",ClassStem);
|
|
Tst_WriteAnsTF (Gbl.Test.Answer.TF);
|
|
fprintf (Gbl.F.Out,")</span>");
|
|
break;
|
|
case Tst_ANS_UNIQUE_CHOICE:
|
|
case Tst_ANS_MULTIPLE_CHOICE:
|
|
case Tst_ANS_TEXT:
|
|
fprintf (Gbl.F.Out,"<table>");
|
|
for (NumOpt = 0;
|
|
NumOpt < Gbl.Test.Answer.NumOptions;
|
|
NumOpt++)
|
|
{
|
|
/* Convert the answer, that is in HTML, to rigorous HTML */
|
|
AnswerTextLength = strlen (Gbl.Test.Answer.Options[NumOpt].Text) *
|
|
Str_MAX_BYTES_PER_CHAR;
|
|
if ((AnswerText = (char *) malloc (AnswerTextLength + 1)) == NULL)
|
|
Lay_NotEnoughMemoryExit ();
|
|
Str_Copy (AnswerText,Gbl.Test.Answer.Options[NumOpt].Text,
|
|
AnswerTextLength);
|
|
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
|
|
AnswerText,AnswerTextLength,false);
|
|
|
|
/* Convert the feedback, that is in HTML, to rigorous HTML */
|
|
AnswerFeedbackLength = 0;
|
|
AnswerFeedback = NULL;
|
|
if (Gbl.Test.Answer.Options[NumOpt].Feedback)
|
|
if (Gbl.Test.Answer.Options[NumOpt].Feedback[0])
|
|
{
|
|
AnswerFeedbackLength = strlen (Gbl.Test.Answer.Options[NumOpt].Feedback) *
|
|
Str_MAX_BYTES_PER_CHAR;
|
|
if ((AnswerFeedback = (char *) malloc (AnswerFeedbackLength + 1)) == NULL)
|
|
Lay_NotEnoughMemoryExit ();
|
|
Str_Copy (AnswerFeedback,
|
|
Gbl.Test.Answer.Options[NumOpt].Feedback,
|
|
AnswerFeedbackLength);
|
|
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
|
|
AnswerFeedback,AnswerFeedbackLength,false);
|
|
}
|
|
|
|
/* Put an icon that indicates whether the answer is correct or wrong */
|
|
fprintf (Gbl.F.Out,"<tr>"
|
|
"<td class=\"BT%u\">",
|
|
Gbl.RowEvenOdd);
|
|
if (Gbl.Test.Answer.Options[NumOpt].Correct)
|
|
fprintf (Gbl.F.Out,"<img src=\"%s/check.svg\""
|
|
" alt=\"%s\" title=\"%s\""
|
|
" class=\"%sCONTEXT_ICO_16x16\" />",
|
|
Cfg_URL_ICON_PUBLIC,
|
|
Txt_TST_Answer_given_by_the_teachers,
|
|
Txt_TST_Answer_given_by_the_teachers,
|
|
QuestionExists ? "ICO_HIDDEN " :
|
|
"");
|
|
fprintf (Gbl.F.Out,"</td>");
|
|
|
|
/* Write the number of option */
|
|
fprintf (Gbl.F.Out,"<td class=\"%s LEFT_TOP\">"
|
|
"%c) "
|
|
"</td>",
|
|
ClassData,'a' + (char) NumOpt);
|
|
|
|
/* Write the text and the feedback of the answer */
|
|
fprintf (Gbl.F.Out,"<td class=\"LEFT_TOP\">"
|
|
"<div class=\"%s\">"
|
|
"%s"
|
|
"</div>",
|
|
ClassStem,AnswerText);
|
|
if (AnswerFeedbackLength)
|
|
fprintf (Gbl.F.Out,"<div class=\"TEST_EDI_LIGHT\">"
|
|
"%s"
|
|
"</div>",
|
|
AnswerFeedback);
|
|
fprintf (Gbl.F.Out,"</td>"
|
|
"</tr>");
|
|
|
|
/* Free memory allocated for the answer and the feedback */
|
|
free ((void *) AnswerText);
|
|
if (AnswerFeedbackLength)
|
|
free ((void *) AnswerFeedback);
|
|
}
|
|
fprintf (Gbl.F.Out,"</table>");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
fprintf (Gbl.F.Out,"</td>"
|
|
"</tr>");
|
|
}
|