swad-core/swad_question_import.c

1065 lines
37 KiB
C
Raw Permalink Normal View History

2014-12-01 23:55:08 +01:00
// 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-2024 Antonio Ca<EFBFBD>as Vargas
2014-12-01 23:55:08 +01:00
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_action_list.h"
#include "swad_alert.h"
2017-06-10 21:38:10 +02:00
#include "swad_box.h"
2014-12-01 23:55:08 +01:00
#include "swad_database.h"
#include "swad_error.h"
2018-11-09 20:47:39 +01:00
#include "swad_form.h"
2014-12-01 23:55:08 +01:00
#include "swad_global.h"
2019-10-23 19:05:05 +02:00
#include "swad_HTML.h"
2014-12-01 23:55:08 +01:00
#include "swad_parameter.h"
#include "swad_tag_database.h"
2014-12-01 23:55:08 +01:00
#include "swad_test.h"
#include "swad_xml.h"
/*****************************************************************************/
/************** External global variables from others modules ****************/
/*****************************************************************************/
extern struct Globals Gbl;
/*****************************************************************************/
2019-11-21 16:47:07 +01:00
/***************************** Private prototypes ****************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
static void QstImp_PutParsExportQsts (void *Questions);
static void QstImp_PutCreateXMLPar (void);
static void QstImp_ExportQuestion (struct Qst_Question *Question,FILE *FileXML);
static void QstImp_GetAndWriteTagsXML (long QstCod,FILE *FileXML);
static void QstImp_WriteAnswersOfAQstXML (const struct Qst_Question *Question,
FILE *FileXML);
static void QstImp_ReadQuestionsFromXMLFileAndStoreInDB (const char *FileNameXML);
static void QstImp_ImportQuestionsFromXMLBuffer (const char *XMLBuffer);
static Qst_AnswerType_t QstImp_ConvertFromStrAnsTypXMLToAnsTyp (const char *StrAnsTypeXML);
static void QstImp_GetAnswerFromXML (struct XMLElement *AnswerElem,
struct Qst_Question *Question);
static void QstImp_WriteHeadingListImportedQst (void);
static void QstImp_WriteRowImportedQst (struct XMLElement *StemElem,
struct XMLElement *FeedbackElem,
const struct Qst_Question *Question,
bool QuestionExists);
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
/**************** Put a link (form) to export test questions *****************/
/*****************************************************************************/
void QstImp_PutIconToExportQuestions (struct Qst_Questions *Questions)
2014-12-01 23:55:08 +01:00
{
2020-05-17 18:45:27 +02:00
Lay_PutContextualLinkOnlyIcon (ActLstTstQst,NULL,
QstImp_PutParsExportQsts,Questions,
"file-import.svg",Ico_BLACK);
2015-12-13 21:30:28 +01:00
}
/*****************************************************************************/
/****************** Put params to export test questions **********************/
/*****************************************************************************/
2014-12-01 23:55:08 +01:00
static void QstImp_PutParsExportQsts (void *Questions)
2015-12-13 21:30:28 +01:00
{
if (Questions)
2020-03-26 02:54:30 +01:00
{
Qst_PutParsEditQst (Questions);
Par_PutParChar ("OnlyThisQst",'N');
Par_PutParUnsigned (NULL,"Order",(unsigned) (((struct Qst_Questions *) Questions)->SelectedOrder));
QstImp_PutCreateXMLPar ();
2020-03-26 02:54:30 +01:00
}
2020-03-17 00:35:11 +01:00
}
/*****************************************************************************/
/************************ Parameter to create XML file ***********************/
/*****************************************************************************/
static void QstImp_PutCreateXMLPar (void)
2020-03-17 00:35:11 +01:00
{
Par_PutParChar ("CreateXML",'Y');
2014-12-01 23:55:08 +01:00
}
bool QstImp_GetCreateXMLParFromForm (void)
2020-03-17 00:35:11 +01:00
{
return Par_GetParBool ("CreateXML");
2020-03-17 00:35:11 +01:00
}
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
/*************** Put a link (form) to import test questions ******************/
/*****************************************************************************/
void QstImp_PutIconToImportQuestions (void)
2014-12-01 23:55:08 +01:00
{
2020-05-17 18:45:27 +02:00
Lay_PutContextualLinkOnlyIcon (ActReqImpTstQst,NULL,
2020-03-26 02:54:30 +01:00
NULL,NULL,
"file-export.svg",Ico_BLACK);
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/*********** Show form to import test questions from an XML file *************/
/*****************************************************************************/
void QstImp_ShowFormImpQstsFromXML (void)
2014-12-01 23:55:08 +01:00
{
2016-11-13 20:18:49 +01:00
extern const char *Hlp_ASSESSMENT_Tests;
2016-03-21 13:08:18 +01:00
extern const char *Txt_Import_questions;
extern const char *Txt_You_need_an_XML_file_containing_a_list_of_questions;
2014-12-01 23:55:08 +01:00
extern const char *Txt_XML_file;
2019-10-26 02:19:42 +02:00
/***** Begin box *****/
Box_BoxBegin (Txt_Import_questions,NULL,NULL,
2017-06-12 15:03:29 +02:00
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
2014-12-01 23:55:08 +01:00
/***** Write help message *****/
Ale_ShowAlert (Ale_INFO,Txt_You_need_an_XML_file_containing_a_list_of_questions);
2014-12-01 23:55:08 +01:00
/***** Write a form to import questions *****/
Frm_BeginForm (ActImpTstQst);
HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ());
HTM_TxtColonNBSP (Txt_XML_file);
HTM_INPUT_FILE (Fil_NAME_OF_PARAM_FILENAME_ORG,".xml",
HTM_SUBMIT_ON_CHANGE,
NULL);
HTM_LABEL_End ();
Frm_EndForm ();
2016-03-21 13:08:18 +01:00
2017-06-12 14:16:33 +02:00
/***** End box *****/
2019-10-25 22:48:34 +02:00
Box_BoxEnd ();
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/*** Create the XML file with test questions and put a link to download it ***/
/*****************************************************************************/
void QstImp_CreateXML (unsigned NumQsts,MYSQL_RES *mysql_res)
2014-12-01 23:55:08 +01:00
{
extern const char *Txt_NEW_LINE;
extern const char *Txt_XML_file;
2017-01-28 15:58:46 +01:00
char PathPubFile[PATH_MAX + 1];
2020-03-17 00:35:11 +01:00
FILE *FileXML;
2020-03-24 01:50:39 +01:00
unsigned NumQst;
struct Qst_Question Question;
2014-12-01 23:55:08 +01:00
MYSQL_ROW row;
2016-04-04 02:12:06 +02:00
/***** Create a temporary public directory
used to download the XML file *****/
Brw_CreateDirDownloadTmp ();
2014-12-01 23:55:08 +01:00
/***** Create public XML file with the questions *****/
snprintf (PathPubFile,sizeof (PathPubFile),"%s/%s/%s/test.xml",
2019-03-20 01:36:36 +01:00
Cfg_PATH_FILE_BROWSER_TMP_PUBLIC,
2019-03-20 14:36:26 +01:00
Gbl.FileBrowser.TmpPubDir.L,
Gbl.FileBrowser.TmpPubDir.R);
2020-03-17 00:35:11 +01:00
if ((FileXML = fopen (PathPubFile,"wb")) == NULL)
Err_ShowErrorAndExit ("Can not open target file.");
2014-12-01 23:55:08 +01:00
/***** Begin XML file *****/
2020-03-17 00:35:11 +01:00
XML_WriteStartFile (FileXML,"test",false);
fprintf (FileXML,"%s",Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
/***** Write rows *****/
2020-03-24 01:50:39 +01:00
for (NumQst = 0;
NumQst < NumQsts;
NumQst++)
2014-12-01 23:55:08 +01:00
{
2020-03-27 18:56:00 +01:00
/* Create test question */
Qst_QstConstructor (&Question);
2020-03-27 18:56:00 +01:00
2020-03-17 00:35:11 +01:00
/* Get question code (row[0]) */
row = mysql_fetch_row (mysql_res);
if ((Question.QstCod = Str_ConvertStrCodToLongCod (row[0])) <= 0)
Err_WrongQuestionExit ();
2020-03-17 00:35:11 +01:00
QstImp_ExportQuestion (&Question,FileXML);
2020-03-27 18:56:00 +01:00
/* Destroy test question */
Qst_QstDestructor (&Question);
2020-03-17 00:35:11 +01:00
}
/***** End XML file *****/
XML_WriteEndFile (FileXML,"test");
/***** Close the XML file *****/
fclose (FileXML);
/***** Return to start of query result *****/
mysql_data_seek (mysql_res,0);
/***** Write the link to XML file *****/
HTM_A_Begin ("href=\"%s/%s/%s/test.xml\" target=\"_blank\""
" class=\"FORM_OUT_%s BOLD\"",
2020-03-17 00:35:11 +01:00
Cfg_URL_FILE_BROWSER_TMP_PUBLIC,
Gbl.FileBrowser.TmpPubDir.L,
Gbl.FileBrowser.TmpPubDir.R,
The_GetSuffix ());
Ico_PutIconTextLink ("file.svg",Ico_BLACK,Txt_XML_file);
2020-03-17 00:35:11 +01:00
HTM_A_End ();
}
/*****************************************************************************/
/****************** Write one question into the XML file *********************/
/*****************************************************************************/
static void QstImp_ExportQuestion (struct Qst_Question *Question,FILE *FileXML)
2020-03-17 00:35:11 +01:00
{
extern const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES];
2020-03-17 00:35:11 +01:00
extern const char *Txt_NEW_LINE;
if (Qst_GetQstDataByCod (Question))
2020-03-17 00:35:11 +01:00
{
2020-04-04 02:07:54 +02:00
/***** Write the answer type *****/
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<question type=\"%s\">%s",
Qst_StrAnswerTypesXML[Question->Answer.Type],Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
2020-03-17 00:35:11 +01:00
/***** Write the question tags *****/
fprintf (FileXML,"<tags>%s",Txt_NEW_LINE);
QstImp_GetAndWriteTagsXML (Question->QstCod,FileXML);
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"</tags>%s",Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
2020-04-04 02:07:54 +02:00
/***** Write the stem, that is in HTML format *****/
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<stem>%s</stem>%s",
2020-04-04 19:20:50 +02:00
Question->Stem,Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
2020-04-04 02:07:54 +02:00
/***** Write the feedback, that is in HTML format *****/
2020-04-04 19:20:50 +02:00
if (Question->Feedback[0])
2020-04-04 02:07:54 +02:00
fprintf (FileXML,"<feedback>%s</feedback>%s",
2020-04-04 19:20:50 +02:00
Question->Feedback,Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
2020-03-17 00:35:11 +01:00
/***** Write the answers of this question.
2020-04-04 02:07:54 +02:00
Shuffle can be enabled or disabled *****/
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<answer");
if (Question->Answer.Type == Qst_ANS_UNIQUE_CHOICE ||
Question->Answer.Type == Qst_ANS_MULTIPLE_CHOICE)
2020-03-17 00:35:11 +01:00
fprintf (FileXML," shuffle=\"%s\"",
2020-04-04 02:07:54 +02:00
Question->Answer.Shuffle ? "yes" :
"no");
2020-03-17 00:35:11 +01:00
fprintf (FileXML,">");
QstImp_WriteAnswersOfAQstXML (Question,FileXML);
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"</answer>%s",Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
2020-03-17 00:35:11 +01:00
/***** End question *****/
fprintf (FileXML,"</question>%s%s",
2016-04-04 02:12:06 +02:00
Txt_NEW_LINE,Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
}
}
/*****************************************************************************/
/************* Get and write tags of a question into the XML file ************/
/*****************************************************************************/
static void QstImp_GetAndWriteTagsXML (long QstCod,FILE *FileXML)
2014-12-01 23:55:08 +01:00
{
extern const char *Txt_NEW_LINE;
unsigned NumTags;
unsigned NumTag;
2014-12-01 23:55:08 +01:00
MYSQL_RES *mysql_res;
MYSQL_ROW row;
if ((NumTags = Tag_DB_GetTagsQst (&mysql_res,QstCod))) // Result: TagTxt
2014-12-01 23:55:08 +01:00
/***** Write the tags *****/
for (NumTag = 1;
NumTag <= NumTags;
NumTag++)
2014-12-01 23:55:08 +01:00
{
row = mysql_fetch_row (mysql_res);
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<tag>%s</tag>%s",
2014-12-01 23:55:08 +01:00
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 QstImp_WriteAnswersOfAQstXML (const struct Qst_Question *Question,
FILE *FileXML)
2014-12-01 23:55:08 +01:00
{
extern const char *Txt_NEW_LINE;
unsigned NumOpt;
2020-03-18 01:57:08 +01:00
/***** Write answers *****/
2020-03-25 01:36:22 +01:00
switch (Question->Answer.Type)
2014-12-01 23:55:08 +01:00
{
case Qst_ANS_INT:
2020-04-04 02:07:54 +02:00
fprintf (FileXML,"%ld",Question->Answer.Integer);
2014-12-01 23:55:08 +01:00
break;
case Qst_ANS_FLOAT:
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"%s"
"<lower>%.15lg</lower>%s"
"<upper>%.15lg</upper>%s",
2014-12-01 23:55:08 +01:00
Txt_NEW_LINE,
2020-04-04 02:07:54 +02:00
Question->Answer.FloatingPoint[0],Txt_NEW_LINE,
Question->Answer.FloatingPoint[1],Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
break;
case Qst_ANS_TRUE_FALSE:
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"%s",
2020-04-04 02:07:54 +02:00
Question->Answer.TF == 'T' ? "true" :
"false");
2014-12-01 23:55:08 +01:00
break;
case Qst_ANS_UNIQUE_CHOICE:
case Qst_ANS_MULTIPLE_CHOICE:
case Qst_ANS_TEXT:
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"%s",Txt_NEW_LINE);
2014-12-01 23:55:08 +01:00
for (NumOpt = 0;
2020-03-25 01:36:22 +01:00
NumOpt < Question->Answer.NumOptions;
2014-12-01 23:55:08 +01:00
NumOpt++)
{
/* Begin answer */
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<option");
2019-05-17 09:19:49 +02:00
2020-04-04 02:07:54 +02:00
/* Write whether the answer is correct or not */
if (Question->Answer.Type != Qst_ANS_TEXT)
2020-03-17 00:35:11 +01:00
fprintf (FileXML," correct=\"%s\"",
2020-04-04 02:07:54 +02:00
Question->Answer.Options[NumOpt].Correct ? "yes" :
"no");
2019-05-17 09:19:49 +02:00
2020-03-17 00:35:11 +01:00
fprintf (FileXML,">%s",Txt_NEW_LINE);
2019-05-17 09:19:49 +02:00
2020-04-04 02:07:54 +02:00
/* Write the answer, that is in HTML */
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<text>%s</text>%s",
2020-04-04 02:07:54 +02:00
Question->Answer.Options[NumOpt].Text,Txt_NEW_LINE);
2016-04-06 14:41:47 +02:00
2020-04-04 02:07:54 +02:00
/* Write the feedback */
if (Question->Answer.Options[NumOpt].Feedback)
if (Question->Answer.Options[NumOpt].Feedback[0])
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"<feedback>%s</feedback>%s",
2020-04-04 02:07:54 +02:00
Question->Answer.Options[NumOpt].Feedback,Txt_NEW_LINE);
2019-05-17 09:19:49 +02:00
/* End answer */
2020-03-17 00:35:11 +01:00
fprintf (FileXML,"</option>%s",
2014-12-01 23:55:08 +01:00
Txt_NEW_LINE);
}
break;
default:
break;
}
}
/*****************************************************************************/
/************ Get questions from XML and store them in database **************/
/*****************************************************************************/
void QstImp_ImpQstsFromXML (void)
2014-12-01 23:55:08 +01:00
{
2015-01-14 00:32:23 +01:00
extern const char *Txt_The_file_is_not_X;
struct Par_Param *Par;
2017-01-15 18:02:52 +01:00
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];
2016-03-28 19:30:37 +02:00
bool WrongType = false;
2014-12-01 23:55:08 +01:00
/***** Creates directory if not exists *****/
2019-03-20 01:36:36 +01:00
Fil_CreateDirIfNotExists (Cfg_PATH_TEST_PRIVATE);
2014-12-01 23:55:08 +01:00
/***** First of all, copy in disk the file received *****/
Par = Fil_StartReceptionOfFile (Fil_NAME_OF_PARAM_FILENAME_ORG,
FileNameXMLSrc,MIMEType);
2014-12-01 23:55:08 +01:00
2016-04-07 01:16:34 +02:00
/* Check if the file type is XML */
2014-12-01 23:55:08 +01:00
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"))
2016-03-28 19:30:37 +02:00
WrongType = true;
2014-12-01 23:55:08 +01:00
2016-03-28 19:30:37 +02:00
if (WrongType)
2019-02-16 19:29:27 +01:00
Ale_ShowAlert (Ale_WARNING,Txt_The_file_is_not_X,
"xml");
2016-03-28 19:30:37 +02:00
else
2014-12-01 23:55:08 +01:00
{
/* End the reception of XML in a temporary file */
snprintf (FileNameXMLTmp,sizeof (FileNameXMLTmp),"%s/%s.xml",
Cfg_PATH_TEST_PRIVATE,Cry_GetUniqueNameEncrypted ());
if (Fil_EndReceptionOfFile (FileNameXMLTmp,Par))
2014-12-01 23:55:08 +01:00
/***** Get questions from XML file and store them in database *****/
QstImp_ReadQuestionsFromXMLFileAndStoreInDB (FileNameXMLTmp);
2014-12-01 23:55:08 +01:00
else
2019-02-16 19:29:27 +01:00
Ale_ShowAlert (Ale_WARNING,"Error copying file.");
2014-12-01 23:55:08 +01:00
}
}
/*****************************************************************************/
/********** Get questions from XML file and store them in database ***********/
/*****************************************************************************/
static void QstImp_ReadQuestionsFromXMLFileAndStoreInDB (const char *FileNameXML)
2014-12-01 23:55:08 +01:00
{
2020-03-17 00:35:11 +01:00
FILE *FileXML;
2014-12-01 23:55:08 +01:00
char *XMLBuffer;
unsigned long FileSize;
/***** Open file *****/
2020-03-17 00:35:11 +01:00
if ((FileXML = fopen (FileNameXML,"rb")) == NULL)
Err_ShowErrorAndExit ("Can not open XML file.");
2014-12-01 23:55:08 +01:00
/***** Compute file size *****/
2020-03-17 00:35:11 +01:00
fseek (FileXML,0L,SEEK_END);
FileSize = (unsigned long) ftell (FileXML);
fseek (FileXML,0L,SEEK_SET);
2014-12-01 23:55:08 +01:00
/***** Allocate memory for XML buffer *****/
if ((XMLBuffer = malloc (FileSize + 1)) == NULL)
Err_NotEnoughMemoryExit ();
2014-12-01 23:55:08 +01:00
else
{
/***** Read file contents into XML buffer *****/
2020-03-17 00:35:11 +01:00
if (fread (XMLBuffer,sizeof (char),(size_t) FileSize,FileXML))
2014-12-01 23:55:08 +01:00
XMLBuffer[FileSize] = '\0';
else
XMLBuffer[0] = '\0';
/***** Import questions from XML buffer *****/
QstImp_ImportQuestionsFromXMLBuffer (XMLBuffer);
2014-12-01 23:55:08 +01:00
free (XMLBuffer);
}
/***** Close file *****/
2020-03-17 00:35:11 +01:00
fclose (FileXML);
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/******************** Import questions from XML buffer ***********************/
/*****************************************************************************/
static void QstImp_ImportQuestionsFromXMLBuffer (const char *XMLBuffer)
2014-12-01 23:55:08 +01:00
{
2016-11-13 20:18:49 +01:00
extern const char *Hlp_ASSESSMENT_Tests;
2014-12-01 23:55:08 +01:00
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;
struct Qst_Question Question;
2020-03-25 01:36:22 +01:00
bool QuestionExists;
bool AnswerTypeFound;
2014-12-01 23:55:08 +01:00
/***** Allocate and get XML tree *****/
XML_GetTree (XMLBuffer,&RootElem);
2019-10-26 02:19:42 +02:00
/***** Begin box *****/
Box_BoxBegin (Txt_Imported_questions,NULL,NULL,
2017-06-12 15:03:29 +02:00
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
2016-11-13 20:18:49 +01:00
/***** Print XML tree *****/
HTM_DIV_Begin ("class=\"Tst_FILE_CONTENT\"");
HTM_TEXTAREA_Begin ("title=\"%s\" cols=\"60\" rows=\"5\""
" spellcheck=\"false\" readonly",
Txt_XML_file_content);
XML_PrintTree (RootElem);
HTM_TEXTAREA_End ();
HTM_DIV_End ();
/***** 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> */
2016-11-13 20:18:49 +01:00
/***** Begin table *****/
HTM_TABLE_BeginWideMarginPadding (5);
2016-11-13 20:18:49 +01:00
/***** Write heading of list of imported questions *****/
QstImp_WriteHeadingListImportedQst ();
2016-11-13 20:18:49 +01:00
/***** For each question... *****/
for (QuestionElem = TestElem->FirstChild;
QuestionElem != NULL;
QuestionElem = QuestionElem->NextBrother)
2020-03-18 18:49:45 +01:00
{
if (!strcmp (QuestionElem->TagName,"question"))
{
/***** Create test question *****/
Qst_QstConstructor (&Question);
2020-03-18 18:49:45 +01:00
/* Get answer type (in mandatory attribute "type") */
AnswerTypeFound = false;
for (Attribute = QuestionElem->FirstAttribute;
Attribute != NULL;
Attribute = Attribute->Next)
if (!strcmp (Attribute->AttributeName,"type"))
2020-03-18 18:49:45 +01:00
{
Question.Answer.Type = QstImp_ConvertFromStrAnsTypXMLToAnsTyp (Attribute->Content);
AnswerTypeFound = true;
break; // Only first attribute "type"
2016-11-13 20:18:49 +01:00
}
if (AnswerTypeFound)
2016-11-13 20:18:49 +01:00
{
/* Get tags */
for (TagsElem = QuestionElem->FirstChild, Question.Tags.Num = 0;
TagsElem != NULL;
TagsElem = TagsElem->NextBrother)
if (!strcmp (TagsElem->TagName,"tags"))
{
for (TagElem = TagsElem->FirstChild;
TagElem != NULL && Question.Tags.Num < Tag_MAX_TAGS_PER_QUESTION;
TagElem = TagElem->NextBrother)
if (!strcmp (TagElem->TagName,"tag"))
{
if (TagElem->Content)
{
Str_Copy (Question.Tags.Txt[Question.Tags.Num],
TagElem->Content,
sizeof (Question.Tags.Txt[Question.Tags.Num]) - 1);
Question.Tags.Num++;
}
}
break; // Only first element "tags"
}
2016-11-13 20:18:49 +01:00
/* Get stem (mandatory) */
for (StemElem = QuestionElem->FirstChild;
StemElem != NULL;
StemElem = StemElem->NextBrother)
if (!strcmp (StemElem->TagName,"stem"))
{
if (StemElem->Content)
2020-03-18 18:49:45 +01:00
{
/* Convert stem from text to HTML (in database stem is stored in HTML) */
Str_Copy (Question.Stem,StemElem->Content,Cns_MAX_BYTES_TEXT);
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
Question.Stem,Cns_MAX_BYTES_TEXT,
Str_REMOVE_SPACES);
2020-03-18 18:49:45 +01:00
}
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 (Question.Feedback,FeedbackElem->Content,Cns_MAX_BYTES_TEXT);
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
Question.Feedback,Cns_MAX_BYTES_TEXT,
Str_REMOVE_SPACES);
}
break; // Only first element "feedback"
}
2016-11-13 20:18:49 +01:00
/* Get shuffle. By default, shuffle is false. */
Question.Answer.Shuffle = false;
for (AnswerElem = QuestionElem->FirstChild;
AnswerElem != NULL;
AnswerElem = AnswerElem->NextBrother)
if (!strcmp (AnswerElem->TagName,"answer"))
{
if (Question.Answer.Type == Qst_ANS_UNIQUE_CHOICE ||
Question.Answer.Type == Qst_ANS_MULTIPLE_CHOICE)
/* Get whether shuffle answers (in attribute "shuffle") */
for (Attribute = AnswerElem->FirstAttribute;
Attribute != NULL;
Attribute = Attribute->Next)
if (!strcmp (Attribute->AttributeName,"shuffle"))
{
Question.Answer.Shuffle = XML_GetAttributteYesNoFromXMLTree (Attribute);
break; // Only first attribute "shuffle"
}
break; // Only first element "answer"
}
2020-03-18 18:49:45 +01:00
/* Get answer (mandatory) */
QstImp_GetAnswerFromXML (AnswerElem,&Question);
2020-03-18 18:49:45 +01:00
/* Make sure that tags, text and answer are not empty */
if (Qst_CheckIfQstFormatIsCorrectAndCountNumOptions (&Question))
{
/* Check if question already exists in database */
QuestionExists = Qst_CheckIfQuestionExistsInDB (&Question);
2020-03-18 18:49:45 +01:00
/* Write row with this imported question */
QstImp_WriteRowImportedQst (StemElem,FeedbackElem,
&Question,QuestionExists);
/***** If a new question ==> insert question, tags and answer in the database *****/
if (!QuestionExists)
{
Question.QstCod = -1L;
Qst_InsertOrUpdateQstTagsAnsIntoDB (&Question);
if (Question.QstCod <= 0)
Err_ShowErrorAndExit ("Can not create question.");
}
}
2020-03-25 01:36:22 +01:00
}
else // Answer type not found
Err_WrongAnswerExit ();
/***** Destroy test question *****/
Qst_QstDestructor (&Question);
2016-11-13 20:18:49 +01:00
}
}
HTM_TABLE_End ();
2016-11-13 20:18:49 +01:00
}
else // TestElem not found
Ale_ShowAlert (Ale_ERROR,"Root element &lt;test&gt; not found.");
2014-12-01 23:55:08 +01:00
2016-11-13 20:18:49 +01:00
/***** End table *****/
2019-10-25 22:48:34 +02:00
Box_BoxEnd ();
2014-12-01 23:55:08 +01:00
/***** Free XML tree *****/
XML_FreeTree (RootElem);
}
/*****************************************************************************/
/***** Convert a string with the type of answer in XML to type of answer *****/
/*****************************************************************************/
static Qst_AnswerType_t QstImp_ConvertFromStrAnsTypXMLToAnsTyp (const char *StrAnsTypeXML)
2014-12-01 23:55:08 +01:00
{
extern const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES];
Qst_AnswerType_t AnsType;
2014-12-01 23:55:08 +01:00
if (StrAnsTypeXML != NULL)
for (AnsType = (Qst_AnswerType_t) 0;
AnsType <= (Qst_AnswerType_t) (Qst_NUM_ANS_TYPES - 1);
2014-12-01 23:55:08 +01:00
AnsType++)
// comparison must be case insensitive, because users can edit XML
if (!strcasecmp (StrAnsTypeXML,Qst_StrAnswerTypesXML[AnsType]))
2014-12-01 23:55:08 +01:00
return AnsType;
Err_WrongAnswerExit ();
return (Qst_AnswerType_t) 0; // Not reached
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/**************** Get answer inside an XML question elements *****************/
/*****************************************************************************/
// Answer is mandatory
static void QstImp_GetAnswerFromXML (struct XMLElement *AnswerElem,
struct Qst_Question *Question)
2014-12-01 23:55:08 +01:00
{
struct XMLElement *OptionElem;
struct XMLElement *TextElem;
struct XMLElement *FeedbackElem;
struct XMLElement *LowerUpperElem;
struct XMLAttribute *Attribute;
unsigned NumOpt;
2020-03-19 20:57:54 +01:00
switch (Question->Answer.Type)
2014-12-01 23:55:08 +01:00
{
case Qst_ANS_INT:
if (!Qst_AllocateTextChoiceAnswer (Question,0))
2019-03-09 20:12:44 +01:00
/* Abort on error */
Ale_ShowAlertsAndExit ();
2014-12-01 23:55:08 +01:00
if (AnswerElem->Content)
2020-03-19 20:57:54 +01:00
Str_Copy (Question->Answer.Options[0].Text,AnswerElem->Content,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK);
2014-12-01 23:55:08 +01:00
break;
case Qst_ANS_FLOAT:
if (!Qst_AllocateTextChoiceAnswer (Question,0))
2019-03-09 20:12:44 +01:00
/* Abort on error */
Ale_ShowAlertsAndExit ();
if (!Qst_AllocateTextChoiceAnswer (Question,1))
2019-03-09 20:12:44 +01:00
/* Abort on error */
Ale_ShowAlertsAndExit ();
2014-12-01 23:55:08 +01:00
for (LowerUpperElem = AnswerElem->FirstChild;
LowerUpperElem != NULL;
LowerUpperElem = LowerUpperElem->NextBrother)
if (!strcmp (LowerUpperElem->TagName,"lower"))
{
if (LowerUpperElem->Content)
2020-03-19 20:57:54 +01:00
Str_Copy (Question->Answer.Options[0].Text,
2017-01-17 03:10:43 +01:00
LowerUpperElem->Content,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK);
2014-12-01 23:55:08 +01:00
break; // Only first element "lower"
}
for (LowerUpperElem = AnswerElem->FirstChild;
LowerUpperElem != NULL;
LowerUpperElem = LowerUpperElem->NextBrother)
if (!strcmp (LowerUpperElem->TagName,"upper"))
{
if (LowerUpperElem->Content)
2020-03-19 20:57:54 +01:00
Str_Copy (Question->Answer.Options[1].Text,
2017-01-17 03:10:43 +01:00
LowerUpperElem->Content,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK);
2014-12-01 23:55:08 +01:00
break; // Only first element "upper"
}
break;
case Qst_ANS_TRUE_FALSE:
2014-12-01 23:55:08 +01:00
// Comparisons must be case insensitive, because users can edit XML
if (!AnswerElem->Content)
2020-03-19 20:57:54 +01:00
Question->Answer.TF = ' ';
2014-12-01 23:55:08 +01:00
else if (!strcasecmp (AnswerElem->Content,"true") ||
!strcasecmp (AnswerElem->Content,"T") ||
!strcasecmp (AnswerElem->Content,"yes") ||
!strcasecmp (AnswerElem->Content,"Y"))
2020-03-19 20:57:54 +01:00
Question->Answer.TF = 'T';
2014-12-01 23:55:08 +01:00
else if (!strcasecmp (AnswerElem->Content,"false") ||
!strcasecmp (AnswerElem->Content,"F") ||
!strcasecmp (AnswerElem->Content,"no") ||
!strcasecmp (AnswerElem->Content,"N"))
2020-03-19 20:57:54 +01:00
Question->Answer.TF = 'F';
2014-12-01 23:55:08 +01:00
else
2020-03-19 20:57:54 +01:00
Question->Answer.TF = ' ';
2014-12-01 23:55:08 +01:00
break;
case Qst_ANS_UNIQUE_CHOICE:
case Qst_ANS_MULTIPLE_CHOICE:
case Qst_ANS_TEXT:
2014-12-01 23:55:08 +01:00
/* Get options */
for (OptionElem = AnswerElem->FirstChild, NumOpt = 0;
OptionElem != NULL && NumOpt < Qst_MAX_OPTIONS_PER_QUESTION;
2014-12-01 23:55:08 +01:00
OptionElem = OptionElem->NextBrother, NumOpt++)
if (!strcmp (OptionElem->TagName,"option"))
{
if (!Qst_AllocateTextChoiceAnswer (Question,NumOpt))
2019-03-09 20:12:44 +01:00
/* Abort on error */
Ale_ShowAlertsAndExit ();
2014-12-01 23:55:08 +01:00
for (TextElem = OptionElem->FirstChild;
TextElem != NULL;
TextElem = TextElem->NextBrother)
if (!strcmp (TextElem->TagName,"text"))
{
if (TextElem->Content)
{
2020-03-19 20:57:54 +01:00
Str_Copy (Question->Answer.Options[NumOpt].Text,
2017-01-15 18:02:52 +01:00
TextElem->Content,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK);
2014-12-01 23:55:08 +01:00
/* Convert answer from text to HTML (in database answer text is stored in HTML) */
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
2020-03-19 20:57:54 +01:00
Question->Answer.Options[NumOpt].Text,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK,
Str_REMOVE_SPACES);
2014-12-01 23:55:08 +01:00
}
break; // Only first element "text"
}
for (FeedbackElem = OptionElem->FirstChild;
FeedbackElem != NULL;
FeedbackElem = FeedbackElem->NextBrother)
if (!strcmp (FeedbackElem->TagName,"feedback"))
{
if (FeedbackElem->Content)
{
2020-03-19 20:57:54 +01:00
Str_Copy (Question->Answer.Options[NumOpt].Feedback,
2017-01-15 18:02:52 +01:00
FeedbackElem->Content,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK);
2014-12-01 23:55:08 +01:00
/* Convert feedback from text to HTML (in database answer feedback is stored in HTML) */
Str_ChangeFormat (Str_FROM_TEXT,Str_TO_HTML,
2020-03-19 20:57:54 +01:00
Question->Answer.Options[NumOpt].Feedback,
Qst_MAX_BYTES_ANSWER_OR_FEEDBACK,
Str_REMOVE_SPACES);
2014-12-01 23:55:08 +01:00
}
break; // Only first element "feedback"
}
if (Question->Answer.Type == Qst_ANS_TEXT)
2020-03-19 20:57:54 +01:00
Question->Answer.Options[NumOpt].Correct = true;
2014-12-01 23:55:08 +01:00
else
/* Check if option is correct or wrong */
for (Attribute = OptionElem->FirstAttribute;
2014-12-01 23:55:08 +01:00
Attribute != NULL;
Attribute = Attribute->Next)
2014-12-01 23:55:08 +01:00
if (!strcmp (Attribute->AttributeName,"correct"))
{
2020-03-19 20:57:54 +01:00
Question->Answer.Options[NumOpt].Correct = XML_GetAttributteYesNoFromXMLTree (Attribute);
2014-12-01 23:55:08 +01:00
break; // Only first attribute "correct"
}
}
break;
default:
break;
}
}
/*****************************************************************************/
/************* Write heading of list of imported test questions **************/
/*****************************************************************************/
static void QstImp_WriteHeadingListImportedQst (void)
2014-12-01 23:55:08 +01:00
{
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 *****/
2019-10-23 19:05:05 +02:00
HTM_TR_Begin (NULL);
HTM_TH_Empty (1);
HTM_TH (Txt_No_INDEX,HTM_HEAD_CENTER);
HTM_TH (Txt_Tags ,HTM_HEAD_CENTER);
HTM_TH (Txt_Type ,HTM_HEAD_CENTER);
HTM_TH (Txt_Shuffle ,HTM_HEAD_CENTER);
HTM_TH (Txt_Question,HTM_HEAD_LEFT );
2019-10-23 19:05:05 +02:00
HTM_TR_End ();
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/**************** Write a row with one imported test question ****************/
/*****************************************************************************/
static void QstImp_WriteRowImportedQst (struct XMLElement *StemElem,
struct XMLElement *FeedbackElem,
const struct Qst_Question *Question,
bool QuestionExists)
2014-12-01 23:55:08 +01:00
{
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[Qst_NUM_ANS_TYPES];
2018-12-09 13:11:20 +01:00
extern const char *Txt_TST_Answer_given_by_the_teachers;
2014-12-01 23:55:08 +01:00
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 ? "Qst_TXT_LIGHT" :
"Qst_TXT";
2014-12-01 23:55:08 +01:00
NumQst++;
2019-10-23 19:05:05 +02:00
HTM_TR_Begin (NULL);
2019-10-10 10:41:00 +02:00
/***** Put icon to indicate that a question does not exist in database *****/
HTM_TD_Begin ("class=\"BT %s\"",The_GetColorRows ());
if (QuestionExists)
Ico_PutIcon ("tr16x16.gif" ,Ico_UNCHANGED,
Txt_Existing_question,"CONTEXT_ICO16x16");
else
Ico_PutIcon ("check-circle.svg",Ico_GREEN ,
Txt_New_question ,"CONTEXT_ICO16x16");
HTM_TD_End ();
/***** Write number of question *****/
HTM_TD_Begin ("class=\"CT %s_%s %s\"",
ClassData,The_GetSuffix (),
The_GetColorRows ());
if (!QuestionExists)
HTM_TxtF ("%u&nbsp;",++NumNonExistingQst);
HTM_TD_End ();
2019-10-07 21:15:14 +02:00
/***** Write the question tags *****/
HTM_TD_Begin ("class=\"LT %s\"",
The_GetColorRows ());
2019-10-07 21:15:14 +02:00
if (Question->Tags.Num)
{
/***** Write the tags *****/
HTM_TABLE_Begin (NULL);
for (NumTag = 0;
NumTag < Question->Tags.Num;
NumTag++)
{
HTM_TR_Begin (NULL);
2019-10-07 21:15:14 +02:00
HTM_TD_Begin ("class=\"%s LT\"",ClassData);
HTM_TxtF ("&nbsp;%s&nbsp;","&#8226;");
HTM_TD_End ();
2014-12-01 23:55:08 +01:00
HTM_TD_Begin ("class=\"%s LT\"",ClassData);
HTM_Txt (Question->Tags.Txt[NumTag]);
HTM_TD_End ();
HTM_TR_End ();
2014-12-01 23:55:08 +01:00
}
HTM_TABLE_End ();
}
else // no tags for this question
{
HTM_SPAN_Begin ("class=\"%s\"",ClassData);
HTM_TxtF ("&nbsp;(%s)&nbsp;",Txt_no_tags);
HTM_SPAN_End ();
}
2014-12-01 23:55:08 +01:00
HTM_TD_End ();
/***** Write the question type *****/
HTM_TD_Begin ("class=\"%s CT %s\"",ClassData,The_GetColorRows ());
HTM_TxtF ("%s&nbsp;",Txt_TST_STR_ANSWER_TYPES[Question->Answer.Type]);
HTM_TD_End ();
/***** Write if shuffle is enabled *****/
HTM_TD_Begin ("class=\"CT %s\"",The_GetColorRows ());
if (Question->Answer.Type == Qst_ANS_UNIQUE_CHOICE ||
Question->Answer.Type == Qst_ANS_MULTIPLE_CHOICE)
/* Put an icon that indicates whether shuffle is enabled or not */
if (Question->Answer.Shuffle)
Ico_PutIcon ("check.svg",Ico_BLACK,
Txt_TST_Answer_given_by_the_teachers,
QuestionExists ? "ICO_HIDDEN ICO16x16" :
"ICO16x16");
HTM_TD_End ();
/***** Write the stem and the answers *****/
HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ());
Qst_WriteQstStem (Stem,ClassStem,
true); // Visible
Qst_WriteQstFeedback (Feedback,"Qst_TXT_LIGHT");
switch (Question->Answer.Type)
{
case Qst_ANS_INT:
HTM_SPAN_Begin ("class=\"%s\"",ClassStem);
HTM_TxtF ("(%ld)",Question->Answer.Integer);
HTM_SPAN_End ();
break;
case Qst_ANS_FLOAT:
HTM_SPAN_Begin ("class=\"%s\"",ClassStem);
HTM_Txt ("([");
HTM_Double (Question->Answer.FloatingPoint[0]);
HTM_Txt ("; ");
HTM_Double (Question->Answer.FloatingPoint[1]);
HTM_Txt ("])");
HTM_SPAN_End ();
break;
case Qst_ANS_TRUE_FALSE:
HTM_SPAN_Begin ("class=\"%s\"",ClassStem);
HTM_Txt ("(");
Qst_WriteAnsTF (Question->Answer.TF);
HTM_Txt (")");
HTM_SPAN_End ();
break;
case Qst_ANS_UNIQUE_CHOICE:
case Qst_ANS_MULTIPLE_CHOICE:
case Qst_ANS_TEXT:
HTM_TABLE_Begin (NULL);
for (NumOpt = 0;
NumOpt < Question->Answer.NumOptions;
NumOpt++)
{
/* Convert the answer, that is in HTML, to rigorous HTML */
AnswerTextLength = strlen (Question->Answer.Options[NumOpt].Text) *
Str_MAX_BYTES_PER_CHAR;
if ((AnswerText = malloc (AnswerTextLength + 1)) == NULL)
Err_NotEnoughMemoryExit ();
Str_Copy (AnswerText,Question->Answer.Options[NumOpt].Text,
AnswerTextLength);
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
AnswerText,AnswerTextLength,
Str_DONT_REMOVE_SPACES);
/* Convert the feedback, that is in HTML, to rigorous HTML */
AnswerFeedbackLength = 0;
AnswerFeedback = NULL;
if (Question->Answer.Options[NumOpt].Feedback)
if (Question->Answer.Options[NumOpt].Feedback[0])
{
AnswerFeedbackLength = strlen (Question->Answer.Options[NumOpt].Feedback) *
Str_MAX_BYTES_PER_CHAR;
if ((AnswerFeedback = malloc (AnswerFeedbackLength + 1)) == NULL)
Err_NotEnoughMemoryExit ();
Str_Copy (AnswerFeedback,
Question->Answer.Options[NumOpt].Feedback,
AnswerFeedbackLength);
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
AnswerFeedback,AnswerFeedbackLength,
Str_DONT_REMOVE_SPACES);
}
2019-10-10 10:41:00 +02:00
HTM_TR_Begin (NULL);
2014-12-01 23:55:08 +01:00
/* Put an icon that indicates whether the answer is correct or wrong */
HTM_TD_Begin ("class=\"BT %s\"",The_GetColorRows ());
if (Question->Answer.Options[NumOpt].Correct)
Ico_PutIcon ("check.svg",Ico_BLACK,
Txt_TST_Answer_given_by_the_teachers,
QuestionExists ? "ICO_HIDDEN CONTEXT_ICO16x16" :
"CONTEXT_ICO16x16");
HTM_TD_End ();
2014-12-01 23:55:08 +01:00
/* Write the number of option */
HTM_TD_Begin ("class=\"%s LT\"",ClassData);
HTM_TxtF ("%c)&nbsp;",'a' + (char) NumOpt);
HTM_TD_End ();
2019-10-24 00:04:40 +02:00
/* Write the text and the feedback of the answer */
HTM_TD_Begin ("class=\"LT\"");
2019-10-24 00:04:40 +02:00
HTM_DIV_Begin ("class=\"%s_%s\"",
ClassStem,The_GetSuffix ());
HTM_Txt (AnswerText);
HTM_DIV_End ();
2019-10-24 00:04:40 +02:00
if (AnswerFeedbackLength)
{
HTM_DIV_Begin ("class=\"Qst_TXT_LIGHT_%s\"",
The_GetSuffix ());
HTM_Txt (AnswerFeedback);
HTM_DIV_End ();
}
2019-10-10 10:41:00 +02:00
HTM_TD_End ();
HTM_TR_End ();
/* Free memory allocated for the answer and the feedback */
free (AnswerText);
if (AnswerFeedbackLength)
free (AnswerFeedback);
}
HTM_TABLE_End ();
break;
default:
break;
}
HTM_TD_End ();
2014-12-01 23:55:08 +01:00
2019-10-23 19:05:05 +02:00
HTM_TR_End ();
The_ChangeRowColor ();
2014-12-01 23:55:08 +01:00
}