// swad_test.c: self-assessment tests
/*
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 .
*/
/*****************************************************************************/
/*********************************** Headers *********************************/
/*****************************************************************************/
#define _GNU_SOURCE // For asprintf
#include // For UINT_MAX
#include // For PATH_MAX
#include // For NULL
#include // To access MySQL databases
#include // For boolean type
#include // For fprintf, asprintf, etc.
#include // For exit, system, malloc, free, etc
#include // For string functions
#include // For mkdir
#include // For mkdir
#include "swad_action.h"
#include "swad_box.h"
#include "swad_database.h"
#include "swad_form.h"
#include "swad_global.h"
#include "swad_HTML.h"
#include "swad_ID.h"
#include "swad_language.h"
#include "swad_match.h"
#include "swad_media.h"
#include "swad_parameter.h"
#include "swad_theme.h"
#include "swad_test.h"
#include "swad_test_import.h"
#include "swad_user.h"
#include "swad_xml.h"
/*****************************************************************************/
/***************************** Public constants ******************************/
/*****************************************************************************/
// strings are limited to Tst_MAX_BYTES_FEEDBACK_TYPE bytes
const char *Tst_FeedbackXML[Tst_NUM_TYPES_FEEDBACK] =
{
"nothing",
"totalResult",
"eachResult",
"eachGoodBad",
"fullFeedback",
};
// strings are limited to Tst_MAX_BYTES_ANSWER_TYPE characters
const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES] =
{
"int",
"float",
"TF",
"uniqueChoice",
"multipleChoice",
"text",
};
/*****************************************************************************/
/**************************** Private constants ******************************/
/*****************************************************************************/
#define Tst_MAX_BYTES_TAGS_LIST (16 * 1024)
#define Tst_MAX_BYTES_FLOAT_ANSWER 30 // Maximum length of the strings that store an floating point answer
const char *Tst_PluggableDB[Tst_NUM_OPTIONS_PLUGGABLE] =
{
"unknown",
"N",
"Y",
};
// Feedback to students in tests
const char *Tst_FeedbackDB[Tst_NUM_TYPES_FEEDBACK] =
{
"nothing", // No feedback
"total_result", // Little
"each_result", // Medium
"each_good_bad", // High
"full_feedback", // Maximum
};
const char *Tst_StrAnswerTypesDB[Tst_NUM_ANS_TYPES] =
{
"int",
"float",
"true_false",
"unique_choice",
"multiple_choice",
"text",
};
// Test images will be saved with:
// - maximum width of Tst_IMAGE_SAVED_MAX_HEIGHT
// - maximum height of Tst_IMAGE_SAVED_MAX_HEIGHT
// - maintaining the original aspect ratio (aspect ratio recommended: 3:2)
#define Tst_IMAGE_SAVED_MAX_WIDTH 768
#define Tst_IMAGE_SAVED_MAX_HEIGHT 512
#define Tst_IMAGE_SAVED_QUALITY 75 // 1 to 100
/*****************************************************************************/
/******************************* Internal types ******************************/
/*****************************************************************************/
#define Tst_NUM_STATUS 2
typedef enum
{
Tst_STATUS_SHOWN_BUT_NOT_ASSESSED = 0,
Tst_STATUS_ASSESSED = 1,
Tst_STATUS_ERROR = 2,
} Tst_Status_t;
/*****************************************************************************/
/************** External global variables from others modules ****************/
/*****************************************************************************/
extern struct Globals Gbl;
/*****************************************************************************/
/************************* Internal global variables *************************/
/*****************************************************************************/
/*****************************************************************************/
/***************************** Internal prototypes ***************************/
/*****************************************************************************/
static void Tst_PutFormToViewTstResults (Act_Action_t Action);
static void Tst_GetQuestionsAndAnswersFromForm (void);
static bool Tst_CheckIfNextTstAllowed (void);
static void Tst_SetTstStatus (unsigned NumTst,Tst_Status_t TstStatus);
static Tst_Status_t Tst_GetTstStatus (unsigned NumTst);
static unsigned Tst_GetNumAccessesTst (void);
static void Tst_ShowTestQuestionsWhenSeeing (MYSQL_RES *mysql_res);
static void Tst_ShowTestResultAfterAssess (long TstCod,unsigned *NumQstsNotBlank,double *TotalScore);
static void Tst_WriteQstAndAnsTest (Tst_ActionToDoWithQuestions_t ActionToDoWithQuestions,
struct UsrData *UsrDat,
unsigned NumQst,long QstCod,MYSQL_ROW row,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_PutFormToEditQstMedia (struct Media *Media,int NumMediaInForm,
bool OptionsDisabled);
static void Tst_UpdateScoreQst (long QstCod,float ScoreThisQst,bool AnswerIsNotBlank);
static void Tst_UpdateMyNumAccessTst (unsigned NumAccessesTst);
static void Tst_UpdateLastAccTst (void);
static bool Tst_CheckIfICanEditTests (void);
static void Tst_PutIconsTests (void);
static void Tst_PutButtonToAddQuestion (void);
static long Tst_GetParamTagCode (void);
static bool Tst_CheckIfCurrentCrsHasTestTags (void);
static unsigned long Tst_GetAllTagsFromCurrentCrs (MYSQL_RES **mysql_res);
static unsigned long Tst_GetEnabledTagsFromThisCrs (MYSQL_RES **mysql_res);
static void Tst_ShowFormSelTags (unsigned long NumRows,MYSQL_RES *mysql_res,
bool ShowOnlyEnabledTags,unsigned NumCols);
static void Tst_ShowFormEditTags (void);
static void Tst_PutIconEnable (long TagCod,const char *TagTxt);
static void Tst_PutIconDisable (long TagCod,const char *TagTxt);
static void Tst_ShowFormConfigTst (void);
static void Tst_PutInputFieldNumQst (const char *Field,const char *Label,
unsigned Value);
static Tst_Pluggable_t Tst_GetPluggableFromForm (void);
static Tst_Feedback_t Tst_GetFeedbackTypeFromForm (void);
static void Tst_CheckAndCorrectNumbersQst (void);
static void Tst_ShowFormAnswerTypes (unsigned NumCols);
static unsigned long Tst_GetQuestions (MYSQL_RES **mysql_res);
static unsigned long Tst_GetQuestionsForTest (MYSQL_RES **mysql_res);
static void Tst_ListOneQstToEdit (void);
static void Tst_ListOneOrMoreQuestionsForEdition (unsigned long NumRows,
MYSQL_RES *mysql_res);
static void Tst_ListOneOrMoreQuestionsForSelection (unsigned long NumRows,
MYSQL_RES *mysql_res);
static void Tst_WriteAnswersTestToAnswer (unsigned NumQst,long QstCod,bool Shuffle);
static void Tst_WriteAnswersTestResult (struct UsrData *UsrDat,
unsigned NumQst,long QstCod,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_WriteTFAnsViewTest (unsigned NumQst);
static void Tst_WriteTFAnsAssessTest (struct UsrData *UsrDat,
unsigned NumQst,MYSQL_RES *mysql_res,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_WriteChoiceAnsViewTest (unsigned NumQst,long QstCod,bool Shuffle);
static void Tst_WriteChoiceAnsAssessTest (struct UsrData *UsrDat,
unsigned NumQst,MYSQL_RES *mysql_res,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_WriteTextAnsViewTest (unsigned NumQst);
static void Tst_WriteTextAnsAssessTest (struct UsrData *UsrDat,
unsigned NumQst,MYSQL_RES *mysql_res,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_WriteIntAnsViewTest (unsigned NumQst);
static void Tst_WriteIntAnsAssessTest (struct UsrData *UsrDat,
unsigned NumQst,MYSQL_RES *mysql_res,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_WriteFloatAnsViewTest (unsigned NumQst);
static void Tst_WriteFloatAnsAssessTest (struct UsrData *UsrDat,
unsigned NumQst,MYSQL_RES *mysql_res,
double *ScoreThisQst,bool *AnswerIsNotBlank);
static void Tst_WriteHeadUserCorrect (struct UsrData *UsrDat);
static void Tst_WriteScoreStart (unsigned ColSpan);
static void Tst_WriteScoreEnd (void);
static void Tst_WriteParamQstCod (unsigned NumQst,long QstCod);
static bool Tst_GetParamsTst (Tst_ActionToDoWithQuestions_t ActionToDoWithQuestions);
static unsigned Tst_GetAndCheckParamNumTst (void);
static void Tst_GetParamNumQst (void);
static bool Tst_GetCreateXMLFromForm (void);
static int Tst_CountNumTagsInList (void);
static int Tst_CountNumAnswerTypesInList (void);
static void Tst_PutFormEditOneQst (char Stem[Cns_MAX_BYTES_TEXT + 1],
char Feedback[Cns_MAX_BYTES_TEXT + 1]);
static void Tst_PutFloatInputField (const char *Label,const char *Field,
double Value);
static void Tst_PutTFInputField (const char *Label,char Value);
static void Tst_FreeTextChoiceAnswers (void);
static void Tst_FreeTextChoiceAnswer (unsigned NumOpt);
static void Tst_ResetMediaOfQuestion (void);
static void Tst_FreeMediaOfQuestion (void);
static void Tst_GetQstDataFromDB (char Stem[Cns_MAX_BYTES_TEXT + 1],
char Feedback[Cns_MAX_BYTES_TEXT + 1]);
static long Tst_GetMedCodFromDB (int NumOpt);
static void Tst_GetMediaFromDB (int NumOpt,struct Media *Media);
static Tst_AnswerType_t Tst_ConvertFromUnsignedStrToAnsTyp (const char *UnsignedStr);
static void Tst_GetQstFromForm (char *Stem,char *Feedback);
static void Tst_MoveMediaToDefinitiveDirectories (void);
static long Tst_GetTagCodFromTagTxt (const char *TagTxt);
static long Tst_CreateNewTag (long CrsCod,const char *TagTxt);
static void Tst_EnableOrDisableTag (long TagCod,bool TagHidden);
static void Tst_PutIconToRemoveOneQst (void);
static void Tst_PutParamsRemoveOneQst (void);
static void Tst_PutParamsRemoveQst (void);
static long Tst_GetQstCod (void);
static void Tst_InsertOrUpdateQstIntoDB (void);
static void Tst_InsertTagsIntoDB (void);
static void Tst_InsertAnswersIntoDB (void);
static void Tst_RemAnsFromQst (void);
static void Tst_RemTagsFromQst (void);
static void Tst_RemoveUnusedTagsFromCurrentCrs (void);
static void Tst_RemoveAllMedFilesFromStemOfAllQstsInCrs (long CrsCod);
static void Tst_RemoveMediaFromAllAnsOfQst (long CrsCod,long QstCod);
static void Tst_RemoveAllMedFilesFromAnsOfAllQstsInCrs (long CrsCod);
static unsigned Tst_GetNumTstQuestions (Hie_Level_t Scope,Tst_AnswerType_t AnsType,struct Tst_Stats *Stats);
static unsigned Tst_GetNumCoursesWithTstQuestions (Hie_Level_t Scope,Tst_AnswerType_t AnsType);
static unsigned Tst_GetNumCoursesWithPluggableTstQuestions (Hie_Level_t Scope,Tst_AnswerType_t AnsType);
static long Tst_CreateTestResultInDB (void);
static void Tst_StoreScoreOfTestResultInDB (long TstCod,
unsigned NumQstsNotBlank,double Score);
static void Tst_ShowHeaderTestResults (void);
static void Tst_ShowTstResults (struct UsrData *UsrDat);
static void Tst_PutParamTstCod (long TstCod);
static long Tst_GetParamTstCod (void);
static void Tst_ShowTestResultsSummaryRow (bool ItsMe,
unsigned NumExams,
unsigned NumTotalQsts,
unsigned NumTotalQstsNotBlank,
double TotalScoreOfAllTests);
static void Tst_GetTestResultDataByTstCod (long TstCod,time_t *TstTimeUTC,
unsigned *NumQstsNotBlank,double *Score);
static void Tst_StoreOneTestResultQstInDB (long TstCod,long QstCod,unsigned NumQst,double Score);
static void Tst_GetTestResultQuestionsFromDB (long TstCod);
/*****************************************************************************/
/*************** Show form to generate a self-assessment test ****************/
/*****************************************************************************/
void Tst_ShowFormAskTst (void)
{
extern const char *Hlp_ASSESSMENT_Tests;
extern const char *The_ClassFormInBox[The_NUM_THEMES];
extern const char *Txt_Take_a_test;
extern const char *Txt_No_of_questions;
extern const char *Txt_Generate_test;
extern const char *Txt_No_test_questions;
MYSQL_RES *mysql_res;
unsigned long NumRows;
/***** Read test configuration from database *****/
Tst_GetConfigTstFromDB ();
/***** Put link to view tests results *****/
switch (Gbl.Usrs.Me.Role.Logged)
{
case Rol_STD:
Tst_PutFormToViewTstResults (ActReqSeeMyTstRes);
break;
case Rol_NET:
case Rol_TCH:
case Rol_SYS_ADM:
Tst_PutFormToViewTstResults (ActReqSeeUsrTstRes);
break;
default:
break;
}
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Take_a_test,Tst_PutIconsTests,
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
/***** Get tags *****/
if ((NumRows = Tst_GetEnabledTagsFromThisCrs (&mysql_res)) != 0)
{
/***** Check if minimum date-time of next access to test is older than now *****/
if (Tst_CheckIfNextTstAllowed ())
{
Frm_StartForm (ActSeeTst);
HTM_TABLE_BeginPadding (2);
/***** Selection of tags *****/
Tst_ShowFormSelTags (NumRows,mysql_res,true,1);
/***** Selection of types of answers *****/
Tst_ShowFormAnswerTypes (1);
/***** Number of questions to generate ****/
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"RM\"");
HTM_LABEL_Begin ("for=\"NumQst\" class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_No_of_questions);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TD_Begin ("class=\"LM\"");
HTM_INPUT_NUMBER ("NumQst",
(long) Gbl.Test.Config.Min,
(long) Gbl.Test.Config.Max,
(long) Gbl.Test.Config.Def,
Gbl.Test.Config.Min == Gbl.Test.Config.Max);
HTM_TD_End ();
HTM_TR_End ();
HTM_TABLE_End ();
/***** Send button *****/
Btn_PutConfirmButton (Txt_Generate_test);
Frm_EndForm ();
}
}
else
{
/***** Warning message *****/
Ale_ShowAlert (Ale_INFO,Txt_No_test_questions);
/***** Button to create a new question *****/
if (Tst_CheckIfICanEditTests ())
Tst_PutButtonToAddQuestion ();
}
/***** End box *****/
Box_BoxEnd ();
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/*************** Write a form to go to result of users' tests ****************/
/*****************************************************************************/
static void Tst_PutFormToViewTstResults (Act_Action_t Action)
{
extern const char *Txt_Results;
/***** Contextual menu *****/
Mnu_ContextMenuBegin ();
Lay_PutContextualLinkIconText (Action,NULL,NULL,
"tasks.svg",
Txt_Results); // Tests results
Mnu_ContextMenuEnd ();
}
/*****************************************************************************/
/********************** Generate self-assessment test ************************/
/*****************************************************************************/
void Tst_ShowNewTest (void)
{
extern const char *Hlp_ASSESSMENT_Tests;
extern const char *The_ClassFormInBox[The_NUM_THEMES];
extern const char *Txt_No_questions_found_matching_your_search_criteria;
extern const char *Txt_Test;
extern const char *Txt_Allow_teachers_to_consult_this_test;
extern const char *Txt_Done_assess_test;
MYSQL_RES *mysql_res;
unsigned long NumRows;
unsigned NumAccessesTst;
/***** Read test configuration from database *****/
Tst_GetConfigTstFromDB ();
if (Tst_CheckIfNextTstAllowed ())
{
/***** Check that all parameters used to generate a test are valid *****/
if (Tst_GetParamsTst (Tst_SHOW_TEST_TO_ANSWER)) // Get parameters from form
{
/***** Get questions *****/
if ((NumRows = Tst_GetQuestionsForTest (&mysql_res)) == 0) // Query database
{
Ale_ShowAlert (Ale_INFO,Txt_No_questions_found_matching_your_search_criteria);
Tst_ShowFormAskTst (); // Show the form again
}
else
{
/***** Get and update number of hits *****/
NumAccessesTst = Tst_GetNumAccessesTst () + 1;
if (Gbl.Usrs.Me.IBelongToCurrentCrs)
Tst_UpdateMyNumAccessTst (NumAccessesTst);
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Test,NULL,
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
Lay_WriteHeaderClassPhoto (false,false,
Gbl.Hierarchy.Ins.InsCod,
Gbl.Hierarchy.Deg.DegCod,
Gbl.Hierarchy.Crs.CrsCod);
/***** Begin form *****/
Frm_StartForm (ActAssTst);
Gbl.Test.NumQsts = (unsigned) NumRows;
Par_PutHiddenParamUnsigned (NULL,"NumTst",NumAccessesTst);
Par_PutHiddenParamUnsigned (NULL,"NumQst",Gbl.Test.NumQsts);
/***** List the questions *****/
HTM_TABLE_BeginWideMarginPadding (10);
Tst_ShowTestQuestionsWhenSeeing (mysql_res);
HTM_TABLE_End ();
/***** Test result will be saved? *****/
HTM_DIV_Begin ("class=\"CM\"");
HTM_LABEL_Begin ("class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
HTM_INPUT_CHECKBOX ("Save",false,
"value=\"Y\"%s",
Gbl.Test.AllowTeachers ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out," %s",Txt_Allow_teachers_to_consult_this_test);
HTM_LABEL_End ();
HTM_DIV_End ();
/***** End form *****/
Btn_PutConfirmButton (Txt_Done_assess_test);
Frm_EndForm ();
/***** End box *****/
Box_BoxEnd ();
/***** Set test status *****/
Tst_SetTstStatus (NumAccessesTst,Tst_STATUS_SHOWN_BUT_NOT_ASSESSED);
/***** Update date-time of my next allowed access to test *****/
if (Gbl.Usrs.Me.Role.Logged == Rol_STD)
Tst_UpdateLastAccTst ();
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
else
Tst_ShowFormAskTst (); // Show the form again
/***** Free memory used for by the list of tags *****/
Tst_FreeTagsList ();
}
}
/*****************************************************************************/
/******************************** Assess a test ******************************/
/*****************************************************************************/
void Tst_AssessTest (void)
{
extern const char *Hlp_ASSESSMENT_Tests;
extern const char *Txt_Test_result;
extern const char *Txt_Test_No_X_that_you_make_in_this_course;
extern const char *Txt_The_test_X_has_already_been_assessed_previously;
extern const char *Txt_There_was_an_error_in_assessing_the_test_X;
unsigned NumTst;
long TstCod = -1L; // Initialized to avoid warning
unsigned NumQstsNotBlank;
double TotalScore;
/***** Read test configuration from database *****/
Tst_GetConfigTstFromDB ();
/***** Get number of this test from form *****/
NumTst = Tst_GetAndCheckParamNumTst ();
/****** Get test status in database for this session-course-num.test *****/
switch (Tst_GetTstStatus (NumTst))
{
case Tst_STATUS_SHOWN_BUT_NOT_ASSESSED:
/***** Get the parameters of the form *****/
/* Get number of questions */
Tst_GetParamNumQst ();
/***** Get if test must be saved *****/
Gbl.Test.AllowTeachers = Par_GetParToBool ("Save");
/***** Get questions and answers from form to assess a test *****/
Tst_GetQuestionsAndAnswersFromForm ();
/***** Create new test in database to store the result *****/
TstCod = Tst_CreateTestResultInDB ();
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Test_result,NULL,
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
Lay_WriteHeaderClassPhoto (false,false,
Gbl.Hierarchy.Ins.InsCod,
Gbl.Hierarchy.Deg.DegCod,
Gbl.Hierarchy.Crs.CrsCod);
/***** Header *****/
if (Gbl.Usrs.Me.IBelongToCurrentCrs)
{
HTM_DIV_Begin ("class=\"TEST_SUBTITLE\"");
fprintf (Gbl.F.Out,Txt_Test_No_X_that_you_make_in_this_course,NumTst);
HTM_DIV_End ();
}
/***** Write answers and solutions *****/
HTM_TABLE_BeginWideMarginPadding (10);
Tst_ShowTestResultAfterAssess (TstCod,&NumQstsNotBlank,&TotalScore);
HTM_TABLE_End ();
/***** Write total mark of test *****/
if (Gbl.Test.Config.Feedback != Tst_FEEDBACK_NOTHING)
Tst_ShowTstTotalMark (Gbl.Test.NumQsts,TotalScore);
/***** End box *****/
Box_BoxEnd ();
/***** Store test result in database *****/
Tst_StoreScoreOfTestResultInDB (TstCod,
NumQstsNotBlank,TotalScore);
/***** Set test status *****/
Tst_SetTstStatus (NumTst,Tst_STATUS_ASSESSED);
break;
case Tst_STATUS_ASSESSED:
Ale_ShowAlert (Ale_WARNING,Txt_The_test_X_has_already_been_assessed_previously,
NumTst);
break;
case Tst_STATUS_ERROR:
Ale_ShowAlert (Ale_WARNING,Txt_There_was_an_error_in_assessing_the_test_X,
NumTst);
break;
}
}
/*****************************************************************************/
/*********** Get questions and answers from form to assess a test ************/
/*****************************************************************************/
static void Tst_GetQuestionsAndAnswersFromForm (void)
{
unsigned NumQst;
char StrQstIndOrAns[3 + 10 + 1]; // "Qstxx...x", "Indxx...x" or "Ansxx...x"
/***** Get questions and answers *****/
for (NumQst = 0;
NumQst < Gbl.Test.NumQsts;
NumQst++)
{
/* Get question code */
snprintf (StrQstIndOrAns,sizeof (StrQstIndOrAns),
"Qst%06u",
NumQst);
if ((Gbl.Test.QstCodes[NumQst] = Par_GetParToLong (StrQstIndOrAns)) <= 0)
Lay_ShowErrorAndExit ("Code of question is missing.");
/* Get indexes for this question */
snprintf (StrQstIndOrAns,sizeof (StrQstIndOrAns),
"Ind%06u",
NumQst);
Par_GetParMultiToText (StrQstIndOrAns,Gbl.Test.StrIndexesOneQst[NumQst],
Tst_MAX_BYTES_INDEXES_ONE_QST); /* If choice ==> "0", "1", "2",... */
/* Get answers selected by user for this question */
snprintf (StrQstIndOrAns,sizeof (StrQstIndOrAns),
"Ans%06u",
NumQst);
Par_GetParMultiToText (StrQstIndOrAns,Gbl.Test.StrAnswersOneQst[NumQst],
Tst_MAX_BYTES_ANSWERS_ONE_QST); /* If answer type == T/F ==> " ", "T", "F"; if choice ==> "0", "2",... */
}
}
/*****************************************************************************/
/************************** Show total mark of a test ************************/
/*****************************************************************************/
void Tst_ShowTstTotalMark (unsigned NumQsts,double TotalScore)
{
extern const char *Txt_Score;
extern const char *Txt_out_of_PART_OF_A_SCORE;
double TotalScoreOverSCORE_MAX = TotalScore * Tst_SCORE_MAX / (double) NumQsts;
/***** Write total mark ****/
HTM_DIV_Begin ("class=\"DAT CM\"");
fprintf (Gbl.F.Out,"%s: %.2lf (%.2lf %s %u)",
Txt_Score,
(TotalScoreOverSCORE_MAX >= (double) TotalScoreOverSCORE_MAX / 2.0) ? "ANS_OK" :
"ANS_BAD",
TotalScore,
TotalScoreOverSCORE_MAX,Txt_out_of_PART_OF_A_SCORE,Tst_SCORE_MAX);
HTM_DIV_End ();
}
/*****************************************************************************/
/************** Check minimum date-time of next access to test ***************/
/*****************************************************************************/
// Return true if allowed date-time of next access to test is older than now
static bool Tst_CheckIfNextTstAllowed (void)
{
extern const char *Hlp_ASSESSMENT_Tests;
extern const char *Txt_Test;
extern const char *Txt_You_can_not_take_a_new_test_until;
extern const char *Txt_Today;
MYSQL_RES *mysql_res;
MYSQL_ROW row;
long NumSecondsFromNowToNextAccTst = -1L; // Access allowed when this number <= 0
time_t TimeNextTestUTC = (time_t) 0;
/***** Teachers and superusers are allowed to do all tests they want *****/
if (Gbl.Usrs.Me.Role.Logged == Rol_TCH ||
Gbl.Usrs.Me.Role.Logged == Rol_SYS_ADM)
return true;
/***** Get date of next allowed access to test from database *****/
if (DB_QuerySELECT (&mysql_res,"can not get last access to test",
"SELECT UNIX_TIMESTAMP(LastAccTst+INTERVAL (NumQstsLastTst*%lu) SECOND)-"
"UNIX_TIMESTAMP()," // row[0]
"UNIX_TIMESTAMP(LastAccTst+INTERVAL (NumQstsLastTst*%lu) SECOND)" // row[1]
" FROM crs_usr"
" WHERE CrsCod=%ld AND UsrCod=%ld",
Gbl.Test.Config.MinTimeNxtTstPerQst,
Gbl.Test.Config.MinTimeNxtTstPerQst,
Gbl.Hierarchy.Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod) == 1)
{
/* Get seconds from now to next access to test */
row = mysql_fetch_row (mysql_res);
if (row[0])
if (sscanf (row[0],"%ld",&NumSecondsFromNowToNextAccTst) == 1)
/* Time UTC of next access allowed (row[1]) */
TimeNextTestUTC = Dat_GetUNIXTimeFromStr (row[1]);
}
else
Lay_ShowErrorAndExit ("Error when reading date of next allowed access to test.");
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
/***** Check if access is allowed *****/
if (NumSecondsFromNowToNextAccTst > 0)
{
/***** Write warning *****/
Ale_ShowAlert (Ale_WARNING,"%s: ."
"",
Txt_You_can_not_take_a_new_test_until,
(long) TimeNextTestUTC,
(unsigned) Gbl.Prefs.DateFormat,Txt_Today);
return false;
}
return true;
}
/*****************************************************************************/
/****************************** Update test status ***************************/
/*****************************************************************************/
static void Tst_SetTstStatus (unsigned NumTst,Tst_Status_t TstStatus)
{
/***** Delete old status from expired sessions *****/
DB_QueryDELETE ("can not remove old status of tests",
"DELETE FROM tst_status"
" WHERE SessionId NOT IN"
" (SELECT SessionId FROM sessions)");
/***** Update database *****/
DB_QueryREPLACE ("can not update status of test",
"REPLACE INTO tst_status"
" (SessionId,CrsCod,NumTst,Status)"
" VALUES"
" ('%s',%ld,%u,%u)",
Gbl.Session.Id,Gbl.Hierarchy.Crs.CrsCod,
NumTst,(unsigned) TstStatus);
}
/*****************************************************************************/
/****************************** Update test status ***************************/
/*****************************************************************************/
static Tst_Status_t Tst_GetTstStatus (unsigned NumTst)
{
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned long NumRows;
unsigned UnsignedNum;
Tst_Status_t TstStatus = Tst_STATUS_ERROR;
/***** Get status of test from database *****/
NumRows = DB_QuerySELECT (&mysql_res,"can not get status of test",
"SELECT Status" // row[0]
" FROM tst_status"
" WHERE SessionId='%s'"
" AND CrsCod=%ld"
" AND NumTst=%u",
Gbl.Session.Id,Gbl.Hierarchy.Crs.CrsCod,NumTst);
if (NumRows == 1)
{
/* Get number of hits */
row = mysql_fetch_row (mysql_res);
if (row[0])
if (sscanf (row[0],"%u",&UnsignedNum) == 1)
if (UnsignedNum < Tst_NUM_STATUS)
TstStatus = (Tst_Status_t) UnsignedNum;
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
return TstStatus;
}
/*****************************************************************************/
/************************* Get number of hits to test ************************/
/*****************************************************************************/
static unsigned Tst_GetNumAccessesTst (void)
{
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned long NumRows;
unsigned NumAccessesTst = 0;
if (Gbl.Usrs.Me.IBelongToCurrentCrs)
{
/***** Get number of hits to test from database *****/
NumRows = DB_QuerySELECT (&mysql_res,"can not get number of hits to test",
"SELECT NumAccTst" // row[0]
" FROM crs_usr"
" WHERE CrsCod=%ld AND UsrCod=%ld",
Gbl.Hierarchy.Crs.CrsCod,
Gbl.Usrs.Me.UsrDat.UsrCod);
if (NumRows == 0)
NumAccessesTst = 0;
else if (NumRows == 1)
{
/* Get number of hits */
row = mysql_fetch_row (mysql_res);
if (row[0] == NULL)
NumAccessesTst = 0;
else if (sscanf (row[0],"%u",&NumAccessesTst) != 1)
NumAccessesTst = 0;
}
else
Lay_ShowErrorAndExit ("Error when getting number of hits to test.");
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
return NumAccessesTst;
}
/*****************************************************************************/
/*************************** Write the test questions ************************/
/*****************************************************************************/
// NumRows must hold the number of rows of a MySQL query
// In each row mysql_res holds: in the column 0 the code of a question, in the column 1 the type of answer, and in the column 2 the stem
static void Tst_ShowTestQuestionsWhenSeeing (MYSQL_RES *mysql_res)
{
unsigned NumQst;
long QstCod;
MYSQL_ROW row;
double ScoreThisQst; // Not used here
bool AnswerIsNotBlank; // Not used here
/*
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
*/
/***** Write rows *****/
for (NumQst = 0;
NumQst < Gbl.Test.NumQsts;
NumQst++)
{
Gbl.RowEvenOdd = NumQst % 2;
/***** Get the row next of the result of the query in the database *****/
row = mysql_fetch_row (mysql_res);
/***** Get the code of question (row[0]) *****/
if ((QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
Lay_ShowErrorAndExit ("Wrong code of question.");
Tst_WriteQstAndAnsTest (Tst_SHOW_TEST_TO_ANSWER,
&Gbl.Usrs.Me.UsrDat,
NumQst,QstCod,row,
&ScoreThisQst, // Not used here
&AnswerIsNotBlank); // Not used here
}
}
/*****************************************************************************/
/******************** Show test tags in this test result *********************/
/*****************************************************************************/
static void Tst_ShowTstTagsPresentInATestResult (long TstCod)
{
MYSQL_RES *mysql_res;
unsigned long NumTags;
/***** Get all tags of questions in this test *****/
NumTags = (unsigned)
DB_QuerySELECT (&mysql_res,"can not get tags"
" present in a test result",
"SELECT tst_tags.TagTxt" // row[0]
" FROM"
" (SELECT DISTINCT(tst_question_tags.TagCod)"
" FROM tst_question_tags,tst_exam_questions"
" WHERE tst_exam_questions.TstCod=%ld"
" AND tst_exam_questions.QstCod=tst_question_tags.QstCod)"
" AS TagsCods,tst_tags"
" WHERE TagsCods.TagCod=tst_tags.TagCod"
" ORDER BY tst_tags.TagTxt",
TstCod);
Tst_ShowTagList (NumTags,mysql_res);
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/************************** Show list of test tags ***************************/
/*****************************************************************************/
void Tst_ShowTagList (unsigned NumTags,MYSQL_RES *mysql_res)
{
extern const char *Txt_no_tags;
MYSQL_ROW row;
unsigned NumTag;
if (NumTags)
{
/***** Write the tags *****/
HTM_UL_Begin (NULL);
for (NumTag = 0;
NumTag < NumTags;
NumTag++)
{
row = mysql_fetch_row (mysql_res);
HTM_LI_Begin (NULL);
fprintf (Gbl.F.Out,"%s",row[0]);
HTM_LI_End ();
}
HTM_UL_End ();
}
else
fprintf (Gbl.F.Out,"%s",
Txt_no_tags);
}
/*****************************************************************************/
/******************* Show the result of assessing a test *********************/
/*****************************************************************************/
static void Tst_ShowTestResultAfterAssess (long TstCod,unsigned *NumQstsNotBlank,double *TotalScore)
{
extern const char *Txt_Question_removed;
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned NumQst;
long QstCod;
double ScoreThisQst;
bool AnswerIsNotBlank;
/***** Initialize score and number of questions not blank *****/
*TotalScore = 0.0;
*NumQstsNotBlank = 0;
for (NumQst = 0;
NumQst < Gbl.Test.NumQsts;
NumQst++)
{
Gbl.RowEvenOdd = NumQst % 2;
/***** Query database *****/
if (Tst_GetOneQuestionByCod (Gbl.Test.QstCodes[NumQst],&mysql_res)) // Question exists
{
/***** Get row of the result of the query *****/
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
*/
/***** Get the code of question (row[0]) *****/
if ((QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
Lay_ShowErrorAndExit ("Wrong code of question.");
/***** Write question and answers *****/
Tst_WriteQstAndAnsTest (Tst_SHOW_TEST_RESULT,
&Gbl.Usrs.Me.UsrDat,
NumQst,QstCod,row,
&ScoreThisQst,&AnswerIsNotBlank);
/***** Store test result question in database *****/
Tst_StoreOneTestResultQstInDB (TstCod,QstCod,
NumQst, // 0, 1, 2, 3...
ScoreThisQst);
/***** Compute total score *****/
*TotalScore += ScoreThisQst;
if (AnswerIsNotBlank)
(*NumQstsNotBlank)++;
/***** Update the number of accesses and the score of this question *****/
if (Gbl.Usrs.Me.Role.Logged == Rol_STD)
Tst_UpdateScoreQst (QstCod,ScoreThisQst,AnswerIsNotBlank);
}
else
{
/***** Question does not exists *****/
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"BIG_INDEX RT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%u",NumQst + 1);
HTM_TD_End ();
HTM_TD_Begin ("class=\"DAT_LIGHT LT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%s",Txt_Question_removed);
HTM_TD_End ();
HTM_TR_End ();
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
}
/*****************************************************************************/
/********** Write a row of a test, with one question and its answer **********/
/*****************************************************************************/
static void Tst_WriteQstAndAnsTest (Tst_ActionToDoWithQuestions_t ActionToDoWithQuestions,
struct UsrData *UsrDat,
unsigned NumQst,long QstCod,MYSQL_ROW row,
double *ScoreThisQst,bool *AnswerIsNotBlank)
{
extern const char *Txt_TST_STR_ANSWER_TYPES[Tst_NUM_ANS_TYPES];
/*
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
*/
/***** Create test question *****/
Tst_QstConstructor ();
Gbl.Test.QstCod = QstCod;
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"RT COLOR%u\"",Gbl.RowEvenOdd);
/***** Write number of question *****/
HTM_DIV_Begin ("class=\"BIG_INDEX\"");
fprintf (Gbl.F.Out,"%u",NumQst + 1);
HTM_DIV_End ();
/***** Write answer type (row[2]) *****/
Gbl.Test.AnswerType = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[2]);
HTM_DIV_Begin ("class=\"DAT_SMALL\"");
fprintf (Gbl.F.Out,"%s",Txt_TST_STR_ANSWER_TYPES[Gbl.Test.AnswerType]);
HTM_DIV_End ();
HTM_TD_End ();
/***** Write stem (row[4]) *****/
HTM_TD_Begin ("class=\"LT COLOR%u\"",Gbl.RowEvenOdd);
Tst_WriteQstStem (row[4],"TEST_EXA");
/***** Get and show media (row[6]) *****/
Gbl.Test.Media.MedCod = Str_ConvertStrCodToLongCod (row[6]);
Med_GetMediaDataByCod (&Gbl.Test.Media);
Med_ShowMedia (&Gbl.Test.Media,
"TEST_MED_SHOW_CONTAINER",
"TEST_MED_SHOW");
/***** Write answers depending on shuffle (row[3]) and feedback (row[5]) *****/
switch (ActionToDoWithQuestions)
{
case Tst_SHOW_TEST_TO_ANSWER:
Tst_WriteAnswersTestToAnswer (NumQst,QstCod,(row[3][0] == 'Y'));
break;
case Tst_SHOW_TEST_RESULT:
Tst_WriteAnswersTestResult (UsrDat,NumQst,QstCod,ScoreThisQst,AnswerIsNotBlank);
/* Write question feedback (row[5]) */
if (Gbl.Test.Config.Feedback == Tst_FEEDBACK_FULL_FEEDBACK)
Tst_WriteQstFeedback (row[5],"TEST_EXA_LIGHT");
break;
default:
break;
}
HTM_TD_End ();
HTM_TR_End ();
/***** Destroy test question *****/
Tst_QstDestructor ();
}
/*****************************************************************************/
/********************* Write the stem of a test question *********************/
/*****************************************************************************/
void Tst_WriteQstStem (const char *Stem,const char *ClassStem)
{
unsigned long StemLength;
char *StemRigorousHTML;
/***** Convert the stem, that is in HTML, to rigorous HTML *****/
StemLength = strlen (Stem) * Str_MAX_BYTES_PER_CHAR;
if ((StemRigorousHTML = (char *) malloc (StemLength + 1)) == NULL)
Lay_NotEnoughMemoryExit ();
Str_Copy (StemRigorousHTML,Stem,
StemLength);
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
StemRigorousHTML,StemLength,false);
/***** Write the stem *****/
HTM_DIV_Begin ("class=\"%s\"",ClassStem);
fprintf (Gbl.F.Out,"%s",StemRigorousHTML);
HTM_DIV_End ();
/***** Free memory allocated for the stem *****/
free ((void *) StemRigorousHTML);
}
/*****************************************************************************/
/************* Put form to upload a new image for a test question ************/
/*****************************************************************************/
static void Tst_PutFormToEditQstMedia (struct Media *Media,int NumMediaInForm,
bool OptionsDisabled)
{
extern const char *The_ClassFormInBox[The_NUM_THEMES];
extern const char *Txt_No_image_video;
extern const char *Txt_Current_image_video;
extern const char *Txt_Change_image_video;
static unsigned UniqueId = 0;
struct ParamUploadMedia ParamUploadMedia;
if (Media->Name[0])
{
/***** Set names of parameters depending on number of image in form *****/
Med_SetParamNames (&ParamUploadMedia,NumMediaInForm);
/***** Start container *****/
HTM_DIV_Begin ("class=\"TEST_MED_EDIT_FORM\"");
/***** Choice 1: No media *****/
HTM_LABEL_Begin ("class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
HTM_INPUT_RADIO (ParamUploadMedia.Action,false,
"value=\"%u\"%s",
(unsigned) Med_ACTION_NO_MEDIA,
OptionsDisabled ? " disabled=\"disabled\"" : "");
fprintf (Gbl.F.Out,"%s",Txt_No_image_video);
HTM_LABEL_End ();
fprintf (Gbl.F.Out," ");
/***** Choice 2: Current media *****/
HTM_LABEL_Begin ("class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
HTM_INPUT_RADIO (ParamUploadMedia.Action,false,
"value=\"%u\"%s checked=\"checked\"",
(unsigned) Med_ACTION_KEEP_MEDIA,
OptionsDisabled ? " disabled=\"disabled\"" : "");
fprintf (Gbl.F.Out,"%s",Txt_Current_image_video);
HTM_LABEL_End ();
Med_ShowMedia (Media,
"TEST_MED_EDIT_ONE_CONTAINER",
"TEST_MED_EDIT_ONE");
/***** Choice 3: Change media *****/
UniqueId++;
HTM_LABEL_Begin ("class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
HTM_INPUT_RADIO (ParamUploadMedia.Action,false,
"id=\"chg_img_%u\" value=\"%u\"%s",
UniqueId,
(unsigned) Med_ACTION_NEW_MEDIA,
OptionsDisabled ? " disabled=\"disabled\"" : "");
fprintf (Gbl.F.Out,"%s: ",Txt_Change_image_video);
HTM_LABEL_End ();
Med_PutMediaUploader (NumMediaInForm,"TEST_MED_INPUT");
/***** End container *****/
HTM_DIV_End ();
}
else // No current image
/***** Attached media *****/
Med_PutMediaUploader (NumMediaInForm,"TEST_MED_INPUT");
}
/*****************************************************************************/
/******************* Write the feedback of a test question *******************/
/*****************************************************************************/
void Tst_WriteQstFeedback (const char *Feedback,const char *ClassFeedback)
{
unsigned long FeedbackLength;
char *FeedbackRigorousHTML;
if (Feedback)
if (Feedback[0])
{
/***** Convert the feedback, that is in HTML, to rigorous HTML *****/
FeedbackLength = strlen (Feedback) * Str_MAX_BYTES_PER_CHAR;
if ((FeedbackRigorousHTML = (char *) malloc (FeedbackLength + 1)) == NULL)
Lay_NotEnoughMemoryExit ();
Str_Copy (FeedbackRigorousHTML,Feedback,
FeedbackLength);
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
FeedbackRigorousHTML,FeedbackLength,false);
/***** Write the feedback *****/
HTM_DIV_Begin ("class=\"%s\"",ClassFeedback);
fprintf (Gbl.F.Out,"%s",FeedbackRigorousHTML);
HTM_DIV_End ();
/***** Free memory allocated for the feedback *****/
free ((void *) FeedbackRigorousHTML);
}
}
/*****************************************************************************/
/*********************** Update the score of a question **********************/
/*****************************************************************************/
static void Tst_UpdateScoreQst (long QstCod,float ScoreThisQst,bool AnswerIsNotBlank)
{
/***** Update number of clicks and score of the question *****/
Str_SetDecimalPointToUS (); // To print the floating point as a dot
if (AnswerIsNotBlank)
DB_QueryUPDATE ("can not update the score of a question",
"UPDATE tst_questions"
" SET NumHits=NumHits+1,NumHitsNotBlank=NumHitsNotBlank+1,"
"Score=Score+(%lf)"
" WHERE QstCod=%ld",
ScoreThisQst,QstCod);
else // The answer is blank
DB_QueryUPDATE ("can not update the score of a question",
"UPDATE tst_questions"
" SET NumHits=NumHits+1"
" WHERE QstCod=%ld",
QstCod);
Str_SetDecimalPointToLocal (); // Return to local system
}
/*****************************************************************************/
/*********** Update my number of accesses to test in this course *************/
/*****************************************************************************/
static void Tst_UpdateMyNumAccessTst (unsigned NumAccessesTst)
{
/***** Update my number of accesses to test in this course *****/
DB_QueryUPDATE ("can not update the number of accesses to test",
"UPDATE crs_usr SET NumAccTst=%u"
" WHERE CrsCod=%ld AND UsrCod=%ld",
NumAccessesTst,
Gbl.Hierarchy.Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod);
}
/*****************************************************************************/
/************ Update date-time of my next allowed access to test *************/
/*****************************************************************************/
static void Tst_UpdateLastAccTst (void)
{
/***** Update date-time and number of questions of this test *****/
DB_QueryUPDATE ("can not update time and number of questions of this test",
"UPDATE crs_usr SET LastAccTst=NOW(),NumQstsLastTst=%u"
" WHERE CrsCod=%ld AND UsrCod=%ld",
Gbl.Test.NumQsts,
Gbl.Hierarchy.Crs.CrsCod,
Gbl.Usrs.Me.UsrDat.UsrCod);
}
/*****************************************************************************/
/******* Select tags and dates for edition of the self-assessment test *******/
/*****************************************************************************/
void Tst_ShowFormAskEditTsts (void)
{
extern const char *Hlp_ASSESSMENT_Tests_editing_questions;
extern const char *Txt_No_test_questions;
extern const char *Txt_List_edit_questions;
extern const char *Txt_Show_questions;
MYSQL_RES *mysql_res;
unsigned long NumRows;
/***** Contextual menu *****/
Mnu_ContextMenuBegin ();
TsI_PutFormToImportQuestions (); // Import questions from XML file
Mnu_ContextMenuEnd ();
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_List_edit_questions,Tst_PutIconsTests,
Hlp_ASSESSMENT_Tests_editing_questions,Box_NOT_CLOSABLE);
/***** Get tags already present in the table of questions *****/
if ((NumRows = Tst_GetAllTagsFromCurrentCrs (&mysql_res)))
{
Frm_StartForm (ActLstTstQst);
Par_PutHiddenParamUnsigned (NULL,"Order",(unsigned) Tst_ORDER_STEM);
HTM_TABLE_BeginPadding (2);
/***** Selection of tags *****/
Tst_ShowFormSelTags (NumRows,mysql_res,false,2);
/***** Selection of types of answers *****/
Tst_ShowFormAnswerTypes (2);
/***** Starting and ending dates in the search *****/
Dat_PutFormStartEndClientLocalDateTimesWithYesterdayToday (false);
HTM_TABLE_End ();
/***** Send button *****/
Btn_PutConfirmButton (Txt_Show_questions);
Frm_EndForm ();
}
else // No test questions
{
/***** Warning message *****/
Ale_ShowAlert (Ale_INFO,Txt_No_test_questions);
/***** Button to create a new question *****/
Tst_PutButtonToAddQuestion ();
}
/***** End box *****/
Box_BoxEnd ();
/* Free structure that stores the query result */
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/************** Show form select test questions for a game *******************/
/*****************************************************************************/
void Tst_ShowFormAskSelectTstsForGame (void)
{
extern const char *Hlp_ASSESSMENT_Games_questions;
extern const char *Txt_No_test_questions;
extern const char *Txt_Select_questions;
extern const char *Txt_Show_questions;
MYSQL_RES *mysql_res;
unsigned long NumRows;
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Select_questions,NULL,
Hlp_ASSESSMENT_Games_questions,Box_NOT_CLOSABLE);
/***** Get tags already present in the table of questions *****/
if ((NumRows = Tst_GetAllTagsFromCurrentCrs (&mysql_res)))
{
Frm_StartForm (ActGamLstTstQst);
Gam_PutParams ();
HTM_TABLE_BeginPadding (2);
/***** Selection of tags *****/
Tst_ShowFormSelTags (NumRows,mysql_res,false,2);
/***** Starting and ending dates in the search *****/
Dat_PutFormStartEndClientLocalDateTimesWithYesterdayToday (false);
HTM_TABLE_End ();
/***** Send button *****/
Btn_PutConfirmButton (Txt_Show_questions);
Frm_EndForm ();
}
else // No test questions
{
/***** Warning message *****/
Ale_ShowAlert (Ale_INFO,Txt_No_test_questions);
/***** Button to create a new question *****/
Tst_PutButtonToAddQuestion ();
}
/***** End box *****/
Box_BoxEnd ();
/* Free structure that stores the query result */
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/************************* Check if I can edit tests *************************/
/*****************************************************************************/
static bool Tst_CheckIfICanEditTests (void)
{
return (bool) (Gbl.Usrs.Me.Role.Logged == Rol_TCH ||
Gbl.Usrs.Me.Role.Logged == Rol_SYS_ADM);
}
/*****************************************************************************/
/********************* Put contextual icons in tests *************************/
/*****************************************************************************/
static void Tst_PutIconsTests (void)
{
extern const char *Txt_New_question;
if (Tst_CheckIfICanEditTests ())
{
/***** Put form to edit existing test questions *****/
if (Gbl.Action.Act != ActEdiTstQst)
Ico_PutContextualIconToEdit (ActEdiTstQst,NULL);
/***** Put form to create a new test question *****/
if (Gbl.Action.Act != ActEdiOneTstQst)
Ico_PutContextualIconToAdd (ActEdiOneTstQst,NULL,NULL,
Txt_New_question);
/***** Put form to go to test configuration *****/
if (Gbl.Action.Act != ActCfgTst)
Ico_PutContextualIconToConfigure (ActCfgTst,NULL);
}
/***** Put icon to show a figure *****/
Gbl.Figures.FigureType = Fig_TESTS;
Fig_PutIconToShowFigure ();
}
/*****************************************************************************/
/**************** Put button to create a new test question *******************/
/*****************************************************************************/
static void Tst_PutButtonToAddQuestion (void)
{
extern const char *Txt_New_question;
Frm_StartForm (ActEdiOneTstQst);
Btn_PutConfirmButton (Txt_New_question);
Frm_EndForm ();
}
/*****************************************************************************/
/***************************** Form to rename tags ***************************/
/*****************************************************************************/
void Tst_ShowFormConfig (void)
{
extern const char *Txt_Please_specify_if_you_allow_access_to_test_questions_from_mobile_applications;
/***** If current course has tests and pluggable is unknown... *****/
if (Tst_CheckIfCourseHaveTestsAndPluggableIsUnknown ())
Ale_ShowAlert (Ale_WARNING,Txt_Please_specify_if_you_allow_access_to_test_questions_from_mobile_applications);
/***** Form to configure test *****/
Tst_ShowFormConfigTst ();
/***** Form to edit tags *****/
Tst_ShowFormEditTags ();
}
/*****************************************************************************/
/******************************* Enable a test tag ***************************/
/*****************************************************************************/
void Tst_EnableTag (void)
{
long TagCod = Tst_GetParamTagCode ();
/***** Change tag status to enabled *****/
Tst_EnableOrDisableTag (TagCod,false);
/***** Show again the form to configure test *****/
Tst_ShowFormConfig ();
}
/*****************************************************************************/
/****************************** Disable a test tag ***************************/
/*****************************************************************************/
void Tst_DisableTag (void)
{
long TagCod = Tst_GetParamTagCode ();
/***** Change tag status to disabled *****/
Tst_EnableOrDisableTag (TagCod,true);
/***** Show again the form to configure test *****/
Tst_ShowFormConfig ();
}
/*****************************************************************************/
/************************* Get parameter with tag code ***********************/
/*****************************************************************************/
static long Tst_GetParamTagCode (void)
{
long TagCod;
/***** Get tag code *****/
if ((TagCod = Par_GetParToLong ("TagCod")) <= 0)
Lay_ShowErrorAndExit ("Code of tag is missing.");
return TagCod;
}
/*****************************************************************************/
/************************ Rename a tag of test questions *********************/
/*****************************************************************************/
void Tst_RenameTag (void)
{
extern const char *Txt_You_can_not_leave_the_name_of_the_tag_X_empty;
extern const char *Txt_The_tag_X_has_been_renamed_as_Y;
extern const char *Txt_The_tag_X_has_not_changed;
char OldTagTxt[Tst_MAX_BYTES_TAG + 1];
char NewTagTxt[Tst_MAX_BYTES_TAG + 1];
long ExistingTagCod;
long OldTagCod;
bool ComplexRenaming;
/***** Get old and new tags from the form *****/
Par_GetParToText ("OldTagTxt",OldTagTxt,Tst_MAX_BYTES_TAG);
Par_GetParToText ("NewTagTxt",NewTagTxt,Tst_MAX_BYTES_TAG);
/***** Check that the new tag is not empty *****/
if (!NewTagTxt[0]) // New tag empty
Ale_ShowAlert (Ale_WARNING,Txt_You_can_not_leave_the_name_of_the_tag_X_empty,
OldTagTxt);
else // New tag not empty
{
/***** Check if the old tag is equal to the new one *****/
if (!strcmp (OldTagTxt,NewTagTxt)) // The old and the new tag
// are exactly the same (case sensitively).
// This happens when user press INTRO
// without changing anything in the form.
Ale_ShowAlert (Ale_INFO,Txt_The_tag_X_has_not_changed,
NewTagTxt);
else // The old and the new tag
// are not exactly the same (case sensitively).
{
/***** Check if renaming is complex or easy *****/
ComplexRenaming = false;
if (strcasecmp (OldTagTxt,NewTagTxt)) // The old and the new tag
// are not the same (case insensitively)
/* Check if the new tag text is equal to any of the tags
already present in the database */
if ((ExistingTagCod = Tst_GetTagCodFromTagTxt (NewTagTxt)) > 0)
// The new tag was already in database
ComplexRenaming = true;
if (ComplexRenaming) // Renaming is not easy
{
/***** Complex update made to not repeat tags:
- If the new tag existed for a question ==>
delete old tag from tst_question_tags;
the new tag will remain
- If the new tag did not exist for a question ==>
change old tag to new tag in tst_question_tags *****/
/* Get tag code of the old tag */
if ((OldTagCod = Tst_GetTagCodFromTagTxt (OldTagTxt)) < 0)
Lay_ShowErrorAndExit ("Tag does not exists.");
/* Create a temporary table with all the question codes
that had the new tag as one of their tags */
DB_Query ("can not remove temporary table",
"DROP TEMPORARY TABLE IF EXISTS tst_question_tags_tmp");
DB_Query ("can not create temporary table",
"CREATE TEMPORARY TABLE tst_question_tags_tmp"
" ENGINE=MEMORY"
" SELECT QstCod FROM tst_question_tags"
" WHERE TagCod=%ld",
ExistingTagCod);
/* Remove old tag in questions where it would be repeated */
// New tag existed for a question ==> delete old tag
DB_QueryDELETE ("can not remove a tag from some questions",
"DELETE FROM tst_question_tags"
" WHERE TagCod=%ld"
" AND QstCod IN"
" (SELECT QstCod FROM tst_question_tags_tmp)",
OldTagCod);
/* Change old tag to new tag in questions where it would not be repeated */
// New tag did not exist for a question ==> change old tag to new tag
DB_QueryUPDATE ("can not update a tag in some questions",
"UPDATE tst_question_tags"
" SET TagCod=%ld"
" WHERE TagCod=%ld"
" AND QstCod NOT IN"
" (SELECT QstCod FROM tst_question_tags_tmp)",
ExistingTagCod,
OldTagCod);
/* Drop temporary table, no longer necessary */
DB_Query ("can not remove temporary table",
"DROP TEMPORARY TABLE IF EXISTS tst_question_tags_tmp");
/***** Delete old tag from tst_tags
because it is not longer used *****/
DB_QueryDELETE ("can not remove old tag",
"DELETE FROM tst_tags WHERE TagCod=%ld",
OldTagCod);
}
else // Renaming is easy
{
/***** Simple update replacing each instance of the old tag by the new tag *****/
DB_QueryUPDATE ("can not update tag",
"UPDATE tst_tags SET TagTxt='%s',ChangeTime=NOW()"
" WHERE tst_tags.CrsCod=%ld"
" AND tst_tags.TagTxt='%s'",
NewTagTxt,Gbl.Hierarchy.Crs.CrsCod,OldTagTxt);
}
/***** Write message to show the change made *****/
Ale_ShowAlert (Ale_SUCCESS,Txt_The_tag_X_has_been_renamed_as_Y,
OldTagTxt,NewTagTxt);
}
}
/***** Show again the form to configure test *****/
Tst_ShowFormConfig ();
}
/*****************************************************************************/
/******************* Check if current course has test tags *******************/
/*****************************************************************************/
// Return the number of rows of the result
static bool Tst_CheckIfCurrentCrsHasTestTags (void)
{
/***** Get available tags from database *****/
return (DB_QueryCOUNT ("can not check if course has tags",
"SELECT COUNT(*) FROM tst_tags"
" WHERE CrsCod=%ld",
Gbl.Hierarchy.Crs.CrsCod) != 0);
}
/*****************************************************************************/
/********* Get all (enabled or disabled) test tags for this course ***********/
/*****************************************************************************/
// Return the number of rows of the result
static unsigned long Tst_GetAllTagsFromCurrentCrs (MYSQL_RES **mysql_res)
{
/***** Get available tags from database *****/
return DB_QuerySELECT (mysql_res,"can not get available tags",
"SELECT TagCod," // row[0]
"TagTxt," // row[1]
"TagHidden" // row[2]
" FROM tst_tags"
" WHERE CrsCod=%ld"
" ORDER BY TagTxt",
Gbl.Hierarchy.Crs.CrsCod);
}
/*****************************************************************************/
/********************** Get enabled test tags for this course ****************/
/*****************************************************************************/
// Return the number of rows of the result
static unsigned long Tst_GetEnabledTagsFromThisCrs (MYSQL_RES **mysql_res)
{
/***** Get available not hidden tags from database *****/
return DB_QuerySELECT (mysql_res,"can not get available enabled tags",
"SELECT TagCod,TagTxt FROM tst_tags"
" WHERE CrsCod=%ld AND TagHidden='N'"
" ORDER BY TagTxt",
Gbl.Hierarchy.Crs.CrsCod);
}
/*****************************************************************************/
/********************* Show a form to select test tags ***********************/
/*****************************************************************************/
static void Tst_ShowFormSelTags (unsigned long NumRows,MYSQL_RES *mysql_res,
bool ShowOnlyEnabledTags,unsigned NumCols)
{
extern const char *The_ClassFormInBox[The_NUM_THEMES];
extern const char *Txt_Tags;
extern const char *Txt_All_tags;
extern const char *Txt_Tag_not_allowed;
extern const char *Txt_Tag_allowed;
unsigned long NumRow;
MYSQL_ROW row;
bool TagHidden = false;
bool Checked;
const char *Ptr;
char TagText[Tst_MAX_BYTES_TAG + 1];
/*
row[0] TagCod
row[1] TagTxt
row[2] TagHidden
*/
HTM_TR_Begin (NULL);
/***** Label *****/
HTM_TD_Begin ("class=\"RT %s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_Tags);
HTM_TD_End ();
/***** Select all tags *****/
if (NumCols > 1)
HTM_TD_Begin ("colspan=\"%u\" class=\"LT\"",NumCols);
else
HTM_TD_Begin ("class=\"LT\"");
HTM_TABLE_BeginPadding (2);
HTM_TR_Begin (NULL);
if (!ShowOnlyEnabledTags)
HTM_TD_Empty (1);
HTM_TD_Begin ("class=\"LM\"");
HTM_LABEL_Begin ("class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
HTM_INPUT_CHECKBOX ("AllTags",false,
"value=\"Y\"%s onclick=\"togglecheckChildren(this,'ChkTag');\"",
Gbl.Test.Tags.All ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out," %s",Txt_All_tags);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TR_End ();
/***** Select tags one by one *****/
for (NumRow = 1;
NumRow <= NumRows;
NumRow++)
{
row = mysql_fetch_row (mysql_res);
HTM_TR_Begin (NULL);
if (!ShowOnlyEnabledTags)
{
TagHidden = (row[2][0] == 'Y');
HTM_TD_Begin ("class=\"LM\"");
if (TagHidden)
HTM_IMG (Cfg_URL_ICON_PUBLIC,"eye-slash.svg",Txt_Tag_not_allowed,
"class=\"ICO_HIDDEN ICO16x16\"");
else
HTM_IMG (Cfg_URL_ICON_PUBLIC,"eye.svg",Txt_Tag_allowed,
"class=\"ICO_HIDDEN ICO16x16\"");
HTM_TD_End ();
}
Checked = false;
if (Gbl.Test.Tags.List)
{
Ptr = Gbl.Test.Tags.List;
while (*Ptr)
{
Par_GetNextStrUntilSeparParamMult (&Ptr,TagText,Tst_MAX_BYTES_TAG);
if (!strcmp (row[1],TagText))
{
Checked = true;
break;
}
}
}
HTM_TD_Begin ("class=\"LM\"");
HTM_LABEL_Begin ("class=\"DAT\"");
HTM_INPUT_CHECKBOX ("ChkTag",false,
"value=\"%s\"%s onclick=\"checkParent(this,'AllTags');\"",
row[1],
Checked ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out," %s",row[1]);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TR_End ();
}
HTM_TABLE_End ();
HTM_TD_End ();
HTM_TR_End ();
}
/*****************************************************************************/
/************* Show a form to enable/disable and rename test tags ************/
/*****************************************************************************/
static void Tst_ShowFormEditTags (void)
{
extern const char *Hlp_ASSESSMENT_Tests_writing_a_question;
extern const char *Txt_No_test_questions;
extern const char *Txt_Tags;
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned long NumRow,NumRows;
long TagCod;
/***** Get current tags in current course *****/
if ((NumRows = Tst_GetAllTagsFromCurrentCrs (&mysql_res)))
{
/***** Begin box and table *****/
Box_StartBoxTable (NULL,Txt_Tags,NULL,
Hlp_ASSESSMENT_Tests_writing_a_question,Box_NOT_CLOSABLE,2);
/***** Show tags *****/
for (NumRow = 0;
NumRow < NumRows;
NumRow++)
{
row = mysql_fetch_row (mysql_res);
/*
row[0] TagCod
row[1] TagTxt
row[2] TagHidden
*/
if ((TagCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
Lay_ShowErrorAndExit ("Wrong code of tag.");
HTM_TR_Begin (NULL);
/* Form to enable / disable this tag */
if (row[2][0] == 'Y') // Tag disabled
Tst_PutIconEnable (TagCod,row[1]);
else
Tst_PutIconDisable (TagCod,row[1]);
/* Form to rename this tag */
HTM_TD_Begin ("class=\"LM\"");
Frm_StartForm (ActRenTag);
Par_PutHiddenParamString (NULL,"OldTagTxt",row[1]);
HTM_INPUT_TEXT ("NewTagTxt",Tst_MAX_CHARS_TAG,row[1],true,
"size=\"36\"");
Frm_EndForm ();
HTM_TD_End ();
HTM_TR_End ();
}
/***** End table and box *****/
Box_EndBoxTable ();
}
else
Ale_ShowAlert (Ale_INFO,Txt_No_test_questions);
/* Free structure that stores the query result */
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/******************* Put a link and an icon to enable a tag ******************/
/*****************************************************************************/
static void Tst_PutIconEnable (long TagCod,const char *TagTxt)
{
extern const char *Txt_Tag_X_not_allowed_Click_to_allow_it;
HTM_TD_Begin ("class=\"BM\"");
Frm_StartForm (ActEnableTag);
Par_PutHiddenParamLong (NULL,"TagCod",TagCod);
snprintf (Gbl.Title,sizeof (Gbl.Title),
Txt_Tag_X_not_allowed_Click_to_allow_it,
TagTxt);
Ico_PutIconLink ("eye-slash.svg",Gbl.Title);
Frm_EndForm ();
HTM_TD_End ();
}
/*****************************************************************************/
/****************** Put a link and an icon to disable a tag ******************/
/*****************************************************************************/
static void Tst_PutIconDisable (long TagCod,const char *TagTxt)
{
extern const char *Txt_Tag_X_allowed_Click_to_disable_it;
HTM_TD_Begin ("class=\"BM\"");
Frm_StartForm (ActDisableTag);
Par_PutHiddenParamLong (NULL,"TagCod",TagCod);
snprintf (Gbl.Title,sizeof (Gbl.Title),
Txt_Tag_X_allowed_Click_to_disable_it,
TagTxt);
Ico_PutIconLink ("eye.svg",Gbl.Title);
Frm_EndForm ();
HTM_TD_End ();
}
/*****************************************************************************/
/********************* Show a form to to configure test **********************/
/*****************************************************************************/
static void Tst_ShowFormConfigTst (void)
{
extern const char *Hlp_ASSESSMENT_Tests;
extern const char *The_ClassFormInBox[The_NUM_THEMES];
extern const char *Txt_Configure_tests;
extern const char *Txt_Plugins;
extern const char *Txt_TST_PLUGGABLE[Tst_NUM_OPTIONS_PLUGGABLE];
extern const char *Txt_No_of_questions;
extern const char *Txt_minimum;
extern const char *Txt_default;
extern const char *Txt_maximum;
extern const char *Txt_Minimum_time_seconds_per_question_between_two_tests;
extern const char *Txt_Feedback_to_students;
extern const char *Txt_TST_STR_FEEDBACK[Tst_NUM_TYPES_FEEDBACK];
extern const char *Txt_Save_changes;
Tst_Pluggable_t Pluggable;
Tst_Feedback_t Feedback;
char StrMinTimeNxtTstPerQst[20 + 1];
/***** Read test configuration from database *****/
Tst_GetConfigTstFromDB ();
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Configure_tests,Tst_PutIconsTests,
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
/***** Begin form *****/
Frm_StartForm (ActRcvCfgTst);
/***** Tests are visible from plugins? *****/
HTM_TABLE_BeginCenterPadding (2);
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"%s RT\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_Plugins);
HTM_TD_End ();
HTM_TD_Begin ("class=\"LB\"");
for (Pluggable = Tst_PLUGGABLE_NO;
Pluggable <= Tst_PLUGGABLE_YES;
Pluggable++)
{
HTM_LABEL_Begin ("class=\"DAT\"");
HTM_INPUT_RADIO ("Pluggable",false,
"value=\"%u\"%s",
(unsigned) Pluggable,
Pluggable == Gbl.Test.Config.Pluggable ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out,"%s",Txt_TST_PLUGGABLE[Pluggable]);
HTM_LABEL_End ();
fprintf (Gbl.F.Out," ");
}
HTM_TD_End ();
HTM_TR_End ();
/***** Number of questions *****/
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"%s RT\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_No_of_questions);
HTM_TD_End ();
HTM_TD_Begin ("class=\"LB\"");
HTM_TABLE_BeginPadding (2);
Tst_PutInputFieldNumQst ("NumQstMin",Txt_minimum,
Gbl.Test.Config.Min); // Minimum number of questions
Tst_PutInputFieldNumQst ("NumQstDef",Txt_default,
Gbl.Test.Config.Def); // Default number of questions
Tst_PutInputFieldNumQst ("NumQstMax",Txt_maximum,
Gbl.Test.Config.Max); // Maximum number of questions
HTM_TABLE_End ();
HTM_TD_End ();
HTM_TR_End ();
/***** Minimum time between consecutive tests, per question *****/
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"RT\"");
HTM_LABEL_Begin ("for=\"MinTimeNxtTstPerQst\" class=\"%s\"",
The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_Minimum_time_seconds_per_question_between_two_tests);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TD_Begin ("class=\"LB\"");
snprintf (StrMinTimeNxtTstPerQst,sizeof (StrMinTimeNxtTstPerQst),
"%lu",
Gbl.Test.Config.MinTimeNxtTstPerQst);
HTM_INPUT_TEXT ("MinTimeNxtTstPerQst",7,StrMinTimeNxtTstPerQst,false,
"size=\"7\" required=\"required\"");
HTM_TD_End ();
HTM_TR_End ();
/***** Feedback to students *****/
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"%s RT\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_Feedback_to_students);
HTM_TD_End ();
HTM_TD_Begin ("class=\"LB\"");
for (Feedback = (Tst_Feedback_t) 0;
Feedback < Tst_NUM_TYPES_FEEDBACK;
Feedback++)
{
HTM_LABEL_Begin ("class=\"DAT\"");
HTM_INPUT_RADIO ("Feedback",false,
"value=\"%u\"%s",
(unsigned) Feedback,
Feedback == Gbl.Test.Config.Feedback ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out,"%s",Txt_TST_STR_FEEDBACK[Feedback]);
HTM_LABEL_End ();
fprintf (Gbl.F.Out," ");
}
HTM_TD_End ();
HTM_TR_End ();
HTM_TABLE_End ();
/***** Send button *****/
Btn_PutConfirmButton (Txt_Save_changes);
/***** End form *****/
Frm_EndForm ();
/***** End box *****/
Box_BoxEnd ();
}
/*****************************************************************************/
/*************** Get configuration of test for current course ****************/
/*****************************************************************************/
static void Tst_PutInputFieldNumQst (const char *Field,const char *Label,
unsigned Value)
{
char StrValue[10 + 1];
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"RM\"");
HTM_LABEL_Begin ("for=\"%s\" class=\"DAT\"",Field);
fprintf (Gbl.F.Out,"%s",Label);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TD_Begin ("class=\"LM\"");
snprintf (StrValue,sizeof (StrValue),
"%u",
Value);
HTM_INPUT_TEXT (Field,3,StrValue,false,
"size=\"3\" required=\"required\"");
HTM_TD_End ();
HTM_TR_End ();
}
/*****************************************************************************/
/*************** Get configuration of test for current course ****************/
/*****************************************************************************/
void Tst_GetConfigTstFromDB (void)
{
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned long NumRows;
/***** Get configuration of test for current course from database *****/
NumRows = DB_QuerySELECT (&mysql_res,"can not get configuration of test",
"SELECT Pluggable," // row[0]
"Min," // row[1]
"Def," // row[2]
"Max," // row[3]
"MinTimeNxtTstPerQst," // row[4]
"Feedback" // row[5]
" FROM tst_config WHERE CrsCod=%ld",
Gbl.Hierarchy.Crs.CrsCod);
Gbl.Test.Config.Feedback = Tst_FEEDBACK_DEFAULT;
Gbl.Test.Config.MinTimeNxtTstPerQst = 0UL;
if (NumRows == 0)
{
Gbl.Test.Config.Pluggable = Tst_PLUGGABLE_UNKNOWN;
Gbl.Test.Config.Min = Tst_CONFIG_DEFAULT_MIN_QUESTIONS;
Gbl.Test.Config.Def = Tst_CONFIG_DEFAULT_DEF_QUESTIONS;
Gbl.Test.Config.Max = Tst_CONFIG_DEFAULT_MAX_QUESTIONS;
}
else // NumRows == 1
{
/***** Get minimun, default and maximum *****/
row = mysql_fetch_row (mysql_res);
Tst_GetConfigFromRow (row);
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/************ Get configuration values from a database table row *************/
/*****************************************************************************/
void Tst_GetConfigFromRow (MYSQL_ROW row)
{
int IntNum;
long LongNum;
Tst_Pluggable_t Pluggable;
Tst_Feedback_t Feedback;
/***** Get whether test are visible via plugins or not *****/
Gbl.Test.Config.Pluggable = Tst_PLUGGABLE_UNKNOWN;
for (Pluggable = Tst_PLUGGABLE_NO;
Pluggable <= Tst_PLUGGABLE_YES;
Pluggable++)
if (!strcmp (row[0],Tst_PluggableDB[Pluggable]))
{
Gbl.Test.Config.Pluggable = Pluggable;
break;
}
/***** Get number of questions *****/
if (sscanf (row[1],"%d",&IntNum) == 1)
Gbl.Test.Config.Min = (IntNum < 1) ? 1 :
(unsigned) IntNum;
else
Gbl.Test.Config.Min = Tst_CONFIG_DEFAULT_MIN_QUESTIONS;
if (sscanf (row[2],"%d",&IntNum) == 1)
Gbl.Test.Config.Def = (IntNum < 1) ? 1 :
(unsigned) IntNum;
else
Gbl.Test.Config.Def = Tst_CONFIG_DEFAULT_DEF_QUESTIONS;
if (sscanf (row[3],"%d",&IntNum) == 1)
Gbl.Test.Config.Max = (IntNum < 1) ? 1 :
(unsigned) IntNum;
else
Gbl.Test.Config.Max = Tst_CONFIG_DEFAULT_MAX_QUESTIONS;
/***** Check and correct numbers *****/
Tst_CheckAndCorrectNumbersQst ();
/***** Get minimum time between consecutive tests, per question (row[4]) *****/
if (sscanf (row[4],"%ld",&LongNum) == 1)
Gbl.Test.Config.MinTimeNxtTstPerQst = (LongNum < 1L) ? 0UL :
(unsigned long) LongNum;
/***** Get feedback type (row[5]) *****/
for (Feedback = (Tst_Feedback_t) 0;
Feedback < Tst_NUM_TYPES_FEEDBACK;
Feedback++)
if (!strcmp (row[5],Tst_FeedbackDB[Feedback]))
{
Gbl.Test.Config.Feedback = Feedback;
break;
}
}
/*****************************************************************************/
/*************** Get configuration of test for current course ****************/
/*****************************************************************************/
// Returns true if course has test tags and pluggable is unknown
// Return false if course has no test tags or pluggable is known
bool Tst_CheckIfCourseHaveTestsAndPluggableIsUnknown (void)
{
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned long NumRows;
Tst_Pluggable_t Pluggable;
/***** Get pluggability of tests for current course from database *****/
NumRows = DB_QuerySELECT (&mysql_res,"can not get configuration of test",
"SELECT Pluggable" // row[0]
" FROM tst_config"
" WHERE CrsCod=%ld",
Gbl.Hierarchy.Crs.CrsCod);
if (NumRows == 0)
Gbl.Test.Config.Pluggable = Tst_PLUGGABLE_UNKNOWN;
else // NumRows == 1
{
/***** Get whether test are visible via plugins or not *****/
row = mysql_fetch_row (mysql_res);
Gbl.Test.Config.Pluggable = Tst_PLUGGABLE_UNKNOWN;
for (Pluggable = Tst_PLUGGABLE_NO;
Pluggable <= Tst_PLUGGABLE_YES;
Pluggable++)
if (!strcmp (row[0],Tst_PluggableDB[Pluggable]))
{
Gbl.Test.Config.Pluggable = Pluggable;
break;
}
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
/***** Get if current course has tests from database *****/
if (Gbl.Test.Config.Pluggable == Tst_PLUGGABLE_UNKNOWN)
return Tst_CheckIfCurrentCrsHasTestTags (); // Return true if course has tests
return false; // Pluggable is not unknown
}
/*****************************************************************************/
/************* Receive configuration of test for current course **************/
/*****************************************************************************/
void Tst_ReceiveConfigTst (void)
{
extern const char *Txt_The_test_configuration_has_been_updated;
/***** Get whether test are visible via plugins or not *****/
Gbl.Test.Config.Pluggable = Tst_GetPluggableFromForm ();
/***** Get number of questions *****/
/* Get minimum number of questions */
Gbl.Test.Config.Min = (unsigned)
Par_GetParToUnsignedLong ("NumQstMin",
1,
UINT_MAX,
1);
/* Get default number of questions */
Gbl.Test.Config.Def = (unsigned)
Par_GetParToUnsignedLong ("NumQstDef",
1,
UINT_MAX,
1);
/* Get maximum number of questions */
Gbl.Test.Config.Max = (unsigned)
Par_GetParToUnsignedLong ("NumQstMax",
1,
UINT_MAX,
1);
/* Check and correct numbers */
Tst_CheckAndCorrectNumbersQst ();
/***** Get minimum time between consecutive tests, per question *****/
Gbl.Test.Config.MinTimeNxtTstPerQst = Par_GetParToUnsignedLong ("MinTimeNxtTstPerQst",
0,
ULONG_MAX,
0);
/***** Get type of feedback from form *****/
Gbl.Test.Config.Feedback = Tst_GetFeedbackTypeFromForm ();
/***** Update database *****/
DB_QueryREPLACE ("can not save configuration of tests",
"REPLACE INTO tst_config"
" (CrsCod,Pluggable,Min,Def,Max,MinTimeNxtTstPerQst,Feedback)"
" VALUES"
" (%ld,'%s',%u,%u,%u,'%lu','%s')",
Gbl.Hierarchy.Crs.CrsCod,
Tst_PluggableDB[Gbl.Test.Config.Pluggable],
Gbl.Test.Config.Min,Gbl.Test.Config.Def,Gbl.Test.Config.Max,
Gbl.Test.Config.MinTimeNxtTstPerQst,
Tst_FeedbackDB[Gbl.Test.Config.Feedback]);
/***** Show confirmation message *****/
Ale_ShowAlert (Ale_SUCCESS,Txt_The_test_configuration_has_been_updated);
/***** Show again the form to configure test *****/
Tst_ShowFormConfig ();
}
/*****************************************************************************/
/******************* Get if tests are pluggable from form ********************/
/*****************************************************************************/
static Tst_Pluggable_t Tst_GetPluggableFromForm (void)
{
return (Tst_Pluggable_t)
Par_GetParToUnsignedLong ("Pluggable",
0,
Tst_NUM_OPTIONS_PLUGGABLE - 1,
(unsigned long) Tst_PLUGGABLE_UNKNOWN);
}
/*****************************************************************************/
/*********************** Get type of feedback from form **********************/
/*****************************************************************************/
static Tst_Feedback_t Tst_GetFeedbackTypeFromForm (void)
{
return (Tst_Feedback_t)
Par_GetParToUnsignedLong ("Feedback",
0,
Tst_NUM_TYPES_FEEDBACK - 1,
(unsigned long) Tst_FEEDBACK_DEFAULT);
}
/*****************************************************************************/
/**** Check and correct minimum, default and maximum numbers of questions ****/
/*****************************************************************************/
static void Tst_CheckAndCorrectNumbersQst (void)
{
/***** Check if minimum is correct *****/
if (Gbl.Test.Config.Min < 1)
Gbl.Test.Config.Min = 1;
else if (Gbl.Test.Config.Min > Tst_MAX_QUESTIONS_PER_TEST)
Gbl.Test.Config.Min = Tst_MAX_QUESTIONS_PER_TEST;
/***** Check if maximum is correct *****/
if (Gbl.Test.Config.Max < 1)
Gbl.Test.Config.Max = 1;
else if (Gbl.Test.Config.Max > Tst_MAX_QUESTIONS_PER_TEST)
Gbl.Test.Config.Max = Tst_MAX_QUESTIONS_PER_TEST;
/***** Check if minimum is lower than maximum *****/
if (Gbl.Test.Config.Min > Gbl.Test.Config.Max)
Gbl.Test.Config.Min = Gbl.Test.Config.Max;
/***** Check if default is correct *****/
if (Gbl.Test.Config.Def < Gbl.Test.Config.Min)
Gbl.Test.Config.Def = Gbl.Test.Config.Min;
else if (Gbl.Test.Config.Def > Gbl.Test.Config.Max)
Gbl.Test.Config.Def = Gbl.Test.Config.Max;
}
/*****************************************************************************/
/***************** Show form for select the types of answers *****************/
/*****************************************************************************/
static void Tst_ShowFormAnswerTypes (unsigned NumCols)
{
extern const char *The_ClassFormInBox[The_NUM_THEMES];
extern const char *Txt_Types_of_answers;
extern const char *Txt_All_types_of_answers;
extern const char *Txt_TST_STR_ANSWER_TYPES[Tst_NUM_ANS_TYPES];
Tst_AnswerType_t AnsType;
bool Checked;
char UnsignedStr[10 + 1];
const char *Ptr;
HTM_TR_Begin (NULL);
/***** Label *****/
HTM_TD_Begin ("class=\"RT %s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
fprintf (Gbl.F.Out,"%s:",Txt_Types_of_answers);
HTM_TD_End ();
/***** Select all types of answers *****/
if (NumCols > 1)
HTM_TD_Begin ("colspan=\"%u\" class=\"LT\"",NumCols);
else
HTM_TD_Begin ("class=\"LT\"");
HTM_TABLE_BeginPadding (2);
HTM_TR_Begin (NULL);
HTM_TD_Begin ("class=\"LM\"");
HTM_LABEL_Begin ("class=\"%s\"",The_ClassFormInBox[Gbl.Prefs.Theme]);
HTM_INPUT_CHECKBOX ("AllAnsTypes",false,
"value=\"Y\"%s onclick=\"togglecheckChildren(this,'AnswerType');\"",
Gbl.Test.AllAnsTypes ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out," %s",Txt_All_types_of_answers);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TR_End ();
/***** Type of answer *****/
for (AnsType = (Tst_AnswerType_t) 0;
AnsType < Tst_NUM_ANS_TYPES;
AnsType++)
{
HTM_TR_Begin (NULL);
Checked = false;
Ptr = Gbl.Test.ListAnsTypes;
while (*Ptr)
{
Par_GetNextStrUntilSeparParamMult (&Ptr,UnsignedStr,10);
if (Tst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr) == AnsType)
{
Checked = true;
break;
}
}
HTM_TD_Begin ("class=\"LM\"");
HTM_LABEL_Begin ("class=\"DAT\"");
HTM_INPUT_CHECKBOX ("AnswerType",false,
"value=\"%u\"%s onclick=\"checkParent(this,'AllAnsTypes');\"",
(unsigned) AnsType,
Checked ? " checked=\"checked\"" : "");
fprintf (Gbl.F.Out," %s",Txt_TST_STR_ANSWER_TYPES[AnsType]);
HTM_LABEL_End ();
HTM_TD_End ();
HTM_TR_End ();
}
HTM_TABLE_End ();
HTM_TD_End ();
HTM_TR_End ();
}
/*****************************************************************************/
/***************** List several test questions for edition *******************/
/*****************************************************************************/
void Tst_ListQuestionsToEdit (void)
{
MYSQL_RES *mysql_res;
unsigned long NumRows;
/***** Get parameters, query the database and list the questions *****/
if (Tst_GetParamsTst (Tst_EDIT_TEST)) // Get parameters from the form
{
if ((NumRows = Tst_GetQuestions (&mysql_res)) != 0) // Query database
{
/***** Contextual menu *****/
Mnu_ContextMenuBegin ();
if (Gbl.Test.XML.CreateXML)
TsI_CreateXML (NumRows,mysql_res); // Create XML file with exported questions...
// ...and put a link to download it
else
TsI_PutFormToExportQuestions (); // Export questions
Mnu_ContextMenuEnd ();
/* Show the table with the questions */
Tst_ListOneOrMoreQuestionsForEdition (NumRows,mysql_res);
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
else
/* Show the form again */
Tst_ShowFormAskEditTsts ();
/***** Free memory used by the list of tags *****/
Tst_FreeTagsList ();
}
/*****************************************************************************/
/**************** List several test questions for selection ******************/
/*****************************************************************************/
void Tst_ListQuestionsToSelect (void)
{
MYSQL_RES *mysql_res;
unsigned long NumRows;
/***** Get parameters, query the database and list the questions *****/
if (Tst_GetParamsTst (Tst_SELECT_QUESTIONS_FOR_GAME)) // Get parameters from the form
{
if ((NumRows = Tst_GetQuestions (&mysql_res)) != 0) // Query database
/* Show the table with the questions */
Tst_ListOneOrMoreQuestionsForSelection (NumRows,mysql_res);
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
else
/* Show the form again */
Tst_ShowFormAskSelectTstsForGame ();
/***** Free memory used by the list of tags *****/
Tst_FreeTagsList ();
}
/*****************************************************************************/
/********** Get from the database several test questions for listing *********/
/*****************************************************************************/
#define Tst_MAX_BYTES_QUERY_TEST (16 * 1024 - 1)
static unsigned long Tst_GetQuestions (MYSQL_RES **mysql_res)
{
extern const char *Txt_No_questions_found_matching_your_search_criteria;
char *Query = NULL;
unsigned long NumRows;
long LengthQuery;
unsigned NumItemInList;
const char *Ptr;
char TagText[Tst_MAX_BYTES_TAG + 1];
char LongStr[1 + 10 + 1];
char UnsignedStr[10 + 1];
Tst_AnswerType_t AnsType;
char CrsCodStr[1 + 10 + 1];
/***** Allocate space for query *****/
if ((Query = (char *) malloc (Tst_MAX_BYTES_QUERY_TEST + 1)) == NULL)
Lay_NotEnoughMemoryExit ();
/***** Select questions *****/
/* Start query */
snprintf (Query,Tst_MAX_BYTES_QUERY_TEST + 1,
"SELECT tst_questions.QstCod," // row[0]
"UNIX_TIMESTAMP(tst_questions.EditTime)," // row[1]
"tst_questions.AnsType," // row[2]
"tst_questions.Shuffle," // row[3]
"tst_questions.Stem," // row[4]
"tst_questions.Feedback," // row[5]
"tst_questions.MedCod," // row[6]
"tst_questions.NumHits," // row[7]
"tst_questions.NumHitsNotBlank," // row[8]
"tst_questions.Score" // row[9]
" FROM tst_questions");
if (!Gbl.Test.Tags.All)
Str_Concat (Query,",tst_question_tags,tst_tags",
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query," WHERE tst_questions.CrsCod='",
Tst_MAX_BYTES_QUERY_TEST);
snprintf (CrsCodStr,sizeof (CrsCodStr),
"%ld",
Gbl.Hierarchy.Crs.CrsCod);
Str_Concat (Query,CrsCodStr,
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"' AND tst_questions.EditTime>=FROM_UNIXTIME('",
Tst_MAX_BYTES_QUERY_TEST);
snprintf (LongStr,sizeof (LongStr),
"%ld",
(long) Gbl.DateRange.TimeUTC[0]);
Str_Concat (Query,LongStr,
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"') AND tst_questions.EditTime<=FROM_UNIXTIME('",
Tst_MAX_BYTES_QUERY_TEST);
snprintf (LongStr,sizeof (LongStr),
"%ld",
(long) Gbl.DateRange.TimeUTC[1]);
Str_Concat (Query,LongStr,
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"')",
Tst_MAX_BYTES_QUERY_TEST);
/* Add the tags selected */
if (!Gbl.Test.Tags.All)
{
Str_Concat (Query," AND tst_questions.QstCod=tst_question_tags.QstCod"
" AND tst_question_tags.TagCod=tst_tags.TagCod"
" AND tst_tags.CrsCod='",
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,CrsCodStr,
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"'",
Tst_MAX_BYTES_QUERY_TEST);
LengthQuery = strlen (Query);
NumItemInList = 0;
Ptr = Gbl.Test.Tags.List;
while (*Ptr)
{
Par_GetNextStrUntilSeparParamMult (&Ptr,TagText,Tst_MAX_BYTES_TAG);
LengthQuery = LengthQuery + 35 + strlen (TagText) + 1;
if (LengthQuery > Tst_MAX_BYTES_QUERY_TEST - 256)
Lay_ShowErrorAndExit ("Query size exceed.");
Str_Concat (Query,
NumItemInList ? " OR tst_tags.TagTxt='" :
" AND (tst_tags.TagTxt='",
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,TagText,
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"'",
Tst_MAX_BYTES_QUERY_TEST);
NumItemInList++;
}
Str_Concat (Query,")",
Tst_MAX_BYTES_QUERY_TEST);
}
/* Add the types of answer selected */
if (!Gbl.Test.AllAnsTypes)
{
LengthQuery = strlen (Query);
NumItemInList = 0;
Ptr = Gbl.Test.ListAnsTypes;
while (*Ptr)
{
Par_GetNextStrUntilSeparParamMult (&Ptr,UnsignedStr,Tst_MAX_BYTES_TAG);
AnsType = Tst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr);
LengthQuery = LengthQuery + 35 + strlen (Tst_StrAnswerTypesDB[AnsType]) + 1;
if (LengthQuery > Tst_MAX_BYTES_QUERY_TEST - 256)
Lay_ShowErrorAndExit ("Query size exceed.");
Str_Concat (Query,
NumItemInList ? " OR tst_questions.AnsType='" :
" AND (tst_questions.AnsType='",
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,Tst_StrAnswerTypesDB[AnsType],
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"'",
Tst_MAX_BYTES_QUERY_TEST);
NumItemInList++;
}
Str_Concat (Query,")",
Tst_MAX_BYTES_QUERY_TEST);
}
/* End the query */
Str_Concat (Query," GROUP BY tst_questions.QstCod",
Tst_MAX_BYTES_QUERY_TEST);
switch (Gbl.Test.SelectedOrder)
{
case Tst_ORDER_STEM:
Str_Concat (Query," ORDER BY tst_questions.Stem",
Tst_MAX_BYTES_QUERY_TEST);
break;
case Tst_ORDER_NUM_HITS:
Str_Concat (Query," ORDER BY tst_questions.NumHits DESC,"
"tst_questions.Stem",
Tst_MAX_BYTES_QUERY_TEST);
break;
case Tst_ORDER_AVERAGE_SCORE:
Str_Concat (Query," ORDER BY tst_questions.Score/tst_questions.NumHits DESC,"
"tst_questions.NumHits DESC,"
"tst_questions.Stem",
Tst_MAX_BYTES_QUERY_TEST);
break;
case Tst_ORDER_NUM_HITS_NOT_BLANK:
Str_Concat (Query," ORDER BY tst_questions.NumHitsNotBlank DESC,"
"tst_questions.Stem",
Tst_MAX_BYTES_QUERY_TEST);
break;
case Tst_ORDER_AVERAGE_SCORE_NOT_BLANK:
Str_Concat (Query," ORDER BY tst_questions.Score/tst_questions.NumHitsNotBlank DESC,"
"tst_questions.NumHitsNotBlank DESC,"
"tst_questions.Stem",
Tst_MAX_BYTES_QUERY_TEST);
break;
}
/* Make the query */
NumRows = DB_QuerySELECT (mysql_res,"can not get questions",
"%s",
Query);
if (NumRows == 0)
Ale_ShowAlert (Ale_INFO,Txt_No_questions_found_matching_your_search_criteria);
return NumRows;
}
/*****************************************************************************/
/********* Get from the database several test questions to list them *********/
/*****************************************************************************/
static unsigned long Tst_GetQuestionsForTest (MYSQL_RES **mysql_res)
{
char *Query = NULL;
long LengthQuery;
unsigned NumItemInList;
const char *Ptr;
char TagText[Tst_MAX_BYTES_TAG + 1];
char UnsignedStr[10 + 1];
Tst_AnswerType_t AnsType;
char StrNumQsts[10 + 1];
/***** Allocate space for query *****/
if ((Query = (char *) malloc (Tst_MAX_BYTES_QUERY_TEST + 1)) == NULL)
Lay_NotEnoughMemoryExit ();
/***** Select questions without hidden tags *****/
/* Start query */
// Reject questions with any tag hidden
// Select only questions with tags
// DISTINCTROW is necessary to not repeat questions
snprintf (Query,Tst_MAX_BYTES_QUERY_TEST + 1,
"SELECT DISTINCTROW tst_questions.QstCod," // row[0]
"UNIX_TIMESTAMP(tst_questions.EditTime)," // row[1]
"tst_questions.AnsType," // row[2]
"tst_questions.Shuffle," // row[3]
"tst_questions.Stem," // row[4]
"tst_questions.Feedback," // row[5]
"tst_questions.MedCod," // row[6]
"tst_questions.NumHits," // row[7]
"tst_questions.NumHitsNotBlank," // row[8]
"tst_questions.Score" // row[9]
" FROM tst_questions,tst_question_tags,tst_tags"
" WHERE tst_questions.CrsCod=%ld"
" AND tst_questions.QstCod NOT IN"
" (SELECT tst_question_tags.QstCod"
" FROM tst_tags,tst_question_tags"
" WHERE tst_tags.CrsCod=%ld AND tst_tags.TagHidden='Y'"
" AND tst_tags.TagCod=tst_question_tags.TagCod)"
" AND tst_questions.QstCod=tst_question_tags.QstCod"
" AND tst_question_tags.TagCod=tst_tags.TagCod"
" AND tst_tags.CrsCod=%ld",
Gbl.Hierarchy.Crs.CrsCod,
Gbl.Hierarchy.Crs.CrsCod,
Gbl.Hierarchy.Crs.CrsCod);
if (!Gbl.Test.Tags.All) // User has not selected all the tags
{
/* Add selected tags */
LengthQuery = strlen (Query);
NumItemInList = 0;
Ptr = Gbl.Test.Tags.List;
while (*Ptr)
{
Par_GetNextStrUntilSeparParamMult (&Ptr,TagText,Tst_MAX_BYTES_TAG);
LengthQuery = LengthQuery + 35 + strlen (TagText) + 1;
if (LengthQuery > Tst_MAX_BYTES_QUERY_TEST - 128)
Lay_ShowErrorAndExit ("Query size exceed.");
Str_Concat (Query,
NumItemInList ? " OR tst_tags.TagTxt='" :
" AND (tst_tags.TagTxt='",
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,TagText,
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"'",
Tst_MAX_BYTES_QUERY_TEST);
NumItemInList++;
}
Str_Concat (Query,")",
Tst_MAX_BYTES_QUERY_TEST);
}
/* Add answer types selected */
if (!Gbl.Test.AllAnsTypes)
{
LengthQuery = strlen (Query);
NumItemInList = 0;
Ptr = Gbl.Test.ListAnsTypes;
while (*Ptr)
{
Par_GetNextStrUntilSeparParamMult (&Ptr,UnsignedStr,Tst_MAX_BYTES_TAG);
AnsType = Tst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr);
LengthQuery = LengthQuery + 35 + strlen (Tst_StrAnswerTypesDB[AnsType]) + 1;
if (LengthQuery > Tst_MAX_BYTES_QUERY_TEST - 128)
Lay_ShowErrorAndExit ("Query size exceed.");
Str_Concat (Query,
NumItemInList ? " OR tst_questions.AnsType='" :
" AND (tst_questions.AnsType='",
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,Tst_StrAnswerTypesDB[AnsType],
Tst_MAX_BYTES_QUERY_TEST);
Str_Concat (Query,"'",
Tst_MAX_BYTES_QUERY_TEST);
NumItemInList++;
}
Str_Concat (Query,")",
Tst_MAX_BYTES_QUERY_TEST);
}
/* End query */
Str_Concat (Query," ORDER BY RAND(NOW()) LIMIT ",
Tst_MAX_BYTES_QUERY_TEST);
snprintf (StrNumQsts,sizeof (StrNumQsts),
"%u",
Gbl.Test.NumQsts);
Str_Concat (Query,StrNumQsts,
Tst_MAX_BYTES_QUERY_TEST);
/*
if (Gbl.Usrs.Me.Roles.LoggedRole == Rol_SYS_ADM)
Lay_ShowAlert (Lay_INFO,Query);
*/
/* Make the query */
return DB_QuerySELECT (mysql_res,"can not get questions",
"%s",
Query);
}
/*****************************************************************************/
/*********************** List a test question for edition ********************/
/*****************************************************************************/
static void Tst_ListOneQstToEdit (void)
{
MYSQL_RES *mysql_res;
/***** Query database *****/
if (Tst_GetOneQuestionByCod (Gbl.Test.QstCod,&mysql_res))
/***** Show the question ready to edit it *****/
Tst_ListOneOrMoreQuestionsForEdition (1,mysql_res);
else
Lay_ShowErrorAndExit ("Can not get question.");
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/*********************** Get data of one test question ***********************/
/*****************************************************************************/
// Return true on success, false on error
bool Tst_GetOneQuestionByCod (long QstCod,MYSQL_RES **mysql_res)
{
/***** Get data of a question from database *****/
return (DB_QuerySELECT (mysql_res,"can not get data of a question",
"SELECT QstCod," // row[0]
"UNIX_TIMESTAMP(EditTime)," // row[1]
"AnsType," // row[2]
"Shuffle," // row[3]
"Stem," // row[4]
"Feedback," // row[5]
"MedCod," // row[6]
"NumHits," // row[7]
"NumHitsNotBlank," // row[8]
"Score" // row[9]
" FROM tst_questions"
" WHERE QstCod=%ld",
QstCod) == 1);
}
/*****************************************************************************/
/****************** List for edition one or more test questions **************/
/*****************************************************************************/
static void Tst_ListOneOrMoreQuestionsForEdition (unsigned long NumRows,
MYSQL_RES *mysql_res)
{
extern const char *Hlp_ASSESSMENT_Tests;
extern const char *Txt_Questions;
extern const char *Txt_No_INDEX;
extern const char *Txt_Code;
extern const char *Txt_Date;
extern const char *Txt_Tags;
extern const char *Txt_TST_STR_ORDER_FULL[Tst_NUM_TYPES_ORDER_QST];
extern const char *Txt_TST_STR_ORDER_SHORT[Tst_NUM_TYPES_ORDER_QST];
extern const char *Txt_TST_STR_ANSWER_TYPES[Tst_NUM_ANS_TYPES];
extern const char *Txt_Shuffle;
Tst_QuestionsOrder_t Order;
unsigned long NumRow;
MYSQL_ROW row;
unsigned UniqueId;
char *Id;
time_t TimeUTC;
unsigned long NumHitsThisQst;
unsigned long NumHitsNotBlankThisQst;
double TotalScoreThisQst;
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Questions,Tst_PutIconsTests,
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
/***** Write the heading *****/
HTM_TABLE_BeginWideMarginPadding (2);
HTM_TR_Begin (NULL);
HTM_TH_Empty (1);
HTM_TH (1,1,"CT",Txt_No_INDEX);
HTM_TH (1,1,"CT",Txt_Code);
HTM_TH (1,1,"CT",Txt_Date);
HTM_TH (1,1,"CT",Txt_Tags);
HTM_TH (1,1,"CT",Txt_Shuffle);
/* Stem and answers of question */
/* Number of times that the question has been answered */
/* Average score */
for (Order = (Tst_QuestionsOrder_t) 0;
Order < (Tst_QuestionsOrder_t) Tst_NUM_TYPES_ORDER_QST;
Order++)
{
HTM_TH_Begin (1,1,"LT");
if (NumRows > 1)
{
Frm_StartForm (ActLstTstQst);
Dat_WriteParamsIniEndDates ();
Tst_WriteParamEditQst ();
Par_PutHiddenParamUnsigned (NULL,"Order",(unsigned) Order);
Frm_LinkFormSubmit (Txt_TST_STR_ORDER_FULL[Order],"TIT_TBL",NULL);
if (Order == Gbl.Test.SelectedOrder)
fprintf (Gbl.F.Out,"");
}
fprintf (Gbl.F.Out,"%s",Txt_TST_STR_ORDER_SHORT[Order]);
if (NumRows > 1)
{
if (Order == Gbl.Test.SelectedOrder)
fprintf (Gbl.F.Out,"");
Frm_LinkFormEnd ();
Frm_EndForm ();
}
HTM_TH_End ();
}
HTM_TR_End ();
/***** Write rows *****/
for (NumRow = 0, UniqueId = 1;
NumRow < NumRows;
NumRow++, UniqueId++)
{
Gbl.RowEvenOdd = NumRow % 2;
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
*/
/***** Create test question *****/
Tst_QstConstructor ();
/* row[0] holds the code of the question */
if ((Gbl.Test.QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
Lay_ShowErrorAndExit ("Wrong code of question.");
HTM_TR_Begin (NULL);
/***** Icons *****/
HTM_TD_Begin ("class=\"BT%u\"",Gbl.RowEvenOdd);
/* Write icon to remove the question */
Frm_StartForm (ActReqRemTstQst);
Tst_PutParamQstCod ();
if (NumRows == 1)
Par_PutHiddenParamChar ("OnlyThisQst",'Y'); // If there are only one row, don't list again after removing
Dat_WriteParamsIniEndDates ();
Tst_WriteParamEditQst ();
Ico_PutIconRemove ();
Frm_EndForm ();
/* Write icon to edit the question */
Ico_PutContextualIconToEdit (ActEdiOneTstQst,Tst_PutParamQstCod);
HTM_TD_End ();
HTM_TD_Begin ("class=\"RT COLOR%u\"",Gbl.RowEvenOdd);
/* Write number of question */
HTM_DIV_Begin ("class=\"BIG_INDEX\"");
fprintf (Gbl.F.Out,"%lu",NumRow + 1);
HTM_DIV_End ();
/* Write answer type (row[2]) */
Gbl.Test.AnswerType = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[2]);
HTM_DIV_Begin ("class=\"DAT_SMALL\"");
fprintf (Gbl.F.Out,"%s",Txt_TST_STR_ANSWER_TYPES[Gbl.Test.AnswerType]);
HTM_DIV_End ();
HTM_TD_End ();
/* Write question code */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%ld ",Gbl.Test.QstCod);
HTM_TD_End ();
/* Write the date (row[1] has the UTC date-time) */
TimeUTC = Dat_GetUNIXTimeFromStr (row[1]);
if (asprintf (&Id,"tst_date_%u",UniqueId) < 0)
Lay_NotEnoughMemoryExit ();
HTM_TD_Begin ("id=\"%s\" class=\"DAT_SMALL CT COLOR%u\"",
Id,Gbl.RowEvenOdd);
Dat_WriteLocalDateHMSFromUTC (Id,TimeUTC,
Gbl.Prefs.DateFormat,Dat_SEPARATOR_BREAK,
true,true,false,0x7);
HTM_TD_End ();
free ((void *) Id);
/* Write the question tags */
HTM_TD_Begin ("class=\"LT COLOR%u\"",Gbl.RowEvenOdd);
Tst_GetAndWriteTagsQst (Gbl.Test.QstCod);
HTM_TD_End ();
/* Write if shuffle is enabled (row[3]) */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
if (Gbl.Test.AnswerType == Tst_ANS_UNIQUE_CHOICE ||
Gbl.Test.AnswerType == Tst_ANS_MULTIPLE_CHOICE)
{
Frm_StartForm (ActShfTstQst);
Tst_PutParamQstCod ();
Dat_WriteParamsIniEndDates ();
Tst_WriteParamEditQst ();
if (NumRows == 1)
Par_PutHiddenParamChar ("OnlyThisQst",'Y'); // If editing only one question, don't edit others
Par_PutHiddenParamUnsigned (NULL,"Order",(unsigned) Gbl.Test.SelectedOrder);
HTM_INPUT_CHECKBOX ("Shuffle",true,
"value=\"Y\"%s",
row[3][0] == 'Y' ? " checked=\"checked\"" : "");
Frm_EndForm ();
}
HTM_TD_End ();
/* Write stem (row[4]) */
HTM_TD_Begin ("class=\"LT COLOR%u\"",Gbl.RowEvenOdd);
Tst_WriteQstStem (row[4],"TEST_EDI");
/***** Get and show media (row[6]) *****/
Gbl.Test.Media.MedCod = Str_ConvertStrCodToLongCod (row[6]);
Med_GetMediaDataByCod (&Gbl.Test.Media);
Med_ShowMedia (&Gbl.Test.Media,
"TEST_MED_EDIT_LIST_CONTAINER",
"TEST_MED_EDIT_LIST");
/* Write feedback (row[5]) and answers */
Tst_WriteQstFeedback (row[5],"TEST_EDI_LIGHT");
Tst_WriteAnswersEdit (Gbl.Test.QstCod);
HTM_TD_End ();
/* Get number of hits
(number of times that the question has been answered,
including blank answers) (row[7]) */
if (sscanf (row[7],"%lu",&NumHitsThisQst) != 1)
Lay_ShowErrorAndExit ("Wrong number of hits to a question.");
/* Get number of hits not blank
(number of times that the question has been answered
with a not blank answer) (row[8]) */
if (sscanf (row[8],"%lu",&NumHitsNotBlankThisQst) != 1)
Lay_ShowErrorAndExit ("Wrong number of hits not blank to a question.");
/* Get the acumulated score of the question (row[9]) */
Str_SetDecimalPointToUS (); // To get the decimal point as a dot
if (sscanf (row[9],"%lf",&TotalScoreThisQst) != 1)
Lay_ShowErrorAndExit ("Wrong score of a question.");
Str_SetDecimalPointToLocal (); // Return to local system
/* Write number of times this question has been answered */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%lu",NumHitsThisQst);
HTM_TD_End ();
/* Write average score */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
if (NumHitsThisQst)
fprintf (Gbl.F.Out,"%.2f",TotalScoreThisQst /
(double) NumHitsThisQst);
else
fprintf (Gbl.F.Out,"N.A.");
HTM_TD_End ();
/* Write number of times this question has been answered (not blank) */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%lu",NumHitsNotBlankThisQst);
HTM_TD_End ();
/* Write average score (not blank) */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
if (NumHitsNotBlankThisQst)
fprintf (Gbl.F.Out,"%.2f",TotalScoreThisQst /
(double) NumHitsNotBlankThisQst);
else
fprintf (Gbl.F.Out,"N.A.");
HTM_TD_End ();
HTM_TR_End ();
/***** Destroy test question *****/
Tst_QstDestructor ();
}
/***** End table *****/
HTM_TABLE_End ();
/***** Button to add a new question *****/
Tst_PutButtonToAddQuestion ();
/***** End box *****/
Box_BoxEnd ();
}
/*****************************************************************************/
/****************** List for edition one or more test questions **************/
/*****************************************************************************/
static void Tst_ListOneOrMoreQuestionsForSelection (unsigned long NumRows,
MYSQL_RES *mysql_res)
{
extern const char *Hlp_ASSESSMENT_Games_questions;
extern const char *Txt_Questions;
extern const char *Txt_No_INDEX;
extern const char *Txt_Code;
extern const char *Txt_Date;
extern const char *Txt_Tags;
extern const char *Txt_Type;
extern const char *Txt_TST_STR_ANSWER_TYPES[Tst_NUM_ANS_TYPES];
extern const char *Txt_Shuffle;
extern const char *Txt_Question;
extern const char *Txt_Add_questions;
unsigned long NumRow;
MYSQL_ROW row;
unsigned UniqueId;
char *Id;
time_t TimeUTC;
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Questions,NULL,
Hlp_ASSESSMENT_Games_questions,Box_NOT_CLOSABLE);
/***** Begin form *****/
Frm_StartForm (ActAddTstQstToGam);
Gam_PutParams ();
/***** Write the heading *****/
HTM_TABLE_BeginWideMarginPadding (2);
HTM_TR_Begin (NULL);
HTM_TH_Empty (1);
HTM_TH (1,1,"CT",Txt_No_INDEX);
HTM_TH (1,1,"CT",Txt_Code);
HTM_TH (1,1,"CT",Txt_Date);
HTM_TH (1,1,"LT",Txt_Tags);
HTM_TH (1,1,"CT",Txt_Type);
HTM_TH (1,1,"CT",Txt_Shuffle);
HTM_TH (1,1,"CT",Txt_Question);
HTM_TR_End ();
/***** Write rows *****/
for (NumRow = 0, UniqueId = 1;
NumRow < NumRows;
NumRow++, UniqueId++)
{
Gbl.RowEvenOdd = NumRow % 2;
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
*/
/***** Create test question *****/
Tst_QstConstructor ();
/* row[0] holds the code of the question */
if ((Gbl.Test.QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
Lay_ShowErrorAndExit ("Wrong code of question.");
HTM_TR_Begin (NULL);
/***** Icons *****/
HTM_TD_Begin ("class=\"BT%u\"",Gbl.RowEvenOdd);
/* Write checkbox to select the question */
HTM_INPUT_CHECKBOX ("QstCods",false,
"value=\"%ld\"",
Gbl.Test.QstCod);
/* Write number of question */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%lu ",NumRow + 1);
HTM_TD_End ();
/* Write question code */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%ld ",Gbl.Test.QstCod);
HTM_TD_End ();
/* Write the date (row[1] has the UTC date-time) */
TimeUTC = Dat_GetUNIXTimeFromStr (row[1]);
if (asprintf (&Id,"tst_date_%u",UniqueId) < 0)
Lay_NotEnoughMemoryExit ();
HTM_TD_Begin ("id=\"%s\" class=\"DAT_SMALL CT COLOR%u\">",
Id,Gbl.RowEvenOdd);
Dat_WriteLocalDateHMSFromUTC (Id,TimeUTC,
Gbl.Prefs.DateFormat,Dat_SEPARATOR_BREAK,
true,true,false,0x7);
HTM_TD_End ();
free ((void *) Id);
/* Write the question tags */
HTM_TD_Begin ("class=\"LT COLOR%u\"",Gbl.RowEvenOdd);
Tst_GetAndWriteTagsQst (Gbl.Test.QstCod);
HTM_TD_End ();
/* Write the question type (row[2]) */
Gbl.Test.AnswerType = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[2]);
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
fprintf (Gbl.F.Out,"%s ",Txt_TST_STR_ANSWER_TYPES[Gbl.Test.AnswerType]);
HTM_TD_End ();
/* Write if shuffle is enabled (row[3]) */
HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd);
HTM_INPUT_CHECKBOX ("Shuffle",false,
"value=\"Y\"%s disabled=\"disabled\"",
row[3][0] == 'Y' ? " checked=\"checked\"" : "");
HTM_TD_End ();
/* Write stem (row[4]) */
HTM_TD_Begin ("class=\"LT COLOR%u\"",Gbl.RowEvenOdd);
Tst_WriteQstStem (row[4],"TEST_EDI");
/***** Get and show media (row[6]) *****/
Gbl.Test.Media.MedCod = Str_ConvertStrCodToLongCod (row[6]);
Med_GetMediaDataByCod (&Gbl.Test.Media);
Med_ShowMedia (&Gbl.Test.Media,
"TEST_MED_EDIT_LIST_CONTAINER",
"TEST_MED_EDIT_LIST");
/* Write feedback (row[5]) */
Tst_WriteQstFeedback (row[5],"TEST_EDI_LIGHT");
/* Write answers */
Tst_WriteAnswersEdit (Gbl.Test.QstCod);
HTM_TD_End ();
HTM_TR_End ();
/***** Destroy test question *****/
Tst_QstDestructor ();
}
/***** End table *****/
HTM_TABLE_End ();
/***** Button to add questions *****/
Btn_PutConfirmButton (Txt_Add_questions);
/***** End form *****/
Frm_EndForm ();
/***** End box *****/
Box_BoxEnd ();
}
/*****************************************************************************/
/*********** Write hidden parameters for edition of test questions ***********/
/*****************************************************************************/
void Tst_WriteParamEditQst (void)
{
Par_PutHiddenParamChar ("AllTags",
Gbl.Test.Tags.All ? 'Y' :
'N');
Par_PutHiddenParamString (NULL,"ChkTag",
Gbl.Test.Tags.List ? Gbl.Test.Tags.List :
"");
Par_PutHiddenParamChar ("AllAnsTypes",
Gbl.Test.AllAnsTypes ? 'Y' :
'N');
Par_PutHiddenParamString (NULL,"AnswerType",Gbl.Test.ListAnsTypes);
}
/*****************************************************************************/
/*************** Get answers of a test question from database ****************/
/*****************************************************************************/
unsigned Tst_GetNumAnswersQst (long QstCod)
{
return (unsigned) DB_QueryCOUNT ("can not get number of answers of a question",
"SELECT COUNT(*)"
" FROM tst_answers"
" WHERE QstCod=%ld",
QstCod);
}
unsigned Tst_GetAnswersQst (long QstCod,MYSQL_RES **mysql_res,bool Shuffle)
{
unsigned long NumRows;
/***** Get answers of a question from database *****/
NumRows = DB_QuerySELECT (mysql_res,"can not get answers of a question",
"SELECT AnsInd," // row[0]
"Answer," // row[1]
"Feedback," // row[2]
"MedCod," // row[3]
"Correct" // row[4]
" FROM tst_answers"
" WHERE QstCod=%ld"
" ORDER BY %s",
QstCod,
Shuffle ? "RAND(NOW())" :
"AnsInd");
if (!NumRows)
Ale_ShowAlert (Ale_ERROR,"Error when getting answers of a question.");
return (unsigned) NumRows;
}
void Tst_GetCorrectAnswersFromDB (long QstCod)
{
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned NumOpt;
/***** Query database *****/
Gbl.Test.Answer.NumOptions = (unsigned)
DB_QuerySELECT (&mysql_res,"can not get answers of a question",
"SELECT Correct" // row[0]
" FROM tst_answers"
" WHERE QstCod=%ld"
" ORDER BY AnsInd",
QstCod);
for (NumOpt = 0;
NumOpt < Gbl.Test.Answer.NumOptions;
NumOpt++)
{
/* Get next answer */
row = mysql_fetch_row (mysql_res);
/* Assign correctness (row[0]) of this answer (this option) */
Gbl.Test.Answer.Options[NumOpt].Correct = (row[0][0] == 'Y');
}
/* Free structure that stores the query result */
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/**************** Get and write the answers of a test question ***************/
/*****************************************************************************/
void Tst_WriteAnswersEdit (long QstCod)
{
extern const char *Txt_TST_Answer_given_by_the_teachers;
unsigned NumOpt;
unsigned i;
MYSQL_RES *mysql_res;
MYSQL_ROW row;
char *Answer;
char *Feedback;
size_t LengthAnswer;
size_t LengthFeedback;
double FloatNum[2];
Gbl.Test.Answer.NumOptions = Tst_GetAnswersQst (QstCod,&mysql_res,false);
/*
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.F.Out,"(%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.F.Out,"([%lg; %lg])",
FloatNum[0],FloatNum[1]);
break;
case Tst_ANS_TRUE_FALSE:
Tst_CheckIfNumberOfAnswersIsOne ();
row = mysql_fetch_row (mysql_res);
fprintf (Gbl.F.Out,"(");
Tst_WriteAnsTF (row[1][0]);
fprintf (Gbl.F.Out,")");
break;
case Tst_ANS_UNIQUE_CHOICE:
case Tst_ANS_MULTIPLE_CHOICE:
case Tst_ANS_TEXT:
HTM_TABLE_BeginPadding (2);
for (NumOpt = 0;
NumOpt < Gbl.Test.Answer.NumOptions;
NumOpt++)
{
row = mysql_fetch_row (mysql_res);
/* Convert the answer (row[1]), that is in HTML, to rigorous HTML */
LengthAnswer = strlen (row[1]) * Str_MAX_BYTES_PER_CHAR;
if ((Answer = (char *) malloc (LengthAnswer + 1)) == NULL)
Lay_NotEnoughMemoryExit ();
Str_Copy (Answer,row[1],
LengthAnswer);
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
Answer,LengthAnswer,false);
/* Convert the feedback (row[2]), that is in HTML, to rigorous HTML */
LengthFeedback = 0;
Feedback = NULL;
if (row[2])
if (row[2][0])
{
LengthFeedback = strlen (row[2]) * Str_MAX_BYTES_PER_CHAR;
if ((Feedback = (char *) malloc (LengthFeedback + 1)) == NULL)
Lay_NotEnoughMemoryExit ();
Str_Copy (Feedback,row[2],
LengthFeedback);
Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
Feedback,LengthFeedback,false);
}
/* Get media (row[3]) */
Gbl.Test.Answer.Options[NumOpt].Media.MedCod = Str_ConvertStrCodToLongCod (row[3]);
Med_GetMediaDataByCod (&Gbl.Test.Answer.Options[NumOpt].Media);
HTM_TR_Begin (NULL);
/* Put an icon that indicates whether the answer
is correct or wrong (row[4]) */
HTM_TD_Begin ("class=\"BT%u\"",Gbl.RowEvenOdd);
if (row[4][0] == 'Y')
Ico_PutIcon ("check.svg",Txt_TST_Answer_given_by_the_teachers,"CONTEXT_ICO_16x16");
HTM_TD_End ();
/* Write the number of option */
HTM_TD_Begin ("class=\"DAT_SMALL LT\"");
fprintf (Gbl.F.Out,"%c) ",'a' + (char) NumOpt);
HTM_TD_End ();
HTM_TD_Begin ("class=\"LT\"");
/* Write the text of the answer and the media */
HTM_DIV_Begin ("class=\"TEST_EDI\"");
fprintf (Gbl.F.Out,"%s",Answer);
Med_ShowMedia (&Gbl.Test.Answer.Options[NumOpt].Media,
"TEST_MED_EDIT_LIST_CONTAINER",
"TEST_MED_EDIT_LIST");
HTM_DIV_End ();
/* Write the text of the feedback */
HTM_DIV_Begin ("class=\"TEST_EDI_LIGHT\"");
if (LengthFeedback)
fprintf (Gbl.F.Out,"%s",Feedback);
HTM_DIV_End ();
HTM_TD_End ();
HTM_TR_End ();
/* Free memory allocated for the answer and the feedback */
free ((void *) Answer);
if (LengthFeedback)
free ((void *) Feedback);
}
HTM_TABLE_End ();
break;
default:
break;
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/************** Write answers of a question when viewing a test **************/
/*****************************************************************************/
static void Tst_WriteAnswersTestToAnswer (unsigned NumQst,long QstCod,bool Shuffle)
{
/***** Write parameter with question code *****/
Tst_WriteParamQstCod (NumQst,QstCod);
/***** Write answer depending on type *****/
switch (Gbl.Test.AnswerType)
{
case Tst_ANS_INT:
Tst_WriteIntAnsViewTest (NumQst);
break;
case Tst_ANS_FLOAT:
Tst_WriteFloatAnsViewTest (NumQst);
break;
case Tst_ANS_TRUE_FALSE:
Tst_WriteTFAnsViewTest (NumQst);
break;
case Tst_ANS_UNIQUE_CHOICE:
case Tst_ANS_MULTIPLE_CHOICE:
Tst_WriteChoiceAnsViewTest (NumQst,QstCod,Shuffle);
break;
case Tst_ANS_TEXT:
Tst_WriteTextAnsViewTest (NumQst);
break;
default:
break;
}
}
/*****************************************************************************/
/************* Write answers of a question when assessing a test *************/
/*****************************************************************************/
static void Tst_WriteAnswersTestResult (struct UsrData *UsrDat,
unsigned NumQst,long QstCod,
double *ScoreThisQst,bool *AnswerIsNotBlank)
{
MYSQL_RES *mysql_res;
/***** Get answers of a question from database *****/
Gbl.Test.Answer.NumOptions = Tst_GetAnswersQst (QstCod,&mysql_res,false);
/*
row[0] AnsInd
row[1] Answer
row[2] Feedback
row[3] MedCod
row[4] Correct
*/
/***** Write answer depending on type *****/
switch (Gbl.Test.AnswerType)
{
case Tst_ANS_INT:
Tst_WriteIntAnsAssessTest (UsrDat,NumQst,mysql_res,ScoreThisQst,AnswerIsNotBlank);
break;
case Tst_ANS_FLOAT:
Tst_WriteFloatAnsAssessTest (UsrDat,NumQst,mysql_res,ScoreThisQst,AnswerIsNotBlank);
break;
case Tst_ANS_TRUE_FALSE:
Tst_WriteTFAnsAssessTest (UsrDat,NumQst,mysql_res,ScoreThisQst,AnswerIsNotBlank);
break;
case Tst_ANS_UNIQUE_CHOICE:
case Tst_ANS_MULTIPLE_CHOICE:
Tst_WriteChoiceAnsAssessTest (UsrDat,NumQst,mysql_res,ScoreThisQst,AnswerIsNotBlank);
break;
case Tst_ANS_TEXT:
Tst_WriteTextAnsAssessTest (UsrDat,NumQst,mysql_res,ScoreThisQst,AnswerIsNotBlank);
break;
default:
break;
}
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/***************** Check if a question is valid for a game *******************/
/*****************************************************************************/
bool Tst_CheckIfQuestionIsValidForGame (long QstCod)
{
/***** Check if a question is valid for a game from database *****/
return DB_QueryCOUNT ("can not check type of a question",
"SELECT COUNT(*)"
" FROM tst_questions"
" WHERE QstCod=%ld AND AnsType='%s'",
QstCod,Tst_StrAnswerTypesDB[Tst_ANS_UNIQUE_CHOICE]) != 0;
}
/*****************************************************************************/
/************** Write false / true answer when viewing a test ****************/
/*****************************************************************************/
static void Tst_WriteTFAnsViewTest (unsigned NumQst)
{
extern const char *Txt_TF_QST[2];
/***** Write selector for the answer *****/
fprintf (Gbl.F.Out,"