From 1a0d5b4471910059712a9237066efabcf5fc56ff Mon Sep 17 00:00:00 2001 From: acanas Date: Wed, 13 May 2020 00:28:32 +0200 Subject: [PATCH] Version19.226 --- sql/swad.sql | 33 +++- swad_changelog.h | 25 ++- swad_database.c | 69 ++++++-- swad_exam.c | 94 ++++++----- swad_exam_event.c | 140 ---------------- swad_exam_print.c | 86 ++++++++-- swad_exam_result.c | 12 +- swad_exam_set.c | 391 +++++++++++++++++++++++++++++++++++++++++---- swad_exam_set.h | 9 +- swad_media.c | 150 +++++++++++++++-- swad_media.h | 2 + swad_message.c | 8 +- swad_test.c | 35 ++-- swad_test.h | 6 +- swad_test_print.c | 2 +- swad_test_type.h | 3 + 16 files changed, 775 insertions(+), 290 deletions(-) diff --git a/sql/swad.sql b/sql/swad.sql index 33a3522b..7c93668c 100644 --- a/sql/swad.sql +++ b/sql/swad.sql @@ -551,13 +551,6 @@ CREATE TABLE IF NOT EXISTS exa_prints ( UNIQUE INDEX(PrnCod), UNIQUE INDEX(EvtCod,UsrCod)); -- --- Table exa_questions: stores the questions in the set of questions for exams --- -CREATE TABLE IF NOT EXISTS exa_questions ( - SetCod INT NOT NULL, - QstCod INT NOT NULL, - UNIQUE INDEX(SetCod,QstCod)); --- -- Table exa_results: stores exam results -- CREATE TABLE IF NOT EXISTS exa_results ( @@ -570,6 +563,32 @@ CREATE TABLE IF NOT EXISTS exa_results ( Score DOUBLE PRECISION NOT NULL DEFAULT 0, UNIQUE INDEX(EvtCod,UsrCod)); -- +-- Table exa_set_answers: stores the answers of questions in exam sets +-- +CREATE TABLE IF NOT EXISTS exa_set_answers ( + QstCod INT NOT NULL, + AnsInd TINYINT NOT NULL, + Answer TEXT NOT NULL, + Feedback TEXT NOT NULL, + MedCod INT NOT NULL DEFAULT -1, + Correct ENUM('N','Y') NOT NULL, + UNIQUE INDEX(QstCod,AnsInd), + INDEX(MedCod)); +-- +-- Table exa_set_questions: stores the questions in exam sets +-- +CREATE TABLE IF NOT EXISTS exa_set_questions ( + QstCod INT NOT NULL AUTO_INCREMENT, + SetCod INT NOT NULL, + AnsType ENUM ('int','float','true_false','unique_choice','multiple_choice','text') NOT NULL, + Shuffle ENUM('N','Y') NOT NULL, + Stem TEXT NOT NULL, + Feedback TEXT NOT NULL, + MedCod INT NOT NULL DEFAULT -1, + UNIQUE INDEX(QstCod), + UNIQUE INDEX(SetCod,QstCod), + INDEX(MedCod)); +-- -- Table exa_sets: stores the question sets in the exams -- CREATE TABLE IF NOT EXISTS exa_sets ( diff --git a/swad_changelog.h b/swad_changelog.h index 14599082..064c7099 100644 --- a/swad_changelog.h +++ b/swad_changelog.h @@ -548,10 +548,33 @@ enscript -2 --landscape --color --file-align=2 --highlight --line-numbers -o - * En OpenSWAD: ps2pdf source.ps destination.pdf */ -#define Log_PLATFORM_VERSION "SWAD 19.225.1 (2020-05-12)" +#define Log_PLATFORM_VERSION "SWAD 19.226 (2020-05-12)" #define CSS_FILE "swad19.217.css" #define JS_FILE "swad19.223.js" /* +// TODO: Public link to images on exams should be cached during the current session. When session is closed ==> remove public link + + Version 19.226: May 12, 2020 Questions and answer are cloned from test bank to exams. (303468 lines) + 18 changes necessary in database: +CREATE TABLE IF NOT EXISTS exa_set_answers (QstCod INT NOT NULL,AnsInd TINYINT NOT NULL,Answer TEXT NOT NULL,Feedback TEXT NOT NULL,MedCod INT NOT NULL DEFAULT -1,Correct ENUM('N','Y') NOT NULL,UNIQUE INDEX(QstCod,AnsInd),INDEX(MedCod)); +CREATE TABLE IF NOT EXISTS exa_set_questions (QstCod INT NOT NULL AUTO_INCREMENT,SetCod INT NOT NULL,AnsType ENUM ('int','float','true_false','unique_choice','multiple_choice','text') NOT NULL,Shuffle ENUM('N','Y') NOT NULL,Stem TEXT NOT NULL,Feedback TEXT NOT NULL,MedCod INT NOT NULL DEFAULT -1,UNIQUE INDEX(QstCod),UNIQUE INDEX(SetCod,QstCod),INDEX(MedCod)); +DELETE FROM exa_answers; +DELETE FROM exa_events; +DELETE FROM exa_exams; +DELETE FROM exa_groups; +DELETE FROM exa_happening; +DELETE FROM exa_indexes; +DELETE FROM exa_participants; +DELETE FROM exa_print_questions; +DELETE FROM exa_prints; +DELETE FROM exa_questions; +DELETE FROM exa_results; +DELETE FROM exa_set_answers; +DELETE FROM exa_set_questions; +DELETE FROM exa_sets; +DELETE FROM exa_times; +DROP TABLE exa_questions; + Version 19.225.1: May 12, 2020 Code refactoring in exam prints. Unckeck radio button in exam prints. (303057 lines) Version 19.225: May 12, 2020 Stored unique/multiple choice questions in exam print. diff --git a/swad_database.c b/swad_database.c index 7e89b57e..913e939b 100644 --- a/swad_database.c +++ b/swad_database.c @@ -1216,22 +1216,6 @@ mysql> DESCRIBE exa_prints; "UNIQUE INDEX(PrnCod)," "UNIQUE INDEX(EvtCod,UsrCod))"); - /***** Table exa_questions *****/ -/* -mysql> DESCRIBE exa_questions; -+--------+---------+------+-----+---------+-------+ -| Field | Type | Null | Key | Default | Extra | -+--------+---------+------+-----+---------+-------+ -| SetCod | int(11) | NO | PRI | NULL | | -| QstCod | int(11) | NO | PRI | NULL | | -+--------+---------+------+-----+---------+-------+ -2 rows in set (0.00 sec) -*/ - DB_CreateTable ("CREATE TABLE IF NOT EXISTS exa_questions (" - "SetCod INT NOT NULL," - "QstCod INT NOT NULL," - "UNIQUE INDEX(SetCod,QstCod))"); - /***** Table exa_results *****/ /* mysql> DESCRIBE exa_results; @@ -1258,6 +1242,59 @@ mysql> DESCRIBE exa_results; "Score DOUBLE PRECISION NOT NULL DEFAULT 0," "UNIQUE INDEX(EvtCod,UsrCod))"); + /***** Table exa_set_answers *****/ +/* +mysql> DESCRIBE exa_set_answers; ++----------+---------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++----------+---------------+------+-----+---------+-------+ +| QstCod | int(11) | NO | PRI | NULL | | +| AnsInd | tinyint(4) | NO | PRI | NULL | | +| Answer | text | NO | | NULL | | +| Feedback | text | NO | | NULL | | +| MedCod | int(11) | NO | MUL | -1 | | +| Correct | enum('N','Y') | NO | | NULL | | ++----------+---------------+------+-----+---------+-------+ +6 rows in set (0.00 sec) +*/ + DB_CreateTable ("CREATE TABLE IF NOT EXISTS exa_set_answers (" + "QstCod INT NOT NULL," + "AnsInd TINYINT NOT NULL," + "Answer TEXT NOT NULL," // Tst_MAX_BYTES_ANSWER_OR_FEEDBACK + "Feedback TEXT NOT NULL," // Tst_MAX_BYTES_ANSWER_OR_FEEDBACK + "MedCod INT NOT NULL DEFAULT -1," + "Correct ENUM('N','Y') NOT NULL," + "UNIQUE INDEX(QstCod,AnsInd)," + "INDEX(MedCod))"); + + /***** Table exa_set_questions *****/ +/* +mysql> DESCRIBE exa_set_questions; ++----------+---------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++----------+---------------+------+-----+---------+-------+ +| QstCod | int(11) | NO | PRI | NULL | | +| AnsInd | tinyint(4) | NO | PRI | NULL | | +| Answer | text | NO | | NULL | | +| Feedback | text | NO | | NULL | | +| MedCod | int(11) | NO | MUL | -1 | | +| Correct | enum('N','Y') | NO | | NULL | | ++----------+---------------+------+-----+---------+-------+ +6 rows in set (0.00 sec) + +*/ + DB_CreateTable ("CREATE TABLE IF NOT EXISTS exa_set_questions (" + "QstCod INT NOT NULL AUTO_INCREMENT," + "SetCod INT NOT NULL," + "AnsType ENUM ('int','float','true_false','unique_choice','multiple_choice','text') NOT NULL," + "Shuffle ENUM('N','Y') NOT NULL," + "Stem TEXT NOT NULL," // Cns_MAX_BYTES_TEXT + "Feedback TEXT NOT NULL," // Cns_MAX_BYTES_TEXT + "MedCod INT NOT NULL DEFAULT -1," + "UNIQUE INDEX(QstCod)," + "UNIQUE INDEX(SetCod,QstCod)," + "INDEX(MedCod))"); + /***** Table exa_sets *****/ /* mysql> DESCRIBE exa_sets; diff --git a/swad_exam.c b/swad_exam.c index 6f9c6730..3eb216ee 100644 --- a/swad_exam.c +++ b/swad_exam.c @@ -1181,9 +1181,18 @@ static void Exa_RemoveExamFromAllTables (long ExaCod) /***** Remove all events in this exam *****/ ExaEvt_RemoveEventsInExamFromAllTables (ExaCod); - /***** Remove exam question *****/ + /***** Remove exam questions *****/ DB_QueryDELETE ("can not remove exam questions", - "DELETE FROM exa_questions WHERE ExaCod=%ld", + "DELETE FROM exa_set_questions" + " USING exa_sets,exa_set_questions" + " WHERE exa_sets.ExaCod=%ld" + " AND exa_sets.SetCod=exa_set_questions.SetCod", + ExaCod); + + /***** Remove exam sets *****/ + DB_QueryDELETE ("can not remove exam sets", + "DELETE FROM exa_sets" + " WHERE ExaCod=%ld", ExaCod); /***** Remove exam *****/ @@ -1203,10 +1212,19 @@ void Exa_RemoveExamsCrs (long CrsCod) /***** Remove the questions in exams *****/ DB_QueryDELETE ("can not remove questions in course exams", - "DELETE FROM exa_questions" - " USING exa_exams,exa_questions" + "DELETE FROM exa_set_questions" + " USING exa_exams,exa_sets,exa_set_questions" " WHERE exa_exams.CrsCod=%ld" - " AND exa_exams.ExaCod=exa_questions.ExaCod", + " AND exa_exams.ExaCod=exa_sets.ExaCod", + " AND exa_sets.SetCod=exa_set_questions.SetCod", + CrsCod); + + /***** Remove the sets in exams *****/ + DB_QueryDELETE ("can not remove sets in course exams", + "DELETE FROM exa_sets" + " USING exa_exams,exa_sets" + " WHERE exa_exams.CrsCod=%ld" + " AND exa_exams.ExaCod=exa_sets.ExaCod", CrsCod); /***** Remove the exams *****/ @@ -1678,7 +1696,7 @@ long Exa_GetQstCodFromQstInd (long ExaCod,unsigned QstInd) /***** Get question code of the question to be moved up *****/ if (!DB_QuerySELECT (&mysql_res,"can not get question code", - "SELECT QstCod FROM exa_questions" + "SELECT QstCod FROM exa_set_questions" " WHERE ExaCod=%ld AND QstInd=%u", ExaCod,QstInd)) Lay_ShowErrorAndExit ("Error: wrong question index."); @@ -1710,7 +1728,7 @@ unsigned Exa_GetPrevQuestionIndexInExam (long ExaCod,unsigned QstInd) // Although indexes are always continuous... // ...this implementation works even with non continuous indexes if (!DB_QuerySELECT (&mysql_res,"can not get previous question index", - "SELECT MAX(QstInd) FROM exa_questions" + "SELECT MAX(QstInd) FROM exa_set_questions" " WHERE ExaCod=%ld AND QstInd<%u", ExaCod,QstInd)) Lay_ShowErrorAndExit ("Error: previous question index not found."); @@ -1744,7 +1762,7 @@ unsigned Exa_GetNextQuestionIndexInExam (long ExaCod,unsigned QstInd) // Although indexes are always continuous... // ...this implementation works even with non continuous indexes if (!DB_QuerySELECT (&mysql_res,"can not get next question index", - "SELECT MIN(QstInd) FROM exa_questions" + "SELECT MIN(QstInd) FROM exa_set_questions" " WHERE ExaCod=%ld AND QstInd>%u", ExaCod,QstInd)) Lay_ShowErrorAndExit ("Error: next question index not found."); @@ -1964,69 +1982,69 @@ double Exa_GetNumQstsPerCrsExam (Hie_Level_t Scope) case Hie_SYS: DB_QuerySELECT (&mysql_res,"can not get number of questions per exam", "SELECT AVG(NumQsts) FROM" - " (SELECT COUNT(exa_questions.QstCod) AS NumQsts" - " FROM exa_exams,exa_questions" - " WHERE exa_exams.ExaCod=exa_questions.ExaCod" - " GROUP BY exa_questions.ExaCod) AS NumQstsTable"); + " (SELECT COUNT(exa_set_questions.QstCod) AS NumQsts" + " FROM exa_exams,exa_set_questions" + " WHERE exa_exams.ExaCod=exa_set_questions.ExaCod" + " GROUP BY exa_set_questions.ExaCod) AS NumQstsTable"); break; case Hie_CTY: DB_QuerySELECT (&mysql_res,"can not get number of questions per exam", "SELECT AVG(NumQsts) FROM" - " (SELECT COUNT(exa_questions.QstCod) AS NumQsts" - " FROM institutions,centres,degrees,courses,exa_exams,exa_questions" + " (SELECT COUNT(exa_set_questions.QstCod) AS NumQsts" + " FROM institutions,centres,degrees,courses,exa_exams,exa_set_questions" " WHERE institutions.CtyCod=%ld" " AND institutions.InsCod=centres.InsCod" " AND centres.CtrCod=degrees.CtrCod" " AND degrees.DegCod=courses.DegCod" " AND courses.CrsCod=exa_exams.CrsCod" - " AND exa_exams.ExaCod=exa_questions.ExaCod" - " GROUP BY exa_questions.ExaCod) AS NumQstsTable", + " AND exa_exams.ExaCod=exa_set_questions.ExaCod" + " GROUP BY exa_set_questions.ExaCod) AS NumQstsTable", Gbl.Hierarchy.Cty.CtyCod); break; case Hie_INS: DB_QuerySELECT (&mysql_res,"can not get number of questions per exam", "SELECT AVG(NumQsts) FROM" - " (SELECT COUNT(exa_questions.QstCod) AS NumQsts" - " FROM centres,degrees,courses,exa_exams,exa_questions" + " (SELECT COUNT(exa_set_questions.QstCod) AS NumQsts" + " FROM centres,degrees,courses,exa_exams,exa_set_questions" " WHERE centres.InsCod=%ld" " AND centres.CtrCod=degrees.CtrCod" " AND degrees.DegCod=courses.DegCod" " AND courses.CrsCod=exa_exams.CrsCod" - " AND exa_exams.ExaCod=exa_questions.ExaCod" - " GROUP BY exa_questions.ExaCod) AS NumQstsTable", + " AND exa_exams.ExaCod=exa_set_questions.ExaCod" + " GROUP BY exa_set_questions.ExaCod) AS NumQstsTable", Gbl.Hierarchy.Ins.InsCod); break; case Hie_CTR: DB_QuerySELECT (&mysql_res,"can not get number of questions per exam", "SELECT AVG(NumQsts) FROM" - " (SELECT COUNT(exa_questions.QstCod) AS NumQsts" - " FROM degrees,courses,exa_exams,exa_questions" + " (SELECT COUNT(exa_set_questions.QstCod) AS NumQsts" + " FROM degrees,courses,exa_exams,exa_set_questions" " WHERE degrees.CtrCod=%ld" " AND degrees.DegCod=courses.DegCod" " AND courses.CrsCod=exa_exams.CrsCod" - " AND exa_exams.ExaCod=exa_questions.ExaCod" - " GROUP BY exa_questions.ExaCod) AS NumQstsTable", + " AND exa_exams.ExaCod=exa_set_questions.ExaCod" + " GROUP BY exa_set_questions.ExaCod) AS NumQstsTable", Gbl.Hierarchy.Ctr.CtrCod); break; case Hie_DEG: DB_QuerySELECT (&mysql_res,"can not get number of questions per exam", "SELECT AVG(NumQsts) FROM" - " (SELECT COUNT(exa_questions.QstCod) AS NumQsts" - " FROM courses,exa_exams,exa_questions" + " (SELECT COUNT(exa_set_questions.QstCod) AS NumQsts" + " FROM courses,exa_exams,exa_set_questions" " WHERE courses.DegCod=%ld" " AND courses.CrsCod=exa_exams.CrsCod" - " AND exa_exams.ExaCod=exa_questions.ExaCod" - " GROUP BY exa_questions.ExaCod) AS NumQstsTable", + " AND exa_exams.ExaCod=exa_set_questions.ExaCod" + " GROUP BY exa_set_questions.ExaCod) AS NumQstsTable", Gbl.Hierarchy.Deg.DegCod); break; case Hie_CRS: DB_QuerySELECT (&mysql_res,"can not get number of questions per exam", "SELECT AVG(NumQsts) FROM" - " (SELECT COUNT(exa_questions.QstCod) AS NumQsts" - " FROM exa_exams,exa_questions" + " (SELECT COUNT(exa_set_questions.QstCod) AS NumQsts" + " FROM exa_exams,exa_set_questions" " WHERE exa_exams.Cod=%ld" - " AND exa_exams.ExaCod=exa_questions.ExaCod" - " GROUP BY exa_questions.ExaCod) AS NumQstsTable", + " AND exa_exams.ExaCod=exa_set_questions.ExaCod" + " GROUP BY exa_set_questions.ExaCod) AS NumQstsTable", Gbl.Hierarchy.Crs.CrsCod); break; default: @@ -2060,9 +2078,9 @@ void Exa_ShowTstTagsPresentInAnExam (long ExaCod) "SELECT tst_tags.TagTxt" // row[0] " FROM" " (SELECT DISTINCT(tst_question_tags.TagCod)" - " FROM tst_question_tags,exa_questions" - " WHERE exa_questions.ExaCod=%ld" - " AND exa_questions.QstCod=tst_question_tags.QstCod)" + " FROM tst_question_tags,exa_set_questions" + " WHERE exa_set_questions.ExaCod=%ld" + " AND exa_set_questions.QstCod=tst_question_tags.QstCod)" " AS TagsCods,tst_tags" " WHERE TagsCods.TagCod=tst_tags.TagCod" " ORDER BY tst_tags.TagTxt", @@ -2089,9 +2107,9 @@ void Exa_GetScoreRange (long ExaCod,double *MinScore,double *MaxScore) NumRows = (unsigned) DB_QuerySELECT (&mysql_res,"can not get data of a question", "SELECT COUNT(tst_answers.AnsInd) AS N" - " FROM tst_answers,exa_questions" - " WHERE exa_questions.ExaCod=%ld" - " AND exa_questions.QstCod=tst_answers.QstCod" + " FROM tst_answers,exa_set_questions" + " WHERE exa_set_questions.ExaCod=%ld" + " AND exa_set_questions.QstCod=tst_answers.QstCod" " GROUP BY tst_answers.QstCod", ExaCod); for (NumRow = 0, *MinScore = *MaxScore = 0.0; diff --git a/swad_exam_event.c b/swad_exam_event.c index e1f55289..460c4810 100644 --- a/swad_exam_event.c +++ b/swad_exam_event.c @@ -1847,146 +1847,6 @@ static void ExaEvt_UpdateEvent (struct ExaEvt_Event *Event) ExaEvt_CreateGrps (Event->EvtCod); // Associate new groups } -/*****************************************************************************/ -/******************** Create indexes for an exam event ***********************/ -/*****************************************************************************/ -/* Everytime a new exam event is created, - the answers of each shufflable question are shuffled. - The shuffling is stored in a table of indexes - that will be read when showing an exam event */ -/* -static void ExaEvt_CreateIndexes (long ExaCod,long EvtCod) - { - MYSQL_RES *mysql_res; - MYSQL_ROW row; - unsigned NumQsts; - unsigned NumQst; - struct Tst_Question Question; - long LongNum; - unsigned QstInd; - - ***** Get questions of the exam ***** - NumQsts = (unsigned) - DB_QuerySELECT (&mysql_res,"can not get questions of an exam", - "SELECT exa_questions.QstCod," // row[0] - "exa_questions.QstInd," // row[1] - "tst_questions.AnsType," // row[2] - "tst_questions.Shuffle" // row[3] - " FROM exa_questions,tst_questions" - " WHERE exa_questions.ExaCod=%ld" - " AND exa_questions.QstCod=tst_questions.QstCod" - " ORDER BY exa_questions.QstInd", - ExaCod); - - ***** For each question in exam... ***** - for (NumQst = 0; - NumQst < NumQsts; - NumQst++) - { - ***** Create test question ***** - Tst_QstConstructor (&Question); - - ***** Get question data ***** - row = mysql_fetch_row (mysql_res); - * - exa_questions.QstCod row[0] - exa_questions.QstInd row[1] - tst_questions.AnsType row[2] - tst_questions.Shuffle row[3] - * - - * Get question code (row[0]) * - if ((Question.QstCod = Str_ConvertStrCodToLongCod (row[0])) < 0) - Lay_ShowErrorAndExit ("Wrong code of question."); - - * Get question index (row[1]) * - if ((LongNum = Str_ConvertStrCodToLongCod (row[1])) < 0) - Lay_ShowErrorAndExit ("Wrong question index."); - QstInd = (unsigned) LongNum; - - * Get answer type (row[2]) * - Question.Answer.Type = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[2]); - if (Question.Answer.Type != Tst_ANS_UNIQUE_CHOICE) - Lay_ShowErrorAndExit ("Wrong answer type."); - - * Get shuffle (row[3]) * - Question.Answer.Shuffle = (row[3][0] == 'Y'); - - ***** Reorder answer ***** - ExaEvt_ReorderAnswer (EvtCod,QstInd,&Question); - - ***** Destroy test question ***** - Tst_QstDestructor (&Question); - } - - ***** Free structure that stores the query result ***** - DB_FreeMySQLResult (&mysql_res); - } -*/ -/*****************************************************************************/ -/**************** Reorder answers of an exam event question ******************/ -/*****************************************************************************/ -/* -static void ExaEvt_ReorderAnswer (long EvtCod,unsigned QstInd, - const struct Tst_Question *Question) - { - MYSQL_RES *mysql_res; - MYSQL_ROW row; - unsigned NumAnss; - unsigned NumAns; - long LongNum; - unsigned AnsInd; - char StrOneAnswer[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; - char StrAnswersOneQst[Tst_MAX_BYTES_ANSWERS_ONE_QST + 1]; - - ***** Initialize list of answers to empty string ***** - StrAnswersOneQst[0] = '\0'; - - ***** Get questions of the exam ***** - NumAnss = (unsigned) - DB_QuerySELECT (&mysql_res,"can not get questions of an exam", - "SELECT AnsInd" // row[0] - " FROM tst_answers" - " WHERE QstCod=%ld" - " ORDER BY %s", - Question->QstCod, - Question->Answer.Shuffle ? "RAND()" : // Use RAND() because is really random; RAND(NOW()) repeats order - "AnsInd"); - - ***** For each answer in question... ***** - for (NumAns = 0; - NumAns < NumAnss; - NumAns++) - { - row = mysql_fetch_row (mysql_res); - - * Get answer index (row[0]) * - if ((LongNum = Str_ConvertStrCodToLongCod (row[0])) < 0) - Lay_ShowErrorAndExit ("Wrong answer index."); - AnsInd = (unsigned) LongNum; - snprintf (StrOneAnswer,sizeof (StrOneAnswer), - "%u",AnsInd); - - * Concatenate answer index to list of answers * - if (NumAns) - Str_Concat (StrAnswersOneQst,",", - Tst_MAX_BYTES_ANSWERS_ONE_QST); - Str_Concat (StrAnswersOneQst,StrOneAnswer, - Tst_MAX_BYTES_ANSWERS_ONE_QST); - } - - ***** Free structure that stores the query result ***** - DB_FreeMySQLResult (&mysql_res); - - ***** Create entry for this question in table of exam event indexes ***** - DB_QueryINSERT ("can not create exam event indexes", - "INSERT INTO exa_indexes" - " (EvtCod,QstInd,Indexes)" - " VALUES" - " (%ld,%u,'%s')", - EvtCod,QstInd,StrAnswersOneQst); - } -*/ /*****************************************************************************/ /***************** Get indexes for a question from database ******************/ /*****************************************************************************/ diff --git a/swad_exam_print.c b/swad_exam_print.c index 22918a4b..af58992c 100644 --- a/swad_exam_print.c +++ b/swad_exam_print.c @@ -93,6 +93,8 @@ static void ExaPrn_GetQuestionsForNewPrintFromDB (struct ExaPrn_Print *Print); static unsigned ExaPrn_GetSomeQstsFromSetToPrint (struct ExaPrn_Print *Print, struct ExaSet_Set *Set, unsigned *NumQstInPrint); +static void ExaPrn_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion, + bool Shuffle); static void ExaPrn_CreatePrintInDB (struct ExaPrn_Print *Print); static void ExaPrn_GetPrintQuestionsFromDB (struct ExaPrn_Print *Print); @@ -363,12 +365,11 @@ static unsigned ExaPrn_GetSomeQstsFromSetToPrint (struct ExaPrn_Print *Print, /***** Get questions from database *****/ NumQstsInSet = (unsigned) DB_QuerySELECT (&mysql_res,"can not get questions from set", - "SELECT tst_questions.QstCod," // row[0] - "tst_questions.AnsType," // row[1] - "tst_questions.Shuffle" // row[2] - " FROM exa_questions,tst_questions" - " WHERE exa_questions.setCod=%ld" - " AND exa_questions.QstCod=tst_questions.QstCod" + "SELECT QstCod," // row[0] + "AnsType," // row[1] + "Shuffle" // row[2] + " FROM exa_set_questions" + " WHERE SetCod=%ld" " ORDER BY RAND()" // Don't use RAND(NOW()) because the same ordering will be repeated across sets " LIMIT %u", Set->SetCod, @@ -414,7 +415,7 @@ static unsigned ExaPrn_GetSomeQstsFromSetToPrint (struct ExaPrn_Print *Print, case Tst_ANS_MULTIPLE_CHOICE: /* If answer type is unique or multiple option, generate indexes of answers depending on shuffle */ - Tst_GenerateChoiceIndexesDependingOnShuffle (&Print->PrintedQuestions[*NumQstInPrint],Shuffle); + ExaPrn_GenerateChoiceIndexes (&Print->PrintedQuestions[*NumQstInPrint],Shuffle); break; default: break; @@ -430,6 +431,71 @@ static unsigned ExaPrn_GetSomeQstsFromSetToPrint (struct ExaPrn_Print *Print, return NumQstsInSet; } +/*****************************************************************************/ +/*************** Generate choice indexes depending on shuffle ****************/ +/*****************************************************************************/ + +static void ExaPrn_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion, + bool Shuffle) + { + struct Tst_Question Question; + unsigned NumOpt; + MYSQL_RES *mysql_res; + MYSQL_ROW row; + unsigned Index; + bool ErrorInIndex; + char StrInd[1 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; + + /***** Create test question *****/ + Tst_QstConstructor (&Question); + Question.QstCod = PrintedQuestion->QstCod; + + /***** Get answers of question from database *****/ + ExaSet_GetAnswersQst (&Question,&mysql_res,Shuffle); + /* + row[0] AnsInd + row[1] Answer + row[2] Feedback + row[3] MedCod + row[4] Correct + */ + + for (NumOpt = 0; + NumOpt < Question.Answer.NumOptions; + NumOpt++) + { + /***** Get next answer *****/ + row = mysql_fetch_row (mysql_res); + + /***** Assign index (row[0]). + Index is 0,1,2,3... if no shuffle + or 1,3,0,2... (example) if shuffle *****/ + ErrorInIndex = false; + if (sscanf (row[0],"%u",&Index) == 1) + { + if (Index >= Tst_MAX_OPTIONS_PER_QUESTION) + ErrorInIndex = true; + } + else + ErrorInIndex = true; + if (ErrorInIndex) + Lay_ShowErrorAndExit ("Wrong index of answer."); + + if (NumOpt == 0) + snprintf (StrInd,sizeof (StrInd),"%u",Index); + else + snprintf (StrInd,sizeof (StrInd),",%u",Index); + Str_Concat (PrintedQuestion->StrIndexes,StrInd, + Tst_MAX_BYTES_INDEXES_ONE_QST); + } + + /***** Free structure that stores the query result *****/ + DB_FreeMySQLResult (&mysql_res); + + /***** Destroy test question *****/ + Tst_QstDestructor (&Question); + } + /*****************************************************************************/ /***************** Create new blank exam print in database *******************/ /*****************************************************************************/ @@ -571,8 +637,7 @@ static void ExaPrn_ShowTableWithQstsToFill (struct ExaPrn_Print *Print) Question.QstCod = Print->PrintedQuestions[NumQst].QstCod; /* Show question */ - if (!Tst_GetQstDataFromDB (&Question)) // Question exists - Lay_ShowErrorAndExit ("Wrong question."); + ExaSet_GetQstDataFromDB (&Question,Print->ExaCod); /* Write question and answers */ ExaPrn_WriteQstAndAnsToFill (Print,NumQst,&Question); @@ -981,7 +1046,7 @@ static void ExaPrn_ComputeScoreAndStoreQuestionOfPrint (struct ExaPrn_Print *Pri /***** Compute question score *****/ Tst_QstConstructor (&Question); Question.QstCod = Print->PrintedQuestions[NumQst].QstCod; - Question.Answer.Type = Tst_GetQstAnswerType (Question.QstCod); + Question.Answer.Type = ExaSet_GetQstAnswerTypeFromDB (Question.QstCod); TstPrn_ComputeAnswerScore (&Print->PrintedQuestions[NumQst],&Question); Tst_QstDestructor (&Question); @@ -1000,7 +1065,6 @@ static void ExaPrn_ComputeScoreAndStoreQuestionOfPrint (struct ExaPrn_Print *Pri NumQst); // 0, 1, 2, 3... } - /*****************************************************************************/ /************* Get the questions of an exam print from database **************/ /*****************************************************************************/ diff --git a/swad_exam_result.c b/swad_exam_result.c index 092078d9..bc760b2d 100644 --- a/swad_exam_result.c +++ b/swad_exam_result.c @@ -1326,15 +1326,15 @@ void ExaRes_GetExamResultQuestionsFromDB (long EvtCod,long UsrCod, Print->NumQsts = (unsigned) DB_QuerySELECT (&mysql_res,"can not get questions and answers" " of a event result", - "SELECT exa_questions.QstCod," // row[0] - "exa_questions.QstInd," // row[1] + "SELECT exa_set_questions.QstCod," // row[0] + "exa_set_questions.QstInd," // row[1] "exa_indexes.Indexes" // row[2] - " FROM exa_events,exa_questions,exa_indexes" + " FROM exa_events,exa_set_questions,exa_indexes" " WHERE exa_events.EvtCod=%ld" - " AND exa_events.ExaCod=exa_questions.ExaCod" + " AND exa_events.ExaCod=exa_set_questions.ExaCod" " AND exa_events.EvtCod=exa_indexes.EvtCod" - " AND exa_questions.QstInd=exa_indexes.QstInd" - " ORDER BY exa_questions.QstInd", + " AND exa_set_questions.QstInd=exa_indexes.QstInd" + " ORDER BY exa_set_questions.QstInd", EvtCod); for (NumQst = 0, Print->NumQstsNotBlank = 0; NumQst < Print->NumQsts; diff --git a/swad_exam_set.c b/swad_exam_set.c index 7254418f..831531e5 100644 --- a/swad_exam_set.c +++ b/swad_exam_set.c @@ -123,10 +123,17 @@ static void ExaSet_ListOneOrMoreQuestionsForEdition (struct Exa_Exams *Exams, unsigned NumQsts, MYSQL_RES *mysql_res, bool ICanEditQuestions); +static void ExaSet_ListQuestionForEdition (const struct Tst_Question *Question, + unsigned QstInd,const char *Anchor); static void ExaSet_AllocateListSelectedQuestions (struct Exa_Exams *Exams); static void ExaSet_FreeListsSelectedQuestions (struct Exa_Exams *Exams); +static void ExaSet_CopyQstFromBankToExamSet (struct ExaSet_Set *Set,long QstCod); + +static long ExaSet_GetParamQstCod (void); +static void ExaSet_PutParamQstCod (void *QstCod); // Should be a pointer to long + static void ExaSet_ExchangeSets (long ExaCod, unsigned SetIndTop,unsigned SetIndBottom); @@ -153,7 +160,7 @@ void ExaSet_PutParamsOneSet (void *Exams) static void ExaSet_PutParamsOneQst (void *Exams) { ExaSet_PutParamsOneSet (Exams); - Tst_PutParamQstCod (&(((struct Exa_Exams *) Exams)->QstCod)); + ExaSet_PutParamQstCod (&(((struct Exa_Exams *) Exams)->QstCod)); } /*****************************************************************************/ @@ -175,7 +182,7 @@ static unsigned ExaSet_GetNumQstsInSet (long SetCod) /***** Get number of questions in set from database *****/ return (unsigned) DB_QueryCOUNT ("can not get number of questions in a set", - "SELECT COUNT(*) FROM exa_questions" + "SELECT COUNT(*) FROM exa_set_questions" " WHERE SetCod=%ld", SetCod); } @@ -1042,11 +1049,10 @@ static void ExaSet_ListSetQuestions (struct Exa_Exams *Exams, /***** Get data of questions from database *****/ NumQsts = (unsigned) DB_QuerySELECT (&mysql_res,"can not get exam questions", - "SELECT exa_questions.QstCod" // row[0] - " FROM exa_questions LEFT JOIN tst_questions" // LEFT JOIN because the question could be removed in table of test questions - " ON (exa_questions.QstCod=tst_questions.QstCod)" - " WHERE exa_questions.SetCod=%ld" - " ORDER BY tst_questions.Stem", + "SELECT QstCod" // row[0] + " FROM exa_set_questions" + " WHERE SetCod=%ld" + " ORDER BY Stem", Set->SetCod); /***** Begin box *****/ @@ -1298,12 +1304,10 @@ static void ExaSet_ListOneOrMoreQuestionsForEdition (struct Exa_Exams *Exams, extern const char *Txt_Questions; extern const char *Txt_No_INDEX; extern const char *Txt_Code; - extern const char *Txt_Tags; extern const char *Txt_Question; unsigned NumQst; MYSQL_ROW row; struct Tst_Question Question; - bool QuestionExists; char *Anchor; /***** Build anchor string *****/ @@ -1317,7 +1321,6 @@ static void ExaSet_ListOneOrMoreQuestionsForEdition (struct Exa_Exams *Exams, HTM_TH (1,1,"CT",Txt_No_INDEX); HTM_TH (1,1,"CT",Txt_Code); - HTM_TH (1,1,"CT",Txt_Tags); HTM_TH (1,1,"CT",Txt_Question); HTM_TR_End (); @@ -1360,13 +1363,13 @@ static void ExaSet_ListOneOrMoreQuestionsForEdition (struct Exa_Exams *Exams, /* Put icon to edit the question */ if (ICanEditQuestions) Ico_PutContextualIconToEdit (ActEdiOneTstQst,NULL, - Tst_PutParamQstCod,&Question.QstCod); + ExaSet_PutParamQstCod,&Question.QstCod); HTM_TD_End (); /***** Question *****/ - QuestionExists = Tst_GetQstDataFromDB (&Question); - Tst_ListQuestionForEdition (&Question,NumQst + 1,QuestionExists,Anchor); + ExaSet_GetQstDataFromDB (&Question,Exams->ExaCod); + ExaSet_ListQuestionForEdition (&Question,NumQst + 1,Anchor); /***** End row *****/ HTM_TR_End (); @@ -1382,6 +1385,233 @@ static void ExaSet_ListOneOrMoreQuestionsForEdition (struct Exa_Exams *Exams, Frm_FreeAnchorStr (Anchor); } +/*****************************************************************************/ +/*************** Get answer type of a question from database *****************/ +/*****************************************************************************/ + +Tst_AnswerType_t ExaSet_GetQstAnswerTypeFromDB (long QstCod) + { + MYSQL_RES *mysql_res; + MYSQL_ROW row; + Tst_AnswerType_t AnswerType; + + /***** Get type of answer from database *****/ + if (!DB_QuerySELECT (&mysql_res,"can not get the type of a question", + "SELECT AnsType" // row[0] + " FROM exa_set_questions" + " WHERE QstCod=%ld", + QstCod)) + Lay_ShowErrorAndExit ("Question does not exist."); + + /* Get type of answer */ + row = mysql_fetch_row (mysql_res); + AnswerType = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[0]); + + /* Free structure that stores the query result */ + DB_FreeMySQLResult (&mysql_res); + + return AnswerType; + } + +/*****************************************************************************/ +/*************** Get data of a question in a set from database ***************/ +/*****************************************************************************/ + +void ExaSet_GetQstDataFromDB (struct Tst_Question *Question,long ExaCod) + { + MYSQL_RES *mysql_res; + MYSQL_ROW row; + bool QuestionExists; + unsigned NumOpt; + + /***** Get question data from database *****/ + QuestionExists = (DB_QuerySELECT (&mysql_res,"can not get a question", + "SELECT AnsType," // row[0] + "Shuffle," // row[1] + "Stem," // row[2] + "Feedback," // row[3] + "MedCod" // row[4] + " FROM exa_set_questions" + " WHERE QstCod=%ld" + " AND SetCod IN" + " (SELECT SetCod FROM exa_sets WHERE ExaCod=%ld)", // Extra check + Question->QstCod, + ExaCod) != 0); + + if (QuestionExists) + { + row = mysql_fetch_row (mysql_res); + + /* Get the type of answer (row[0]) */ + Question->Answer.Type = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[0]); + + /* Get shuffle (row[1]) */ + Question->Answer.Shuffle = (row[1][0] == 'Y'); + + /* Get the stem (row[2]) */ + Question->Stem[0] = '\0'; + if (row[2]) + if (row[2][0]) + Str_Copy (Question->Stem,row[2], + Cns_MAX_BYTES_TEXT); + + /* Get the feedback (row[3]) */ + Question->Feedback[0] = '\0'; + if (row[3]) + if (row[3][0]) + Str_Copy (Question->Feedback,row[3], + Cns_MAX_BYTES_TEXT); + + /* Get media (row[4]) */ + Question->Media.MedCod = Str_ConvertStrCodToLongCod (row[4]); + Med_GetMediaDataByCod (&Question->Media); + + /* Free structure that stores the query result */ + DB_FreeMySQLResult (&mysql_res); + + /***** Get the answers from the database *****/ + ExaSet_GetAnswersQst (Question,&mysql_res, + false); // Don't shuffle + /* + row[0] AnsInd + row[1] Answer + row[2] Feedback + row[3] MedCod + row[4] Correct + */ + for (NumOpt = 0; + NumOpt < Question->Answer.NumOptions; + NumOpt++) + { + row = mysql_fetch_row (mysql_res); + switch (Question->Answer.Type) + { + case Tst_ANS_INT: + Tst_CheckIfNumberOfAnswersIsOne (Question); + Question->Answer.Integer = Tst_GetIntAnsFromStr (row[1]); + break; + case Tst_ANS_FLOAT: + if (Question->Answer.NumOptions != 2) + Lay_ShowErrorAndExit ("Wrong answer."); + Question->Answer.FloatingPoint[NumOpt] = Str_GetDoubleFromStr (row[1]); + break; + case Tst_ANS_TRUE_FALSE: + Tst_CheckIfNumberOfAnswersIsOne (Question); + Question->Answer.TF = row[1][0]; + break; + case Tst_ANS_UNIQUE_CHOICE: + case Tst_ANS_MULTIPLE_CHOICE: + case Tst_ANS_TEXT: + /* Check number of options */ + if (Question->Answer.NumOptions > Tst_MAX_OPTIONS_PER_QUESTION) + Lay_ShowErrorAndExit ("Wrong answer."); + + /* Allocate space for text and feedback */ + if (!Tst_AllocateTextChoiceAnswer (Question,NumOpt)) + /* Abort on error */ + Ale_ShowAlertsAndExit (); + + /* Get text (row[1]) */ + Question->Answer.Options[NumOpt].Text[0] = '\0'; + if (row[1]) + if (row[1][0]) + Str_Copy (Question->Answer.Options[NumOpt].Text,row[1], + Tst_MAX_BYTES_ANSWER_OR_FEEDBACK); + + /* Get feedback (row[2]) */ + Question->Answer.Options[NumOpt].Feedback[0] = '\0'; + if (row[2]) + if (row[2][0]) + Str_Copy (Question->Answer.Options[NumOpt].Feedback,row[2], + Tst_MAX_BYTES_ANSWER_OR_FEEDBACK); + + /* Get media (row[3]) */ + Question->Answer.Options[NumOpt].Media.MedCod = Str_ConvertStrCodToLongCod (row[3]); + Med_GetMediaDataByCod (&Question->Answer.Options[NumOpt].Media); + + /* Get if this option is correct (row[4]) */ + Question->Answer.Options[NumOpt].Correct = (row[4][0] == 'Y'); + break; + default: + break; + } + } + } + + /* Free structure that stores the query result */ + DB_FreeMySQLResult (&mysql_res); + + if (!QuestionExists) + Lay_ShowErrorAndExit ("Wrong question."); + } + +/*****************************************************************************/ +/*************** Get answers of a test question from database ****************/ +/*****************************************************************************/ + +void ExaSet_GetAnswersQst (struct Tst_Question *Question,MYSQL_RES **mysql_res, + bool Shuffle) + { + /***** Get answers of a question from database *****/ + Question->Answer.NumOptions = (unsigned) + 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 exa_set_answers" + " WHERE QstCod=%ld" + " ORDER BY %s", + Question->QstCod, + Shuffle ? "RAND()" : + "AnsInd"); + if (!Question->Answer.NumOptions) + Ale_ShowAlert (Ale_ERROR,"Error when getting answers of a question."); + } + + +/*****************************************************************************/ +/********************* List question in set for edition **********************/ +/*****************************************************************************/ + +static void ExaSet_ListQuestionForEdition (const struct Tst_Question *Question, + unsigned QstInd,const char *Anchor) + { + /***** Number of question and answer type (row[1]) *****/ + HTM_TD_Begin ("class=\"RT COLOR%u\"",Gbl.RowEvenOdd); + Tst_WriteNumQst (QstInd); + Tst_WriteAnswerType (Question->Answer.Type); + HTM_TD_End (); + + /***** Write question code *****/ + HTM_TD_Begin ("class=\"DAT_SMALL CT COLOR%u\"",Gbl.RowEvenOdd); + HTM_TxtF ("%ld ",Question->QstCod); + HTM_TD_End (); + + /***** Write stem (row[3]) and media *****/ + HTM_TD_Begin ("class=\"LT COLOR%u\"",Gbl.RowEvenOdd); + HTM_ARTICLE_Begin (Anchor); + + /* Write stem */ + Tst_WriteQstStem (Question->Stem,"TEST_EDI", + true); // Visible + + /* Show media */ + Med_ShowMedia (&Question->Media, + "TEST_MED_EDIT_LIST_STEM_CONTAINER", + "TEST_MED_EDIT_LIST_STEM"); + + /* Show feedback */ + Tst_WriteQstFeedback (Question->Feedback,"TEST_EDI_LIGHT"); + + /* Show answers */ + Tst_WriteAnswersListing (Question); + + HTM_ARTICLE_End (); + HTM_TD_End (); + } + /*****************************************************************************/ /************* Add selected test questions to set of questions ***************/ /*****************************************************************************/ @@ -1448,13 +1678,7 @@ void ExaSet_AddQstsToSet (void) if (sscanf (LongStr,"%ld",&QstCod) != 1) Lay_ShowErrorAndExit ("Wrong question code."); - /* Insert question in the table of questions */ - DB_QueryINSERT ("can not add question to set", - "INSERT INTO exa_questions" - " (SetCod,QstCod)" - " VALUES" - " (%ld,%ld)", - Set.SetCod,QstCod); + ExaSet_CopyQstFromBankToExamSet (&Set,QstCod); } } else @@ -1495,6 +1719,93 @@ static void ExaSet_FreeListsSelectedQuestions (struct Exa_Exams *Exams) } } +/*****************************************************************************/ +/******* Copy question and answers from back of questions to exam set ********/ +/*****************************************************************************/ + +static void ExaSet_CopyQstFromBankToExamSet (struct ExaSet_Set *Set,long QstCod) + { + extern const char *Tst_StrAnswerTypesDB[Tst_NUM_ANS_TYPES]; + extern const char *Txt_Question_removed; + struct Tst_Question Question; + long CloneMedCod; + long QstCodInSet; + unsigned NumOpt; + MYSQL_RES *mysql_res; + MYSQL_ROW row; + + /***** Create test question *****/ + Tst_QstConstructor (&Question); + Question.QstCod = QstCod; + + /***** Get data of question from database *****/ + if (Tst_GetQstDataFromDB (&Question)) + { + /***** Clone media *****/ + CloneMedCod = Med_CloneMedia (&Question.Media); + Ale_ShowAlert (Ale_INFO,"DEBUG: CloneMedCod = %ld",CloneMedCod); + + /***** Insert question in table of questions *****/ + QstCodInSet = DB_QueryINSERTandReturnCode ("can not add question to set", + "INSERT INTO exa_set_questions" + " (SetCod,AnsType,Shuffle,Stem,Feedback,MedCod)" + " VALUES" + " (%ld,'%s','%c','%s','%s',%ld)", + Set->SetCod, + Tst_StrAnswerTypesDB[Question.Answer.Type], + Question.Answer.Shuffle ? 'Y' : + 'N', + Question.Stem, + Question.Feedback, + CloneMedCod); + + /***** Get the answers from the database *****/ + Tst_GetAnswersQst (&Question,&mysql_res, + false); // Don't shuffle + /* + row[0] AnsInd + row[1] Answer + row[2] Feedback + row[3] MedCod + row[4] Correct + */ + for (NumOpt = 0; + NumOpt < Question.Answer.NumOptions; + NumOpt++) + { + row = mysql_fetch_row (mysql_res); + + /* Get media (row[3]) */ + Question.Answer.Options[NumOpt].Media.MedCod = Str_ConvertStrCodToLongCod (row[3]); + Med_GetMediaDataByCod (&Question.Answer.Options[NumOpt].Media); + + /* Clone media */ + CloneMedCod = Med_CloneMedia (&Question.Answer.Options[NumOpt].Media); + + /* Copy answer option to exam set */ + DB_QueryINSERT ("can not add answer to set", + "INSERT INTO exa_set_answers" + " (QstCod,AnsInd,Answer,Feedback,MedCod,Correct)" + " VALUES" + " (%ld,%u,'%s','%s',%ld,'%s')", + QstCodInSet, // Question code in set + NumOpt, // Answer index (number of option) + row[1], // Copy of text + row[2], // Copy of feedback + CloneMedCod, // Media code of the new cloned media + row[4]); // Copy of correct + } + + /* Free structure that stores the query result */ + DB_FreeMySQLResult (&mysql_res); + } + else + Ale_ShowAlert (Ale_WARNING,Txt_Question_removed); + + /***** Destroy test question *****/ + Tst_QstDestructor (&Question); + } + /*****************************************************************************/ /***************** Request the removal of a set of questions *****************/ /*****************************************************************************/ @@ -1581,10 +1892,10 @@ void ExaSet_RemoveSet (void) /***** Remove the set from all the tables *****/ /* Remove questions associated to set */ DB_QueryDELETE ("can not remove questions associated to set", - "DELETE FROM exa_questions" - " USING exa_questions,exa_sets" - " WHERE exa_questions.SetCod=%ld" - " AND exa_questions.SetCod=exa_sets.SetCod" + "DELETE FROM exa_set_questions" + " USING exa_set_questions,exa_sets" + " WHERE exa_set_questions.SetCod=%ld" + " AND exa_set_questions.SetCod=exa_sets.SetCod" " AND exa_sets.ExaCod=%ld", // Extra check Set.SetCod,Set.ExaCod); @@ -1771,7 +2082,7 @@ void ExaSet_RequestRemoveQstFromSet (void) Exams.SetCod = Set.SetCod; /***** Get question index *****/ - Exams.QstCod = Tst_GetParamQstCod (); + Exams.QstCod = ExaSet_GetParamQstCod (); /***** Build anchor string *****/ Frm_SetAnchorStr (Set.SetCod,&Anchor); @@ -1828,14 +2139,15 @@ void ExaSet_RemoveQstFromSet (void) Exams.SetCod = Set.SetCod; /***** Get question index *****/ - QstCod = Tst_GetParamQstCod (); + QstCod = ExaSet_GetParamQstCod (); /***** Remove the question from set *****/ /* Remove the question itself */ DB_QueryDELETE ("can not remove a question from a set", - "DELETE FROM exa_questions" - " WHERE SetCod=%ld AND QstCod=%ld", - Set.SetCod,QstCod); + "DELETE FROM exa_set_questions" + " WHERE QstCod=%ld" + " AND SetCod=%ld", // Extra check + QstCod,Set.SetCod); if (!mysql_affected_rows (&Gbl.mysql)) Lay_ShowErrorAndExit ("The question to be removed does not exist."); @@ -1847,6 +2159,27 @@ void ExaSet_RemoveQstFromSet (void) false); // It's not a new exam } +/*****************************************************************************/ +/************ Get the parameter with the code of a test question *************/ +/*****************************************************************************/ + +static long ExaSet_GetParamQstCod (void) + { + /***** Get code of test question *****/ + return Par_GetParToLong ("QstCod"); + } + +/*****************************************************************************/ +/************ Put parameter with question code to edit, remove... ************/ +/*****************************************************************************/ + +static void ExaSet_PutParamQstCod (void *QstCod) // Should be a pointer to long + { + if (QstCod) + if (*((long *) QstCod) > 0) // If question exists + Par_PutHiddenParamLong (NULL,"QstCod",*((long *) QstCod)); + } + /*****************************************************************************/ /*********** Exchange the order of two consecutive sets in an exam ***********/ /*****************************************************************************/ diff --git a/swad_exam_set.h b/swad_exam_set.h index 27018191..c20c0bd1 100644 --- a/swad_exam_set.h +++ b/swad_exam_set.h @@ -28,10 +28,8 @@ /********************************* Headers ***********************************/ /*****************************************************************************/ -// #include "swad_date.h" -// #include "swad_exam_event.h" #include "swad_exam_type.h" -// #include "swad_scope.h" +#include "swad_test_type.h" /*****************************************************************************/ /************************** Public types and constants ***********************/ @@ -63,6 +61,11 @@ void ExaSet_ListExamSets (struct Exa_Exams *Exams, void ExaSet_ResetSet (struct ExaSet_Set *Set); +Tst_AnswerType_t ExaSet_GetQstAnswerTypeFromDB (long QstCod); +void ExaSet_GetQstDataFromDB (struct Tst_Question *Question,long ExaCod); +void ExaSet_GetAnswersQst (struct Tst_Question *Question,MYSQL_RES **mysql_res, + bool Shuffle); + void ExaSet_AddQstsToSet (void); void ExaSet_RequestRemoveSet (void); diff --git a/swad_media.c b/swad_media.c index 553f2856..a9ea0dbc 100644 --- a/swad_media.c +++ b/swad_media.c @@ -307,7 +307,7 @@ void Med_GetMediaDataByCod (struct Media *Media) Length = Cns_MAX_BYTES_WWW; if ((Media->URL = (char *) malloc (Length + 1)) == NULL) - Lay_ShowErrorAndExit ("Error allocating memory for image URL."); + Lay_ShowErrorAndExit ("Error allocating memory for media URL."); Str_Copy (Media->URL,row[2], Length); } @@ -324,7 +324,7 @@ void Med_GetMediaDataByCod (struct Media *Media) Length = Med_MAX_BYTES_TITLE; if ((Media->Title = (char *) malloc (Length + 1)) == NULL) - Lay_ShowErrorAndExit ("Error allocating memory for image title."); + Lay_ShowErrorAndExit ("Error allocating memory for media title."); Str_Copy (Media->Title,row[3], Length); } @@ -1405,13 +1405,6 @@ static bool Med_MoveTmpFileToDefDir (struct Media *Media, void Med_StoreMediaInDB (struct Media *Media) { - /***** Trivial case *****/ - if (Media->Status != Med_MOVED) - { - Med_ResetMedia (Media); // No media inserted in database - return; - } - /***** Insert media into database *****/ Media->MedCod = DB_QueryINSERTandReturnCode ("can not create media", "INSERT INTO media" @@ -1436,9 +1429,8 @@ void Med_ShowMedia (const struct Media *Media, char PathMedPriv[PATH_MAX + 1]; /***** If no media to show ==> nothing to do *****/ - if (Media->Status != Med_STORED_IN_DB) - return; - if (Media->Type == Med_TYPE_NONE) + if (Media->Status != Med_STORED_IN_DB || + Media->Type == Med_TYPE_NONE) return; /***** Start media container *****/ @@ -1808,6 +1800,140 @@ static void Med_AlertThirdPartyCookies (void) Btn_NO_BUTTON,NULL); } +/*****************************************************************************/ +/************************* Create duplicate of media *************************/ +/*****************************************************************************/ + +#define Med_NUM_MEDIA 2 +#define Med_SRC 0 +#define Med_DST 1 + +long Med_CloneMedia (const struct Media *MediaSrc) + { + long MedCod = -1L; + struct Media MediaDst; + struct + { + char Path[PATH_MAX + 1]; + char FullPath[PATH_MAX + 1 + NAME_MAX + 1]; + } MediaPriv[Med_NUM_MEDIA]; + size_t Length; + + /***** If no media ==> nothing to do *****/ + if (MediaSrc->Type == Med_TYPE_NONE) + return MedCod; + + /***** If no media name ==> nothing to do *****/ + if (!MediaSrc->Name) + return MedCod; + if (!MediaSrc->Name[0]) + return MedCod; + + /***** Initialize media *****/ + Med_MediaConstructor (&MediaDst); + + /***** Copy type *****/ + MediaDst.Type = MediaSrc->Type; + + /***** Assign a unique name for the destination media *****/ + Cry_CreateUniqueNameEncrypted (MediaDst.Name); + + /***** Copy media URL *****/ + Med_FreeMediaURL (&MediaDst); + if (MediaSrc->URL) + { + /* Get and limit length of the URL */ + Length = strlen (MediaSrc->URL); + if (Length > Cns_MAX_BYTES_WWW) + Length = Cns_MAX_BYTES_WWW; + if ((MediaDst.URL = (char *) malloc (Length + 1)) == NULL) + Lay_ShowErrorAndExit ("Error allocating memory for media URL."); + Str_Copy (MediaDst.URL,MediaSrc->URL, + Length); + } + + /***** Copy media title *****/ + Med_FreeMediaTitle (&MediaDst); + if (MediaSrc->Title) + { + /* Get and limit length of the title */ + Length = strlen (MediaSrc->Title); + if (Length > Cns_MAX_BYTES_WWW) + Length = Cns_MAX_BYTES_WWW; + if ((MediaDst.Title = (char *) malloc (Length + 1)) == NULL) + Lay_ShowErrorAndExit ("Error allocating memory for media title."); + Str_Copy (MediaDst.Title,MediaSrc->Title, + Length); + } + + /***** Create duplicate of files *****/ + switch (MediaSrc->Type) + { + case Med_JPG: + case Med_GIF: + case Med_MP4: + case Med_WEBM: + case Med_OGG: + /***** Create private directories if not exist *****/ + /* Create private directory for images/videos if it does not exist */ + Fil_CreateDirIfNotExists (Cfg_PATH_MEDIA_PRIVATE); + + /* Build paths to private directories */ + snprintf (MediaPriv[Med_SRC].Path,sizeof (MediaPriv[Med_SRC].Path), + "%s/%c%c", + Cfg_PATH_MEDIA_PRIVATE,MediaSrc->Name[0],MediaSrc->Name[1]); + snprintf (MediaPriv[Med_DST].Path,sizeof (MediaPriv[Med_DST].Path), + "%s/%c%c", + Cfg_PATH_MEDIA_PRIVATE,MediaDst.Name[0],MediaDst.Name[1]); + Fil_CreateDirIfNotExists (MediaPriv[Med_DST].Path); + + /* Build paths to private files */ + snprintf (MediaPriv[Med_SRC].FullPath,sizeof (MediaPriv[Med_SRC].FullPath), + "%s/%s.%s", + MediaPriv[Med_SRC].Path,MediaSrc->Name,Med_Extensions[MediaSrc->Type]); + snprintf (MediaPriv[Med_DST].FullPath,sizeof (MediaPriv[Med_DST].FullPath), + "%s/%s.%s", + MediaPriv[Med_DST].Path,MediaDst.Name,Med_Extensions[MediaSrc->Type]); + + /* Copy file */ + Ale_ShowAlert (Ale_INFO,"DEBUG
" + "MediaPriv[Med_SRC].FullPath = "%s"
" + "MediaPriv[Med_DST].FullPath = "%s"", + MediaPriv[Med_SRC].FullPath, + MediaPriv[Med_DST].FullPath); + + Fil_FastCopyOfFiles (MediaPriv[Med_SRC].FullPath, + MediaPriv[Med_DST].FullPath); + + if (MediaSrc->Type == Med_GIF) + { + /* Build private paths to PNG */ + snprintf (MediaPriv[Med_SRC].FullPath,sizeof (MediaPriv[Med_SRC].FullPath), + "%s/%s.png", + MediaPriv[Med_SRC].Path,MediaSrc->Name); + snprintf (MediaPriv[Med_DST].FullPath,sizeof (MediaPriv[Med_DST].FullPath), + "%s/%s.png", + MediaPriv[Med_DST].Path,MediaDst.Name); + + /* Copy PNG file */ + Fil_FastCopyOfFiles (MediaPriv[Med_SRC].FullPath, + MediaPriv[Med_DST].FullPath); + } + break; + default: + break; + } + + /***** Code to return *****/ + Med_StoreMediaInDB (&MediaDst); + MedCod = MediaDst.MedCod; + + /***** Free media *****/ + Med_MediaDestructor (&MediaDst); + + return MedCod; + } + /*****************************************************************************/ /********** Remove several media files and entries in database ***************/ /*****************************************************************************/ diff --git a/swad_media.h b/swad_media.h index e44272c5..095a6594 100644 --- a/swad_media.h +++ b/swad_media.h @@ -155,6 +155,8 @@ void Med_StoreMediaInDB (struct Media *Media); void Med_ShowMedia (const struct Media *Media, const char *ClassContainer,const char *ClassMedia); +long Med_CloneMedia (const struct Media *MediaSrc); + void Med_RemoveMediaFromAllRows (unsigned NumMedia,MYSQL_RES *mysql_res); void Med_RemoveMedia (long MedCod); diff --git a/swad_message.c b/swad_message.c index 54066b11..02e8f391 100644 --- a/swad_message.c +++ b/swad_message.c @@ -3197,13 +3197,13 @@ static void Msg_ShowASentOrReceivedMessage (struct Msg_Messages *Messages, HTM_TxtColonNBSP (Txt_MSG_Content); HTM_TD_End (); - /***** Initialize image *****/ + /***** Initialize media *****/ Med_MediaConstructor (&Media); - /***** Get message content and optional image *****/ + /***** Get message content and optional media *****/ Msg_GetMsgContent (MsgCod,Content,&Media); - /***** Show content and image *****/ + /***** Show content and media *****/ HTM_TD_Begin ("colspan=\"2\" class=\"MSG_TXT LT\""); if (Content[0]) Msg_WriteMsgContent (Content,Cns_MAX_BYTES_LONG_TEXT,true,false); @@ -3212,7 +3212,7 @@ static void Msg_ShowASentOrReceivedMessage (struct Msg_Messages *Messages, HTM_TR_End (); - /***** Free image *****/ + /***** Free media *****/ Med_MediaDestructor (&Media); } diff --git a/swad_test.c b/swad_test.c index e90c410e..75049914 100644 --- a/swad_test.c +++ b/swad_test.c @@ -74,13 +74,7 @@ const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES] = [Tst_ANS_TEXT ] = "text", }; -/*****************************************************************************/ -/**************************** Private constants ******************************/ -/*****************************************************************************/ - -#define Tst_MAX_BYTES_TAGS_LIST (16 * 1024) - -static const char *Tst_StrAnswerTypesDB[Tst_NUM_ANS_TYPES] = +const char *Tst_StrAnswerTypesDB[Tst_NUM_ANS_TYPES] = { [Tst_ANS_INT ] = "int", [Tst_ANS_FLOAT ] = "float", @@ -90,6 +84,12 @@ static const char *Tst_StrAnswerTypesDB[Tst_NUM_ANS_TYPES] = [Tst_ANS_TEXT ] = "text", }; +/*****************************************************************************/ +/**************************** Private constants ******************************/ +/*****************************************************************************/ + +#define Tst_MAX_BYTES_TAGS_LIST (16 * 1024) + // Test images will be saved with: // - maximum width of Tst_IMAGE_SAVED_MAX_HEIGHT // - maximum height of Tst_IMAGE_SAVED_MAX_HEIGHT @@ -936,7 +936,7 @@ static void TstPrn_WriteQstAndAnsToFill (struct TstPrn_PrintedQuestion *PrintedQ } /*****************************************************************************/ -/******************* List exam/game question for edition *********************/ +/********************* List game question for edition ************************/ /*****************************************************************************/ void Tst_ListQuestionForEdition (const struct Tst_Question *Question, @@ -2662,7 +2662,7 @@ static void Tst_GetQuestionsForNewTestFromDB (struct Tst_Test *Test, case Tst_ANS_MULTIPLE_CHOICE: /* If answer type is unique or multiple option, generate indexes of answers depending on shuffle */ - Tst_GenerateChoiceIndexesDependingOnShuffle (&Print->PrintedQuestions[NumQst],Shuffle); + Tst_GenerateChoiceIndexes (&Print->PrintedQuestions[NumQst],Shuffle); break; default: break; @@ -2680,13 +2680,12 @@ static void Tst_GetQuestionsForNewTestFromDB (struct Tst_Test *Test, } /*****************************************************************************/ -/********* Get single or multiple choice answer when seeing a test ***********/ +/*************** Generate choice indexes depending on shuffle ****************/ /*****************************************************************************/ -void Tst_GenerateChoiceIndexesDependingOnShuffle (struct TstPrn_PrintedQuestion *PrintedQuestion, - bool Shuffle) +void Tst_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion, + bool Shuffle) { - // extern const char *Par_SEPARATOR_PARAM_MULTIPLE; struct Tst_Question Question; unsigned NumOpt; MYSQL_RES *mysql_res; @@ -2733,7 +2732,6 @@ void Tst_GenerateChoiceIndexesDependingOnShuffle (struct TstPrn_PrintedQuestion if (NumOpt == 0) snprintf (StrInd,sizeof (StrInd),"%u",Index); else - // snprintf (StrInd,sizeof (StrInd),"%s%u",Par_SEPARATOR_PARAM_MULTIPLE,Index); snprintf (StrInd,sizeof (StrInd),",%u",Index); Str_Concat (PrintedQuestion->StrIndexes,StrInd, Tst_MAX_BYTES_INDEXES_ONE_QST); @@ -4699,19 +4697,18 @@ static void Tst_FreeMediaOfQuestion (struct Tst_Question *Question) /*************** Get answer type of a question from database *****************/ /*****************************************************************************/ -Tst_AnswerType_t Tst_GetQstAnswerType (long QstCod) +Tst_AnswerType_t Tst_GetQstAnswerTypeFromDB (long QstCod) { MYSQL_RES *mysql_res; MYSQL_ROW row; Tst_AnswerType_t AnswerType; /***** Get type of answer from database *****/ - if (!DB_QuerySELECT (&mysql_res,"can not get a question", + if (!DB_QuerySELECT (&mysql_res,"can not get the type of a question", "SELECT AnsType" // row[0] " FROM tst_questions" - " WHERE QstCod=%ld" - " AND CrsCod=%ld", // Extra check - QstCod,Gbl.Hierarchy.Crs.CrsCod)) + " WHERE QstCod=%ld", + QstCod)) Lay_ShowErrorAndExit ("Question does not exist."); /* Get type of answer */ diff --git a/swad_test.h b/swad_test.h index 83a3bb6a..71da8647 100644 --- a/swad_test.h +++ b/swad_test.h @@ -122,8 +122,8 @@ void Tst_ListQuestionsToEdit (void); void Tst_ListQuestionsToSelectForSet (struct Exa_Exams *Exams); void Tst_ListQuestionsToSelectForGame (struct Gam_Games *Games); -void Tst_GenerateChoiceIndexesDependingOnShuffle (struct TstPrn_PrintedQuestion *PrintedQuestion, - bool Shuffle); +void Tst_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion, + bool Shuffle); void Tst_WriteParamEditQst (const struct Tst_Test *Test); @@ -158,7 +158,7 @@ void Tst_QstDestructor (struct Tst_Question *Question); bool Tst_AllocateTextChoiceAnswer (struct Tst_Question *Question,unsigned NumOpt); -Tst_AnswerType_t Tst_GetQstAnswerType (long QstCod); +Tst_AnswerType_t Tst_GetQstAnswerTypeFromDB (long QstCod); bool Tst_GetQstDataFromDB (struct Tst_Question *Question); Tst_AnswerType_t Tst_ConvertFromStrAnsTypDBToAnsTyp (const char *StrAnsTypeBD); void Tst_ReceiveQst (void); diff --git a/swad_test_print.c b/swad_test_print.c index 926031a5..f2394a0e 100644 --- a/swad_test_print.c +++ b/swad_test_print.c @@ -374,7 +374,7 @@ void TstPrn_ComputeScoresAndStoreQuestionsOfPrint (struct TstPrn_Print *Print, /* Compute question score */ Tst_QstConstructor (&Question); Question.QstCod = Print->PrintedQuestions[NumQst].QstCod; - Question.Answer.Type = Tst_GetQstAnswerType (Question.QstCod); + Question.Answer.Type = Tst_GetQstAnswerTypeFromDB (Question.QstCod); TstPrn_ComputeAnswerScore (&Print->PrintedQuestions[NumQst],&Question); Tst_QstDestructor (&Question); diff --git a/swad_test_type.h b/swad_test_type.h index 8ab03d89..a8f083fc 100644 --- a/swad_test_type.h +++ b/swad_test_type.h @@ -26,6 +26,9 @@ /*****************************************************************************/ /********************************* Headers ***********************************/ /*****************************************************************************/ + +#include "swad_media.h" + /*****************************************************************************/ /***************************** Public constants ******************************/ /*****************************************************************************/