exercise.class.php 407 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. use Chamilo\CoreBundle\Entity\GradebookLink;
  4. use Chamilo\CoreBundle\Entity\Resource\ResourceLink;
  5. use Chamilo\CoreBundle\Entity\TrackEHotspot;
  6. use Chamilo\CoreBundle\Framework\Container;
  7. use Chamilo\CourseBundle\Entity\CExerciseCategory;
  8. use Chamilo\CourseBundle\Entity\CQuizCategory;
  9. use ChamiloSession as Session;
  10. use Doctrine\DBAL\Types\Type;
  11. /**
  12. * Class Exercise.
  13. *
  14. * Allows to instantiate an object of type Exercise
  15. *
  16. * @todo use getters and setters correctly
  17. *
  18. * @author Olivier Brouckaert
  19. * @author Julio Montoya Cleaning exercises
  20. * Modified by Hubert Borderiou #294
  21. */
  22. class Exercise
  23. {
  24. const PAGINATION_ITEMS_PER_PAGE = 20;
  25. public $iId;
  26. public $id;
  27. public $name;
  28. public $title;
  29. public $exercise;
  30. public $description;
  31. public $sound;
  32. public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
  33. public $random;
  34. public $random_answers;
  35. public $active;
  36. public $timeLimit;
  37. public $attempts;
  38. public $feedback_type;
  39. public $end_time;
  40. public $start_time;
  41. public $questionList; // array with the list of this exercise's questions
  42. /* including question list of the media */
  43. public $questionListUncompressed;
  44. public $results_disabled;
  45. public $expired_time;
  46. public $course;
  47. public $course_id;
  48. public $propagate_neg;
  49. public $saveCorrectAnswers;
  50. public $review_answers;
  51. public $randomByCat;
  52. public $text_when_finished;
  53. public $display_category_name;
  54. public $pass_percentage;
  55. public $edit_exercise_in_lp = false;
  56. public $is_gradebook_locked = false;
  57. public $exercise_was_added_in_lp = false;
  58. public $lpList = [];
  59. public $force_edit_exercise_in_lp = false;
  60. public $categories;
  61. public $categories_grouping = true;
  62. public $endButton = 0;
  63. public $categoryWithQuestionList;
  64. public $mediaList;
  65. public $loadQuestionAJAX = false;
  66. // Notification send to the teacher.
  67. public $emailNotificationTemplate = null;
  68. // Notification send to the student.
  69. public $emailNotificationTemplateToUser = null;
  70. public $countQuestions = 0;
  71. public $fastEdition = false;
  72. public $modelType = 1;
  73. public $questionSelectionType = EX_Q_SELECTION_ORDERED;
  74. public $hideQuestionTitle = 0;
  75. public $scoreTypeModel = 0;
  76. public $categoryMinusOne = true; // Shows the category -1: See BT#6540
  77. public $globalCategoryId = null;
  78. public $onSuccessMessage = null;
  79. public $onFailedMessage = null;
  80. public $emailAlert;
  81. public $notifyUserByEmail = '';
  82. public $sessionId = 0;
  83. public $questionFeedbackEnabled = false;
  84. public $questionTypeWithFeedback;
  85. public $showPreviousButton;
  86. public $notifications;
  87. public $export = false;
  88. public $autolaunch;
  89. public $exerciseCategoryId;
  90. public $pageResultConfiguration;
  91. /**
  92. * Constructor of the class.
  93. *
  94. * @param int $courseId
  95. *
  96. * @author Olivier Brouckaert
  97. */
  98. public function __construct($courseId = 0)
  99. {
  100. $this->iId = 0;
  101. $this->id = 0;
  102. $this->exercise = '';
  103. $this->description = '';
  104. $this->sound = '';
  105. $this->type = ALL_ON_ONE_PAGE;
  106. $this->random = 0;
  107. $this->random_answers = 0;
  108. $this->active = 1;
  109. $this->questionList = [];
  110. $this->timeLimit = 0;
  111. $this->end_time = '';
  112. $this->start_time = '';
  113. $this->results_disabled = 1;
  114. $this->expired_time = 0;
  115. $this->propagate_neg = 0;
  116. $this->saveCorrectAnswers = 0;
  117. $this->review_answers = false;
  118. $this->randomByCat = 0;
  119. $this->text_when_finished = '';
  120. $this->display_category_name = 0;
  121. $this->pass_percentage = 0;
  122. $this->modelType = 1;
  123. $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
  124. $this->endButton = 0;
  125. $this->scoreTypeModel = 0;
  126. $this->globalCategoryId = null;
  127. $this->notifications = [];
  128. $this->exerciseCategoryId = 0;
  129. $this->pageResultConfiguration;
  130. if (!empty($courseId)) {
  131. $courseInfo = api_get_course_info_by_id($courseId);
  132. } else {
  133. $courseInfo = api_get_course_info();
  134. }
  135. $this->course_id = $courseInfo['real_id'];
  136. $this->course = $courseInfo;
  137. $this->sessionId = api_get_session_id();
  138. // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
  139. $this->questionFeedbackEnabled = api_get_configuration_value('allow_quiz_question_feedback');
  140. $this->showPreviousButton = true;
  141. }
  142. /**
  143. * Reads exercise information from the data base.
  144. *
  145. * @author Olivier Brouckaert
  146. *
  147. * @param int $id - exercise Id
  148. * @param bool $parseQuestionList
  149. *
  150. * @return bool - true if exercise exists, otherwise false
  151. */
  152. public function read($id, $parseQuestionList = true)
  153. {
  154. $table = Database::get_course_table(TABLE_QUIZ_TEST);
  155. $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
  156. $id = (int) $id;
  157. if (empty($this->course_id)) {
  158. return false;
  159. }
  160. $sql = "SELECT * FROM $table
  161. WHERE c_id = ".$this->course_id." AND id = ".$id;
  162. $result = Database::query($sql);
  163. // if the exercise has been found
  164. if ($object = Database::fetch_object($result)) {
  165. $this->iId = $object->iid;
  166. $this->id = $id;
  167. $this->exercise = $object->title;
  168. $this->name = $object->title;
  169. $this->title = $object->title;
  170. $this->description = $object->description;
  171. $this->sound = $object->sound;
  172. $this->type = $object->type;
  173. if (empty($this->type)) {
  174. $this->type = ONE_PER_PAGE;
  175. }
  176. $this->random = $object->random;
  177. $this->random_answers = $object->random_answers;
  178. $this->active = $object->active;
  179. $this->results_disabled = $object->results_disabled;
  180. $this->attempts = $object->max_attempt;
  181. $this->feedback_type = $object->feedback_type;
  182. $this->sessionId = $object->session_id;
  183. $this->propagate_neg = $object->propagate_neg;
  184. $this->saveCorrectAnswers = $object->save_correct_answers;
  185. $this->randomByCat = $object->random_by_category;
  186. $this->text_when_finished = $object->text_when_finished;
  187. $this->display_category_name = $object->display_category_name;
  188. $this->pass_percentage = $object->pass_percentage;
  189. $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
  190. $this->review_answers = (isset($object->review_answers) && $object->review_answers == 1) ? true : false;
  191. $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
  192. $this->questionSelectionType = isset($object->question_selection_type) ? $object->question_selection_type : null;
  193. $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
  194. $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
  195. $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : 0;
  196. $this->notifications = [];
  197. if (!empty($object->notifications)) {
  198. $this->notifications = explode(',', $object->notifications);
  199. }
  200. if (!empty($object->page_result_configuration)) {
  201. $this->pageResultConfiguration = $object->page_result_configuration;
  202. }
  203. if (isset($object->show_previous_button)) {
  204. $this->showPreviousButton = $object->show_previous_button == 1 ? true : false;
  205. }
  206. $sql = "SELECT lp_id, max_score
  207. FROM $tableLpItem
  208. WHERE
  209. c_id = {$this->course_id} AND
  210. item_type = '".TOOL_QUIZ."' AND
  211. path = '".$id."'";
  212. $result = Database::query($sql);
  213. if (Database::num_rows($result) > 0) {
  214. $this->exercise_was_added_in_lp = true;
  215. $this->lpList = Database::store_result($result, 'ASSOC');
  216. }
  217. $this->force_edit_exercise_in_lp = api_get_setting('lp.show_invisible_exercise_in_lp_toc') === 'true';
  218. $this->edit_exercise_in_lp = true;
  219. if ($this->exercise_was_added_in_lp) {
  220. $this->edit_exercise_in_lp = $this->force_edit_exercise_in_lp == true;
  221. }
  222. if (!empty($object->end_time)) {
  223. $this->end_time = $object->end_time;
  224. }
  225. if (!empty($object->start_time)) {
  226. $this->start_time = $object->start_time;
  227. }
  228. // Control time
  229. $this->expired_time = $object->expired_time;
  230. // Checking if question_order is correctly set
  231. if ($parseQuestionList) {
  232. $this->setQuestionList(true);
  233. }
  234. //overload questions list with recorded questions list
  235. //load questions only for exercises of type 'one question per page'
  236. //this is needed only is there is no questions
  237. // @todo not sure were in the code this is used somebody mess with the exercise tool
  238. // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
  239. /*global $_configuration, $questionList;
  240. if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
  241. && defined('QUESTION_LIST_ALREADY_LOGGED') &&
  242. isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
  243. ) {
  244. $this->questionList = $questionList;
  245. }*/
  246. return true;
  247. }
  248. return false;
  249. }
  250. /**
  251. * @return string
  252. */
  253. public function getCutTitle()
  254. {
  255. $title = $this->getUnformattedTitle();
  256. return cut($title, EXERCISE_MAX_NAME_SIZE);
  257. }
  258. /**
  259. * returns the exercise ID.
  260. *
  261. * @author Olivier Brouckaert
  262. *
  263. * @return int - exercise ID
  264. */
  265. public function selectId()
  266. {
  267. return $this->id;
  268. }
  269. /**
  270. * returns the exercise title.
  271. *
  272. * @author Olivier Brouckaert
  273. *
  274. * @param bool $unformattedText Optional. Get the title without HTML tags
  275. *
  276. * @return string - exercise title
  277. */
  278. public function selectTitle($unformattedText = false)
  279. {
  280. if ($unformattedText) {
  281. return $this->getUnformattedTitle();
  282. }
  283. return $this->exercise;
  284. }
  285. /**
  286. * returns the number of attempts setted.
  287. *
  288. * @return int - exercise attempts
  289. */
  290. public function selectAttempts()
  291. {
  292. return $this->attempts;
  293. }
  294. /**
  295. * Returns the number of FeedbackType
  296. * 0: Feedback , 1: DirectFeedback, 2: NoFeedback.
  297. *
  298. * @return int - exercise attempts
  299. */
  300. public function getFeedbackType()
  301. {
  302. return (int) $this->feedback_type;
  303. }
  304. /**
  305. * returns the time limit.
  306. *
  307. * @return int
  308. */
  309. public function selectTimeLimit()
  310. {
  311. return $this->timeLimit;
  312. }
  313. /**
  314. * returns the exercise description.
  315. *
  316. * @author Olivier Brouckaert
  317. *
  318. * @return string - exercise description
  319. */
  320. public function selectDescription()
  321. {
  322. return $this->description;
  323. }
  324. /**
  325. * returns the exercise sound file.
  326. *
  327. * @author Olivier Brouckaert
  328. *
  329. * @return string - exercise description
  330. */
  331. public function selectSound()
  332. {
  333. return $this->sound;
  334. }
  335. /**
  336. * returns the exercise type.
  337. *
  338. * @author Olivier Brouckaert
  339. *
  340. * @return int - exercise type
  341. */
  342. public function selectType()
  343. {
  344. return $this->type;
  345. }
  346. /**
  347. * @return int
  348. */
  349. public function getModelType()
  350. {
  351. return $this->modelType;
  352. }
  353. /**
  354. * @return int
  355. */
  356. public function selectEndButton()
  357. {
  358. return $this->endButton;
  359. }
  360. /**
  361. * @author hubert borderiou 30-11-11
  362. *
  363. * @return int : do we display the question category name for students
  364. */
  365. public function selectDisplayCategoryName()
  366. {
  367. return $this->display_category_name;
  368. }
  369. /**
  370. * @return int
  371. */
  372. public function selectPassPercentage()
  373. {
  374. return $this->pass_percentage;
  375. }
  376. /**
  377. * Modify object to update the switch display_category_name.
  378. *
  379. * @author hubert borderiou 30-11-11
  380. *
  381. * @param int $value is an integer 0 or 1
  382. */
  383. public function updateDisplayCategoryName($value)
  384. {
  385. $this->display_category_name = $value;
  386. }
  387. /**
  388. * @author hubert borderiou 28-11-11
  389. *
  390. * @return string html text : the text to display ay the end of the test
  391. */
  392. public function getTextWhenFinished()
  393. {
  394. return $this->text_when_finished;
  395. }
  396. /**
  397. * @param string $text
  398. *
  399. * @author hubert borderiou 28-11-11
  400. */
  401. public function updateTextWhenFinished($text)
  402. {
  403. $this->text_when_finished = $text;
  404. }
  405. /**
  406. * return 1 or 2 if randomByCat.
  407. *
  408. * @author hubert borderiou
  409. *
  410. * @return int - quiz random by category
  411. */
  412. public function getRandomByCategory()
  413. {
  414. return $this->randomByCat;
  415. }
  416. /**
  417. * return 0 if no random by cat
  418. * return 1 if random by cat, categories shuffled
  419. * return 2 if random by cat, categories sorted by alphabetic order.
  420. *
  421. * @author hubert borderiou
  422. *
  423. * @return int - quiz random by category
  424. */
  425. public function isRandomByCat()
  426. {
  427. $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
  428. if ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_SHUFFLED) {
  429. $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
  430. } elseif ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_ORDERED) {
  431. $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
  432. }
  433. return $res;
  434. }
  435. /**
  436. * return nothing
  437. * update randomByCat value for object.
  438. *
  439. * @param int $random
  440. *
  441. * @author hubert borderiou
  442. */
  443. public function updateRandomByCat($random)
  444. {
  445. $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
  446. if (in_array(
  447. $random,
  448. [
  449. EXERCISE_CATEGORY_RANDOM_SHUFFLED,
  450. EXERCISE_CATEGORY_RANDOM_ORDERED,
  451. EXERCISE_CATEGORY_RANDOM_DISABLED,
  452. ]
  453. )) {
  454. $this->randomByCat = $random;
  455. }
  456. }
  457. /**
  458. * Tells if questions are selected randomly, and if so returns the draws.
  459. *
  460. * @author Carlos Vargas
  461. *
  462. * @return int - results disabled exercise
  463. */
  464. public function selectResultsDisabled()
  465. {
  466. return $this->results_disabled;
  467. }
  468. /**
  469. * tells if questions are selected randomly, and if so returns the draws.
  470. *
  471. * @author Olivier Brouckaert
  472. *
  473. * @return bool
  474. */
  475. public function isRandom()
  476. {
  477. $isRandom = false;
  478. // "-1" means all questions will be random
  479. if ($this->random > 0 || $this->random == -1) {
  480. $isRandom = true;
  481. }
  482. return $isRandom;
  483. }
  484. /**
  485. * returns random answers status.
  486. *
  487. * @author Juan Carlos Rana
  488. */
  489. public function getRandomAnswers()
  490. {
  491. return $this->random_answers;
  492. }
  493. /**
  494. * Same as isRandom() but has a name applied to values different than 0 or 1.
  495. *
  496. * @return int
  497. */
  498. public function getShuffle()
  499. {
  500. return $this->random;
  501. }
  502. /**
  503. * returns the exercise status (1 = enabled ; 0 = disabled).
  504. *
  505. * @author Olivier Brouckaert
  506. *
  507. * @return int - 1 if enabled, otherwise 0
  508. */
  509. public function selectStatus()
  510. {
  511. return $this->active;
  512. }
  513. /**
  514. * If false the question list will be managed as always if true
  515. * the question will be filtered
  516. * depending of the exercise settings (table c_quiz_rel_category).
  517. *
  518. * @param bool $status active or inactive grouping
  519. */
  520. public function setCategoriesGrouping($status)
  521. {
  522. $this->categories_grouping = (bool) $status;
  523. }
  524. /**
  525. * @return int
  526. */
  527. public function getHideQuestionTitle()
  528. {
  529. return $this->hideQuestionTitle;
  530. }
  531. /**
  532. * @param $value
  533. */
  534. public function setHideQuestionTitle($value)
  535. {
  536. $this->hideQuestionTitle = (int) $value;
  537. }
  538. /**
  539. * @return int
  540. */
  541. public function getScoreTypeModel()
  542. {
  543. return $this->scoreTypeModel;
  544. }
  545. /**
  546. * @param int $value
  547. */
  548. public function setScoreTypeModel($value)
  549. {
  550. $this->scoreTypeModel = (int) $value;
  551. }
  552. /**
  553. * @return int
  554. */
  555. public function getGlobalCategoryId()
  556. {
  557. return $this->globalCategoryId;
  558. }
  559. /**
  560. * @param int $value
  561. */
  562. public function setGlobalCategoryId($value)
  563. {
  564. if (is_array($value) && isset($value[0])) {
  565. $value = $value[0];
  566. }
  567. $this->globalCategoryId = (int) $value;
  568. }
  569. /**
  570. * @param int $start
  571. * @param int $limit
  572. * @param int $sidx
  573. * @param string $sord
  574. * @param array $whereCondition
  575. * @param array $extraFields
  576. *
  577. * @return array
  578. */
  579. public function getQuestionListPagination(
  580. $start,
  581. $limit,
  582. $sidx,
  583. $sord,
  584. $whereCondition = [],
  585. $extraFields = []
  586. ) {
  587. if (!empty($this->id)) {
  588. $category_list = TestCategory::getListOfCategoriesNameForTest(
  589. $this->id,
  590. false
  591. );
  592. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  593. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  594. $sql = "SELECT q.iid
  595. FROM $TBL_EXERCICE_QUESTION e
  596. INNER JOIN $TBL_QUESTIONS q
  597. ON (e.question_id = q.id AND e.c_id = ".$this->course_id." )
  598. WHERE e.exercice_id = '".$this->id."' ";
  599. $orderCondition = ' ORDER BY question_order ';
  600. if (!empty($sidx) && !empty($sord)) {
  601. if ($sidx === 'question') {
  602. if (in_array(strtolower($sord), ['desc', 'asc'])) {
  603. $orderCondition = " ORDER BY q.$sidx $sord";
  604. }
  605. }
  606. }
  607. $sql .= $orderCondition;
  608. $limitCondition = null;
  609. if (isset($start) && isset($limit)) {
  610. $start = (int) $start;
  611. $limit = (int) $limit;
  612. $limitCondition = " LIMIT $start, $limit";
  613. }
  614. $sql .= $limitCondition;
  615. $result = Database::query($sql);
  616. $questions = [];
  617. if (Database::num_rows($result)) {
  618. if (!empty($extraFields)) {
  619. $extraFieldValue = new ExtraFieldValue('question');
  620. }
  621. while ($question = Database::fetch_array($result, 'ASSOC')) {
  622. /** @var Question $objQuestionTmp */
  623. $objQuestionTmp = Question::read($question['iid']);
  624. $category_labels = TestCategory::return_category_labels(
  625. $objQuestionTmp->category_list,
  626. $category_list
  627. );
  628. if (empty($category_labels)) {
  629. $category_labels = '-';
  630. }
  631. // Question type
  632. $typeImg = $objQuestionTmp->getTypePicture();
  633. $typeExpl = $objQuestionTmp->getExplanation();
  634. $question_media = null;
  635. if (!empty($objQuestionTmp->parent_id)) {
  636. $objQuestionMedia = Question::read($objQuestionTmp->parent_id);
  637. $question_media = Question::getMediaLabel($objQuestionMedia->question);
  638. }
  639. $questionType = Display::tag(
  640. 'div',
  641. Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
  642. );
  643. $question = [
  644. 'id' => $question['iid'],
  645. 'question' => $objQuestionTmp->selectTitle(),
  646. 'type' => $questionType,
  647. 'category' => Display::tag(
  648. 'div',
  649. '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
  650. ),
  651. 'score' => $objQuestionTmp->selectWeighting(),
  652. 'level' => $objQuestionTmp->level,
  653. ];
  654. if (!empty($extraFields)) {
  655. foreach ($extraFields as $extraField) {
  656. $value = $extraFieldValue->get_values_by_handler_and_field_id(
  657. $question['id'],
  658. $extraField['id']
  659. );
  660. $stringValue = null;
  661. if ($value) {
  662. $stringValue = $value['field_value'];
  663. }
  664. $question[$extraField['field_variable']] = $stringValue;
  665. }
  666. }
  667. $questions[] = $question;
  668. }
  669. }
  670. return $questions;
  671. }
  672. }
  673. /**
  674. * Get question count per exercise from DB (any special treatment).
  675. *
  676. * @return int
  677. */
  678. public function getQuestionCount()
  679. {
  680. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  681. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  682. $sql = "SELECT count(q.id) as count
  683. FROM $TBL_EXERCICE_QUESTION e
  684. INNER JOIN $TBL_QUESTIONS q
  685. ON (e.question_id = q.id AND e.c_id = q.c_id)
  686. WHERE
  687. e.c_id = {$this->course_id} AND
  688. e.exercice_id = ".$this->id;
  689. $result = Database::query($sql);
  690. $count = 0;
  691. if (Database::num_rows($result)) {
  692. $row = Database::fetch_array($result);
  693. $count = (int) $row['count'];
  694. }
  695. return $count;
  696. }
  697. /**
  698. * @return array
  699. */
  700. public function getQuestionOrderedListByName()
  701. {
  702. if (empty($this->course_id) || empty($this->id)) {
  703. return [];
  704. }
  705. $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  706. $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
  707. // Getting question list from the order (question list drag n drop interface ).
  708. $sql = "SELECT e.question_id
  709. FROM $exerciseQuestionTable e
  710. INNER JOIN $questionTable q
  711. ON (e.question_id= q.id AND e.c_id = q.c_id)
  712. WHERE
  713. e.c_id = {$this->course_id} AND
  714. e.exercice_id = '".$this->id."'
  715. ORDER BY q.question";
  716. $result = Database::query($sql);
  717. $list = [];
  718. if (Database::num_rows($result)) {
  719. $list = Database::store_result($result, 'ASSOC');
  720. }
  721. return $list;
  722. }
  723. /**
  724. * Selecting question list depending in the exercise-category
  725. * relationship (category table in exercise settings).
  726. *
  727. * @param array $question_list
  728. * @param int $questionSelectionType
  729. *
  730. * @return array
  731. */
  732. public function getQuestionListWithCategoryListFilteredByCategorySettings(
  733. $question_list,
  734. $questionSelectionType
  735. ) {
  736. $result = [
  737. 'question_list' => [],
  738. 'category_with_questions_list' => [],
  739. ];
  740. // Order/random categories
  741. $cat = new TestCategory();
  742. // Setting category order.
  743. switch ($questionSelectionType) {
  744. case EX_Q_SELECTION_ORDERED: // 1
  745. case EX_Q_SELECTION_RANDOM: // 2
  746. // This options are not allowed here.
  747. break;
  748. case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
  749. $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
  750. $this,
  751. $this->course['real_id'],
  752. 'title ASC',
  753. false,
  754. true
  755. );
  756. $questions_by_category = TestCategory::getQuestionsByCat(
  757. $this->id,
  758. $question_list,
  759. $categoriesAddedInExercise
  760. );
  761. $question_list = $this->pickQuestionsPerCategory(
  762. $categoriesAddedInExercise,
  763. $question_list,
  764. $questions_by_category,
  765. true,
  766. false
  767. );
  768. break;
  769. case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
  770. case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
  771. $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
  772. $this,
  773. $this->course['real_id'],
  774. null,
  775. true,
  776. true
  777. );
  778. $questions_by_category = TestCategory::getQuestionsByCat(
  779. $this->id,
  780. $question_list,
  781. $categoriesAddedInExercise
  782. );
  783. $question_list = $this->pickQuestionsPerCategory(
  784. $categoriesAddedInExercise,
  785. $question_list,
  786. $questions_by_category,
  787. true,
  788. false
  789. );
  790. break;
  791. case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
  792. $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
  793. $this,
  794. $this->course['real_id'],
  795. 'title ASC',
  796. false,
  797. true
  798. );
  799. $questions_by_category = TestCategory::getQuestionsByCat(
  800. $this->id,
  801. $question_list,
  802. $categoriesAddedInExercise
  803. );
  804. $question_list = $this->pickQuestionsPerCategory(
  805. $categoriesAddedInExercise,
  806. $question_list,
  807. $questions_by_category,
  808. true,
  809. true
  810. );
  811. break;
  812. case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
  813. case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
  814. $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
  815. $this,
  816. $this->course['real_id'],
  817. null,
  818. true,
  819. true
  820. );
  821. $questions_by_category = TestCategory::getQuestionsByCat(
  822. $this->id,
  823. $question_list,
  824. $categoriesAddedInExercise
  825. );
  826. $question_list = $this->pickQuestionsPerCategory(
  827. $categoriesAddedInExercise,
  828. $question_list,
  829. $questions_by_category,
  830. true,
  831. true
  832. );
  833. break;
  834. case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
  835. break;
  836. case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: // 8
  837. break;
  838. case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
  839. $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
  840. $this,
  841. $this->course['real_id'],
  842. 'root ASC, lft ASC',
  843. false,
  844. true
  845. );
  846. $questions_by_category = TestCategory::getQuestionsByCat(
  847. $this->id,
  848. $question_list,
  849. $categoriesAddedInExercise
  850. );
  851. $question_list = $this->pickQuestionsPerCategory(
  852. $categoriesAddedInExercise,
  853. $question_list,
  854. $questions_by_category,
  855. true,
  856. false
  857. );
  858. break;
  859. case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
  860. $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
  861. $this,
  862. $this->course['real_id'],
  863. 'root, lft ASC',
  864. false,
  865. true
  866. );
  867. $questions_by_category = TestCategory::getQuestionsByCat(
  868. $this->id,
  869. $question_list,
  870. $categoriesAddedInExercise
  871. );
  872. $question_list = $this->pickQuestionsPerCategory(
  873. $categoriesAddedInExercise,
  874. $question_list,
  875. $questions_by_category,
  876. true,
  877. true
  878. );
  879. break;
  880. }
  881. $result['question_list'] = isset($question_list) ? $question_list : [];
  882. $result['category_with_questions_list'] = isset($questions_by_category) ? $questions_by_category : [];
  883. $parentsLoaded = [];
  884. // Adding category info in the category list with question list:
  885. if (!empty($questions_by_category)) {
  886. $newCategoryList = [];
  887. $em = Database::getManager();
  888. foreach ($questions_by_category as $categoryId => $questionList) {
  889. $cat = new TestCategory();
  890. $cat = $cat->getCategory($categoryId);
  891. if ($cat) {
  892. $cat = (array) $cat;
  893. $cat['iid'] = $cat['id'];
  894. }
  895. $categoryParentInfo = null;
  896. // Parent is not set no loop here
  897. if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
  898. /** @var CQuizCategory $categoryEntity */
  899. if (!isset($parentsLoaded[$cat['parent_id']])) {
  900. $categoryEntity = $em->find('ChamiloCoreBundle:CQuizCategory', $cat['parent_id']);
  901. $parentsLoaded[$cat['parent_id']] = $categoryEntity;
  902. } else {
  903. $categoryEntity = $parentsLoaded[$cat['parent_id']];
  904. }
  905. $repo = $em->getRepository('ChamiloCoreBundle:CQuizCategory');
  906. $path = $repo->getPath($categoryEntity);
  907. $index = 0;
  908. if ($this->categoryMinusOne) {
  909. //$index = 1;
  910. }
  911. /** @var CQuizCategory $categoryParent */
  912. foreach ($path as $categoryParent) {
  913. $visibility = $categoryParent->getVisibility();
  914. if ($visibility == 0) {
  915. $categoryParentId = $categoryId;
  916. $categoryTitle = $cat['title'];
  917. if (count($path) > 1) {
  918. continue;
  919. }
  920. } else {
  921. $categoryParentId = $categoryParent->getIid();
  922. $categoryTitle = $categoryParent->getTitle();
  923. }
  924. $categoryParentInfo['id'] = $categoryParentId;
  925. $categoryParentInfo['iid'] = $categoryParentId;
  926. $categoryParentInfo['parent_path'] = null;
  927. $categoryParentInfo['title'] = $categoryTitle;
  928. $categoryParentInfo['name'] = $categoryTitle;
  929. $categoryParentInfo['parent_id'] = null;
  930. break;
  931. }
  932. }
  933. $cat['parent_info'] = $categoryParentInfo;
  934. $newCategoryList[$categoryId] = [
  935. 'category' => $cat,
  936. 'question_list' => $questionList,
  937. ];
  938. }
  939. $result['category_with_questions_list'] = $newCategoryList;
  940. }
  941. return $result;
  942. }
  943. /**
  944. * returns the array with the question ID list.
  945. *
  946. * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
  947. * @param bool $adminView Whether we should return all questions (admin view) or
  948. * just a list limited by the max number of random questions
  949. *
  950. * @author Olivier Brouckaert
  951. *
  952. * @return array - question ID list
  953. */
  954. public function selectQuestionList($fromDatabase = false, $adminView = false)
  955. {
  956. if ($fromDatabase && !empty($this->id)) {
  957. $nbQuestions = $this->getQuestionCount();
  958. $questionSelectionType = $this->getQuestionSelectionType();
  959. switch ($questionSelectionType) {
  960. case EX_Q_SELECTION_ORDERED:
  961. $questionList = $this->getQuestionOrderedList($adminView);
  962. break;
  963. case EX_Q_SELECTION_RANDOM:
  964. // Not a random exercise, or if there are not at least 2 questions
  965. if ($this->random == 0 || $nbQuestions < 2) {
  966. $questionList = $this->getQuestionOrderedList($adminView);
  967. } else {
  968. $questionList = $this->getRandomList($adminView);
  969. }
  970. break;
  971. default:
  972. $questionList = $this->getQuestionOrderedList($adminView);
  973. $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
  974. $questionList,
  975. $questionSelectionType
  976. );
  977. $this->categoryWithQuestionList = $result['category_with_questions_list'];
  978. $questionList = $result['question_list'];
  979. break;
  980. }
  981. return $questionList;
  982. }
  983. return $this->questionList;
  984. }
  985. /**
  986. * returns the number of questions in this exercise.
  987. *
  988. * @author Olivier Brouckaert
  989. *
  990. * @return int - number of questions
  991. */
  992. public function selectNbrQuestions()
  993. {
  994. return count($this->questionList);
  995. }
  996. /**
  997. * @return int
  998. */
  999. public function selectPropagateNeg()
  1000. {
  1001. return $this->propagate_neg;
  1002. }
  1003. /**
  1004. * @return int
  1005. */
  1006. public function getSaveCorrectAnswers()
  1007. {
  1008. return $this->saveCorrectAnswers;
  1009. }
  1010. /**
  1011. * Selects questions randomly in the question list.
  1012. *
  1013. * @author Olivier Brouckaert
  1014. * @author Hubert Borderiou 15 nov 2011
  1015. *
  1016. * @param bool $adminView Whether we should return all
  1017. * questions (admin view) or just a list limited by the max number of random questions
  1018. *
  1019. * @return array - if the exercise is not set to take questions randomly, returns the question list
  1020. * without randomizing, otherwise, returns the list with questions selected randomly
  1021. */
  1022. public function getRandomList($adminView = false)
  1023. {
  1024. $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1025. $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1026. $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
  1027. // Random with limit
  1028. $randomLimit = " ORDER BY RAND() LIMIT $random";
  1029. // Random with no limit
  1030. if ($random == -1) {
  1031. $randomLimit = ' ORDER BY RAND() ';
  1032. }
  1033. // Admin see the list in default order
  1034. if ($adminView === true) {
  1035. // If viewing it as admin for edition, don't show it randomly, use title + id
  1036. $randomLimit = 'ORDER BY e.question_order';
  1037. }
  1038. $sql = "SELECT e.question_id
  1039. FROM $quizRelQuestion e
  1040. INNER JOIN $question q
  1041. ON (e.question_id= q.id AND e.c_id = q.c_id)
  1042. WHERE
  1043. e.c_id = {$this->course_id} AND
  1044. e.exercice_id = '".Database::escape_string($this->id)."'
  1045. $randomLimit ";
  1046. $result = Database::query($sql);
  1047. $questionList = [];
  1048. while ($row = Database::fetch_object($result)) {
  1049. $questionList[] = $row->question_id;
  1050. }
  1051. return $questionList;
  1052. }
  1053. /**
  1054. * returns 'true' if the question ID is in the question list.
  1055. *
  1056. * @author Olivier Brouckaert
  1057. *
  1058. * @param int $questionId - question ID
  1059. *
  1060. * @return bool - true if in the list, otherwise false
  1061. */
  1062. public function isInList($questionId)
  1063. {
  1064. $inList = false;
  1065. if (is_array($this->questionList)) {
  1066. $inList = in_array($questionId, $this->questionList);
  1067. }
  1068. return $inList;
  1069. }
  1070. /**
  1071. * If current exercise has a question.
  1072. *
  1073. * @param int $questionId
  1074. *
  1075. * @return int
  1076. */
  1077. public function hasQuestion($questionId)
  1078. {
  1079. $questionId = (int) $questionId;
  1080. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1081. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  1082. $sql = "SELECT q.id
  1083. FROM $TBL_EXERCICE_QUESTION e
  1084. INNER JOIN $TBL_QUESTIONS q
  1085. ON (e.question_id = q.id AND e.c_id = q.c_id)
  1086. WHERE
  1087. q.id = $questionId AND
  1088. e.c_id = {$this->course_id} AND
  1089. e.exercice_id = ".$this->id;
  1090. $result = Database::query($sql);
  1091. return Database::num_rows($result) > 0;
  1092. }
  1093. /**
  1094. * changes the exercise title.
  1095. *
  1096. * @author Olivier Brouckaert
  1097. *
  1098. * @param string $title - exercise title
  1099. */
  1100. public function updateTitle($title)
  1101. {
  1102. $this->title = $this->exercise = $title;
  1103. }
  1104. /**
  1105. * changes the exercise max attempts.
  1106. *
  1107. * @param int $attempts - exercise max attempts
  1108. */
  1109. public function updateAttempts($attempts)
  1110. {
  1111. $this->attempts = $attempts;
  1112. }
  1113. /**
  1114. * changes the exercise feedback type.
  1115. *
  1116. * @param int $feedback_type
  1117. */
  1118. public function updateFeedbackType($feedback_type)
  1119. {
  1120. $this->feedback_type = $feedback_type;
  1121. }
  1122. /**
  1123. * changes the exercise description.
  1124. *
  1125. * @author Olivier Brouckaert
  1126. *
  1127. * @param string $description - exercise description
  1128. */
  1129. public function updateDescription($description)
  1130. {
  1131. $this->description = $description;
  1132. }
  1133. /**
  1134. * changes the exercise expired_time.
  1135. *
  1136. * @author Isaac flores
  1137. *
  1138. * @param int $expired_time The expired time of the quiz
  1139. */
  1140. public function updateExpiredTime($expired_time)
  1141. {
  1142. $this->expired_time = $expired_time;
  1143. }
  1144. /**
  1145. * @param $value
  1146. */
  1147. public function updatePropagateNegative($value)
  1148. {
  1149. $this->propagate_neg = $value;
  1150. }
  1151. /**
  1152. * @param $value int
  1153. */
  1154. public function updateSaveCorrectAnswers($value)
  1155. {
  1156. $this->saveCorrectAnswers = (int) $value;
  1157. }
  1158. /**
  1159. * @param $value
  1160. */
  1161. public function updateReviewAnswers($value)
  1162. {
  1163. $this->review_answers = isset($value) && $value ? true : false;
  1164. }
  1165. /**
  1166. * @param $value
  1167. */
  1168. public function updatePassPercentage($value)
  1169. {
  1170. $this->pass_percentage = $value;
  1171. }
  1172. /**
  1173. * @param string $text
  1174. */
  1175. public function updateEmailNotificationTemplate($text)
  1176. {
  1177. $this->emailNotificationTemplate = $text;
  1178. }
  1179. /**
  1180. * @param string $text
  1181. */
  1182. public function setEmailNotificationTemplateToUser($text)
  1183. {
  1184. $this->emailNotificationTemplateToUser = $text;
  1185. }
  1186. /**
  1187. * @param string $value
  1188. */
  1189. public function setNotifyUserByEmail($value)
  1190. {
  1191. $this->notifyUserByEmail = $value;
  1192. }
  1193. /**
  1194. * @param int $value
  1195. */
  1196. public function updateEndButton($value)
  1197. {
  1198. $this->endButton = (int) $value;
  1199. }
  1200. /**
  1201. * @param string $value
  1202. */
  1203. public function setOnSuccessMessage($value)
  1204. {
  1205. $this->onSuccessMessage = $value;
  1206. }
  1207. /**
  1208. * @param string $value
  1209. */
  1210. public function setOnFailedMessage($value)
  1211. {
  1212. $this->onFailedMessage = $value;
  1213. }
  1214. /**
  1215. * @param $value
  1216. */
  1217. public function setModelType($value)
  1218. {
  1219. $this->modelType = (int) $value;
  1220. }
  1221. /**
  1222. * @param int $value
  1223. */
  1224. public function setQuestionSelectionType($value)
  1225. {
  1226. $this->questionSelectionType = (int) $value;
  1227. }
  1228. /**
  1229. * @return int
  1230. */
  1231. public function getQuestionSelectionType()
  1232. {
  1233. return (int) $this->questionSelectionType;
  1234. }
  1235. /**
  1236. * @param array $categories
  1237. */
  1238. public function updateCategories($categories)
  1239. {
  1240. if (!empty($categories)) {
  1241. $categories = array_map('intval', $categories);
  1242. $this->categories = $categories;
  1243. }
  1244. }
  1245. /**
  1246. * changes the exercise sound file.
  1247. *
  1248. * @author Olivier Brouckaert
  1249. *
  1250. * @param string $sound - exercise sound file
  1251. * @param string $delete - ask to delete the file
  1252. */
  1253. public function updateSound($sound, $delete)
  1254. {
  1255. global $audioPath, $documentPath;
  1256. $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
  1257. if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) {
  1258. $this->sound = $sound['name'];
  1259. if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
  1260. $sql = "SELECT 1 FROM $TBL_DOCUMENT
  1261. WHERE
  1262. c_id = ".$this->course_id." AND
  1263. path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
  1264. $result = Database::query($sql);
  1265. if (!Database::num_rows($result)) {
  1266. DocumentManager::addDocument(
  1267. $this->course,
  1268. str_replace($documentPath, '', $audioPath).'/'.$this->sound,
  1269. 'file',
  1270. $sound['size'],
  1271. $sound['name']
  1272. );
  1273. }
  1274. }
  1275. } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
  1276. $this->sound = '';
  1277. }
  1278. }
  1279. /**
  1280. * changes the exercise type.
  1281. *
  1282. * @author Olivier Brouckaert
  1283. *
  1284. * @param int $type - exercise type
  1285. */
  1286. public function updateType($type)
  1287. {
  1288. $this->type = $type;
  1289. }
  1290. /**
  1291. * sets to 0 if questions are not selected randomly
  1292. * if questions are selected randomly, sets the draws.
  1293. *
  1294. * @author Olivier Brouckaert
  1295. *
  1296. * @param int $random - 0 if not random, otherwise the draws
  1297. */
  1298. public function setRandom($random)
  1299. {
  1300. $this->random = $random;
  1301. }
  1302. /**
  1303. * sets to 0 if answers are not selected randomly
  1304. * if answers are selected randomly.
  1305. *
  1306. * @author Juan Carlos Rana
  1307. *
  1308. * @param int $random_answers - random answers
  1309. */
  1310. public function updateRandomAnswers($random_answers)
  1311. {
  1312. $this->random_answers = $random_answers;
  1313. }
  1314. /**
  1315. * enables the exercise.
  1316. *
  1317. * @author Olivier Brouckaert
  1318. */
  1319. public function enable()
  1320. {
  1321. $this->active = 1;
  1322. }
  1323. /**
  1324. * disables the exercise.
  1325. *
  1326. * @author Olivier Brouckaert
  1327. */
  1328. public function disable()
  1329. {
  1330. $this->active = 0;
  1331. }
  1332. /**
  1333. * Set disable results.
  1334. */
  1335. public function disable_results()
  1336. {
  1337. $this->results_disabled = true;
  1338. }
  1339. /**
  1340. * Enable results.
  1341. */
  1342. public function enable_results()
  1343. {
  1344. $this->results_disabled = false;
  1345. }
  1346. /**
  1347. * @param int $results_disabled
  1348. */
  1349. public function updateResultsDisabled($results_disabled)
  1350. {
  1351. $this->results_disabled = (int) $results_disabled;
  1352. }
  1353. /**
  1354. * updates the exercise in the data base.
  1355. *
  1356. * @author Olivier Brouckaert
  1357. */
  1358. public function save()
  1359. {
  1360. $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
  1361. $id = $this->id;
  1362. $exercise = $this->exercise;
  1363. $description = $this->description;
  1364. $sound = $this->sound;
  1365. $type = $this->type;
  1366. $attempts = isset($this->attempts) ? $this->attempts : 0;
  1367. $feedback_type = isset($this->feedback_type) ? $this->feedback_type : 0;
  1368. $random = $this->random;
  1369. $random_answers = $this->random_answers;
  1370. $active = $this->active;
  1371. $propagate_neg = (int) $this->propagate_neg;
  1372. $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
  1373. $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
  1374. $randomByCat = (int) $this->randomByCat;
  1375. $text_when_finished = $this->text_when_finished;
  1376. $display_category_name = (int) $this->display_category_name;
  1377. $pass_percentage = (int) $this->pass_percentage;
  1378. $session_id = $this->sessionId;
  1379. // If direct we do not show results
  1380. $results_disabled = (int) $this->results_disabled;
  1381. if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
  1382. $results_disabled = 0;
  1383. }
  1384. $expired_time = (int) $this->expired_time;
  1385. $repo = Container::getExerciseRepository();
  1386. // Exercise already exists
  1387. if ($id) {
  1388. // we prepare date in the database using the api_get_utc_datetime() function
  1389. $start_time = null;
  1390. if (!empty($this->start_time)) {
  1391. $start_time = $this->start_time;
  1392. }
  1393. $end_time = null;
  1394. if (!empty($this->end_time)) {
  1395. $end_time = $this->end_time;
  1396. }
  1397. $params = [
  1398. 'title' => $exercise,
  1399. 'description' => $description,
  1400. ];
  1401. $paramsExtra = [
  1402. 'sound' => $sound,
  1403. 'type' => $type,
  1404. 'random' => $random,
  1405. 'random_answers' => $random_answers,
  1406. 'active' => $active,
  1407. 'feedback_type' => $feedback_type,
  1408. 'start_time' => $start_time,
  1409. 'end_time' => $end_time,
  1410. 'max_attempt' => $attempts,
  1411. 'expired_time' => $expired_time,
  1412. 'propagate_neg' => $propagate_neg,
  1413. 'save_correct_answers' => $saveCorrectAnswers,
  1414. 'review_answers' => $review_answers,
  1415. 'random_by_category' => $randomByCat,
  1416. 'text_when_finished' => $text_when_finished,
  1417. 'display_category_name' => $display_category_name,
  1418. 'pass_percentage' => $pass_percentage,
  1419. 'results_disabled' => $results_disabled,
  1420. 'question_selection_type' => $this->getQuestionSelectionType(),
  1421. 'hide_question_title' => $this->getHideQuestionTitle(),
  1422. ];
  1423. $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
  1424. if ($allow === true) {
  1425. $paramsExtra['show_previous_button'] = $this->showPreviousButton();
  1426. }
  1427. $allow = api_get_configuration_value('allow_exercise_categories');
  1428. if ($allow === true) {
  1429. if (!empty($this->getExerciseCategoryId())) {
  1430. $paramsExtra['exercise_category_id'] = $this->getExerciseCategoryId();
  1431. }
  1432. }
  1433. $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
  1434. if ($allow === true) {
  1435. $notifications = $this->getNotifications();
  1436. $notifications = implode(',', $notifications);
  1437. $paramsExtra['notifications'] = $notifications;
  1438. }
  1439. if (!empty($this->pageResultConfiguration)) {
  1440. $paramsExtra['page_result_configuration'] = $this->pageResultConfiguration;
  1441. }
  1442. $params = array_merge($params, $paramsExtra);
  1443. Database::update(
  1444. $TBL_EXERCISES,
  1445. $params,
  1446. ['c_id = ? AND id = ?' => [$this->course_id, $id]]
  1447. );
  1448. $exerciseEntity = $repo->find($id);
  1449. $node = $exerciseEntity->getResourceNode();
  1450. $node->setName($exercise);
  1451. $repo->getEntityManager()->merge($node);
  1452. $repo->getEntityManager()->flush();
  1453. if (api_get_setting('search_enabled') == 'true') {
  1454. $this->search_engine_edit();
  1455. }
  1456. } else {
  1457. // Creates a new exercise
  1458. // In this case of new exercise, we don't do the api_get_utc_datetime()
  1459. // for date because, bellow, we call function api_set_default_visibility()
  1460. // In this function, api_set_default_visibility,
  1461. // the Quiz is saved too, with an $id and api_get_utc_datetime() is done.
  1462. // If we do it now, it will be done twice (cf. https://support.chamilo.org/issues/6586)
  1463. $start_time = null;
  1464. if (!empty($this->start_time)) {
  1465. $start_time = $this->start_time;
  1466. }
  1467. $end_time = null;
  1468. if (!empty($this->end_time)) {
  1469. $end_time = $this->end_time;
  1470. }
  1471. $params = [
  1472. 'c_id' => $this->course_id,
  1473. 'start_time' => $start_time,
  1474. 'end_time' => $end_time,
  1475. 'title' => $exercise,
  1476. 'description' => $description,
  1477. 'sound' => $sound,
  1478. 'type' => $type,
  1479. 'random' => $random,
  1480. 'random_answers' => $random_answers,
  1481. 'active' => $active,
  1482. 'results_disabled' => $results_disabled,
  1483. 'max_attempt' => $attempts,
  1484. 'feedback_type' => $feedback_type,
  1485. 'expired_time' => $expired_time,
  1486. 'session_id' => $session_id,
  1487. 'review_answers' => $review_answers,
  1488. 'random_by_category' => $randomByCat,
  1489. 'text_when_finished' => $text_when_finished,
  1490. 'display_category_name' => $display_category_name,
  1491. 'pass_percentage' => $pass_percentage,
  1492. 'save_correct_answers' => $saveCorrectAnswers,
  1493. 'propagate_neg' => $propagate_neg,
  1494. 'hide_question_title' => $this->getHideQuestionTitle(),
  1495. ];
  1496. $allow = api_get_configuration_value('allow_exercise_categories');
  1497. if ($allow === true) {
  1498. if (!empty($this->getExerciseCategoryId())) {
  1499. $params['exercise_category_id'] = $this->getExerciseCategoryId();
  1500. }
  1501. }
  1502. $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
  1503. if ($allow === true) {
  1504. $params['show_previous_button'] = $this->showPreviousButton();
  1505. }
  1506. $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
  1507. if ($allow === true) {
  1508. $notifications = $this->getNotifications();
  1509. $params['notifications'] = '';
  1510. if (!empty($notifications)) {
  1511. $notifications = implode(',', $notifications);
  1512. $params['notifications'] = $notifications;
  1513. }
  1514. }
  1515. if (!empty($this->pageResultConfiguration)) {
  1516. $params['page_result_configuration'] = $this->pageResultConfiguration;
  1517. }
  1518. $this->id = $this->iId = Database::insert($TBL_EXERCISES, $params);
  1519. if ($this->id) {
  1520. $sql = "UPDATE $TBL_EXERCISES SET id = iid WHERE iid = {$this->id} ";
  1521. Database::query($sql);
  1522. $sql = "UPDATE $TBL_EXERCISES
  1523. SET question_selection_type= ".$this->getQuestionSelectionType()."
  1524. WHERE id = ".$this->id." AND c_id = ".$this->course_id;
  1525. Database::query($sql);
  1526. $exercise = $repo->find($this->id);
  1527. $node = $repo->addResourceNode($exercise, api_get_user_entity(api_get_user_id()));
  1528. $repo->addResourceToCourse(
  1529. $node,
  1530. ResourceLink::VISIBILITY_PUBLISHED,
  1531. api_get_course_entity(),
  1532. api_get_session_entity(),
  1533. api_get_group_entity()
  1534. );
  1535. // insert into the item_property table
  1536. api_item_property_update(
  1537. $this->course,
  1538. TOOL_QUIZ,
  1539. $this->id,
  1540. 'QuizAdded',
  1541. api_get_user_id()
  1542. );
  1543. // This function save the quiz again, carefull about start_time
  1544. // and end_time if you remove this line (see above)
  1545. api_set_default_visibility(
  1546. $this->id,
  1547. TOOL_QUIZ,
  1548. null,
  1549. $this->course
  1550. );
  1551. if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
  1552. $this->search_engine_save();
  1553. }
  1554. }
  1555. }
  1556. $this->save_categories_in_exercise($this->categories);
  1557. return $this->iId;
  1558. }
  1559. /**
  1560. * Updates question position.
  1561. *
  1562. * @return bool
  1563. */
  1564. public function update_question_positions()
  1565. {
  1566. $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  1567. // Fixes #3483 when updating order
  1568. $questionList = $this->selectQuestionList(true);
  1569. $this->id = (int) $this->id;
  1570. if (empty($this->id)) {
  1571. return false;
  1572. }
  1573. if (!empty($questionList)) {
  1574. foreach ($questionList as $position => $questionId) {
  1575. $position = (int) $position;
  1576. $questionId = (int) $questionId;
  1577. $sql = "UPDATE $table SET
  1578. question_order ='".$position."'
  1579. WHERE
  1580. c_id = ".$this->course_id." AND
  1581. question_id = ".$questionId." AND
  1582. exercice_id=".$this->id;
  1583. Database::query($sql);
  1584. }
  1585. }
  1586. return true;
  1587. }
  1588. /**
  1589. * Adds a question into the question list.
  1590. *
  1591. * @author Olivier Brouckaert
  1592. *
  1593. * @param int $questionId - question ID
  1594. *
  1595. * @return bool - true if the question has been added, otherwise false
  1596. */
  1597. public function addToList($questionId)
  1598. {
  1599. // checks if the question ID is not in the list
  1600. if (!$this->isInList($questionId)) {
  1601. // selects the max position
  1602. if (!$this->selectNbrQuestions()) {
  1603. $pos = 1;
  1604. } else {
  1605. if (is_array($this->questionList)) {
  1606. $pos = max(array_keys($this->questionList)) + 1;
  1607. }
  1608. }
  1609. $this->questionList[$pos] = $questionId;
  1610. return true;
  1611. }
  1612. return false;
  1613. }
  1614. /**
  1615. * removes a question from the question list.
  1616. *
  1617. * @author Olivier Brouckaert
  1618. *
  1619. * @param int $questionId - question ID
  1620. *
  1621. * @return bool - true if the question has been removed, otherwise false
  1622. */
  1623. public function removeFromList($questionId)
  1624. {
  1625. // searches the position of the question ID in the list
  1626. $pos = array_search($questionId, $this->questionList);
  1627. // question not found
  1628. if ($pos === false) {
  1629. return false;
  1630. } else {
  1631. // dont reduce the number of random question if we use random by category option, or if
  1632. // random all questions
  1633. if ($this->isRandom() && $this->isRandomByCat() == 0) {
  1634. if (count($this->questionList) >= $this->random && $this->random > 0) {
  1635. $this->random--;
  1636. $this->save();
  1637. }
  1638. }
  1639. // deletes the position from the array containing the wanted question ID
  1640. unset($this->questionList[$pos]);
  1641. return true;
  1642. }
  1643. }
  1644. /**
  1645. * deletes the exercise from the database
  1646. * Notice : leaves the question in the data base.
  1647. *
  1648. * @author Olivier Brouckaert
  1649. */
  1650. public function delete()
  1651. {
  1652. $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
  1653. if ($limitTeacherAccess && !api_is_platform_admin()) {
  1654. return false;
  1655. }
  1656. $locked = api_resource_is_locked_by_gradebook(
  1657. $this->id,
  1658. LINK_EXERCISE
  1659. );
  1660. if ($locked) {
  1661. return false;
  1662. }
  1663. $table = Database::get_course_table(TABLE_QUIZ_TEST);
  1664. $sql = "UPDATE $table SET active='-1'
  1665. WHERE c_id = ".$this->course_id." AND id = ".intval($this->id);
  1666. Database::query($sql);
  1667. api_item_property_update(
  1668. $this->course,
  1669. TOOL_QUIZ,
  1670. $this->id,
  1671. 'QuizDeleted',
  1672. api_get_user_id()
  1673. );
  1674. api_item_property_update(
  1675. $this->course,
  1676. TOOL_QUIZ,
  1677. $this->id,
  1678. 'delete',
  1679. api_get_user_id()
  1680. );
  1681. Skill::deleteSkillsFromItem($this->iId, ITEM_TYPE_EXERCISE);
  1682. if (api_get_setting('search_enabled') === 'true' &&
  1683. extension_loaded('xapian')
  1684. ) {
  1685. $this->search_engine_delete();
  1686. }
  1687. $linkInfo = GradebookUtils::isResourceInCourseGradebook(
  1688. $this->course['code'],
  1689. LINK_EXERCISE,
  1690. $this->id,
  1691. $this->sessionId
  1692. );
  1693. if ($linkInfo !== false) {
  1694. GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
  1695. }
  1696. return true;
  1697. }
  1698. /**
  1699. * Creates the form to create / edit an exercise.
  1700. *
  1701. * @param FormValidator $form
  1702. * @param string $type
  1703. */
  1704. public function createForm($form, $type = 'full')
  1705. {
  1706. if (empty($type)) {
  1707. $type = 'full';
  1708. }
  1709. // Form title
  1710. $form_title = get_lang('Create a new test');
  1711. if (!empty($_GET['exerciseId'])) {
  1712. $form_title = get_lang('Edit test name and settings');
  1713. }
  1714. $form->addHeader($form_title);
  1715. // Title.
  1716. if (api_get_configuration_value('save_titles_as_html')) {
  1717. $form->addHtmlEditor(
  1718. 'exerciseTitle',
  1719. get_lang('Test name'),
  1720. false,
  1721. false,
  1722. ['ToolbarSet' => 'TitleAsHtml']
  1723. );
  1724. } else {
  1725. $form->addElement(
  1726. 'text',
  1727. 'exerciseTitle',
  1728. get_lang('Test name'),
  1729. ['id' => 'exercise_title']
  1730. );
  1731. }
  1732. $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
  1733. $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
  1734. if (api_get_configuration_value('allow_exercise_categories')) {
  1735. $categoryManager = new ExerciseCategoryManager();
  1736. $categories = $categoryManager->getCategories(api_get_course_int_id());
  1737. $options = [];
  1738. if (!empty($categories)) {
  1739. /** @var CExerciseCategory $category */
  1740. foreach ($categories as $category) {
  1741. $options[$category->getId()] = $category->getName();
  1742. }
  1743. }
  1744. $form->addSelect(
  1745. 'exercise_category_id',
  1746. get_lang('Category'),
  1747. $options,
  1748. ['placeholder' => get_lang('Please select an option')]
  1749. );
  1750. }
  1751. $editor_config = [
  1752. 'ToolbarSet' => 'TestQuestionDescription',
  1753. 'Width' => '100%',
  1754. 'Height' => '150',
  1755. ];
  1756. if (is_array($type)) {
  1757. $editor_config = array_merge($editor_config, $type);
  1758. }
  1759. $form->addHtmlEditor(
  1760. 'exerciseDescription',
  1761. get_lang('Give a context to the test'),
  1762. false,
  1763. false,
  1764. $editor_config
  1765. );
  1766. $skillList = [];
  1767. if ($type === 'full') {
  1768. // Can't modify a DirectFeedback question.
  1769. if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
  1770. $this->setResultFeedbackGroup($form);
  1771. // Type of results display on the final page
  1772. $this->setResultDisabledGroup($form);
  1773. // Type of questions disposition on page
  1774. $radios = [];
  1775. $radios[] = $form->createElement(
  1776. 'radio',
  1777. 'exerciseType',
  1778. null,
  1779. get_lang('All questions on one page'),
  1780. '1',
  1781. [
  1782. 'onclick' => 'check_per_page_all()',
  1783. 'id' => 'option_page_all',
  1784. ]
  1785. );
  1786. $radios[] = $form->createElement(
  1787. 'radio',
  1788. 'exerciseType',
  1789. null,
  1790. get_lang('One question by page'),
  1791. '2',
  1792. [
  1793. 'onclick' => 'check_per_page_one()',
  1794. 'id' => 'option_page_one',
  1795. ]
  1796. );
  1797. $form->addGroup($radios, null, get_lang('Questions per page'));
  1798. } else {
  1799. // if is Direct feedback but has not questions we can allow to modify the question type
  1800. if ($this->getQuestionCount() === 0) {
  1801. $this->setResultFeedbackGroup($form);
  1802. $this->setResultDisabledGroup($form);
  1803. // Type of questions disposition on page
  1804. $radios = [];
  1805. $radios[] = $form->createElement('radio', 'exerciseType', null, get_lang('All questions on one page'), '1');
  1806. $radios[] = $form->createElement(
  1807. 'radio',
  1808. 'exerciseType',
  1809. null,
  1810. get_lang('One question by page'),
  1811. '2'
  1812. );
  1813. $form->addGroup($radios, null, get_lang('Sequential'));
  1814. } else {
  1815. $group = $this->setResultDisabledGroup($form);
  1816. $group->freeze();
  1817. // we force the options to the DirectFeedback exercisetype
  1818. $form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
  1819. $form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
  1820. // Type of questions disposition on page
  1821. $radios[] = $form->createElement(
  1822. 'radio',
  1823. 'exerciseType',
  1824. null,
  1825. get_lang('All questions on one page'),
  1826. '1',
  1827. [
  1828. 'onclick' => 'check_per_page_all()',
  1829. 'id' => 'option_page_all',
  1830. ]
  1831. );
  1832. $radios[] = $form->createElement(
  1833. 'radio',
  1834. 'exerciseType',
  1835. null,
  1836. get_lang('One question by page'),
  1837. '2',
  1838. [
  1839. 'onclick' => 'check_per_page_one()',
  1840. 'id' => 'option_page_one',
  1841. ]
  1842. );
  1843. $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
  1844. $type_group->freeze();
  1845. }
  1846. }
  1847. $option = [
  1848. EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
  1849. // Defined by user
  1850. EX_Q_SELECTION_RANDOM => get_lang('Random'),
  1851. // 1-10, All
  1852. 'per_categories' => '--------'.get_lang('Using categories').'----------',
  1853. // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
  1854. EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang('Ordered categories alphabetically with questions ordered'),
  1855. // A 123 B 456 C 78 (0, 1, all)
  1856. EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang('Random categories with questions ordered'),
  1857. // C 78 B 456 A 123
  1858. EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang('Ordered categories alphabetically with random questions'),
  1859. // A 321 B 654 C 87
  1860. EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang('Random categories with random questions'),
  1861. // C 87 B 654 A 321
  1862. //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
  1863. /* B 456 C 78 A 123
  1864. 456 78 123
  1865. 123 456 78
  1866. */
  1867. //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
  1868. /*
  1869. A 123 B 456 C 78
  1870. B 456 C 78 A 123
  1871. B 654 C 87 A 321
  1872. 654 87 321
  1873. 165 842 73
  1874. */
  1875. //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
  1876. //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
  1877. ];
  1878. $form->addElement(
  1879. 'select',
  1880. 'question_selection_type',
  1881. [get_lang('Question selection type')],
  1882. $option,
  1883. [
  1884. 'id' => 'questionSelection',
  1885. 'onchange' => 'checkQuestionSelection()',
  1886. ]
  1887. );
  1888. $group = [
  1889. $form->createElement(
  1890. 'checkbox',
  1891. 'hide_expected_answer',
  1892. null,
  1893. get_lang('Hide expected answers column')
  1894. ),
  1895. $form->createElement(
  1896. 'checkbox',
  1897. 'hide_total_score',
  1898. null,
  1899. get_lang('Hide total score')
  1900. ),
  1901. $form->createElement(
  1902. 'checkbox',
  1903. 'hide_question_score',
  1904. null,
  1905. get_lang('Hide question score')
  1906. ),
  1907. ];
  1908. $form->addGroup($group, null, get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback page configuration'));
  1909. $displayMatrix = 'none';
  1910. $displayRandom = 'none';
  1911. $selectionType = $this->getQuestionSelectionType();
  1912. switch ($selectionType) {
  1913. case EX_Q_SELECTION_RANDOM:
  1914. $displayRandom = 'block';
  1915. break;
  1916. case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
  1917. $displayMatrix = 'block';
  1918. break;
  1919. }
  1920. $form->addElement(
  1921. 'html',
  1922. '<div id="hidden_random" style="display:'.$displayRandom.'">'
  1923. );
  1924. // Number of random question.
  1925. $max = ($this->id > 0) ? $this->getQuestionCount() : 10;
  1926. $option = range(0, $max);
  1927. $option[0] = get_lang('No');
  1928. $option[-1] = get_lang('All');
  1929. $form->addElement(
  1930. 'select',
  1931. 'randomQuestions',
  1932. [
  1933. get_lang('Random questions'),
  1934. get_lang('Random questionsHelp'),
  1935. ],
  1936. $option,
  1937. ['id' => 'randomQuestions']
  1938. );
  1939. $form->addElement('html', '</div>');
  1940. $form->addElement(
  1941. 'html',
  1942. '<div id="hidden_matrix" style="display:'.$displayMatrix.'">'
  1943. );
  1944. // Category selection.
  1945. $cat = new TestCategory();
  1946. $cat_form = $cat->returnCategoryForm($this);
  1947. if (empty($cat_form)) {
  1948. $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
  1949. }
  1950. $form->addElement('label', null, $cat_form);
  1951. $form->addElement('html', '</div>');
  1952. // Random answers.
  1953. $radios_random_answers = [
  1954. $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
  1955. $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
  1956. ];
  1957. $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
  1958. // Category name.
  1959. $radio_display_cat_name = [
  1960. $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
  1961. $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
  1962. ];
  1963. $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
  1964. // Hide question title.
  1965. $group = [
  1966. $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
  1967. $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
  1968. ];
  1969. $form->addGroup($group, null, get_lang('Hide question title'));
  1970. $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
  1971. if ($allow === true) {
  1972. // Hide question title.
  1973. $group = [
  1974. $form->createElement(
  1975. 'radio',
  1976. 'show_previous_button',
  1977. null,
  1978. get_lang('Yes'),
  1979. '1'
  1980. ),
  1981. $form->createElement(
  1982. 'radio',
  1983. 'show_previous_button',
  1984. null,
  1985. get_lang('No'),
  1986. '0'
  1987. ),
  1988. ];
  1989. $form->addGroup($group, null, get_lang('Show previous button'));
  1990. }
  1991. // Attempts
  1992. $attempt_option = range(0, 10);
  1993. $attempt_option[0] = get_lang('Infinite');
  1994. $form->addElement(
  1995. 'select',
  1996. 'exerciseAttempts',
  1997. get_lang('max. 20 characters, e.g. <i>INNOV21</i> number of attempts'),
  1998. $attempt_option,
  1999. ['id' => 'exerciseAttempts']
  2000. );
  2001. // Exercise time limit
  2002. $form->addElement(
  2003. 'checkbox',
  2004. 'activate_start_date_check',
  2005. null,
  2006. get_lang('Enable start time'),
  2007. ['onclick' => 'activate_start_date()']
  2008. );
  2009. if (!empty($this->start_time)) {
  2010. $form->addElement('html', '<div id="start_date_div" style="display:block;">');
  2011. } else {
  2012. $form->addElement('html', '<div id="start_date_div" style="display:none;">');
  2013. }
  2014. $form->addElement('date_time_picker', 'start_time');
  2015. $form->addElement('html', '</div>');
  2016. $form->addElement(
  2017. 'checkbox',
  2018. 'activate_end_date_check',
  2019. null,
  2020. get_lang('Enable end time'),
  2021. ['onclick' => 'activate_end_date()']
  2022. );
  2023. if (!empty($this->end_time)) {
  2024. $form->addElement('html', '<div id="end_date_div" style="display:block;">');
  2025. } else {
  2026. $form->addElement('html', '<div id="end_date_div" style="display:none;">');
  2027. }
  2028. $form->addElement('date_time_picker', 'end_time');
  2029. $form->addElement('html', '</div>');
  2030. $display = 'block';
  2031. $form->addElement(
  2032. 'checkbox',
  2033. 'propagate_neg',
  2034. null,
  2035. get_lang('Propagate negative results between questions')
  2036. );
  2037. $options = [
  2038. '' => get_lang('Please select an option'),
  2039. 1 => get_lang('Save the correct answer for the next attempt'),
  2040. 2 => get_lang('Pre-fill with answers from previous attempt'),
  2041. ];
  2042. $form->addSelect(
  2043. 'save_correct_answers',
  2044. get_lang('Save answers'),
  2045. $options
  2046. );
  2047. $form->addElement('html', '<div class="clear">&nbsp;</div>');
  2048. $form->addElement('checkbox', 'review_answers', null, get_lang('Review my answers'));
  2049. $form->addElement('html', '<div id="divtimecontrol" style="display:'.$display.';">');
  2050. // Timer control
  2051. $form->addElement(
  2052. 'checkbox',
  2053. 'enabletimercontrol',
  2054. null,
  2055. get_lang('Enable time control'),
  2056. [
  2057. 'onclick' => 'option_time_expired()',
  2058. 'id' => 'enabletimercontrol',
  2059. 'onload' => 'check_load_time()',
  2060. ]
  2061. );
  2062. $expired_date = (int) $this->selectExpiredTime();
  2063. if (($expired_date != '0')) {
  2064. $form->addElement('html', '<div id="timercontrol" style="display:block;">');
  2065. } else {
  2066. $form->addElement('html', '<div id="timercontrol" style="display:none;">');
  2067. }
  2068. $form->addText(
  2069. 'enabletimercontroltotalminutes',
  2070. get_lang('Total duration in minutes of the test'),
  2071. false,
  2072. [
  2073. 'id' => 'enabletimercontroltotalminutes',
  2074. 'cols-size' => [2, 2, 8],
  2075. ]
  2076. );
  2077. $form->addElement('html', '</div>');
  2078. $form->addElement(
  2079. 'text',
  2080. 'pass_percentage',
  2081. [get_lang('Pass percentage'), null, '%'],
  2082. ['id' => 'pass_percentage']
  2083. );
  2084. $form->addRule('pass_percentage', get_lang('Numericalal'), 'numeric');
  2085. $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
  2086. $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
  2087. // add the text_when_finished textbox
  2088. $form->addHtmlEditor(
  2089. 'text_when_finished',
  2090. get_lang('Text appearing at the end of the test'),
  2091. false,
  2092. false,
  2093. $editor_config
  2094. );
  2095. $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
  2096. if ($allow === true) {
  2097. $settings = ExerciseLib::getNotificationSettings();
  2098. $group = [];
  2099. foreach ($settings as $itemId => $label) {
  2100. $group[] = $form->createElement(
  2101. 'checkbox',
  2102. 'notifications[]',
  2103. null,
  2104. $label,
  2105. ['value' => $itemId]
  2106. );
  2107. }
  2108. $form->addGroup($group, '', [get_lang('E-mail notifications')]);
  2109. }
  2110. $form->addCheckBox(
  2111. 'update_title_in_lps',
  2112. null,
  2113. get_lang('Update this title in learning paths')
  2114. );
  2115. $defaults = [];
  2116. if (api_get_setting('search_enabled') === 'true') {
  2117. require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
  2118. $form->addElement('checkbox', 'index_document', '', get_lang('Index document text?'));
  2119. $form->addSelectLanguage('language', get_lang('Document language for indexation'));
  2120. $specific_fields = get_specific_field_list();
  2121. foreach ($specific_fields as $specific_field) {
  2122. $form->addElement('text', $specific_field['code'], $specific_field['name']);
  2123. $filter = [
  2124. 'c_id' => api_get_course_int_id(),
  2125. 'field_id' => $specific_field['id'],
  2126. 'ref_id' => $this->id,
  2127. 'tool_id' => "'".TOOL_QUIZ."'",
  2128. ];
  2129. $values = get_specific_field_values_list($filter, ['value']);
  2130. if (!empty($values)) {
  2131. $arr_str_values = [];
  2132. foreach ($values as $value) {
  2133. $arr_str_values[] = $value['value'];
  2134. }
  2135. $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
  2136. }
  2137. }
  2138. }
  2139. $skillList = Skill::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
  2140. $extraField = new ExtraField('exercise');
  2141. $extraField->addElements(
  2142. $form,
  2143. $this->iId,
  2144. [], //exclude
  2145. false, // filter
  2146. false, // tag as select
  2147. [], //show only fields
  2148. [], // order fields
  2149. [] // extra data
  2150. );
  2151. $form->addElement('html', '</div>'); //End advanced setting
  2152. $form->addElement('html', '</div>');
  2153. }
  2154. // submit
  2155. if (isset($_GET['exerciseId'])) {
  2156. $form->addButtonSave(get_lang('Edit test name and settings'), 'submitTest');
  2157. } else {
  2158. $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitTest');
  2159. }
  2160. $form->addRule('exerciseTitle', get_lang('GiveTest name'), 'required');
  2161. // defaults
  2162. if ($type == 'full') {
  2163. // rules
  2164. $form->addRule('exerciseAttempts', get_lang('Numericalal'), 'numeric');
  2165. $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
  2166. $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
  2167. if ($this->id > 0) {
  2168. $defaults['randomQuestions'] = $this->random;
  2169. $defaults['randomAnswers'] = $this->getRandomAnswers();
  2170. $defaults['exerciseType'] = $this->selectType();
  2171. $defaults['exerciseTitle'] = $this->get_formated_title();
  2172. $defaults['exerciseDescription'] = $this->selectDescription();
  2173. $defaults['exerciseAttempts'] = $this->selectAttempts();
  2174. $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
  2175. $defaults['results_disabled'] = $this->selectResultsDisabled();
  2176. $defaults['propagate_neg'] = $this->selectPropagateNeg();
  2177. $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
  2178. $defaults['review_answers'] = $this->review_answers;
  2179. $defaults['randomByCat'] = $this->getRandomByCategory();
  2180. $defaults['text_when_finished'] = $this->getTextWhenFinished();
  2181. $defaults['display_category_name'] = $this->selectDisplayCategoryName();
  2182. $defaults['pass_percentage'] = $this->selectPassPercentage();
  2183. $defaults['question_selection_type'] = $this->getQuestionSelectionType();
  2184. $defaults['hide_question_title'] = $this->getHideQuestionTitle();
  2185. $defaults['show_previous_button'] = $this->showPreviousButton();
  2186. $defaults['exercise_category_id'] = $this->getExerciseCategoryId();
  2187. if (!empty($this->start_time)) {
  2188. $defaults['activate_start_date_check'] = 1;
  2189. }
  2190. if (!empty($this->end_time)) {
  2191. $defaults['activate_end_date_check'] = 1;
  2192. }
  2193. $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date('Y-m-d 12:00:00');
  2194. $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date('Y-m-d 12:00:00', time() + 84600);
  2195. // Get expired time
  2196. if ($this->expired_time != '0') {
  2197. $defaults['enabletimercontrol'] = 1;
  2198. $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
  2199. } else {
  2200. $defaults['enabletimercontroltotalminutes'] = 0;
  2201. }
  2202. $defaults['skills'] = array_keys($skillList);
  2203. $defaults['notifications'] = $this->getNotifications();
  2204. } else {
  2205. $defaults['exerciseType'] = 2;
  2206. $defaults['exerciseAttempts'] = 0;
  2207. $defaults['randomQuestions'] = 0;
  2208. $defaults['randomAnswers'] = 0;
  2209. $defaults['exerciseDescription'] = '';
  2210. $defaults['exerciseFeedbackType'] = 0;
  2211. $defaults['results_disabled'] = 0;
  2212. $defaults['randomByCat'] = 0;
  2213. $defaults['text_when_finished'] = '';
  2214. $defaults['start_time'] = date('Y-m-d 12:00:00');
  2215. $defaults['display_category_name'] = 1;
  2216. $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
  2217. $defaults['pass_percentage'] = '';
  2218. $defaults['end_button'] = $this->selectEndButton();
  2219. $defaults['question_selection_type'] = 1;
  2220. $defaults['hide_question_title'] = 0;
  2221. $defaults['show_previous_button'] = 1;
  2222. $defaults['on_success_message'] = null;
  2223. $defaults['on_failed_message'] = null;
  2224. }
  2225. } else {
  2226. $defaults['exerciseTitle'] = $this->selectTitle();
  2227. $defaults['exerciseDescription'] = $this->selectDescription();
  2228. }
  2229. if (api_get_setting('search_enabled') === 'true') {
  2230. $defaults['index_document'] = 'checked="checked"';
  2231. }
  2232. $this->setPageResultConfigurationDefaults($defaults);
  2233. $form->setDefaults($defaults);
  2234. // Freeze some elements.
  2235. if ($this->id != 0 && $this->edit_exercise_in_lp == false) {
  2236. $elementsToFreeze = [
  2237. 'randomQuestions',
  2238. //'randomByCat',
  2239. 'exerciseAttempts',
  2240. 'propagate_neg',
  2241. 'enabletimercontrol',
  2242. 'review_answers',
  2243. ];
  2244. foreach ($elementsToFreeze as $elementName) {
  2245. /** @var HTML_QuickForm_element $element */
  2246. $element = $form->getElement($elementName);
  2247. $element->freeze();
  2248. }
  2249. }
  2250. }
  2251. /**
  2252. * @param $form
  2253. */
  2254. public function setResultFeedbackGroup(FormValidator $form)
  2255. {
  2256. // Feedback type.
  2257. $feedback = [];
  2258. $feedback[] = $form->createElement(
  2259. 'radio',
  2260. 'exerciseFeedbackType',
  2261. null,
  2262. get_lang('At end of test'),
  2263. EXERCISE_FEEDBACK_TYPE_END,
  2264. [
  2265. 'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
  2266. 'onclick' => 'check_feedback()',
  2267. ]
  2268. );
  2269. if (api_get_setting('enable_quiz_scenario') === 'true') {
  2270. // Can't convert a question from one feedback to another
  2271. // if there is more than 1 question already added
  2272. if ($this->selectNbrQuestions() == 0) {
  2273. $feedback[] = $form->createElement(
  2274. 'radio',
  2275. 'exerciseFeedbackType',
  2276. null,
  2277. get_lang('Adaptative test with immediate feedback'),
  2278. EXERCISE_FEEDBACK_TYPE_DIRECT,
  2279. [
  2280. 'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
  2281. 'onclick' => 'check_direct_feedback()',
  2282. ]
  2283. );
  2284. }
  2285. }
  2286. $feedback[] = $form->createElement(
  2287. 'radio',
  2288. 'exerciseFeedbackType',
  2289. null,
  2290. get_lang('Direct feedback as pop-up'),
  2291. EXERCISE_FEEDBACK_TYPE_POPUP,
  2292. ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
  2293. );
  2294. $feedback[] = $form->createElement(
  2295. 'radio',
  2296. 'exerciseFeedbackType',
  2297. null,
  2298. get_lang('Exam (no feedback)'),
  2299. EXERCISE_FEEDBACK_TYPE_EXAM,
  2300. ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM]
  2301. );
  2302. $form->addGroup(
  2303. $feedback,
  2304. null,
  2305. [
  2306. get_lang('Feedback'),
  2307. get_lang('How should we show the feedback/comment for each question? This option defines how it will be shown to the learner when taking the test. We recommend you try different options by editing your test options before having learners take it.'),
  2308. ]
  2309. );
  2310. }
  2311. /**
  2312. * function which process the creation of exercises.
  2313. *
  2314. * @param FormValidator $form
  2315. *
  2316. * @return int c_quiz.iid
  2317. */
  2318. public function processCreation($form)
  2319. {
  2320. $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
  2321. $this->updateDescription($form->getSubmitValue('exerciseDescription'));
  2322. $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
  2323. $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
  2324. $this->updateType($form->getSubmitValue('exerciseType'));
  2325. $this->setRandom($form->getSubmitValue('randomQuestions'));
  2326. $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
  2327. $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
  2328. $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
  2329. $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
  2330. $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
  2331. $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
  2332. $this->updateTextWhenFinished($form->getSubmitValue('text_when_finished'));
  2333. $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
  2334. $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
  2335. $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
  2336. $this->updateCategories($form->getSubmitValue('category'));
  2337. $this->updateEndButton($form->getSubmitValue('end_button'));
  2338. $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
  2339. $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
  2340. $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
  2341. $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
  2342. $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
  2343. $this->setModelType($form->getSubmitValue('model_type'));
  2344. $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
  2345. $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
  2346. $this->sessionId = api_get_session_id();
  2347. $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
  2348. $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
  2349. $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
  2350. $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
  2351. $this->setNotifications($form->getSubmitValue('notifications'));
  2352. $this->setExerciseCategoryId($form->getSubmitValue('exercise_category_id'));
  2353. $this->setPageResultConfiguration($form->getSubmitValues());
  2354. $this->start_time = null;
  2355. if ($form->getSubmitValue('activate_start_date_check') == 1) {
  2356. $start_time = $form->getSubmitValue('start_time');
  2357. $this->start_time = api_get_utc_datetime($start_time);
  2358. }
  2359. $this->end_time = null;
  2360. if ($form->getSubmitValue('activate_end_date_check') == 1) {
  2361. $end_time = $form->getSubmitValue('end_time');
  2362. $this->end_time = api_get_utc_datetime($end_time);
  2363. }
  2364. $this->expired_time = 0;
  2365. if ($form->getSubmitValue('enabletimercontrol') == 1) {
  2366. $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
  2367. if ($this->expired_time == 0) {
  2368. $this->expired_time = $expired_total_time;
  2369. }
  2370. }
  2371. $this->random_answers = 0;
  2372. if ($form->getSubmitValue('randomAnswers') == 1) {
  2373. $this->random_answers = 1;
  2374. }
  2375. // Update title in all LPs that have this quiz added
  2376. if ($form->getSubmitValue('update_title_in_lps') == 1) {
  2377. $courseId = api_get_course_int_id();
  2378. $table = Database::get_course_table(TABLE_LP_ITEM);
  2379. $sql = "SELECT * FROM $table
  2380. WHERE
  2381. c_id = $courseId AND
  2382. item_type = 'quiz' AND
  2383. path = '".$this->id."'
  2384. ";
  2385. $result = Database::query($sql);
  2386. $items = Database::store_result($result);
  2387. if (!empty($items)) {
  2388. foreach ($items as $item) {
  2389. $itemId = $item['iid'];
  2390. $sql = "UPDATE $table SET title = '".$this->title."'
  2391. WHERE iid = $itemId AND c_id = $courseId ";
  2392. Database::query($sql);
  2393. }
  2394. }
  2395. }
  2396. $iId = $this->save();
  2397. if (!empty($iId)) {
  2398. $values = $form->getSubmitValues();
  2399. $values['item_id'] = $iId;
  2400. $extraFieldValue = new ExtraFieldValue('exercise');
  2401. $extraFieldValue->saveFieldValues($values);
  2402. Skill::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
  2403. }
  2404. }
  2405. public function search_engine_save()
  2406. {
  2407. if ($_POST['index_document'] != 1) {
  2408. return;
  2409. }
  2410. $course_id = api_get_course_id();
  2411. require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
  2412. $specific_fields = get_specific_field_list();
  2413. $ic_slide = new IndexableChunk();
  2414. $all_specific_terms = '';
  2415. foreach ($specific_fields as $specific_field) {
  2416. if (isset($_REQUEST[$specific_field['code']])) {
  2417. $sterms = trim($_REQUEST[$specific_field['code']]);
  2418. if (!empty($sterms)) {
  2419. $all_specific_terms .= ' '.$sterms;
  2420. $sterms = explode(',', $sterms);
  2421. foreach ($sterms as $sterm) {
  2422. $ic_slide->addTerm(trim($sterm), $specific_field['code']);
  2423. add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->id, $sterm);
  2424. }
  2425. }
  2426. }
  2427. }
  2428. // build the chunk to index
  2429. $ic_slide->addValue("title", $this->exercise);
  2430. $ic_slide->addCourseId($course_id);
  2431. $ic_slide->addToolId(TOOL_QUIZ);
  2432. $xapian_data = [
  2433. SE_COURSE_ID => $course_id,
  2434. SE_TOOL_ID => TOOL_QUIZ,
  2435. SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->id],
  2436. SE_USER => (int) api_get_user_id(),
  2437. ];
  2438. $ic_slide->xapian_data = serialize($xapian_data);
  2439. $exercise_description = $all_specific_terms.' '.$this->description;
  2440. $ic_slide->addValue("content", $exercise_description);
  2441. $di = new ChamiloIndexer();
  2442. isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
  2443. $di->connectDb(null, null, $lang);
  2444. $di->addChunk($ic_slide);
  2445. //index and return search engine document id
  2446. $did = $di->index();
  2447. if ($did) {
  2448. // save it to db
  2449. $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
  2450. $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
  2451. VALUES (NULL , \'%s\', \'%s\', %s, %s)';
  2452. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id, $did);
  2453. Database::query($sql);
  2454. }
  2455. }
  2456. public function search_engine_edit()
  2457. {
  2458. // update search enchine and its values table if enabled
  2459. if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
  2460. $course_id = api_get_course_id();
  2461. // actually, it consists on delete terms from db,
  2462. // insert new ones, create a new search engine document, and remove the old one
  2463. // get search_did
  2464. $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
  2465. $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
  2466. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  2467. $res = Database::query($sql);
  2468. if (Database::num_rows($res) > 0) {
  2469. require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
  2470. $se_ref = Database::fetch_array($res);
  2471. $specific_fields = get_specific_field_list();
  2472. $ic_slide = new IndexableChunk();
  2473. $all_specific_terms = '';
  2474. foreach ($specific_fields as $specific_field) {
  2475. delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->id);
  2476. if (isset($_REQUEST[$specific_field['code']])) {
  2477. $sterms = trim($_REQUEST[$specific_field['code']]);
  2478. $all_specific_terms .= ' '.$sterms;
  2479. $sterms = explode(',', $sterms);
  2480. foreach ($sterms as $sterm) {
  2481. $ic_slide->addTerm(trim($sterm), $specific_field['code']);
  2482. add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->id, $sterm);
  2483. }
  2484. }
  2485. }
  2486. // build the chunk to index
  2487. $ic_slide->addValue('title', $this->exercise);
  2488. $ic_slide->addCourseId($course_id);
  2489. $ic_slide->addToolId(TOOL_QUIZ);
  2490. $xapian_data = [
  2491. SE_COURSE_ID => $course_id,
  2492. SE_TOOL_ID => TOOL_QUIZ,
  2493. SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->id],
  2494. SE_USER => (int) api_get_user_id(),
  2495. ];
  2496. $ic_slide->xapian_data = serialize($xapian_data);
  2497. $exercise_description = $all_specific_terms.' '.$this->description;
  2498. $ic_slide->addValue("content", $exercise_description);
  2499. $di = new ChamiloIndexer();
  2500. isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
  2501. $di->connectDb(null, null, $lang);
  2502. $di->remove_document($se_ref['search_did']);
  2503. $di->addChunk($ic_slide);
  2504. //index and return search engine document id
  2505. $did = $di->index();
  2506. if ($did) {
  2507. // save it to db
  2508. $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
  2509. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  2510. Database::query($sql);
  2511. $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
  2512. VALUES (NULL , \'%s\', \'%s\', %s, %s)';
  2513. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id, $did);
  2514. Database::query($sql);
  2515. }
  2516. } else {
  2517. $this->search_engine_save();
  2518. }
  2519. }
  2520. }
  2521. public function search_engine_delete()
  2522. {
  2523. // remove from search engine if enabled
  2524. if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
  2525. $course_id = api_get_course_id();
  2526. $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
  2527. $sql = 'SELECT * FROM %s
  2528. WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
  2529. LIMIT 1';
  2530. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  2531. $res = Database::query($sql);
  2532. if (Database::num_rows($res) > 0) {
  2533. $row = Database::fetch_array($res);
  2534. $di = new ChamiloIndexer();
  2535. $di->remove_document($row['search_did']);
  2536. unset($di);
  2537. $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
  2538. foreach ($this->questionList as $question_i) {
  2539. $sql = 'SELECT type FROM %s WHERE id=%s';
  2540. $sql = sprintf($sql, $tbl_quiz_question, $question_i);
  2541. $qres = Database::query($sql);
  2542. if (Database::num_rows($qres) > 0) {
  2543. $qrow = Database::fetch_array($qres);
  2544. $objQuestion = Question::getInstance($qrow['type']);
  2545. $objQuestion = Question::read((int) $question_i);
  2546. $objQuestion->search_engine_edit($this->id, false, true);
  2547. unset($objQuestion);
  2548. }
  2549. }
  2550. }
  2551. $sql = 'DELETE FROM %s
  2552. WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
  2553. LIMIT 1';
  2554. $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
  2555. Database::query($sql);
  2556. // remove terms from db
  2557. require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
  2558. delete_all_values_for_item($course_id, TOOL_QUIZ, $this->id);
  2559. }
  2560. }
  2561. public function selectExpiredTime()
  2562. {
  2563. return $this->expired_time;
  2564. }
  2565. /**
  2566. * Cleans the student's results only for the Exercise tool (Not from the LP)
  2567. * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
  2568. * Works with exercises in sessions.
  2569. *
  2570. * @param bool $cleanLpTests
  2571. * @param string $cleanResultBeforeDate
  2572. *
  2573. * @return int quantity of user's exercises deleted
  2574. */
  2575. public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
  2576. {
  2577. $sessionId = api_get_session_id();
  2578. $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  2579. $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
  2580. $sql_where = ' AND
  2581. orig_lp_id = 0 AND
  2582. orig_lp_item_id = 0';
  2583. // if we want to delete results from LP too
  2584. if ($cleanLpTests) {
  2585. $sql_where = '';
  2586. }
  2587. // if we want to delete attempts before date $cleanResultBeforeDate
  2588. // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
  2589. if (!empty($cleanResultBeforeDate)) {
  2590. $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
  2591. if (api_is_valid_date($cleanResultBeforeDate)) {
  2592. $sql_where .= " AND exe_date <= '$cleanResultBeforeDate' ";
  2593. } else {
  2594. return 0;
  2595. }
  2596. }
  2597. $sql = "SELECT exe_id
  2598. FROM $table_track_e_exercises
  2599. WHERE
  2600. c_id = ".api_get_course_int_id()." AND
  2601. exe_exo_id = ".$this->id." AND
  2602. session_id = ".$sessionId." ".
  2603. $sql_where;
  2604. $result = Database::query($sql);
  2605. $exe_list = Database::store_result($result);
  2606. // deleting TRACK_E_ATTEMPT table
  2607. // check if exe in learning path or not
  2608. $i = 0;
  2609. if (is_array($exe_list) && count($exe_list) > 0) {
  2610. foreach ($exe_list as $item) {
  2611. $sql = "DELETE FROM $table_track_e_attempt
  2612. WHERE exe_id = '".$item['exe_id']."'";
  2613. Database::query($sql);
  2614. $i++;
  2615. }
  2616. }
  2617. // delete TRACK_E_EXERCISES table
  2618. $sql = "DELETE FROM $table_track_e_exercises
  2619. WHERE
  2620. c_id = ".api_get_course_int_id()." AND
  2621. exe_exo_id = ".$this->id." $sql_where AND
  2622. session_id = ".$sessionId;
  2623. Database::query($sql);
  2624. $this->generateStats($this->id, api_get_course_info(), $sessionId);
  2625. Event::addEvent(
  2626. LOG_EXERCISE_RESULT_DELETE,
  2627. LOG_EXERCISE_ID,
  2628. $this->id,
  2629. null,
  2630. null,
  2631. api_get_course_int_id(),
  2632. $sessionId
  2633. );
  2634. return $i;
  2635. }
  2636. /**
  2637. * Copies an exercise (duplicate all questions and answers).
  2638. */
  2639. public function copyExercise()
  2640. {
  2641. $exerciseObject = $this;
  2642. $categories = $exerciseObject->getCategoriesInExercise();
  2643. // Get all questions no matter the order/category settings
  2644. $questionList = $exerciseObject->getQuestionOrderedList();
  2645. // Force the creation of a new exercise
  2646. $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
  2647. // Hides the new exercise
  2648. $exerciseObject->updateStatus(false);
  2649. $exerciseObject->updateId(0);
  2650. $exerciseObject->sessionId = api_get_session_id();
  2651. $exerciseObject->save();
  2652. $newId = $exerciseObject->selectId();
  2653. if ($newId && !empty($questionList)) {
  2654. // Question creation
  2655. foreach ($questionList as $oldQuestionId) {
  2656. $oldQuestionObj = Question::read($oldQuestionId);
  2657. $newQuestionId = $oldQuestionObj->duplicate();
  2658. if ($newQuestionId) {
  2659. $newQuestionObj = Question::read($newQuestionId);
  2660. if (isset($newQuestionObj) && $newQuestionObj) {
  2661. $newQuestionObj->addToList($newId);
  2662. if (!empty($oldQuestionObj->category)) {
  2663. $newQuestionObj->saveCategory($oldQuestionObj->category);
  2664. }
  2665. // This should be moved to the duplicate function
  2666. $newAnswerObj = new Answer($oldQuestionId);
  2667. $newAnswerObj->read();
  2668. $newAnswerObj->duplicate($newQuestionObj);
  2669. }
  2670. }
  2671. }
  2672. if (!empty($categories)) {
  2673. $newCategoryList = [];
  2674. foreach ($categories as $category) {
  2675. $newCategoryList[$category['category_id']] = $category['count_questions'];
  2676. }
  2677. $exerciseObject->save_categories_in_exercise($newCategoryList);
  2678. }
  2679. }
  2680. }
  2681. /**
  2682. * Changes the exercise status.
  2683. *
  2684. * @param string $status - exercise status
  2685. */
  2686. public function updateStatus($status)
  2687. {
  2688. $this->active = $status;
  2689. }
  2690. /**
  2691. * @param int $lp_id
  2692. * @param int $lp_item_id
  2693. * @param int $lp_item_view_id
  2694. * @param string $status
  2695. *
  2696. * @return array
  2697. */
  2698. public function get_stat_track_exercise_info(
  2699. $lp_id = 0,
  2700. $lp_item_id = 0,
  2701. $lp_item_view_id = 0,
  2702. $status = 'incomplete'
  2703. ) {
  2704. $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  2705. if (empty($lp_id)) {
  2706. $lp_id = 0;
  2707. }
  2708. if (empty($lp_item_id)) {
  2709. $lp_item_id = 0;
  2710. }
  2711. if (empty($lp_item_view_id)) {
  2712. $lp_item_view_id = 0;
  2713. }
  2714. $condition = ' WHERE exe_exo_id = '."'".$this->id."'".' AND
  2715. exe_user_id = '."'".api_get_user_id()."'".' AND
  2716. c_id = '.api_get_course_int_id().' AND
  2717. status = '."'".Database::escape_string($status)."'".' AND
  2718. orig_lp_id = '."'".$lp_id."'".' AND
  2719. orig_lp_item_id = '."'".$lp_item_id."'".' AND
  2720. orig_lp_item_view_id = '."'".$lp_item_view_id."'".' AND
  2721. session_id = '."'".api_get_session_id()."' LIMIT 1"; //Adding limit 1 just in case
  2722. $sql_track = 'SELECT * FROM '.$track_exercises.$condition;
  2723. $result = Database::query($sql_track);
  2724. $new_array = [];
  2725. if (Database::num_rows($result) > 0) {
  2726. $new_array = Database::fetch_array($result, 'ASSOC');
  2727. $new_array['num_exe'] = Database::num_rows($result);
  2728. }
  2729. return $new_array;
  2730. }
  2731. /**
  2732. * Saves a test attempt.
  2733. *
  2734. * @param int $clock_expired_time clock_expired_time
  2735. * @param int int lp id
  2736. * @param int int lp item id
  2737. * @param int int lp item_view id
  2738. * @param array $questionList
  2739. * @param float $weight
  2740. *
  2741. * @return int
  2742. */
  2743. public function save_stat_track_exercise_info(
  2744. $clock_expired_time = 0,
  2745. $safe_lp_id = 0,
  2746. $safe_lp_item_id = 0,
  2747. $safe_lp_item_view_id = 0,
  2748. $questionList = [],
  2749. $weight = 0
  2750. ) {
  2751. $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  2752. $safe_lp_id = (int) $safe_lp_id;
  2753. $safe_lp_item_id = (int) $safe_lp_item_id;
  2754. $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
  2755. if (empty($clock_expired_time)) {
  2756. $clock_expired_time = null;
  2757. }
  2758. $questionList = array_map('intval', $questionList);
  2759. $params = [
  2760. 'exe_exo_id' => $this->id,
  2761. 'exe_user_id' => api_get_user_id(),
  2762. 'c_id' => api_get_course_int_id(),
  2763. 'status' => 'incomplete',
  2764. 'session_id' => api_get_session_id(),
  2765. 'data_tracking' => implode(',', $questionList),
  2766. 'start_date' => api_get_utc_datetime(),
  2767. 'orig_lp_id' => $safe_lp_id,
  2768. 'orig_lp_item_id' => $safe_lp_item_id,
  2769. 'orig_lp_item_view_id' => $safe_lp_item_view_id,
  2770. 'max_score' => $weight,
  2771. 'user_ip' => Database::escape_string(api_get_real_ip()),
  2772. 'exe_date' => api_get_utc_datetime(),
  2773. 'score' => 0,
  2774. 'steps_counter' => 0,
  2775. 'exe_duration' => 0,
  2776. 'expired_time_control' => $clock_expired_time,
  2777. 'questions_to_check' => '',
  2778. ];
  2779. $id = Database::insert($track_exercises, $params);
  2780. return $id;
  2781. }
  2782. /**
  2783. * @param int $question_id
  2784. * @param int $questionNum
  2785. * @param array $questions_in_media
  2786. * @param string $currentAnswer
  2787. * @param array $myRemindList
  2788. *
  2789. * @return string
  2790. */
  2791. public function show_button(
  2792. $question_id,
  2793. $questionNum,
  2794. $questions_in_media = [],
  2795. $currentAnswer = '',
  2796. $myRemindList = []
  2797. ) {
  2798. global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
  2799. $nbrQuestions = $this->getQuestionCount();
  2800. $buttonList = [];
  2801. $html = $label = '';
  2802. $hotspot_get = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
  2803. if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
  2804. $this->type == ONE_PER_PAGE
  2805. ) {
  2806. $urlTitle = get_lang('Proceed with the test');
  2807. if ($questionNum == count($this->questionList)) {
  2808. $urlTitle = get_lang('End test');
  2809. }
  2810. $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
  2811. $url .= '&'.http_build_query([
  2812. 'learnpath_id' => $safe_lp_id,
  2813. 'learnpath_item_id' => $safe_lp_item_id,
  2814. 'learnpath_item_view_id' => $safe_lp_item_view_id,
  2815. 'hotspot' => $hotspot_get,
  2816. 'nbrQuestions' => $nbrQuestions,
  2817. 'num' => $questionNum,
  2818. 'exerciseType' => $this->type,
  2819. 'exerciseId' => $this->id,
  2820. 'reminder' => empty($myRemindList) ? null : 2,
  2821. ]);
  2822. $params = [
  2823. 'class' => 'ajax btn btn-default no-close-button',
  2824. 'data-title' => Security::remove_XSS(get_lang('Comment')),
  2825. 'data-size' => 'md',
  2826. 'id' => "button_$question_id",
  2827. ];
  2828. if ($this->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP) {
  2829. //$params['data-block-div-after-closing'] = "question_div_$question_id";
  2830. $params['data-block-closing'] = 'true';
  2831. }
  2832. $html .= Display::url(
  2833. $urlTitle,
  2834. $url,
  2835. $params
  2836. );
  2837. $html .= '<br />';
  2838. } else {
  2839. // User
  2840. if (api_is_allowed_to_session_edit()) {
  2841. $endReminderValue = false;
  2842. if (!empty($myRemindList)) {
  2843. $endValue = end($myRemindList);
  2844. if ($endValue == $question_id) {
  2845. $endReminderValue = true;
  2846. }
  2847. }
  2848. if ($this->type == ALL_ON_ONE_PAGE || $nbrQuestions == $questionNum || $endReminderValue) {
  2849. if ($this->review_answers) {
  2850. $label = get_lang('Review selected questions');
  2851. $class = 'btn btn-success';
  2852. } else {
  2853. $label = get_lang('End test');
  2854. $class = 'btn btn-warning';
  2855. }
  2856. } else {
  2857. $label = get_lang('Next question');
  2858. $class = 'btn btn-primary';
  2859. }
  2860. // used to select it with jquery
  2861. $class .= ' question-validate-btn';
  2862. if ($this->type == ONE_PER_PAGE) {
  2863. if ($questionNum != 1) {
  2864. if ($this->showPreviousButton()) {
  2865. $prev_question = $questionNum - 2;
  2866. $showPreview = true;
  2867. if (!empty($myRemindList)) {
  2868. $beforeId = null;
  2869. for ($i = 0; $i < count($myRemindList); $i++) {
  2870. if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
  2871. $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
  2872. break;
  2873. }
  2874. }
  2875. if (empty($beforeId)) {
  2876. $showPreview = false;
  2877. } else {
  2878. $num = 0;
  2879. foreach ($this->questionList as $originalQuestionId) {
  2880. if ($originalQuestionId == $beforeId) {
  2881. break;
  2882. }
  2883. $num++;
  2884. }
  2885. $prev_question = $num;
  2886. }
  2887. }
  2888. if ($showPreview) {
  2889. $buttonList[] = Display::button(
  2890. 'previous_question_and_save',
  2891. get_lang('Previous question'),
  2892. [
  2893. 'type' => 'button',
  2894. 'class' => 'btn btn-default',
  2895. 'data-prev' => $prev_question,
  2896. 'data-question' => $question_id,
  2897. ]
  2898. );
  2899. }
  2900. }
  2901. }
  2902. // Next question
  2903. if (!empty($questions_in_media)) {
  2904. $buttonList[] = Display::button(
  2905. 'save_question_list',
  2906. $label,
  2907. [
  2908. 'type' => 'button',
  2909. 'class' => $class,
  2910. 'data-list' => implode(",", $questions_in_media),
  2911. ]
  2912. );
  2913. } else {
  2914. $buttonList[] = Display::button(
  2915. 'save_now',
  2916. $label,
  2917. ['type' => 'button', 'class' => $class, 'data-question' => $question_id]
  2918. );
  2919. }
  2920. $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>&nbsp;';
  2921. $html .= implode(PHP_EOL, $buttonList);
  2922. } else {
  2923. if ($this->review_answers) {
  2924. $all_label = get_lang('Review selected questions');
  2925. $class = 'btn btn-success';
  2926. } else {
  2927. $all_label = get_lang('End test');
  2928. $class = 'btn btn-warning';
  2929. }
  2930. // used to select it with jquery
  2931. $class .= ' question-validate-btn';
  2932. $buttonList[] = Display::button(
  2933. 'validate_all',
  2934. $all_label,
  2935. ['type' => 'button', 'class' => $class]
  2936. );
  2937. $buttonList[] = '&nbsp;'.Display::span(null, ['id' => 'save_all_response']);
  2938. $html .= implode(PHP_EOL, $buttonList);
  2939. }
  2940. }
  2941. }
  2942. return $html;
  2943. }
  2944. /**
  2945. * @param int $timeLeft in seconds
  2946. * @param string $url
  2947. *
  2948. * @return string
  2949. */
  2950. public function showSimpleTimeControl($timeLeft, $url = '')
  2951. {
  2952. $timeLeft = (int) $timeLeft;
  2953. return "<script>
  2954. function openClockWarning() {
  2955. $('#clock_warning').dialog({
  2956. modal:true,
  2957. height:320,
  2958. width:550,
  2959. closeOnEscape: false,
  2960. resizable: false,
  2961. buttons: {
  2962. '".addslashes(get_lang('Close'))."': function() {
  2963. $('#clock_warning').dialog('close');
  2964. }
  2965. },
  2966. close: function() {
  2967. window.location.href = '$url';
  2968. }
  2969. });
  2970. $('#clock_warning').dialog('open');
  2971. $('#counter_to_redirect').epiclock({
  2972. mode: $.epiclock.modes.countdown,
  2973. offset: {seconds: 5},
  2974. format: 's'
  2975. }).bind('timer', function () {
  2976. window.location.href = '$url';
  2977. });
  2978. }
  2979. function onExpiredTimeExercise() {
  2980. $('#wrapper-clock').hide();
  2981. $('#expired-message-id').show();
  2982. // Fixes bug #5263
  2983. $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
  2984. openClockWarning();
  2985. }
  2986. $(function() {
  2987. // time in seconds when using minutes there are some seconds lost
  2988. var time_left = parseInt(".$timeLeft.");
  2989. $('#exercise_clock_warning').epiclock({
  2990. mode: $.epiclock.modes.countdown,
  2991. offset: {seconds: time_left},
  2992. format: 'x:i:s',
  2993. renderer: 'minute'
  2994. }).bind('timer', function () {
  2995. onExpiredTimeExercise();
  2996. });
  2997. $('#submit_save').click(function () {});
  2998. });
  2999. </script>";
  3000. }
  3001. /**
  3002. * So the time control will work.
  3003. *
  3004. * @param int $timeLeft
  3005. *
  3006. * @return string
  3007. */
  3008. public function showTimeControlJS($timeLeft)
  3009. {
  3010. $timeLeft = (int) $timeLeft;
  3011. $script = 'redirectExerciseToResult();';
  3012. if ($this->type == ALL_ON_ONE_PAGE) {
  3013. $script = "save_now_all('validate');";
  3014. }
  3015. return "<script>
  3016. function openClockWarning() {
  3017. $('#clock_warning').dialog({
  3018. modal:true,
  3019. height:320,
  3020. width:550,
  3021. closeOnEscape: false,
  3022. resizable: false,
  3023. buttons: {
  3024. '".addslashes(get_lang('End test'))."': function() {
  3025. $('#clock_warning').dialog('close');
  3026. }
  3027. },
  3028. close: function() {
  3029. send_form();
  3030. }
  3031. });
  3032. $('#clock_warning').dialog('open');
  3033. $('#counter_to_redirect').epiclock({
  3034. mode: $.epiclock.modes.countdown,
  3035. offset: {seconds: 5},
  3036. format: 's'
  3037. }).bind('timer', function () {
  3038. send_form();
  3039. });
  3040. }
  3041. function send_form() {
  3042. if ($('#exercise_form').length) {
  3043. $script
  3044. } else {
  3045. // In exercise_reminder.php
  3046. final_submit();
  3047. }
  3048. }
  3049. function onExpiredTimeExercise() {
  3050. $('#wrapper-clock').hide();
  3051. $('#expired-message-id').show();
  3052. // Fixes bug #5263
  3053. $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
  3054. openClockWarning();
  3055. }
  3056. $(function() {
  3057. // time in seconds when using minutes there are some seconds lost
  3058. var time_left = parseInt(".$timeLeft.");
  3059. $('#exercise_clock_warning').epiclock({
  3060. mode: $.epiclock.modes.countdown,
  3061. offset: {seconds: time_left},
  3062. format: 'x:i:s',
  3063. renderer: 'minute'
  3064. }).bind('timer', function () {
  3065. onExpiredTimeExercise();
  3066. });
  3067. $('#submit_save').click(function () {});
  3068. });
  3069. </script>";
  3070. }
  3071. /**
  3072. * This function was originally found in the exercise_show.php.
  3073. *
  3074. * @param int $exeId
  3075. * @param int $questionId
  3076. * @param mixed $choice the user-selected option
  3077. * @param string $from function is called from 'exercise_show' or
  3078. * 'exercise_result'
  3079. * @param array $exerciseResultCoordinates the hotspot coordinates $hotspot[$question_id] =
  3080. * coordinates
  3081. * @param bool $saved_results save results in the DB or just show the reponse
  3082. * @param bool $from_database gets information from DB or from the current selection
  3083. * @param bool $show_result show results or not
  3084. * @param int $propagate_neg
  3085. * @param array $hotspot_delineation_result
  3086. * @param bool $showTotalScoreAndUserChoicesInLastAttempt
  3087. * @param bool $updateResults
  3088. * @param bool $showHotSpotDelineationTable
  3089. *
  3090. * @todo reduce parameters of this function
  3091. *
  3092. * @return string html code
  3093. */
  3094. public function manage_answer(
  3095. $exeId,
  3096. $questionId,
  3097. $choice,
  3098. $from = 'exercise_show',
  3099. $exerciseResultCoordinates = [],
  3100. $saved_results = true,
  3101. $from_database = false,
  3102. $show_result = true,
  3103. $propagate_neg = 0,
  3104. $hotspot_delineation_result = [],
  3105. $showTotalScoreAndUserChoicesInLastAttempt = true,
  3106. $updateResults = false,
  3107. $showHotSpotDelineationTable = false
  3108. ) {
  3109. $debug = false;
  3110. //needed in order to use in the exercise_attempt() for the time
  3111. global $learnpath_id, $learnpath_item_id;
  3112. require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
  3113. $em = Database::getManager();
  3114. $feedback_type = $this->getFeedbackType();
  3115. $results_disabled = $this->selectResultsDisabled();
  3116. if ($debug) {
  3117. error_log("<------ manage_answer ------> ");
  3118. error_log('exe_id: '.$exeId);
  3119. error_log('$from: '.$from);
  3120. error_log('$saved_results: '.intval($saved_results));
  3121. error_log('$from_database: '.intval($from_database));
  3122. error_log('$show_result: '.intval($show_result));
  3123. error_log('$propagate_neg: '.$propagate_neg);
  3124. error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
  3125. error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
  3126. error_log('$learnpath_id: '.$learnpath_id);
  3127. error_log('$learnpath_item_id: '.$learnpath_item_id);
  3128. error_log('$choice: '.print_r($choice, 1));
  3129. }
  3130. $final_overlap = 0;
  3131. $final_missing = 0;
  3132. $final_excess = 0;
  3133. $overlap_color = 0;
  3134. $missing_color = 0;
  3135. $excess_color = 0;
  3136. $threadhold1 = 0;
  3137. $threadhold2 = 0;
  3138. $threadhold3 = 0;
  3139. $arrques = null;
  3140. $arrans = null;
  3141. $studentChoice = null;
  3142. $expectedAnswer = '';
  3143. $calculatedChoice = '';
  3144. $calculatedStatus = '';
  3145. $questionId = (int) $questionId;
  3146. $exeId = (int) $exeId;
  3147. $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
  3148. $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
  3149. $studentChoiceDegree = null;
  3150. // Creates a temporary Question object
  3151. $course_id = $this->course_id;
  3152. $objQuestionTmp = Question::read($questionId, $this->course);
  3153. if ($objQuestionTmp === false) {
  3154. return false;
  3155. }
  3156. $questionName = $objQuestionTmp->selectTitle();
  3157. $questionWeighting = $objQuestionTmp->selectWeighting();
  3158. $answerType = $objQuestionTmp->selectType();
  3159. $quesId = $objQuestionTmp->selectId();
  3160. $extra = $objQuestionTmp->extra;
  3161. $next = 1; //not for now
  3162. $totalWeighting = 0;
  3163. $totalScore = 0;
  3164. // Extra information of the question
  3165. if ((
  3166. $answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
  3167. $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
  3168. )
  3169. && !empty($extra)
  3170. ) {
  3171. $extra = explode(':', $extra);
  3172. if ($debug) {
  3173. error_log(print_r($extra, 1));
  3174. }
  3175. // Fixes problems with negatives values using intval
  3176. $true_score = floatval(trim($extra[0]));
  3177. $false_score = floatval(trim($extra[1]));
  3178. $doubt_score = floatval(trim($extra[2]));
  3179. }
  3180. // Construction of the Answer object
  3181. $objAnswerTmp = new Answer($questionId, $course_id);
  3182. $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
  3183. if ($debug) {
  3184. error_log('Count of answers: '.$nbrAnswers);
  3185. error_log('$answerType: '.$answerType);
  3186. }
  3187. if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
  3188. $choiceTmp = $choice;
  3189. $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
  3190. $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
  3191. }
  3192. if ($answerType == FREE_ANSWER ||
  3193. $answerType == ORAL_EXPRESSION ||
  3194. $answerType == CALCULATED_ANSWER ||
  3195. $answerType == ANNOTATION
  3196. ) {
  3197. $nbrAnswers = 1;
  3198. }
  3199. $generatedFile = '';
  3200. if ($answerType == ORAL_EXPRESSION) {
  3201. $exe_info = Event::get_exercise_results_by_attempt($exeId);
  3202. $exe_info = isset($exe_info[$exeId]) ? $exe_info[$exeId] : null;
  3203. $objQuestionTmp->initFile(
  3204. api_get_session_id(),
  3205. isset($exe_info['exe_user_id']) ? $exe_info['exe_user_id'] : api_get_user_id(),
  3206. isset($exe_info['exe_exo_id']) ? $exe_info['exe_exo_id'] : $this->id,
  3207. isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
  3208. );
  3209. // Probably this attempt came in an exercise all question by page
  3210. if ($feedback_type == 0) {
  3211. $objQuestionTmp->replaceWithRealExe($exeId);
  3212. }
  3213. $generatedFile = $objQuestionTmp->getFileUrl();
  3214. }
  3215. $user_answer = '';
  3216. // Get answer list for matching
  3217. $sql = "SELECT id_auto, id, answer
  3218. FROM $table_ans
  3219. WHERE c_id = $course_id AND question_id = $questionId";
  3220. $res_answer = Database::query($sql);
  3221. $answerMatching = [];
  3222. while ($real_answer = Database::fetch_array($res_answer)) {
  3223. $answerMatching[$real_answer['id_auto']] = $real_answer['answer'];
  3224. }
  3225. $real_answers = [];
  3226. $quiz_question_options = Question::readQuestionOption(
  3227. $questionId,
  3228. $course_id
  3229. );
  3230. $organs_at_risk_hit = 0;
  3231. $questionScore = 0;
  3232. $orderedHotSpots = [];
  3233. if ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
  3234. $orderedHotSpots = $em->getRepository('ChamiloCoreBundle:TrackEHotspot')->findBy(
  3235. [
  3236. 'hotspotQuestionId' => $questionId,
  3237. 'course' => $course_id,
  3238. 'hotspotExeId' => $exeId,
  3239. ],
  3240. ['hotspotAnswerId' => 'ASC']
  3241. );
  3242. }
  3243. if ($debug) {
  3244. error_log('Start answer loop ');
  3245. }
  3246. $answerDestination = null;
  3247. $userAnsweredQuestion = false;
  3248. $correctAnswerId = null;
  3249. for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
  3250. $answer = $objAnswerTmp->selectAnswer($answerId);
  3251. $answerComment = $objAnswerTmp->selectComment($answerId);
  3252. $answerCorrect = $objAnswerTmp->isCorrect($answerId);
  3253. $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
  3254. $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
  3255. $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
  3256. if ($debug) {
  3257. error_log("answer auto id: $answerAutoId ");
  3258. error_log("answer correct: $answerCorrect ");
  3259. }
  3260. // Delineation
  3261. $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
  3262. $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
  3263. switch ($answerType) {
  3264. case UNIQUE_ANSWER:
  3265. case UNIQUE_ANSWER_IMAGE:
  3266. case UNIQUE_ANSWER_NO_OPTION:
  3267. case READING_COMPREHENSION:
  3268. if ($from_database) {
  3269. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3270. WHERE
  3271. exe_id = $exeId AND
  3272. question_id = $questionId";
  3273. $result = Database::query($sql);
  3274. $choice = Database::result($result, 0, 'answer');
  3275. if ($userAnsweredQuestion === false) {
  3276. $userAnsweredQuestion = !empty($choice);
  3277. }
  3278. $studentChoice = $choice == $answerAutoId ? 1 : 0;
  3279. if ($studentChoice) {
  3280. $questionScore += $answerWeighting;
  3281. $answerDestination = $objAnswerTmp->selectDestination($answerId);
  3282. $correctAnswerId = $answerId;
  3283. }
  3284. } else {
  3285. $studentChoice = $choice == $answerAutoId ? 1 : 0;
  3286. if ($studentChoice) {
  3287. $questionScore += $answerWeighting;
  3288. $answerDestination = $objAnswerTmp->selectDestination($answerId);
  3289. $correctAnswerId = $answerId;
  3290. }
  3291. }
  3292. break;
  3293. case MULTIPLE_ANSWER_TRUE_FALSE:
  3294. if ($from_database) {
  3295. $choice = [];
  3296. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3297. WHERE
  3298. exe_id = $exeId AND
  3299. question_id = ".$questionId;
  3300. $result = Database::query($sql);
  3301. while ($row = Database::fetch_array($result)) {
  3302. $values = explode(':', $row['answer']);
  3303. $my_answer_id = isset($values[0]) ? $values[0] : '';
  3304. $option = isset($values[1]) ? $values[1] : '';
  3305. $choice[$my_answer_id] = $option;
  3306. }
  3307. }
  3308. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3309. if (!empty($studentChoice)) {
  3310. if ($studentChoice == $answerCorrect) {
  3311. $questionScore += $true_score;
  3312. } else {
  3313. if ($quiz_question_options[$studentChoice]['name'] == "Don't know" ||
  3314. $quiz_question_options[$studentChoice]['name'] == "DoubtScore"
  3315. ) {
  3316. $questionScore += $doubt_score;
  3317. } else {
  3318. $questionScore += $false_score;
  3319. }
  3320. }
  3321. } else {
  3322. // If no result then the user just hit don't know
  3323. $studentChoice = 3;
  3324. $questionScore += $doubt_score;
  3325. }
  3326. $totalScore = $questionScore;
  3327. break;
  3328. case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
  3329. if ($from_database) {
  3330. $choice = [];
  3331. $choiceDegreeCertainty = [];
  3332. $sql = "SELECT answer
  3333. FROM $TBL_TRACK_ATTEMPT
  3334. WHERE
  3335. exe_id = $exeId AND question_id = $questionId";
  3336. $result = Database::query($sql);
  3337. while ($row = Database::fetch_array($result)) {
  3338. $ind = $row['answer'];
  3339. $values = explode(':', $ind);
  3340. $myAnswerId = $values[0];
  3341. $option = $values[1];
  3342. $percent = $values[2];
  3343. $choice[$myAnswerId] = $option;
  3344. $choiceDegreeCertainty[$myAnswerId] = $percent;
  3345. }
  3346. }
  3347. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3348. $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
  3349. // student score update
  3350. if (!empty($studentChoice)) {
  3351. if ($studentChoice == $answerCorrect) {
  3352. // correct answer and student is Unsure or PrettySur
  3353. if (isset($quiz_question_options[$studentChoiceDegree]) &&
  3354. $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
  3355. $quiz_question_options[$studentChoiceDegree]['position'] < 9
  3356. ) {
  3357. $questionScore += $true_score;
  3358. } else {
  3359. // student ignore correct answer
  3360. $questionScore += $doubt_score;
  3361. }
  3362. } else {
  3363. // false answer and student is Unsure or PrettySur
  3364. if ($quiz_question_options[$studentChoiceDegree]['position'] >= 3
  3365. && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
  3366. $questionScore += $false_score;
  3367. } else {
  3368. // student ignore correct answer
  3369. $questionScore += $doubt_score;
  3370. }
  3371. }
  3372. }
  3373. $totalScore = $questionScore;
  3374. break;
  3375. case MULTIPLE_ANSWER:
  3376. if ($from_database) {
  3377. $choice = [];
  3378. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3379. WHERE exe_id = $exeId AND question_id = $questionId ";
  3380. $resultans = Database::query($sql);
  3381. while ($row = Database::fetch_array($resultans)) {
  3382. $choice[$row['answer']] = 1;
  3383. }
  3384. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3385. $real_answers[$answerId] = (bool) $studentChoice;
  3386. if ($studentChoice) {
  3387. $questionScore += $answerWeighting;
  3388. }
  3389. } else {
  3390. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3391. $real_answers[$answerId] = (bool) $studentChoice;
  3392. if (isset($studentChoice)) {
  3393. $questionScore += $answerWeighting;
  3394. }
  3395. }
  3396. $totalScore += $answerWeighting;
  3397. if ($debug) {
  3398. error_log("studentChoice: $studentChoice");
  3399. }
  3400. break;
  3401. case GLOBAL_MULTIPLE_ANSWER:
  3402. if ($from_database) {
  3403. $choice = [];
  3404. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3405. WHERE exe_id = $exeId AND question_id = $questionId ";
  3406. $resultans = Database::query($sql);
  3407. while ($row = Database::fetch_array($resultans)) {
  3408. $choice[$row['answer']] = 1;
  3409. }
  3410. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3411. $real_answers[$answerId] = (bool) $studentChoice;
  3412. if ($studentChoice) {
  3413. $questionScore += $answerWeighting;
  3414. }
  3415. } else {
  3416. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3417. if (isset($studentChoice)) {
  3418. $questionScore += $answerWeighting;
  3419. }
  3420. $real_answers[$answerId] = (bool) $studentChoice;
  3421. }
  3422. $totalScore += $answerWeighting;
  3423. if ($debug) {
  3424. error_log("studentChoice: $studentChoice");
  3425. }
  3426. break;
  3427. case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
  3428. if ($from_database) {
  3429. $choice = [];
  3430. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3431. WHERE exe_id = $exeId AND question_id = $questionId";
  3432. $resultans = Database::query($sql);
  3433. while ($row = Database::fetch_array($resultans)) {
  3434. $result = explode(':', $row['answer']);
  3435. if (isset($result[0])) {
  3436. $my_answer_id = isset($result[0]) ? $result[0] : '';
  3437. $option = isset($result[1]) ? $result[1] : '';
  3438. $choice[$my_answer_id] = $option;
  3439. }
  3440. }
  3441. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
  3442. $real_answers[$answerId] = false;
  3443. if ($answerCorrect == $studentChoice) {
  3444. $real_answers[$answerId] = true;
  3445. }
  3446. } else {
  3447. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
  3448. $real_answers[$answerId] = false;
  3449. if ($answerCorrect == $studentChoice) {
  3450. $real_answers[$answerId] = true;
  3451. }
  3452. }
  3453. break;
  3454. case MULTIPLE_ANSWER_COMBINATION:
  3455. if ($from_database) {
  3456. $choice = [];
  3457. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3458. WHERE exe_id = $exeId AND question_id = $questionId";
  3459. $resultans = Database::query($sql);
  3460. while ($row = Database::fetch_array($resultans)) {
  3461. $choice[$row['answer']] = 1;
  3462. }
  3463. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3464. if ($answerCorrect == 1) {
  3465. $real_answers[$answerId] = false;
  3466. if ($studentChoice) {
  3467. $real_answers[$answerId] = true;
  3468. }
  3469. } else {
  3470. $real_answers[$answerId] = true;
  3471. if ($studentChoice) {
  3472. $real_answers[$answerId] = false;
  3473. }
  3474. }
  3475. } else {
  3476. $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
  3477. if ($answerCorrect == 1) {
  3478. $real_answers[$answerId] = false;
  3479. if ($studentChoice) {
  3480. $real_answers[$answerId] = true;
  3481. }
  3482. } else {
  3483. $real_answers[$answerId] = true;
  3484. if ($studentChoice) {
  3485. $real_answers[$answerId] = false;
  3486. }
  3487. }
  3488. }
  3489. break;
  3490. case FILL_IN_BLANKS:
  3491. $str = '';
  3492. $answerFromDatabase = '';
  3493. if ($from_database) {
  3494. $sql = "SELECT answer
  3495. FROM $TBL_TRACK_ATTEMPT
  3496. WHERE
  3497. exe_id = $exeId AND
  3498. question_id= $questionId ";
  3499. $result = Database::query($sql);
  3500. $str = $answerFromDatabase = Database::result($result, 0, 'answer');
  3501. }
  3502. // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
  3503. if (false) {
  3504. // the question is encoded like this
  3505. // [A] B [C] D [E] F::10,10,10@1
  3506. // number 1 before the "@" means that is a switchable fill in blank question
  3507. // [A] B [C] D [E] F::10,10,10@ or [A] B [C] D [E] F::10,10,10
  3508. // means that is a normal fill blank question
  3509. // first we explode the "::"
  3510. $pre_array = explode('::', $answer);
  3511. // is switchable fill blank or not
  3512. $last = count($pre_array) - 1;
  3513. $is_set_switchable = explode('@', $pre_array[$last]);
  3514. $switchable_answer_set = false;
  3515. if (isset($is_set_switchable[1]) && $is_set_switchable[1] == 1) {
  3516. $switchable_answer_set = true;
  3517. }
  3518. $answer = '';
  3519. for ($k = 0; $k < $last; $k++) {
  3520. $answer .= $pre_array[$k];
  3521. }
  3522. // splits weightings that are joined with a comma
  3523. $answerWeighting = explode(',', $is_set_switchable[0]);
  3524. // we save the answer because it will be modified
  3525. $temp = $answer;
  3526. $answer = '';
  3527. $j = 0;
  3528. //initialise answer tags
  3529. $user_tags = $correct_tags = $real_text = [];
  3530. // the loop will stop at the end of the text
  3531. while (1) {
  3532. // quits the loop if there are no more blanks (detect '[')
  3533. if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
  3534. // adds the end of the text
  3535. $answer = $temp;
  3536. $real_text[] = $answer;
  3537. break; //no more "blanks", quit the loop
  3538. }
  3539. // adds the piece of text that is before the blank
  3540. //and ends with '[' into a general storage array
  3541. $real_text[] = api_substr($temp, 0, $pos + 1);
  3542. $answer .= api_substr($temp, 0, $pos + 1);
  3543. //take the string remaining (after the last "[" we found)
  3544. $temp = api_substr($temp, $pos + 1);
  3545. // quit the loop if there are no more blanks, and update $pos to the position of next ']'
  3546. if (($pos = api_strpos($temp, ']')) === false) {
  3547. // adds the end of the text
  3548. $answer .= $temp;
  3549. break;
  3550. }
  3551. if ($from_database) {
  3552. $str = $answerFromDatabase;
  3553. api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
  3554. $str = str_replace('\r\n', '', $str);
  3555. $choice = $arr[1];
  3556. if (isset($choice[$j])) {
  3557. $tmp = api_strrpos($choice[$j], ' / ');
  3558. $choice[$j] = api_substr($choice[$j], 0, $tmp);
  3559. $choice[$j] = trim($choice[$j]);
  3560. // Needed to let characters ' and " to work as part of an answer
  3561. $choice[$j] = stripslashes($choice[$j]);
  3562. } else {
  3563. $choice[$j] = null;
  3564. }
  3565. } else {
  3566. // This value is the user input, not escaped while correct answer is escaped by ckeditor
  3567. $choice[$j] = api_htmlentities(trim($choice[$j]));
  3568. }
  3569. $user_tags[] = $choice[$j];
  3570. // Put the contents of the [] answer tag into correct_tags[]
  3571. $correct_tags[] = api_substr($temp, 0, $pos);
  3572. $j++;
  3573. $temp = api_substr($temp, $pos + 1);
  3574. }
  3575. $answer = '';
  3576. $real_correct_tags = $correct_tags;
  3577. $chosen_list = [];
  3578. for ($i = 0; $i < count($real_correct_tags); $i++) {
  3579. if ($i == 0) {
  3580. $answer .= $real_text[0];
  3581. }
  3582. if (!$switchable_answer_set) {
  3583. // Needed to parse ' and " characters
  3584. $user_tags[$i] = stripslashes($user_tags[$i]);
  3585. if ($correct_tags[$i] == $user_tags[$i]) {
  3586. // gives the related weighting to the student
  3587. $questionScore += $answerWeighting[$i];
  3588. // increments total score
  3589. $totalScore += $answerWeighting[$i];
  3590. // adds the word in green at the end of the string
  3591. $answer .= $correct_tags[$i];
  3592. } elseif (!empty($user_tags[$i])) {
  3593. // else if the word entered by the student IS NOT the same as
  3594. // the one defined by the professor
  3595. // adds the word in red at the end of the string, and strikes it
  3596. $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
  3597. } else {
  3598. // adds a tabulation if no word has been typed by the student
  3599. $answer .= ''; // remove &nbsp; that causes issue
  3600. }
  3601. } else {
  3602. // switchable fill in the blanks
  3603. if (in_array($user_tags[$i], $correct_tags)) {
  3604. $chosen_list[] = $user_tags[$i];
  3605. $correct_tags = array_diff($correct_tags, $chosen_list);
  3606. // gives the related weighting to the student
  3607. $questionScore += $answerWeighting[$i];
  3608. // increments total score
  3609. $totalScore += $answerWeighting[$i];
  3610. // adds the word in green at the end of the string
  3611. $answer .= $user_tags[$i];
  3612. } elseif (!empty($user_tags[$i])) {
  3613. // else if the word entered by the student IS NOT the same
  3614. // as the one defined by the professor
  3615. // adds the word in red at the end of the string, and strikes it
  3616. $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
  3617. } else {
  3618. // adds a tabulation if no word has been typed by the student
  3619. $answer .= ''; // remove &nbsp; that causes issue
  3620. }
  3621. }
  3622. // adds the correct word, followed by ] to close the blank
  3623. $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
  3624. if (isset($real_text[$i + 1])) {
  3625. $answer .= $real_text[$i + 1];
  3626. }
  3627. }
  3628. } else {
  3629. // insert the student result in the track_e_attempt table, field answer
  3630. // $answer is the answer like in the c_quiz_answer table for the question
  3631. // student data are choice[]
  3632. $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
  3633. $switchableAnswerSet = $listCorrectAnswers['switchable'];
  3634. $answerWeighting = $listCorrectAnswers['weighting'];
  3635. // user choices is an array $choice
  3636. // get existing user data in n the BDD
  3637. if ($from_database) {
  3638. $listStudentResults = FillBlanks::getAnswerInfo(
  3639. $answerFromDatabase,
  3640. true
  3641. );
  3642. $choice = $listStudentResults['student_answer'];
  3643. }
  3644. // loop other all blanks words
  3645. if (!$switchableAnswerSet) {
  3646. // not switchable answer, must be in the same place than teacher order
  3647. for ($i = 0; $i < count($listCorrectAnswers['words']); $i++) {
  3648. $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
  3649. $correctAnswer = $listCorrectAnswers['words'][$i];
  3650. if ($debug) {
  3651. error_log("Student answer: $i");
  3652. error_log($studentAnswer);
  3653. }
  3654. // This value is the user input, not escaped while correct answer is escaped by ckeditor
  3655. // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
  3656. // ENT_QUOTES is used in order to transform ' to &#039;
  3657. if (!$from_database) {
  3658. $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
  3659. if ($debug) {
  3660. error_log("Student answer cleaned:");
  3661. error_log($studentAnswer);
  3662. }
  3663. }
  3664. $isAnswerCorrect = 0;
  3665. if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
  3666. // gives the related weighting to the student
  3667. $questionScore += $answerWeighting[$i];
  3668. // increments total score
  3669. $totalScore += $answerWeighting[$i];
  3670. $isAnswerCorrect = 1;
  3671. }
  3672. if ($debug) {
  3673. error_log("isAnswerCorrect $i: $isAnswerCorrect");
  3674. }
  3675. $studentAnswerToShow = $studentAnswer;
  3676. $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
  3677. if ($debug) {
  3678. error_log("Fill in blank type: $type");
  3679. }
  3680. if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
  3681. $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
  3682. if ($studentAnswer != '') {
  3683. foreach ($listMenu as $item) {
  3684. if (sha1($item) == $studentAnswer) {
  3685. $studentAnswerToShow = $item;
  3686. }
  3687. }
  3688. }
  3689. }
  3690. $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
  3691. $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
  3692. }
  3693. } else {
  3694. // switchable answer
  3695. $listStudentAnswerTemp = $choice;
  3696. $listTeacherAnswerTemp = $listCorrectAnswers['words'];
  3697. // for every teacher answer, check if there is a student answer
  3698. for ($i = 0; $i < count($listStudentAnswerTemp); $i++) {
  3699. $studentAnswer = trim($listStudentAnswerTemp[$i]);
  3700. $studentAnswerToShow = $studentAnswer;
  3701. if ($debug) {
  3702. error_log("Student answer: $i");
  3703. error_log($studentAnswer);
  3704. }
  3705. $found = false;
  3706. for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
  3707. $correctAnswer = $listTeacherAnswerTemp[$j];
  3708. $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
  3709. if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
  3710. $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
  3711. if (!empty($studentAnswer)) {
  3712. foreach ($listMenu as $key => $item) {
  3713. if ($key == $correctAnswer) {
  3714. $studentAnswerToShow = $item;
  3715. break;
  3716. }
  3717. }
  3718. }
  3719. }
  3720. if (!$found) {
  3721. if (FillBlanks::isStudentAnswerGood(
  3722. $studentAnswer,
  3723. $correctAnswer,
  3724. $from_database
  3725. )
  3726. ) {
  3727. $questionScore += $answerWeighting[$i];
  3728. $totalScore += $answerWeighting[$i];
  3729. $listTeacherAnswerTemp[$j] = '';
  3730. $found = true;
  3731. }
  3732. }
  3733. }
  3734. $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
  3735. if (!$found) {
  3736. $listCorrectAnswers['student_score'][$i] = 0;
  3737. } else {
  3738. $listCorrectAnswers['student_score'][$i] = 1;
  3739. }
  3740. }
  3741. }
  3742. $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
  3743. }
  3744. break;
  3745. case CALCULATED_ANSWER:
  3746. $calculatedAnswerList = Session::read('calculatedAnswerId');
  3747. if (!empty($calculatedAnswerList)) {
  3748. $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
  3749. $preArray = explode('@@', $answer);
  3750. $last = count($preArray) - 1;
  3751. $answer = '';
  3752. for ($k = 0; $k < $last; $k++) {
  3753. $answer .= $preArray[$k];
  3754. }
  3755. $answerWeighting = [$answerWeighting];
  3756. // we save the answer because it will be modified
  3757. $temp = $answer;
  3758. $answer = '';
  3759. $j = 0;
  3760. // initialise answer tags
  3761. $userTags = $correctTags = $realText = [];
  3762. // the loop will stop at the end of the text
  3763. while (1) {
  3764. // quits the loop if there are no more blanks (detect '[')
  3765. if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
  3766. // adds the end of the text
  3767. $answer = $temp;
  3768. $realText[] = $answer;
  3769. break; //no more "blanks", quit the loop
  3770. }
  3771. // adds the piece of text that is before the blank
  3772. // and ends with '[' into a general storage array
  3773. $realText[] = api_substr($temp, 0, $pos + 1);
  3774. $answer .= api_substr($temp, 0, $pos + 1);
  3775. // take the string remaining (after the last "[" we found)
  3776. $temp = api_substr($temp, $pos + 1);
  3777. // quit the loop if there are no more blanks, and update $pos to the position of next ']'
  3778. if (($pos = api_strpos($temp, ']')) === false) {
  3779. // adds the end of the text
  3780. $answer .= $temp;
  3781. break;
  3782. }
  3783. if ($from_database) {
  3784. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3785. WHERE
  3786. exe_id = $exeId AND
  3787. question_id = $questionId ";
  3788. $result = Database::query($sql);
  3789. $str = Database::result($result, 0, 'answer');
  3790. api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
  3791. $str = str_replace('\r\n', '', $str);
  3792. $choice = $arr[1];
  3793. if (isset($choice[$j])) {
  3794. $tmp = api_strrpos($choice[$j], ' / ');
  3795. if ($tmp) {
  3796. $choice[$j] = api_substr($choice[$j], 0, $tmp);
  3797. } else {
  3798. $tmp = ltrim($tmp, '[');
  3799. $tmp = rtrim($tmp, ']');
  3800. }
  3801. $choice[$j] = trim($choice[$j]);
  3802. // Needed to let characters ' and " to work as part of an answer
  3803. $choice[$j] = stripslashes($choice[$j]);
  3804. } else {
  3805. $choice[$j] = null;
  3806. }
  3807. } else {
  3808. // This value is the user input not escaped while correct answer is escaped by ckeditor
  3809. $choice[$j] = api_htmlentities(trim($choice[$j]));
  3810. }
  3811. $userTags[] = $choice[$j];
  3812. // put the contents of the [] answer tag into correct_tags[]
  3813. $correctTags[] = api_substr($temp, 0, $pos);
  3814. $j++;
  3815. $temp = api_substr($temp, $pos + 1);
  3816. }
  3817. $answer = '';
  3818. $realCorrectTags = $correctTags;
  3819. $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
  3820. $expectedAnswer = '';
  3821. $calculatedChoice = '';
  3822. for ($i = 0; $i < count($realCorrectTags); $i++) {
  3823. if ($i == 0) {
  3824. $answer .= $realText[0];
  3825. }
  3826. // Needed to parse ' and " characters
  3827. $userTags[$i] = stripslashes($userTags[$i]);
  3828. if ($correctTags[$i] == $userTags[$i]) {
  3829. // gives the related weighting to the student
  3830. $questionScore += $answerWeighting[$i];
  3831. // increments total score
  3832. $totalScore += $answerWeighting[$i];
  3833. // adds the word in green at the end of the string
  3834. $answer .= $correctTags[$i];
  3835. $calculatedChoice = $correctTags[$i];
  3836. } elseif (!empty($userTags[$i])) {
  3837. // else if the word entered by the student IS NOT the same as
  3838. // the one defined by the professor
  3839. // adds the word in red at the end of the string, and strikes it
  3840. $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
  3841. $calculatedChoice = $userTags[$i];
  3842. } else {
  3843. // adds a tabulation if no word has been typed by the student
  3844. $answer .= ''; // remove &nbsp; that causes issue
  3845. }
  3846. // adds the correct word, followed by ] to close the blank
  3847. if ($this->results_disabled != EXERCISE_FEEDBACK_TYPE_EXAM) {
  3848. $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
  3849. $calculatedStatus = Display::label(get_lang('Correct'), 'success');
  3850. $expectedAnswer = $realCorrectTags[$i];
  3851. }
  3852. $answer .= ']';
  3853. if (isset($realText[$i + 1])) {
  3854. $answer .= $realText[$i + 1];
  3855. }
  3856. }
  3857. } else {
  3858. if ($from_database) {
  3859. $sql = "SELECT *
  3860. FROM $TBL_TRACK_ATTEMPT
  3861. WHERE
  3862. exe_id = $exeId AND
  3863. question_id = $questionId ";
  3864. $result = Database::query($sql);
  3865. $resultData = Database::fetch_array($result, 'ASSOC');
  3866. $answer = $resultData['answer'];
  3867. $questionScore = $resultData['marks'];
  3868. }
  3869. }
  3870. break;
  3871. case FREE_ANSWER:
  3872. if ($from_database) {
  3873. $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
  3874. WHERE
  3875. exe_id = $exeId AND
  3876. question_id= ".$questionId;
  3877. $result = Database::query($sql);
  3878. $data = Database::fetch_array($result);
  3879. $choice = $data['answer'];
  3880. $choice = str_replace('\r\n', '', $choice);
  3881. $choice = stripslashes($choice);
  3882. $questionScore = $data['marks'];
  3883. if ($questionScore == -1) {
  3884. $totalScore += 0;
  3885. } else {
  3886. $totalScore += $questionScore;
  3887. }
  3888. if ($questionScore == '') {
  3889. $questionScore = 0;
  3890. }
  3891. $arrques = $questionName;
  3892. $arrans = $choice;
  3893. } else {
  3894. $studentChoice = $choice;
  3895. if ($studentChoice) {
  3896. //Fixing negative puntation see #2193
  3897. $questionScore = 0;
  3898. $totalScore += 0;
  3899. }
  3900. }
  3901. break;
  3902. case ORAL_EXPRESSION:
  3903. if ($from_database) {
  3904. $query = "SELECT answer, marks
  3905. FROM $TBL_TRACK_ATTEMPT
  3906. WHERE
  3907. exe_id = $exeId AND
  3908. question_id = $questionId
  3909. ";
  3910. $resq = Database::query($query);
  3911. $row = Database::fetch_assoc($resq);
  3912. $choice = $row['answer'];
  3913. $choice = str_replace('\r\n', '', $choice);
  3914. $choice = stripslashes($choice);
  3915. $questionScore = $row['marks'];
  3916. if ($questionScore == -1) {
  3917. $totalScore += 0;
  3918. } else {
  3919. $totalScore += $questionScore;
  3920. }
  3921. $arrques = $questionName;
  3922. $arrans = $choice;
  3923. } else {
  3924. $studentChoice = $choice;
  3925. if ($studentChoice) {
  3926. //Fixing negative puntation see #2193
  3927. $questionScore = 0;
  3928. $totalScore += 0;
  3929. }
  3930. }
  3931. break;
  3932. case DRAGGABLE:
  3933. case MATCHING_DRAGGABLE:
  3934. case MATCHING:
  3935. if ($from_database) {
  3936. $sql = "SELECT id, answer, id_auto
  3937. FROM $table_ans
  3938. WHERE
  3939. c_id = $course_id AND
  3940. question_id = $questionId AND
  3941. correct = 0
  3942. ";
  3943. $result = Database::query($sql);
  3944. // Getting the real answer
  3945. $real_list = [];
  3946. while ($realAnswer = Database::fetch_array($result)) {
  3947. $real_list[$realAnswer['id_auto']] = $realAnswer['answer'];
  3948. }
  3949. $sql = "SELECT id, answer, correct, id_auto, ponderation
  3950. FROM $table_ans
  3951. WHERE
  3952. c_id = $course_id AND
  3953. question_id = $questionId AND
  3954. correct <> 0
  3955. ORDER BY id_auto";
  3956. $result = Database::query($sql);
  3957. $options = [];
  3958. while ($row = Database::fetch_array($result, 'ASSOC')) {
  3959. $options[] = $row;
  3960. }
  3961. $questionScore = 0;
  3962. $counterAnswer = 1;
  3963. foreach ($options as $a_answers) {
  3964. $i_answer_id = $a_answers['id']; //3
  3965. $s_answer_label = $a_answers['answer']; // your daddy - your mother
  3966. $i_answer_correct_answer = $a_answers['correct']; //1 - 2
  3967. $i_answer_id_auto = $a_answers['id_auto']; // 3 - 4
  3968. $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
  3969. WHERE
  3970. exe_id = '$exeId' AND
  3971. question_id = '$questionId' AND
  3972. position = '$i_answer_id_auto'";
  3973. $result = Database::query($sql);
  3974. $s_user_answer = 0;
  3975. if (Database::num_rows($result) > 0) {
  3976. // rich - good looking
  3977. $s_user_answer = Database::result($result, 0, 0);
  3978. }
  3979. $i_answerWeighting = $a_answers['ponderation'];
  3980. $user_answer = '';
  3981. $status = Display::label(get_lang('Incorrect'), 'danger');
  3982. if (!empty($s_user_answer)) {
  3983. if ($answerType == DRAGGABLE) {
  3984. if ($s_user_answer == $i_answer_correct_answer) {
  3985. $questionScore += $i_answerWeighting;
  3986. $totalScore += $i_answerWeighting;
  3987. $user_answer = Display::label(get_lang('Correct'), 'success');
  3988. if ($this->showExpectedChoice()) {
  3989. $user_answer = $answerMatching[$i_answer_id_auto];
  3990. }
  3991. $status = Display::label(get_lang('Correct'), 'success');
  3992. } else {
  3993. $user_answer = Display::label(get_lang('Incorrect'), 'danger');
  3994. if ($this->showExpectedChoice()) {
  3995. $data = $options[$real_list[$s_user_answer] - 1];
  3996. $user_answer = $data['answer'];
  3997. }
  3998. }
  3999. } else {
  4000. if ($s_user_answer == $i_answer_correct_answer) {
  4001. $questionScore += $i_answerWeighting;
  4002. $totalScore += $i_answerWeighting;
  4003. $status = Display::label(get_lang('Correct'), 'success');
  4004. // Try with id
  4005. if (isset($real_list[$i_answer_id])) {
  4006. $user_answer = Display::span(
  4007. $real_list[$i_answer_id],
  4008. ['style' => 'color: #008000; font-weight: bold;']
  4009. );
  4010. }
  4011. // Try with $i_answer_id_auto
  4012. if (empty($user_answer)) {
  4013. if (isset($real_list[$i_answer_id_auto])) {
  4014. $user_answer = Display::span(
  4015. $real_list[$i_answer_id_auto],
  4016. ['style' => 'color: #008000; font-weight: bold;']
  4017. );
  4018. }
  4019. }
  4020. if (isset($real_list[$i_answer_correct_answer])) {
  4021. $user_answer = Display::span(
  4022. $real_list[$i_answer_correct_answer],
  4023. ['style' => 'color: #008000; font-weight: bold;']
  4024. );
  4025. }
  4026. } else {
  4027. $user_answer = Display::span(
  4028. $real_list[$s_user_answer],
  4029. ['style' => 'color: #FF0000; text-decoration: line-through;']
  4030. );
  4031. if ($this->showExpectedChoice()) {
  4032. if (isset($real_list[$s_user_answer])) {
  4033. $user_answer = Display::span($real_list[$s_user_answer]);
  4034. }
  4035. }
  4036. }
  4037. }
  4038. } elseif ($answerType == DRAGGABLE) {
  4039. $user_answer = Display::label(get_lang('Incorrect'), 'danger');
  4040. if ($this->showExpectedChoice()) {
  4041. $user_answer = '';
  4042. }
  4043. } else {
  4044. $user_answer = Display::span(
  4045. get_lang('Incorrect').' &nbsp;',
  4046. ['style' => 'color: #FF0000; text-decoration: line-through;']
  4047. );
  4048. if ($this->showExpectedChoice()) {
  4049. $user_answer = '';
  4050. }
  4051. }
  4052. if ($show_result) {
  4053. if ($this->showExpectedChoice() === false &&
  4054. $showTotalScoreAndUserChoicesInLastAttempt === false
  4055. ) {
  4056. $user_answer = '';
  4057. }
  4058. switch ($answerType) {
  4059. case MATCHING:
  4060. case MATCHING_DRAGGABLE:
  4061. echo '<tr>';
  4062. if (!in_array(
  4063. $this->results_disabled,
  4064. [
  4065. RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
  4066. ]
  4067. )
  4068. ) {
  4069. echo '<td>'.$s_answer_label.'</td>';
  4070. echo '<td>'.$user_answer.'</td>';
  4071. } else {
  4072. echo '<td>'.$s_answer_label.'</td>';
  4073. $status = Display::label(get_lang('Correct'), 'success');
  4074. }
  4075. if ($this->showExpectedChoice()) {
  4076. if ($this->showExpectedChoiceColumn()) {
  4077. echo '<td>';
  4078. if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
  4079. if (isset($real_list[$i_answer_correct_answer]) &&
  4080. $showTotalScoreAndUserChoicesInLastAttempt == true
  4081. ) {
  4082. echo Display::span(
  4083. $real_list[$i_answer_correct_answer]
  4084. );
  4085. }
  4086. }
  4087. echo '</td>';
  4088. }
  4089. echo '<td>'.$status.'</td>';
  4090. } else {
  4091. if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
  4092. if (isset($real_list[$i_answer_correct_answer]) &&
  4093. $showTotalScoreAndUserChoicesInLastAttempt === true
  4094. ) {
  4095. if ($this->showExpectedChoiceColumn()) {
  4096. echo '<td>';
  4097. echo Display::span(
  4098. $real_list[$i_answer_correct_answer],
  4099. ['style' => 'color: #008000; font-weight: bold;']
  4100. );
  4101. echo '</td>';
  4102. }
  4103. }
  4104. }
  4105. }
  4106. echo '</tr>';
  4107. break;
  4108. case DRAGGABLE:
  4109. if ($showTotalScoreAndUserChoicesInLastAttempt == false) {
  4110. $s_answer_label = '';
  4111. }
  4112. echo '<tr>';
  4113. if ($this->showExpectedChoice()) {
  4114. if (!in_array($this->results_disabled, [
  4115. RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
  4116. //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
  4117. ])
  4118. ) {
  4119. echo '<td>'.$user_answer.'</td>';
  4120. } else {
  4121. $status = Display::label(get_lang('Correct'), 'success');
  4122. }
  4123. echo '<td>'.$s_answer_label.'</td>';
  4124. echo '<td>'.$status.'</td>';
  4125. } else {
  4126. echo '<td>'.$s_answer_label.'</td>';
  4127. echo '<td>'.$user_answer.'</td>';
  4128. echo '<td>';
  4129. if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
  4130. if (isset($real_list[$i_answer_correct_answer]) &&
  4131. $showTotalScoreAndUserChoicesInLastAttempt === true
  4132. ) {
  4133. echo Display::span(
  4134. $real_list[$i_answer_correct_answer],
  4135. ['style' => 'color: #008000; font-weight: bold;']
  4136. );
  4137. }
  4138. }
  4139. echo '</td>';
  4140. }
  4141. echo '</tr>';
  4142. break;
  4143. }
  4144. }
  4145. $counterAnswer++;
  4146. }
  4147. break 2; // break the switch and the "for" condition
  4148. } else {
  4149. if ($answerCorrect) {
  4150. if (isset($choice[$answerAutoId]) &&
  4151. $answerCorrect == $choice[$answerAutoId]
  4152. ) {
  4153. $questionScore += $answerWeighting;
  4154. $totalScore += $answerWeighting;
  4155. $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
  4156. } else {
  4157. if (isset($answerMatching[$choice[$answerAutoId]])) {
  4158. $user_answer = Display::span(
  4159. $answerMatching[$choice[$answerAutoId]],
  4160. ['style' => 'color: #FF0000; text-decoration: line-through;']
  4161. );
  4162. }
  4163. }
  4164. $matching[$answerAutoId] = $choice[$answerAutoId];
  4165. }
  4166. }
  4167. break;
  4168. case HOT_SPOT:
  4169. if ($from_database) {
  4170. $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
  4171. // Check auto id
  4172. $sql = "SELECT hotspot_correct
  4173. FROM $TBL_TRACK_HOTSPOT
  4174. WHERE
  4175. hotspot_exe_id = $exeId AND
  4176. hotspot_question_id= $questionId AND
  4177. hotspot_answer_id = $answerAutoId
  4178. ORDER BY hotspot_id ASC";
  4179. $result = Database::query($sql);
  4180. if (Database::num_rows($result)) {
  4181. $studentChoice = Database::result(
  4182. $result,
  4183. 0,
  4184. 'hotspot_correct'
  4185. );
  4186. if ($studentChoice) {
  4187. $questionScore += $answerWeighting;
  4188. $totalScore += $answerWeighting;
  4189. }
  4190. } else {
  4191. // If answer.id is different:
  4192. $sql = "SELECT hotspot_correct
  4193. FROM $TBL_TRACK_HOTSPOT
  4194. WHERE
  4195. hotspot_exe_id = $exeId AND
  4196. hotspot_question_id= $questionId AND
  4197. hotspot_answer_id = ".intval($answerId)."
  4198. ORDER BY hotspot_id ASC";
  4199. $result = Database::query($sql);
  4200. if (Database::num_rows($result)) {
  4201. $studentChoice = Database::result(
  4202. $result,
  4203. 0,
  4204. 'hotspot_correct'
  4205. );
  4206. if ($studentChoice) {
  4207. $questionScore += $answerWeighting;
  4208. $totalScore += $answerWeighting;
  4209. }
  4210. } else {
  4211. // check answer.iid
  4212. if (!empty($answerIid)) {
  4213. $sql = "SELECT hotspot_correct
  4214. FROM $TBL_TRACK_HOTSPOT
  4215. WHERE
  4216. hotspot_exe_id = $exeId AND
  4217. hotspot_question_id= $questionId AND
  4218. hotspot_answer_id = $answerIid
  4219. ORDER BY hotspot_id ASC";
  4220. $result = Database::query($sql);
  4221. $studentChoice = Database::result(
  4222. $result,
  4223. 0,
  4224. 'hotspot_correct'
  4225. );
  4226. if ($studentChoice) {
  4227. $questionScore += $answerWeighting;
  4228. $totalScore += $answerWeighting;
  4229. }
  4230. }
  4231. }
  4232. }
  4233. } else {
  4234. if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
  4235. $choice[$answerAutoId] = 0;
  4236. $choice[$answerIid] = 0;
  4237. } else {
  4238. $studentChoice = $choice[$answerAutoId];
  4239. if (empty($studentChoice)) {
  4240. $studentChoice = $choice[$answerIid];
  4241. }
  4242. $choiceIsValid = false;
  4243. if (!empty($studentChoice)) {
  4244. $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
  4245. $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
  4246. $choicePoint = Geometry::decodePoint($studentChoice);
  4247. switch ($hotspotType) {
  4248. case 'square':
  4249. $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
  4250. $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
  4251. break;
  4252. case 'circle':
  4253. $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
  4254. $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
  4255. break;
  4256. case 'poly':
  4257. $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
  4258. $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
  4259. break;
  4260. }
  4261. }
  4262. $choice[$answerAutoId] = 0;
  4263. if ($choiceIsValid) {
  4264. $questionScore += $answerWeighting;
  4265. $totalScore += $answerWeighting;
  4266. $choice[$answerAutoId] = 1;
  4267. $choice[$answerIid] = 1;
  4268. }
  4269. }
  4270. }
  4271. break;
  4272. case HOT_SPOT_ORDER:
  4273. // @todo never added to chamilo
  4274. // for hotspot with fixed order
  4275. $studentChoice = $choice['order'][$answerId];
  4276. if ($studentChoice == $answerId) {
  4277. $questionScore += $answerWeighting;
  4278. $totalScore += $answerWeighting;
  4279. $studentChoice = true;
  4280. } else {
  4281. $studentChoice = false;
  4282. }
  4283. break;
  4284. case HOT_SPOT_DELINEATION:
  4285. // for hotspot with delineation
  4286. if ($from_database) {
  4287. // getting the user answer
  4288. $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
  4289. $query = "SELECT hotspot_correct, hotspot_coordinate
  4290. FROM $TBL_TRACK_HOTSPOT
  4291. WHERE
  4292. hotspot_exe_id = $exeId AND
  4293. hotspot_question_id= $questionId AND
  4294. hotspot_answer_id = '1'";
  4295. // By default we take 1 because it's a delineation
  4296. $resq = Database::query($query);
  4297. $row = Database::fetch_array($resq, 'ASSOC');
  4298. $choice = $row['hotspot_correct'];
  4299. $user_answer = $row['hotspot_coordinate'];
  4300. // THIS is very important otherwise the poly_compile will throw an error!!
  4301. // round-up the coordinates
  4302. $coords = explode('/', $user_answer);
  4303. $coords = array_filter($coords);
  4304. $user_array = '';
  4305. foreach ($coords as $coord) {
  4306. list($x, $y) = explode(';', $coord);
  4307. $user_array .= round($x).';'.round($y).'/';
  4308. }
  4309. $user_array = substr($user_array, 0, -1) ?: '';
  4310. } else {
  4311. if (!empty($studentChoice)) {
  4312. $newquestionList[] = $questionId;
  4313. }
  4314. if ($answerId === 1) {
  4315. $studentChoice = $choice[$answerId];
  4316. $questionScore += $answerWeighting;
  4317. }
  4318. if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
  4319. $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
  4320. }
  4321. }
  4322. $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
  4323. $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
  4324. break;
  4325. case ANNOTATION:
  4326. if ($from_database) {
  4327. $sql = "SELECT answer, marks
  4328. FROM $TBL_TRACK_ATTEMPT
  4329. WHERE
  4330. exe_id = $exeId AND
  4331. question_id = $questionId ";
  4332. $resq = Database::query($sql);
  4333. $data = Database::fetch_array($resq);
  4334. $questionScore = empty($data['marks']) ? 0 : $data['marks'];
  4335. $arrques = $questionName;
  4336. break;
  4337. }
  4338. $studentChoice = $choice;
  4339. if ($studentChoice) {
  4340. $questionScore = 0;
  4341. }
  4342. break;
  4343. }
  4344. if ($show_result) {
  4345. if ($from === 'exercise_result') {
  4346. // Display answers (if not matching type, or if the answer is correct)
  4347. if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
  4348. $answerCorrect
  4349. ) {
  4350. if (in_array(
  4351. $answerType,
  4352. [
  4353. UNIQUE_ANSWER,
  4354. UNIQUE_ANSWER_IMAGE,
  4355. UNIQUE_ANSWER_NO_OPTION,
  4356. MULTIPLE_ANSWER,
  4357. MULTIPLE_ANSWER_COMBINATION,
  4358. GLOBAL_MULTIPLE_ANSWER,
  4359. READING_COMPREHENSION,
  4360. ]
  4361. )) {
  4362. ExerciseShowFunctions::display_unique_or_multiple_answer(
  4363. $this,
  4364. $feedback_type,
  4365. $answerType,
  4366. $studentChoice,
  4367. $answer,
  4368. $answerComment,
  4369. $answerCorrect,
  4370. 0,
  4371. 0,
  4372. 0,
  4373. $results_disabled,
  4374. $showTotalScoreAndUserChoicesInLastAttempt,
  4375. $this->export
  4376. );
  4377. } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
  4378. ExerciseShowFunctions::display_multiple_answer_true_false(
  4379. $this,
  4380. $feedback_type,
  4381. $answerType,
  4382. $studentChoice,
  4383. $answer,
  4384. $answerComment,
  4385. $answerCorrect,
  4386. 0,
  4387. $questionId,
  4388. 0,
  4389. $results_disabled,
  4390. $showTotalScoreAndUserChoicesInLastAttempt
  4391. );
  4392. } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
  4393. ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
  4394. $this,
  4395. $feedback_type,
  4396. $studentChoice,
  4397. $studentChoiceDegree,
  4398. $answer,
  4399. $answerComment,
  4400. $answerCorrect,
  4401. $questionId,
  4402. $results_disabled
  4403. );
  4404. } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE) {
  4405. ExerciseShowFunctions::display_multiple_answer_combination_true_false(
  4406. $this,
  4407. $feedback_type,
  4408. $answerType,
  4409. $studentChoice,
  4410. $answer,
  4411. $answerComment,
  4412. $answerCorrect,
  4413. 0,
  4414. 0,
  4415. 0,
  4416. $results_disabled,
  4417. $showTotalScoreAndUserChoicesInLastAttempt
  4418. );
  4419. } elseif ($answerType == FILL_IN_BLANKS) {
  4420. ExerciseShowFunctions::display_fill_in_blanks_answer(
  4421. $this,
  4422. $feedback_type,
  4423. $answer,
  4424. 0,
  4425. 0,
  4426. $results_disabled,
  4427. '',
  4428. $showTotalScoreAndUserChoicesInLastAttempt
  4429. );
  4430. } elseif ($answerType == CALCULATED_ANSWER) {
  4431. ExerciseShowFunctions::display_calculated_answer(
  4432. $this,
  4433. $feedback_type,
  4434. $answer,
  4435. 0,
  4436. 0,
  4437. $results_disabled,
  4438. $showTotalScoreAndUserChoicesInLastAttempt,
  4439. $expectedAnswer,
  4440. $calculatedChoice,
  4441. $calculatedStatus
  4442. );
  4443. } elseif ($answerType == FREE_ANSWER) {
  4444. ExerciseShowFunctions::display_free_answer(
  4445. $feedback_type,
  4446. $choice,
  4447. $exeId,
  4448. $questionId,
  4449. $questionScore,
  4450. $results_disabled
  4451. );
  4452. } elseif ($answerType == ORAL_EXPRESSION) {
  4453. // to store the details of open questions in an array to be used in mail
  4454. /** @var OralExpression $objQuestionTmp */
  4455. ExerciseShowFunctions::display_oral_expression_answer(
  4456. $feedback_type,
  4457. $choice,
  4458. 0,
  4459. 0,
  4460. $objQuestionTmp->getFileUrl(true),
  4461. $results_disabled,
  4462. $questionScore
  4463. );
  4464. } elseif ($answerType == HOT_SPOT) {
  4465. $correctAnswerId = 0;
  4466. /** @var TrackEHotspot $hotspot */
  4467. foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
  4468. if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
  4469. break;
  4470. }
  4471. }
  4472. // force to show whether the choice is correct or not
  4473. $showTotalScoreAndUserChoicesInLastAttempt = true;
  4474. ExerciseShowFunctions::display_hotspot_answer(
  4475. $feedback_type,
  4476. ++$correctAnswerId,
  4477. $answer,
  4478. $studentChoice,
  4479. $answerComment,
  4480. $results_disabled,
  4481. $correctAnswerId,
  4482. $showTotalScoreAndUserChoicesInLastAttempt
  4483. );
  4484. } elseif ($answerType == HOT_SPOT_ORDER) {
  4485. ExerciseShowFunctions::display_hotspot_order_answer(
  4486. $feedback_type,
  4487. $answerId,
  4488. $answer,
  4489. $studentChoice,
  4490. $answerComment
  4491. );
  4492. } elseif ($answerType == HOT_SPOT_DELINEATION) {
  4493. $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
  4494. // Round-up the coordinates
  4495. $coords = explode('/', $user_answer);
  4496. $coords = array_filter($coords);
  4497. $user_array = '';
  4498. foreach ($coords as $coord) {
  4499. if (!empty($coord)) {
  4500. $parts = explode(';', $coord);
  4501. if (!empty($parts)) {
  4502. $user_array .= round($parts[0]).';'.round($parts[1]).'/';
  4503. }
  4504. }
  4505. }
  4506. $user_array = substr($user_array, 0, -1) ?: '';
  4507. if ($next) {
  4508. $user_answer = $user_array;
  4509. // We compare only the delineation not the other points
  4510. $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
  4511. $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
  4512. // Calculating the area
  4513. $poly_user = convert_coordinates($user_answer, '/');
  4514. $poly_answer = convert_coordinates($answer_question, '|');
  4515. $max_coord = poly_get_max($poly_user, $poly_answer);
  4516. $poly_user_compiled = poly_compile($poly_user, $max_coord);
  4517. $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
  4518. $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
  4519. $overlap = $poly_results['both'];
  4520. $poly_answer_area = $poly_results['s1'];
  4521. $poly_user_area = $poly_results['s2'];
  4522. $missing = $poly_results['s1Only'];
  4523. $excess = $poly_results['s2Only'];
  4524. // //this is an area in pixels
  4525. if ($debug > 0) {
  4526. error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
  4527. }
  4528. if ($overlap < 1) {
  4529. // Shortcut to avoid complicated calculations
  4530. $final_overlap = 0;
  4531. $final_missing = 100;
  4532. $final_excess = 100;
  4533. } else {
  4534. // the final overlap is the percentage of the initial polygon
  4535. // that is overlapped by the user's polygon
  4536. $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
  4537. if ($debug > 1) {
  4538. error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
  4539. }
  4540. // the final missing area is the percentage of the initial polygon
  4541. // that is not overlapped by the user's polygon
  4542. $final_missing = 100 - $final_overlap;
  4543. if ($debug > 1) {
  4544. error_log(__LINE__.' - Final missing is '.$final_missing, 0);
  4545. }
  4546. // the final excess area is the percentage of the initial polygon's size
  4547. // that is covered by the user's polygon outside of the initial polygon
  4548. $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
  4549. if ($debug > 1) {
  4550. error_log(__LINE__.' - Final excess is '.$final_excess, 0);
  4551. }
  4552. }
  4553. // Checking the destination parameters parsing the "@@"
  4554. $destination_items = explode('@@', $answerDestination);
  4555. $threadhold_total = $destination_items[0];
  4556. $threadhold_items = explode(';', $threadhold_total);
  4557. $threadhold1 = $threadhold_items[0]; // overlap
  4558. $threadhold2 = $threadhold_items[1]; // excess
  4559. $threadhold3 = $threadhold_items[2]; // missing
  4560. // if is delineation
  4561. if ($answerId === 1) {
  4562. //setting colors
  4563. if ($final_overlap >= $threadhold1) {
  4564. $overlap_color = true;
  4565. }
  4566. if ($final_excess <= $threadhold2) {
  4567. $excess_color = true;
  4568. }
  4569. if ($final_missing <= $threadhold3) {
  4570. $missing_color = true;
  4571. }
  4572. // if pass
  4573. if ($final_overlap >= $threadhold1 &&
  4574. $final_missing <= $threadhold3 &&
  4575. $final_excess <= $threadhold2
  4576. ) {
  4577. $next = 1; //go to the oars
  4578. $result_comment = get_lang('Acceptable');
  4579. $final_answer = 1; // do not update with update_exercise_attempt
  4580. } else {
  4581. $next = 0;
  4582. $result_comment = get_lang('Unacceptable');
  4583. $comment = $answerDestination = $objAnswerTmp->selectComment(1);
  4584. $answerDestination = $objAnswerTmp->selectDestination(1);
  4585. // checking the destination parameters parsing the "@@"
  4586. $destination_items = explode('@@', $answerDestination);
  4587. }
  4588. } elseif ($answerId > 1) {
  4589. if ($objAnswerTmp->selectHotspotType($answerId) == 'noerror') {
  4590. if ($debug > 0) {
  4591. error_log(__LINE__.' - answerId is of type noerror', 0);
  4592. }
  4593. //type no error shouldn't be treated
  4594. $next = 1;
  4595. continue;
  4596. }
  4597. if ($debug > 0) {
  4598. error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
  4599. }
  4600. $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
  4601. $poly_answer = convert_coordinates($delineation_cord, '|');
  4602. $max_coord = poly_get_max($poly_user, $poly_answer);
  4603. $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
  4604. $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
  4605. if ($overlap == false) {
  4606. //all good, no overlap
  4607. $next = 1;
  4608. continue;
  4609. } else {
  4610. if ($debug > 0) {
  4611. error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
  4612. }
  4613. $organs_at_risk_hit++;
  4614. //show the feedback
  4615. $next = 0;
  4616. $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
  4617. $answerDestination = $objAnswerTmp->selectDestination($answerId);
  4618. $destination_items = explode('@@', $answerDestination);
  4619. $try_hotspot = $destination_items[1];
  4620. $lp_hotspot = $destination_items[2];
  4621. $select_question_hotspot = $destination_items[3];
  4622. $url_hotspot = $destination_items[4];
  4623. }
  4624. }
  4625. } else {
  4626. // the first delineation feedback
  4627. if ($debug > 0) {
  4628. error_log(__LINE__.' first', 0);
  4629. }
  4630. }
  4631. } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
  4632. echo '<tr>';
  4633. echo Display::tag('td', $answerMatching[$answerId]);
  4634. echo Display::tag(
  4635. 'td',
  4636. "$user_answer / ".Display::tag(
  4637. 'strong',
  4638. $answerMatching[$answerCorrect],
  4639. ['style' => 'color: #008000; font-weight: bold;']
  4640. )
  4641. );
  4642. echo '</tr>';
  4643. } elseif ($answerType == ANNOTATION) {
  4644. ExerciseShowFunctions::displayAnnotationAnswer(
  4645. $feedback_type,
  4646. $exeId,
  4647. $questionId,
  4648. $questionScore,
  4649. $results_disabled
  4650. );
  4651. }
  4652. }
  4653. } else {
  4654. if ($debug) {
  4655. error_log('Showing questions $from '.$from);
  4656. }
  4657. switch ($answerType) {
  4658. case UNIQUE_ANSWER:
  4659. case UNIQUE_ANSWER_IMAGE:
  4660. case UNIQUE_ANSWER_NO_OPTION:
  4661. case MULTIPLE_ANSWER:
  4662. case GLOBAL_MULTIPLE_ANSWER:
  4663. case MULTIPLE_ANSWER_COMBINATION:
  4664. case READING_COMPREHENSION:
  4665. if ($answerId == 1) {
  4666. ExerciseShowFunctions::display_unique_or_multiple_answer(
  4667. $this,
  4668. $feedback_type,
  4669. $answerType,
  4670. $studentChoice,
  4671. $answer,
  4672. $answerComment,
  4673. $answerCorrect,
  4674. $exeId,
  4675. $questionId,
  4676. $answerId,
  4677. $results_disabled,
  4678. $showTotalScoreAndUserChoicesInLastAttempt,
  4679. $this->export
  4680. );
  4681. } else {
  4682. ExerciseShowFunctions::display_unique_or_multiple_answer(
  4683. $this,
  4684. $feedback_type,
  4685. $answerType,
  4686. $studentChoice,
  4687. $answer,
  4688. $answerComment,
  4689. $answerCorrect,
  4690. $exeId,
  4691. $questionId,
  4692. '',
  4693. $results_disabled,
  4694. $showTotalScoreAndUserChoicesInLastAttempt,
  4695. $this->export
  4696. );
  4697. }
  4698. break;
  4699. case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
  4700. if ($answerId == 1) {
  4701. ExerciseShowFunctions::display_multiple_answer_combination_true_false(
  4702. $this,
  4703. $feedback_type,
  4704. $answerType,
  4705. $studentChoice,
  4706. $answer,
  4707. $answerComment,
  4708. $answerCorrect,
  4709. $exeId,
  4710. $questionId,
  4711. $answerId,
  4712. $results_disabled,
  4713. $showTotalScoreAndUserChoicesInLastAttempt
  4714. );
  4715. } else {
  4716. ExerciseShowFunctions::display_multiple_answer_combination_true_false(
  4717. $this,
  4718. $feedback_type,
  4719. $answerType,
  4720. $studentChoice,
  4721. $answer,
  4722. $answerComment,
  4723. $answerCorrect,
  4724. $exeId,
  4725. $questionId,
  4726. '',
  4727. $results_disabled,
  4728. $showTotalScoreAndUserChoicesInLastAttempt
  4729. );
  4730. }
  4731. break;
  4732. case MULTIPLE_ANSWER_TRUE_FALSE:
  4733. if ($answerId == 1) {
  4734. ExerciseShowFunctions::display_multiple_answer_true_false(
  4735. $this,
  4736. $feedback_type,
  4737. $answerType,
  4738. $studentChoice,
  4739. $answer,
  4740. $answerComment,
  4741. $answerCorrect,
  4742. $exeId,
  4743. $questionId,
  4744. $answerId,
  4745. $results_disabled,
  4746. $showTotalScoreAndUserChoicesInLastAttempt
  4747. );
  4748. } else {
  4749. ExerciseShowFunctions::display_multiple_answer_true_false(
  4750. $this,
  4751. $feedback_type,
  4752. $answerType,
  4753. $studentChoice,
  4754. $answer,
  4755. $answerComment,
  4756. $answerCorrect,
  4757. $exeId,
  4758. $questionId,
  4759. '',
  4760. $results_disabled,
  4761. $showTotalScoreAndUserChoicesInLastAttempt
  4762. );
  4763. }
  4764. break;
  4765. case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
  4766. if ($answerId == 1) {
  4767. ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
  4768. $this,
  4769. $feedback_type,
  4770. $studentChoice,
  4771. $studentChoiceDegree,
  4772. $answer,
  4773. $answerComment,
  4774. $answerCorrect,
  4775. $questionId,
  4776. $results_disabled
  4777. );
  4778. } else {
  4779. ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
  4780. $this,
  4781. $feedback_type,
  4782. $studentChoice,
  4783. $studentChoiceDegree,
  4784. $answer,
  4785. $answerComment,
  4786. $answerCorrect,
  4787. $questionId,
  4788. $results_disabled
  4789. );
  4790. }
  4791. break;
  4792. case FILL_IN_BLANKS:
  4793. ExerciseShowFunctions::display_fill_in_blanks_answer(
  4794. $this,
  4795. $feedback_type,
  4796. $answer,
  4797. $exeId,
  4798. $questionId,
  4799. $results_disabled,
  4800. $str,
  4801. $showTotalScoreAndUserChoicesInLastAttempt
  4802. );
  4803. break;
  4804. case CALCULATED_ANSWER:
  4805. ExerciseShowFunctions::display_calculated_answer(
  4806. $this,
  4807. $feedback_type,
  4808. $answer,
  4809. $exeId,
  4810. $questionId,
  4811. $results_disabled,
  4812. '',
  4813. $showTotalScoreAndUserChoicesInLastAttempt
  4814. );
  4815. break;
  4816. case FREE_ANSWER:
  4817. echo ExerciseShowFunctions::display_free_answer(
  4818. $feedback_type,
  4819. $choice,
  4820. $exeId,
  4821. $questionId,
  4822. $questionScore,
  4823. $results_disabled
  4824. );
  4825. break;
  4826. case ORAL_EXPRESSION:
  4827. echo '<tr>
  4828. <td valign="top">'.
  4829. ExerciseShowFunctions::display_oral_expression_answer(
  4830. $feedback_type,
  4831. $choice,
  4832. $exeId,
  4833. $questionId,
  4834. $objQuestionTmp->getFileUrl(),
  4835. $results_disabled,
  4836. $questionScore
  4837. ).'</td>
  4838. </tr>
  4839. </table>';
  4840. break;
  4841. case HOT_SPOT:
  4842. ExerciseShowFunctions::display_hotspot_answer(
  4843. $feedback_type,
  4844. $answerId,
  4845. $answer,
  4846. $studentChoice,
  4847. $answerComment,
  4848. $results_disabled,
  4849. $answerId,
  4850. $showTotalScoreAndUserChoicesInLastAttempt
  4851. );
  4852. break;
  4853. case HOT_SPOT_DELINEATION:
  4854. $user_answer = $user_array;
  4855. if ($next) {
  4856. $user_answer = $user_array;
  4857. // we compare only the delineation not the other points
  4858. $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
  4859. $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
  4860. // calculating the area
  4861. $poly_user = convert_coordinates($user_answer, '/');
  4862. $poly_answer = convert_coordinates($answer_question, '|');
  4863. $max_coord = poly_get_max($poly_user, $poly_answer);
  4864. $poly_user_compiled = poly_compile($poly_user, $max_coord);
  4865. $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
  4866. $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
  4867. $overlap = $poly_results['both'];
  4868. $poly_answer_area = $poly_results['s1'];
  4869. $poly_user_area = $poly_results['s2'];
  4870. $missing = $poly_results['s1Only'];
  4871. $excess = $poly_results['s2Only'];
  4872. if ($debug > 0) {
  4873. error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
  4874. }
  4875. if ($overlap < 1) {
  4876. //shortcut to avoid complicated calculations
  4877. $final_overlap = 0;
  4878. $final_missing = 100;
  4879. $final_excess = 100;
  4880. } else {
  4881. // the final overlap is the percentage of the initial polygon
  4882. // that is overlapped by the user's polygon
  4883. $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
  4884. // the final missing area is the percentage of the initial polygon that
  4885. // is not overlapped by the user's polygon
  4886. $final_missing = 100 - $final_overlap;
  4887. // the final excess area is the percentage of the initial polygon's size that is
  4888. // covered by the user's polygon outside of the initial polygon
  4889. $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
  4890. if ($debug > 1) {
  4891. error_log(__LINE__.' - Final overlap is '.$final_overlap);
  4892. error_log(__LINE__.' - Final excess is '.$final_excess);
  4893. error_log(__LINE__.' - Final missing is '.$final_missing);
  4894. }
  4895. }
  4896. // Checking the destination parameters parsing the "@@"
  4897. $destination_items = explode('@@', $answerDestination);
  4898. $threadhold_total = $destination_items[0];
  4899. $threadhold_items = explode(';', $threadhold_total);
  4900. $threadhold1 = $threadhold_items[0]; // overlap
  4901. $threadhold2 = $threadhold_items[1]; // excess
  4902. $threadhold3 = $threadhold_items[2]; //missing
  4903. // if is delineation
  4904. if ($answerId === 1) {
  4905. //setting colors
  4906. if ($final_overlap >= $threadhold1) {
  4907. $overlap_color = true;
  4908. }
  4909. if ($final_excess <= $threadhold2) {
  4910. $excess_color = true;
  4911. }
  4912. if ($final_missing <= $threadhold3) {
  4913. $missing_color = true;
  4914. }
  4915. // if pass
  4916. if ($final_overlap >= $threadhold1 &&
  4917. $final_missing <= $threadhold3 &&
  4918. $final_excess <= $threadhold2
  4919. ) {
  4920. $next = 1; //go to the oars
  4921. $result_comment = get_lang('Acceptable');
  4922. $final_answer = 1; // do not update with update_exercise_attempt
  4923. } else {
  4924. $next = 0;
  4925. $result_comment = get_lang('Unacceptable');
  4926. $comment = $answerDestination = $objAnswerTmp->selectComment(1);
  4927. $answerDestination = $objAnswerTmp->selectDestination(1);
  4928. //checking the destination parameters parsing the "@@"
  4929. $destination_items = explode('@@', $answerDestination);
  4930. }
  4931. } elseif ($answerId > 1) {
  4932. if ($objAnswerTmp->selectHotspotType($answerId) === 'noerror') {
  4933. if ($debug > 0) {
  4934. error_log(__LINE__.' - answerId is of type noerror', 0);
  4935. }
  4936. //type no error shouldn't be treated
  4937. $next = 1;
  4938. break;
  4939. }
  4940. if ($debug > 0) {
  4941. error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
  4942. }
  4943. $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
  4944. $poly_answer = convert_coordinates($delineation_cord, '|');
  4945. $max_coord = poly_get_max($poly_user, $poly_answer);
  4946. $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
  4947. $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
  4948. if ($overlap == false) {
  4949. //all good, no overlap
  4950. $next = 1;
  4951. break;
  4952. } else {
  4953. if ($debug > 0) {
  4954. error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
  4955. }
  4956. $organs_at_risk_hit++;
  4957. //show the feedback
  4958. $next = 0;
  4959. $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
  4960. $answerDestination = $objAnswerTmp->selectDestination($answerId);
  4961. $destination_items = explode('@@', $answerDestination);
  4962. $try_hotspot = $destination_items[1];
  4963. $lp_hotspot = $destination_items[2];
  4964. $select_question_hotspot = $destination_items[3];
  4965. $url_hotspot = $destination_items[4];
  4966. }
  4967. }
  4968. }
  4969. break;
  4970. case HOT_SPOT_ORDER:
  4971. ExerciseShowFunctions::display_hotspot_order_answer(
  4972. $feedback_type,
  4973. $answerId,
  4974. $answer,
  4975. $studentChoice,
  4976. $answerComment
  4977. );
  4978. break;
  4979. case DRAGGABLE:
  4980. case MATCHING_DRAGGABLE:
  4981. case MATCHING:
  4982. echo '<tr>';
  4983. echo Display::tag('td', $answerMatching[$answerId]);
  4984. echo Display::tag(
  4985. 'td',
  4986. "$user_answer / ".Display::tag(
  4987. 'strong',
  4988. $answerMatching[$answerCorrect],
  4989. ['style' => 'color: #008000; font-weight: bold;']
  4990. )
  4991. );
  4992. echo '</tr>';
  4993. break;
  4994. case ANNOTATION:
  4995. ExerciseShowFunctions::displayAnnotationAnswer(
  4996. $feedback_type,
  4997. $exeId,
  4998. $questionId,
  4999. $questionScore,
  5000. $results_disabled
  5001. );
  5002. break;
  5003. }
  5004. }
  5005. }
  5006. } // end for that loops over all answers of the current question
  5007. if ($debug) {
  5008. error_log('-- end answer loop --');
  5009. }
  5010. $final_answer = true;
  5011. foreach ($real_answers as $my_answer) {
  5012. if (!$my_answer) {
  5013. $final_answer = false;
  5014. }
  5015. }
  5016. //we add the total score after dealing with the answers
  5017. if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
  5018. $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
  5019. ) {
  5020. if ($final_answer) {
  5021. //getting only the first score where we save the weight of all the question
  5022. $answerWeighting = $objAnswerTmp->selectWeighting(1);
  5023. $questionScore += $answerWeighting;
  5024. }
  5025. }
  5026. $extra_data = [
  5027. 'final_overlap' => $final_overlap,
  5028. 'final_missing' => $final_missing,
  5029. 'final_excess' => $final_excess,
  5030. 'overlap_color' => $overlap_color,
  5031. 'missing_color' => $missing_color,
  5032. 'excess_color' => $excess_color,
  5033. 'threadhold1' => $threadhold1,
  5034. 'threadhold2' => $threadhold2,
  5035. 'threadhold3' => $threadhold3,
  5036. ];
  5037. if ($from === 'exercise_result') {
  5038. // if answer is hotspot. To the difference of exercise_show.php,
  5039. // we use the results from the session (from_db=0)
  5040. // TODO Change this, because it is wrong to show the user
  5041. // some results that haven't been stored in the database yet
  5042. if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER || $answerType == HOT_SPOT_DELINEATION) {
  5043. if ($debug) {
  5044. error_log('$from AND this is a hotspot kind of question ');
  5045. }
  5046. if ($answerType === HOT_SPOT_DELINEATION) {
  5047. if ($showHotSpotDelineationTable) {
  5048. if (!is_numeric($final_overlap)) {
  5049. $final_overlap = 0;
  5050. }
  5051. if (!is_numeric($final_missing)) {
  5052. $final_missing = 0;
  5053. }
  5054. if (!is_numeric($final_excess)) {
  5055. $final_excess = 0;
  5056. }
  5057. if ($final_overlap > 100) {
  5058. $final_overlap = 100;
  5059. }
  5060. $table_resume = '<table class="data_table">
  5061. <tr class="row_odd" >
  5062. <td></td>
  5063. <td ><b>'.get_lang('Requirements').'</b></td>
  5064. <td><b>'.get_lang('Your answer').'</b></td>
  5065. </tr>
  5066. <tr class="row_even">
  5067. <td><b>'.get_lang('Overlapping areaping area').'</b></td>
  5068. <td>'.get_lang('Minimumimum').' '.$threadhold1.'</td>
  5069. <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
  5070. .(($final_overlap < 0) ? 0 : intval($final_overlap)).'</td>
  5071. </tr>
  5072. <tr>
  5073. <td><b>'.get_lang('Excessive areaive area').'</b></td>
  5074. <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
  5075. <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
  5076. .(($final_excess < 0) ? 0 : intval($final_excess)).'</td>
  5077. </tr>
  5078. <tr class="row_even">
  5079. <td><b>'.get_lang('Missing area area').'</b></td>
  5080. <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
  5081. <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
  5082. .(($final_missing < 0) ? 0 : intval($final_missing)).'</td>
  5083. </tr>
  5084. </table>';
  5085. if ($next == 0) {
  5086. /*$try = $try_hotspot;
  5087. $lp = $lp_hotspot;
  5088. $destinationid = $select_question_hotspot;
  5089. $url = $url_hotspot;*/
  5090. } else {
  5091. $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
  5092. $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
  5093. }
  5094. $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
  5095. <p style="text-align:center">';
  5096. $message .= '<p>'.get_lang('Your delineation :').'</p>';
  5097. $message .= $table_resume;
  5098. $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
  5099. if ($organs_at_risk_hit > 0) {
  5100. $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
  5101. }
  5102. $message .= '<p>'.$comment.'</p>';
  5103. echo $message;
  5104. $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
  5105. $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
  5106. } else {
  5107. echo $hotspot_delineation_result[0];
  5108. }
  5109. // Save the score attempts
  5110. if (1) {
  5111. //getting the answer 1 or 0 comes from exercise_submit_modal.php
  5112. $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
  5113. if ($final_answer == 0) {
  5114. $questionScore = 0;
  5115. }
  5116. // we always insert the answer_id 1 = delineation
  5117. Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
  5118. //in delineation mode, get the answer from $hotspot_delineation_result[1]
  5119. $hotspotValue = isset($hotspot_delineation_result[1]) ? (int) $hotspot_delineation_result[1] === 1 ? 1 : 0 : 0;
  5120. Event::saveExerciseAttemptHotspot(
  5121. $exeId,
  5122. $quesId,
  5123. 1,
  5124. $hotspotValue,
  5125. $exerciseResultCoordinates[$quesId]
  5126. );
  5127. } else {
  5128. if ($final_answer == 0) {
  5129. $questionScore = 0;
  5130. $answer = 0;
  5131. Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
  5132. if (is_array($exerciseResultCoordinates[$quesId])) {
  5133. foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
  5134. Event::saveExerciseAttemptHotspot(
  5135. $exeId,
  5136. $quesId,
  5137. $idx,
  5138. 0,
  5139. $val
  5140. );
  5141. }
  5142. }
  5143. } else {
  5144. Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
  5145. if (is_array($exerciseResultCoordinates[$quesId])) {
  5146. foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
  5147. $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
  5148. Event::saveExerciseAttemptHotspot(
  5149. $exeId,
  5150. $quesId,
  5151. $idx,
  5152. $hotspotValue,
  5153. $val
  5154. );
  5155. }
  5156. }
  5157. }
  5158. }
  5159. }
  5160. }
  5161. $relPath = api_get_path(WEB_CODE_PATH);
  5162. if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER) {
  5163. // We made an extra table for the answers
  5164. if ($show_result) {
  5165. echo '</table></td></tr>';
  5166. echo "
  5167. <tr>
  5168. <td colspan=\"2\">
  5169. <p><em>".get_lang('Image zones')."</em></p>
  5170. <div id=\"hotspot-solution-$questionId\"></div>
  5171. <script>
  5172. $(function() {
  5173. new HotspotQuestion({
  5174. questionId: $questionId,
  5175. exerciseId: {$this->id},
  5176. exeId: $exeId,
  5177. selector: '#hotspot-solution-$questionId',
  5178. for: 'solution',
  5179. relPath: '$relPath'
  5180. });
  5181. });
  5182. </script>
  5183. </td>
  5184. </tr>
  5185. ";
  5186. }
  5187. } elseif ($answerType == ANNOTATION) {
  5188. if ($show_result) {
  5189. echo '
  5190. <p><em>'.get_lang('Annotation').'</em></p>
  5191. <div id="annotation-canvas-'.$questionId.'"></div>
  5192. <script>
  5193. AnnotationQuestion({
  5194. questionId: parseInt('.$questionId.'),
  5195. exerciseId: parseInt('.$exeId.'),
  5196. relPath: \''.$relPath.'\',
  5197. courseId: parseInt('.$course_id.')
  5198. });
  5199. </script>
  5200. ';
  5201. }
  5202. }
  5203. //if ($origin != 'learnpath') {
  5204. if ($show_result && $answerType != ANNOTATION) {
  5205. echo '</table>';
  5206. }
  5207. // }
  5208. }
  5209. unset($objAnswerTmp);
  5210. $totalWeighting += $questionWeighting;
  5211. // Store results directly in the database
  5212. // For all in one page exercises, the results will be
  5213. // stored by exercise_results.php (using the session)
  5214. if ($saved_results) {
  5215. if ($debug) {
  5216. error_log("Save question results $saved_results");
  5217. error_log('choice: ');
  5218. error_log(print_r($choice, 1));
  5219. }
  5220. if (empty($choice)) {
  5221. $choice = 0;
  5222. }
  5223. // with certainty degree
  5224. if (empty($choiceDegreeCertainty)) {
  5225. $choiceDegreeCertainty = 0;
  5226. }
  5227. if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
  5228. $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE ||
  5229. $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
  5230. ) {
  5231. if ($choice != 0) {
  5232. $reply = array_keys($choice);
  5233. $countReply = count($reply);
  5234. for ($i = 0; $i < $countReply; $i++) {
  5235. $chosenAnswer = $reply[$i];
  5236. if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
  5237. if ($choiceDegreeCertainty != 0) {
  5238. $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
  5239. $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
  5240. $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
  5241. Event::saveQuestionAttempt(
  5242. $questionScore,
  5243. $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
  5244. $quesId,
  5245. $exeId,
  5246. $i,
  5247. $this->id,
  5248. $updateResults
  5249. );
  5250. }
  5251. } else {
  5252. Event::saveQuestionAttempt(
  5253. $questionScore,
  5254. $chosenAnswer.':'.$choice[$chosenAnswer],
  5255. $quesId,
  5256. $exeId,
  5257. $i,
  5258. $this->id,
  5259. $updateResults
  5260. );
  5261. }
  5262. if ($debug) {
  5263. error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
  5264. }
  5265. }
  5266. } else {
  5267. Event::saveQuestionAttempt(
  5268. $questionScore,
  5269. 0,
  5270. $quesId,
  5271. $exeId,
  5272. 0,
  5273. $this->id
  5274. );
  5275. }
  5276. } elseif ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) {
  5277. if ($choice != 0) {
  5278. $reply = array_keys($choice);
  5279. for ($i = 0; $i < count($reply); $i++) {
  5280. $ans = $reply[$i];
  5281. Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id);
  5282. }
  5283. } else {
  5284. Event::saveQuestionAttempt($questionScore, 0, $quesId, $exeId, 0, $this->id);
  5285. }
  5286. } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
  5287. if ($choice != 0) {
  5288. $reply = array_keys($choice);
  5289. for ($i = 0; $i < count($reply); $i++) {
  5290. $ans = $reply[$i];
  5291. Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id);
  5292. }
  5293. } else {
  5294. Event::saveQuestionAttempt(
  5295. $questionScore,
  5296. 0,
  5297. $quesId,
  5298. $exeId,
  5299. 0,
  5300. $this->id
  5301. );
  5302. }
  5303. } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
  5304. if (isset($matching)) {
  5305. foreach ($matching as $j => $val) {
  5306. Event::saveQuestionAttempt(
  5307. $questionScore,
  5308. $val,
  5309. $quesId,
  5310. $exeId,
  5311. $j,
  5312. $this->id
  5313. );
  5314. }
  5315. }
  5316. } elseif ($answerType == FREE_ANSWER) {
  5317. $answer = $choice;
  5318. Event::saveQuestionAttempt(
  5319. $questionScore,
  5320. $answer,
  5321. $quesId,
  5322. $exeId,
  5323. 0,
  5324. $this->id
  5325. );
  5326. } elseif ($answerType == ORAL_EXPRESSION) {
  5327. $answer = $choice;
  5328. Event::saveQuestionAttempt(
  5329. $questionScore,
  5330. $answer,
  5331. $quesId,
  5332. $exeId,
  5333. 0,
  5334. $this->id,
  5335. false,
  5336. $objQuestionTmp->getAbsoluteFilePath()
  5337. );
  5338. } elseif (
  5339. in_array(
  5340. $answerType,
  5341. [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
  5342. )
  5343. ) {
  5344. $answer = $choice;
  5345. Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id);
  5346. } elseif ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
  5347. $answer = [];
  5348. if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
  5349. if ($debug) {
  5350. error_log('Checking result coordinates');
  5351. }
  5352. Database::delete(
  5353. Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
  5354. [
  5355. 'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
  5356. $exeId,
  5357. $questionId,
  5358. api_get_course_int_id(),
  5359. ],
  5360. ]
  5361. );
  5362. foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
  5363. $answer[] = $val;
  5364. $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
  5365. if ($debug) {
  5366. error_log('Hotspot value: '.$hotspotValue);
  5367. }
  5368. Event::saveExerciseAttemptHotspot(
  5369. $exeId,
  5370. $quesId,
  5371. $idx,
  5372. $hotspotValue,
  5373. $val,
  5374. false,
  5375. $this->id
  5376. );
  5377. }
  5378. } else {
  5379. if ($debug) {
  5380. error_log('Empty: exerciseResultCoordinates');
  5381. }
  5382. }
  5383. Event::saveQuestionAttempt($questionScore, implode('|', $answer), $quesId, $exeId, 0, $this->id);
  5384. } else {
  5385. Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id);
  5386. }
  5387. }
  5388. if ($propagate_neg == 0 && $questionScore < 0) {
  5389. $questionScore = 0;
  5390. }
  5391. if ($saved_results) {
  5392. $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  5393. $sql = "UPDATE $statsTable SET
  5394. score = score + ".floatval($questionScore)."
  5395. WHERE exe_id = $exeId";
  5396. Database::query($sql);
  5397. }
  5398. $return = [
  5399. 'score' => $questionScore,
  5400. 'weight' => $questionWeighting,
  5401. 'extra' => $extra_data,
  5402. 'open_question' => $arrques,
  5403. 'open_answer' => $arrans,
  5404. 'answer_type' => $answerType,
  5405. 'generated_oral_file' => $generatedFile,
  5406. 'user_answered' => $userAnsweredQuestion,
  5407. 'correct_answer_id' => $correctAnswerId,
  5408. 'answer_destination' => $answerDestination,
  5409. ];
  5410. return $return;
  5411. }
  5412. /**
  5413. * Sends a notification when a user ends an examn.
  5414. *
  5415. * @param string $type 'start' or 'end' of an exercise
  5416. * @param array $question_list_answers
  5417. * @param string $origin
  5418. * @param int $exe_id
  5419. * @param float $score
  5420. * @param float $weight
  5421. *
  5422. * @return bool
  5423. */
  5424. public function send_mail_notification_for_exam(
  5425. $type = 'end',
  5426. $question_list_answers,
  5427. $origin,
  5428. $exe_id,
  5429. $score = null,
  5430. $weight = null
  5431. ) {
  5432. $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
  5433. if (empty($setting) && empty($this->getNotifications())) {
  5434. return false;
  5435. }
  5436. $settingFromExercise = $this->getNotifications();
  5437. if (!empty($settingFromExercise)) {
  5438. $setting = $settingFromExercise;
  5439. }
  5440. // Email configuration settings
  5441. $courseCode = api_get_course_id();
  5442. $courseInfo = api_get_course_info($courseCode);
  5443. if (empty($courseInfo)) {
  5444. return false;
  5445. }
  5446. $sessionId = api_get_session_id();
  5447. $sessionData = '';
  5448. if (!empty($sessionId)) {
  5449. $sessionInfo = api_get_session_info($sessionId);
  5450. if (!empty($sessionInfo)) {
  5451. $sessionData = '<tr>'
  5452. .'<td>'.get_lang('Session name').'</td>'
  5453. .'<td>'.$sessionInfo['name'].'</td>'
  5454. .'</tr>';
  5455. }
  5456. }
  5457. $sendStart = false;
  5458. $sendEnd = false;
  5459. $sendEndOpenQuestion = false;
  5460. $sendEndOralQuestion = false;
  5461. foreach ($setting as $option) {
  5462. switch ($option) {
  5463. case 0:
  5464. return false;
  5465. break;
  5466. case 1: // End
  5467. if ($type == 'end') {
  5468. $sendEnd = true;
  5469. }
  5470. break;
  5471. case 2: // start
  5472. if ($type == 'start') {
  5473. $sendStart = true;
  5474. }
  5475. break;
  5476. case 3: // end + open
  5477. if ($type == 'end') {
  5478. $sendEndOpenQuestion = true;
  5479. }
  5480. break;
  5481. case 4: // end + oral
  5482. if ($type == 'end') {
  5483. $sendEndOralQuestion = true;
  5484. }
  5485. break;
  5486. }
  5487. }
  5488. $user_info = api_get_user_info(api_get_user_id());
  5489. $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
  5490. api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
  5491. if (!empty($sessionId)) {
  5492. $addGeneralCoach = true;
  5493. $setting = api_get_configuration_value('block_quiz_mail_notification_general_coach');
  5494. if ($setting === true) {
  5495. $addGeneralCoach = false;
  5496. }
  5497. $teachers = CourseManager::get_coach_list_from_course_code(
  5498. $courseCode,
  5499. $sessionId,
  5500. $addGeneralCoach
  5501. );
  5502. } else {
  5503. $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
  5504. }
  5505. if ($sendEndOpenQuestion) {
  5506. $this->sendNotificationForOpenQuestions(
  5507. $question_list_answers,
  5508. $origin,
  5509. $user_info,
  5510. $url,
  5511. $teachers
  5512. );
  5513. }
  5514. if ($sendEndOralQuestion) {
  5515. $this->sendNotificationForOralQuestions(
  5516. $question_list_answers,
  5517. $origin,
  5518. $exe_id,
  5519. $user_info,
  5520. $url,
  5521. $teachers
  5522. );
  5523. }
  5524. if (!$sendEnd && !$sendStart) {
  5525. return false;
  5526. }
  5527. $scoreLabel = '';
  5528. if ($sendEnd &&
  5529. api_get_configuration_value('send_score_in_exam_notification_mail_to_manager') == true
  5530. ) {
  5531. $notificationPercentage = api_get_configuration_value('send_notification_score_in_percentage');
  5532. $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
  5533. $scoreLabel = "<tr>
  5534. <td>".get_lang('Score')."</td>
  5535. <td>&nbsp;$scoreLabel</td>
  5536. </tr>";
  5537. }
  5538. if ($sendEnd) {
  5539. $msg = get_lang('A learner attempted an exercise').'<br /><br />';
  5540. } else {
  5541. $msg = get_lang('Student just started an exercise').'<br /><br />';
  5542. }
  5543. $msg .= get_lang('Attempt details').' : <br /><br />
  5544. <table>
  5545. <tr>
  5546. <td>'.get_lang('Course name').'</td>
  5547. <td>#course#</td>
  5548. </tr>
  5549. '.$sessionData.'
  5550. <tr>
  5551. <td>'.get_lang('Test').'</td>
  5552. <td>&nbsp;#exercise#</td>
  5553. </tr>
  5554. <tr>
  5555. <td>'.get_lang('Learner name').'</td>
  5556. <td>&nbsp;#student_complete_name#</td>
  5557. </tr>
  5558. <tr>
  5559. <td>'.get_lang('Learner e-mail').'</td>
  5560. <td>&nbsp;#email#</td>
  5561. </tr>
  5562. '.$scoreLabel.'
  5563. </table>';
  5564. $variables = [
  5565. '#email#' => $user_info['email'],
  5566. '#exercise#' => $this->exercise,
  5567. '#student_complete_name#' => $user_info['complete_name'],
  5568. '#course#' => Display::url(
  5569. $courseInfo['title'],
  5570. $courseInfo['course_public_url'].'?id_session='.$sessionId
  5571. ),
  5572. ];
  5573. if ($sendEnd) {
  5574. $msg .= '<br /><a href="#url#">'.get_lang('Click this link to check the answer and/or give feedback').'</a>';
  5575. $variables['#url#'] = $url;
  5576. }
  5577. $content = str_replace(array_keys($variables), array_values($variables), $msg);
  5578. if ($sendEnd) {
  5579. $subject = get_lang('A learner attempted an exercise');
  5580. } else {
  5581. $subject = get_lang('Student just started an exercise');
  5582. }
  5583. if (!empty($teachers)) {
  5584. foreach ($teachers as $user_id => $teacher_data) {
  5585. MessageManager::send_message_simple(
  5586. $user_id,
  5587. $subject,
  5588. $content
  5589. );
  5590. }
  5591. }
  5592. }
  5593. /**
  5594. * @param array $user_data result of api_get_user_info()
  5595. * @param array $trackExerciseInfo result of get_stat_track_exercise_info
  5596. *
  5597. * @return string
  5598. */
  5599. public function showExerciseResultHeader(
  5600. $user_data,
  5601. $trackExerciseInfo
  5602. ) {
  5603. if (api_get_configuration_value('hide_user_info_in_quiz_result')) {
  5604. return '';
  5605. }
  5606. $start_date = null;
  5607. if (isset($trackExerciseInfo['start_date'])) {
  5608. $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
  5609. }
  5610. $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
  5611. $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
  5612. if (!empty($user_data)) {
  5613. $userFullName = $user_data['complete_name'];
  5614. if (api_is_teacher() || api_is_platform_admin(true, true)) {
  5615. $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
  5616. $user_data['complete_name'].'</a>';
  5617. }
  5618. $data = [
  5619. 'name_url' => $userFullName,
  5620. 'complete_name' => $user_data['complete_name'],
  5621. 'username' => $user_data['username'],
  5622. 'avatar' => $user_data['avatar_medium'],
  5623. 'url' => $user_data['profile_url'],
  5624. ];
  5625. if (!empty($user_data['official_code'])) {
  5626. $data['code'] = $user_data['official_code'];
  5627. }
  5628. }
  5629. // Description can be very long and is generally meant to explain
  5630. // rules *before* the exam. Leaving here to make display easier if
  5631. // necessary
  5632. /*
  5633. if (!empty($this->description)) {
  5634. $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
  5635. }
  5636. */
  5637. if (!empty($start_date)) {
  5638. $data['start_date'] = $start_date;
  5639. }
  5640. if (!empty($duration)) {
  5641. $data['duration'] = $duration;
  5642. }
  5643. if (!empty($ip)) {
  5644. $data['ip'] = $ip;
  5645. }
  5646. if (api_get_configuration_value('save_titles_as_html')) {
  5647. $data['title'] = $this->get_formated_title().get_lang('Result');
  5648. } else {
  5649. $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
  5650. }
  5651. $tpl = new Template(null, false, false, false, false, false, false);
  5652. $tpl->assign('data', $data);
  5653. $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
  5654. $content = $tpl->fetch($layoutTemplate);
  5655. return $content;
  5656. }
  5657. /**
  5658. * Returns the exercise result.
  5659. *
  5660. * @param int attempt id
  5661. *
  5662. * @return array
  5663. */
  5664. public function get_exercise_result($exe_id)
  5665. {
  5666. $result = [];
  5667. $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
  5668. if (!empty($track_exercise_info)) {
  5669. $totalScore = 0;
  5670. $objExercise = new Exercise();
  5671. $objExercise->read($track_exercise_info['exe_exo_id']);
  5672. if (!empty($track_exercise_info['data_tracking'])) {
  5673. $question_list = explode(',', $track_exercise_info['data_tracking']);
  5674. }
  5675. foreach ($question_list as $questionId) {
  5676. $question_result = $objExercise->manage_answer(
  5677. $exe_id,
  5678. $questionId,
  5679. '',
  5680. 'exercise_show',
  5681. [],
  5682. false,
  5683. true,
  5684. false,
  5685. $objExercise->selectPropagateNeg()
  5686. );
  5687. $totalScore += $question_result['score'];
  5688. }
  5689. if ($objExercise->selectPropagateNeg() == 0 && $totalScore < 0) {
  5690. $totalScore = 0;
  5691. }
  5692. $result = [
  5693. 'score' => $totalScore,
  5694. 'weight' => $track_exercise_info['max_score'],
  5695. ];
  5696. }
  5697. return $result;
  5698. }
  5699. /**
  5700. * Checks if the exercise is visible due a lot of conditions
  5701. * visibility, time limits, student attempts
  5702. * Return associative array
  5703. * value : true if exercise visible
  5704. * message : HTML formatted message
  5705. * rawMessage : text message.
  5706. *
  5707. * @param int $lpId
  5708. * @param int $lpItemId
  5709. * @param int $lpItemViewId
  5710. * @param bool $filterByAdmin
  5711. *
  5712. * @return array
  5713. */
  5714. public function is_visible(
  5715. $lpId = 0,
  5716. $lpItemId = 0,
  5717. $lpItemViewId = 0,
  5718. $filterByAdmin = true
  5719. ) {
  5720. // 1. By default the exercise is visible
  5721. $isVisible = true;
  5722. $message = null;
  5723. // 1.1 Admins and teachers can access to the exercise
  5724. if ($filterByAdmin) {
  5725. if (api_is_platform_admin() || api_is_course_admin()) {
  5726. return ['value' => true, 'message' => ''];
  5727. }
  5728. }
  5729. // Deleted exercise.
  5730. if ($this->active == -1) {
  5731. return [
  5732. 'value' => false,
  5733. 'message' => Display::return_message(
  5734. get_lang('TestNotFound'),
  5735. 'warning',
  5736. false
  5737. ),
  5738. 'rawMessage' => get_lang('TestNotFound'),
  5739. ];
  5740. }
  5741. // Checking visibility in the item_property table.
  5742. $visibility = api_get_item_visibility(
  5743. api_get_course_info(),
  5744. TOOL_QUIZ,
  5745. $this->id,
  5746. api_get_session_id()
  5747. );
  5748. if ($visibility == 0 || $visibility == 2) {
  5749. $this->active = 0;
  5750. }
  5751. // 2. If the exercise is not active.
  5752. if (empty($lpId)) {
  5753. // 2.1 LP is OFF
  5754. if ($this->active == 0) {
  5755. return [
  5756. 'value' => false,
  5757. 'message' => Display::return_message(
  5758. get_lang('TestNotFound'),
  5759. 'warning',
  5760. false
  5761. ),
  5762. 'rawMessage' => get_lang('TestNotFound'),
  5763. ];
  5764. }
  5765. } else {
  5766. // 2.1 LP is loaded
  5767. if ($this->active == 0 &&
  5768. !learnpath::is_lp_visible_for_student($lpId, api_get_user_id())
  5769. ) {
  5770. return [
  5771. 'value' => false,
  5772. 'message' => Display::return_message(
  5773. get_lang('TestNotFound'),
  5774. 'warning',
  5775. false
  5776. ),
  5777. 'rawMessage' => get_lang('TestNotFound'),
  5778. ];
  5779. }
  5780. }
  5781. // 3. We check if the time limits are on
  5782. $limitTimeExists = false;
  5783. if (!empty($this->start_time) || !empty($this->end_time)) {
  5784. $limitTimeExists = true;
  5785. }
  5786. if ($limitTimeExists) {
  5787. $timeNow = time();
  5788. $existsStartDate = false;
  5789. $nowIsAfterStartDate = true;
  5790. $existsEndDate = false;
  5791. $nowIsBeforeEndDate = true;
  5792. if (!empty($this->start_time)) {
  5793. $existsStartDate = true;
  5794. }
  5795. if (!empty($this->end_time)) {
  5796. $existsEndDate = true;
  5797. }
  5798. // check if we are before-or-after end-or-start date
  5799. if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
  5800. $nowIsAfterStartDate = false;
  5801. }
  5802. if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
  5803. $nowIsBeforeEndDate = false;
  5804. }
  5805. // lets check all cases
  5806. if ($existsStartDate && !$existsEndDate) {
  5807. // exists start date and dont exists end date
  5808. if ($nowIsAfterStartDate) {
  5809. // after start date, no end date
  5810. $isVisible = true;
  5811. $message = sprintf(
  5812. get_lang('TestAvailableSinceX'),
  5813. api_convert_and_format_date($this->start_time)
  5814. );
  5815. } else {
  5816. // before start date, no end date
  5817. $isVisible = false;
  5818. $message = sprintf(
  5819. get_lang('TestAvailableFromX'),
  5820. api_convert_and_format_date($this->start_time)
  5821. );
  5822. }
  5823. } elseif (!$existsStartDate && $existsEndDate) {
  5824. // doesnt exist start date, exists end date
  5825. if ($nowIsBeforeEndDate) {
  5826. // before end date, no start date
  5827. $isVisible = true;
  5828. $message = sprintf(
  5829. get_lang('TestAvailableUntilX'),
  5830. api_convert_and_format_date($this->end_time)
  5831. );
  5832. } else {
  5833. // after end date, no start date
  5834. $isVisible = false;
  5835. $message = sprintf(
  5836. get_lang('TestAvailableUntilX'),
  5837. api_convert_and_format_date($this->end_time)
  5838. );
  5839. }
  5840. } elseif ($existsStartDate && $existsEndDate) {
  5841. // exists start date and end date
  5842. if ($nowIsAfterStartDate) {
  5843. if ($nowIsBeforeEndDate) {
  5844. // after start date and before end date
  5845. $isVisible = true;
  5846. $message = sprintf(
  5847. get_lang('TestIsActivatedFromXToY'),
  5848. api_convert_and_format_date($this->start_time),
  5849. api_convert_and_format_date($this->end_time)
  5850. );
  5851. } else {
  5852. // after start date and after end date
  5853. $isVisible = false;
  5854. $message = sprintf(
  5855. get_lang('TestWasActivatedFromXToY'),
  5856. api_convert_and_format_date($this->start_time),
  5857. api_convert_and_format_date($this->end_time)
  5858. );
  5859. }
  5860. } else {
  5861. if ($nowIsBeforeEndDate) {
  5862. // before start date and before end date
  5863. $isVisible = false;
  5864. $message = sprintf(
  5865. get_lang('TestWillBeActivatedFromXToY'),
  5866. api_convert_and_format_date($this->start_time),
  5867. api_convert_and_format_date($this->end_time)
  5868. );
  5869. }
  5870. // case before start date and after end date is impossible
  5871. }
  5872. } elseif (!$existsStartDate && !$existsEndDate) {
  5873. // doesnt exist start date nor end date
  5874. $isVisible = true;
  5875. $message = '';
  5876. }
  5877. }
  5878. // 4. We check if the student have attempts
  5879. if ($isVisible) {
  5880. $exerciseAttempts = $this->selectAttempts();
  5881. if ($exerciseAttempts > 0) {
  5882. $attemptCount = Event::get_attempt_count_not_finished(
  5883. api_get_user_id(),
  5884. $this->id,
  5885. $lpId,
  5886. $lpItemId,
  5887. $lpItemViewId
  5888. );
  5889. if ($attemptCount >= $exerciseAttempts) {
  5890. $message = sprintf(
  5891. get_lang('Reachedmax. 20 characters, e.g. <i>INNOV21</i>Attempts'),
  5892. $this->name,
  5893. $exerciseAttempts
  5894. );
  5895. $isVisible = false;
  5896. }
  5897. }
  5898. }
  5899. $rawMessage = '';
  5900. if (!empty($message)) {
  5901. $rawMessage = $message;
  5902. $message = Display::return_message($message, 'warning', false);
  5903. }
  5904. return [
  5905. 'value' => $isVisible,
  5906. 'message' => $message,
  5907. 'rawMessage' => $rawMessage,
  5908. ];
  5909. }
  5910. /**
  5911. * @return bool
  5912. */
  5913. public function added_in_lp()
  5914. {
  5915. $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
  5916. $sql = "SELECT max_score FROM $TBL_LP_ITEM
  5917. WHERE
  5918. c_id = {$this->course_id} AND
  5919. item_type = '".TOOL_QUIZ."' AND
  5920. path = '{$this->id}'";
  5921. $result = Database::query($sql);
  5922. if (Database::num_rows($result) > 0) {
  5923. return true;
  5924. }
  5925. return false;
  5926. }
  5927. /**
  5928. * Returns an array with this form.
  5929. *
  5930. * @example
  5931. * <code>
  5932. * array (size=3)
  5933. * 999 =>
  5934. * array (size=3)
  5935. * 0 => int 3422
  5936. * 1 => int 3423
  5937. * 2 => int 3424
  5938. * 100 =>
  5939. * array (size=2)
  5940. * 0 => int 3469
  5941. * 1 => int 3470
  5942. * 101 =>
  5943. * array (size=1)
  5944. * 0 => int 3482
  5945. * </code>
  5946. * The array inside the key 999 means the question list that belongs to the media id = 999,
  5947. * this case is special because 999 means "no media".
  5948. *
  5949. * @return array
  5950. */
  5951. public function getMediaList()
  5952. {
  5953. return $this->mediaList;
  5954. }
  5955. /**
  5956. * Is media question activated?
  5957. *
  5958. * @return bool
  5959. */
  5960. public function mediaIsActivated()
  5961. {
  5962. $mediaQuestions = $this->getMediaList();
  5963. $active = false;
  5964. if (isset($mediaQuestions) && !empty($mediaQuestions)) {
  5965. $media_count = count($mediaQuestions);
  5966. if ($media_count > 1) {
  5967. return true;
  5968. } elseif ($media_count == 1) {
  5969. if (isset($mediaQuestions[999])) {
  5970. return false;
  5971. } else {
  5972. return true;
  5973. }
  5974. }
  5975. }
  5976. return $active;
  5977. }
  5978. /**
  5979. * Gets question list from the exercise.
  5980. *
  5981. * @return array
  5982. */
  5983. public function getQuestionList()
  5984. {
  5985. return $this->questionList;
  5986. }
  5987. /**
  5988. * Question list with medias compressed like this.
  5989. *
  5990. * @example
  5991. * <code>
  5992. * array(
  5993. * question_id_1,
  5994. * question_id_2,
  5995. * media_id, <- this media id contains question ids
  5996. * question_id_3,
  5997. * )
  5998. * </code>
  5999. *
  6000. * @return array
  6001. */
  6002. public function getQuestionListWithMediasCompressed()
  6003. {
  6004. return $this->questionList;
  6005. }
  6006. /**
  6007. * Question list with medias uncompressed like this.
  6008. *
  6009. * @example
  6010. * <code>
  6011. * array(
  6012. * question_id,
  6013. * question_id,
  6014. * question_id, <- belongs to a media id
  6015. * question_id, <- belongs to a media id
  6016. * question_id,
  6017. * )
  6018. * </code>
  6019. *
  6020. * @return array
  6021. */
  6022. public function getQuestionListWithMediasUncompressed()
  6023. {
  6024. return $this->questionListUncompressed;
  6025. }
  6026. /**
  6027. * Sets the question list when the exercise->read() is executed.
  6028. *
  6029. * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
  6030. */
  6031. public function setQuestionList($adminView = false)
  6032. {
  6033. // Getting question list.
  6034. $questionList = $this->selectQuestionList(true, $adminView);
  6035. $this->setMediaList($questionList);
  6036. $this->questionList = $this->transformQuestionListWithMedias(
  6037. $questionList,
  6038. false
  6039. );
  6040. $this->questionListUncompressed = $this->transformQuestionListWithMedias(
  6041. $questionList,
  6042. true
  6043. );
  6044. }
  6045. /**
  6046. * @params array question list
  6047. * @params bool expand or not question list (true show all questions,
  6048. * false show media question id instead of the question ids)
  6049. */
  6050. public function transformQuestionListWithMedias(
  6051. $question_list,
  6052. $expand_media_questions = false
  6053. ) {
  6054. $new_question_list = [];
  6055. if (!empty($question_list)) {
  6056. $media_questions = $this->getMediaList();
  6057. $media_active = $this->mediaIsActivated($media_questions);
  6058. if ($media_active) {
  6059. $counter = 1;
  6060. foreach ($question_list as $question_id) {
  6061. $add_question = true;
  6062. foreach ($media_questions as $media_id => $question_list_in_media) {
  6063. if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
  6064. $add_question = false;
  6065. if (!in_array($media_id, $new_question_list)) {
  6066. $new_question_list[$counter] = $media_id;
  6067. $counter++;
  6068. }
  6069. break;
  6070. }
  6071. }
  6072. if ($add_question) {
  6073. $new_question_list[$counter] = $question_id;
  6074. $counter++;
  6075. }
  6076. }
  6077. if ($expand_media_questions) {
  6078. $media_key_list = array_keys($media_questions);
  6079. foreach ($new_question_list as &$question_id) {
  6080. if (in_array($question_id, $media_key_list)) {
  6081. $question_id = $media_questions[$question_id];
  6082. }
  6083. }
  6084. $new_question_list = array_flatten($new_question_list);
  6085. }
  6086. } else {
  6087. $new_question_list = $question_list;
  6088. }
  6089. }
  6090. return $new_question_list;
  6091. }
  6092. /**
  6093. * Get question list depend on the random settings.
  6094. *
  6095. * @return array
  6096. */
  6097. public function get_validated_question_list()
  6098. {
  6099. $isRandomByCategory = $this->isRandomByCat();
  6100. if ($isRandomByCategory == 0) {
  6101. if ($this->isRandom()) {
  6102. return $this->getRandomList();
  6103. }
  6104. return $this->selectQuestionList();
  6105. }
  6106. if ($this->isRandom()) {
  6107. // USE question categories
  6108. // get questions by category for this exercise
  6109. // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
  6110. // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
  6111. // value is the array of question id of this category
  6112. $questionList = [];
  6113. $tabCategoryQuestions = TestCategory::getQuestionsByCat($this->id);
  6114. $isRandomByCategory = $this->getRandomByCategory();
  6115. // We sort categories based on the term between [] in the head
  6116. // of the category's description
  6117. /* examples of categories :
  6118. * [biologie] Maitriser les mecanismes de base de la genetique
  6119. * [biologie] Relier les moyens de depenses et les agents infectieux
  6120. * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
  6121. * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
  6122. * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
  6123. * [chimie] Connaître les charges des particules
  6124. * We want that in the order of the groups defined by the term
  6125. * between brackets at the beginning of the category title
  6126. */
  6127. // If test option is Grouped By Categories
  6128. if ($isRandomByCategory == 2) {
  6129. $tabCategoryQuestions = TestCategory::sortTabByBracketLabel($tabCategoryQuestions);
  6130. }
  6131. foreach ($tabCategoryQuestions as $tabquestion) {
  6132. $number_of_random_question = $this->random;
  6133. if ($this->random == -1) {
  6134. $number_of_random_question = count($this->questionList);
  6135. }
  6136. $questionList = array_merge(
  6137. $questionList,
  6138. TestCategory::getNElementsFromArray(
  6139. $tabquestion,
  6140. $number_of_random_question
  6141. )
  6142. );
  6143. }
  6144. // shuffle the question list if test is not grouped by categories
  6145. if ($isRandomByCategory == 1) {
  6146. shuffle($questionList); // or not
  6147. }
  6148. return $questionList;
  6149. }
  6150. // Problem, random by category has been selected and
  6151. // we have no $this->isRandom number of question selected
  6152. // Should not happened
  6153. return [];
  6154. }
  6155. public function get_question_list($expand_media_questions = false)
  6156. {
  6157. $question_list = $this->get_validated_question_list();
  6158. $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
  6159. return $question_list;
  6160. }
  6161. public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
  6162. {
  6163. $new_question_list = [];
  6164. if (!empty($question_list)) {
  6165. $media_questions = $this->getMediaList();
  6166. $media_active = $this->mediaIsActivated($media_questions);
  6167. if ($media_active) {
  6168. $counter = 1;
  6169. foreach ($question_list as $question_id) {
  6170. $add_question = true;
  6171. foreach ($media_questions as $media_id => $question_list_in_media) {
  6172. if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
  6173. $add_question = false;
  6174. if (!in_array($media_id, $new_question_list)) {
  6175. $new_question_list[$counter] = $media_id;
  6176. $counter++;
  6177. }
  6178. break;
  6179. }
  6180. }
  6181. if ($add_question) {
  6182. $new_question_list[$counter] = $question_id;
  6183. $counter++;
  6184. }
  6185. }
  6186. if ($expand_media_questions) {
  6187. $media_key_list = array_keys($media_questions);
  6188. foreach ($new_question_list as &$question_id) {
  6189. if (in_array($question_id, $media_key_list)) {
  6190. $question_id = $media_questions[$question_id];
  6191. }
  6192. }
  6193. $new_question_list = array_flatten($new_question_list);
  6194. }
  6195. } else {
  6196. $new_question_list = $question_list;
  6197. }
  6198. }
  6199. return $new_question_list;
  6200. }
  6201. /**
  6202. * @param int $exe_id
  6203. *
  6204. * @return array
  6205. */
  6206. public function get_stat_track_exercise_info_by_exe_id($exe_id)
  6207. {
  6208. $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  6209. $exe_id = (int) $exe_id;
  6210. $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
  6211. $result = Database::query($sql_track);
  6212. $new_array = [];
  6213. if (Database::num_rows($result) > 0) {
  6214. $new_array = Database::fetch_array($result, 'ASSOC');
  6215. $start_date = api_get_utc_datetime($new_array['start_date'], true);
  6216. $end_date = api_get_utc_datetime($new_array['exe_date'], true);
  6217. $new_array['duration_formatted'] = '';
  6218. if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
  6219. $time = api_format_time($new_array['exe_duration'], 'js');
  6220. $new_array['duration_formatted'] = $time;
  6221. }
  6222. }
  6223. return $new_array;
  6224. }
  6225. /**
  6226. * @param int $exeId
  6227. *
  6228. * @return bool
  6229. */
  6230. public function removeAllQuestionToRemind($exeId)
  6231. {
  6232. $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  6233. $exeId = (int) $exeId;
  6234. if (empty($exeId)) {
  6235. return false;
  6236. }
  6237. $sql = "UPDATE $table
  6238. SET questions_to_check = ''
  6239. WHERE exe_id = $exeId ";
  6240. Database::query($sql);
  6241. return true;
  6242. }
  6243. /**
  6244. * @param int $exeId
  6245. * @param array $questionList
  6246. *
  6247. * @return bool
  6248. */
  6249. public function addAllQuestionToRemind($exeId, $questionList = [])
  6250. {
  6251. $exeId = (int) $exeId;
  6252. if (empty($questionList)) {
  6253. return false;
  6254. }
  6255. $questionListToString = implode(',', $questionList);
  6256. $questionListToString = Database::escape_string($questionListToString);
  6257. $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  6258. $sql = "UPDATE $table
  6259. SET questions_to_check = '$questionListToString'
  6260. WHERE exe_id = $exeId";
  6261. Database::query($sql);
  6262. return true;
  6263. }
  6264. /**
  6265. * @param int $exe_id
  6266. * @param int $question_id
  6267. * @param string $action
  6268. */
  6269. public function editQuestionToRemind($exe_id, $question_id, $action = 'add')
  6270. {
  6271. $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exe_id);
  6272. $question_id = (int) $question_id;
  6273. $exe_id = (int) $exe_id;
  6274. $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  6275. if ($exercise_info) {
  6276. if (empty($exercise_info['questions_to_check'])) {
  6277. if ($action == 'add') {
  6278. $sql = "UPDATE $track_exercises
  6279. SET questions_to_check = '$question_id'
  6280. WHERE exe_id = $exe_id ";
  6281. Database::query($sql);
  6282. }
  6283. } else {
  6284. $remind_list = explode(',', $exercise_info['questions_to_check']);
  6285. $remind_list_string = '';
  6286. if ($action == 'add') {
  6287. if (!in_array($question_id, $remind_list)) {
  6288. $newRemindList = [];
  6289. $remind_list[] = $question_id;
  6290. $questionListInSession = Session::read('questionList');
  6291. if (!empty($questionListInSession)) {
  6292. foreach ($questionListInSession as $originalQuestionId) {
  6293. if (in_array($originalQuestionId, $remind_list)) {
  6294. $newRemindList[] = $originalQuestionId;
  6295. }
  6296. }
  6297. }
  6298. $remind_list_string = implode(',', $newRemindList);
  6299. }
  6300. } elseif ($action == 'delete') {
  6301. if (!empty($remind_list)) {
  6302. if (in_array($question_id, $remind_list)) {
  6303. $remind_list = array_flip($remind_list);
  6304. unset($remind_list[$question_id]);
  6305. $remind_list = array_flip($remind_list);
  6306. if (!empty($remind_list)) {
  6307. sort($remind_list);
  6308. array_filter($remind_list);
  6309. $remind_list_string = implode(',', $remind_list);
  6310. }
  6311. }
  6312. }
  6313. }
  6314. $value = Database::escape_string($remind_list_string);
  6315. $sql = "UPDATE $track_exercises
  6316. SET questions_to_check = '$value'
  6317. WHERE exe_id = $exe_id ";
  6318. Database::query($sql);
  6319. }
  6320. }
  6321. }
  6322. /**
  6323. * @param string $answer
  6324. *
  6325. * @return mixed
  6326. */
  6327. public function fill_in_blank_answer_to_array($answer)
  6328. {
  6329. api_preg_match_all('/\[[^]]+\]/', $answer, $teacher_answer_list);
  6330. $teacher_answer_list = $teacher_answer_list[0];
  6331. return $teacher_answer_list;
  6332. }
  6333. /**
  6334. * @param string $answer
  6335. *
  6336. * @return string
  6337. */
  6338. public function fill_in_blank_answer_to_string($answer)
  6339. {
  6340. $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
  6341. $result = '';
  6342. if (!empty($teacher_answer_list)) {
  6343. $i = 0;
  6344. foreach ($teacher_answer_list as $teacher_item) {
  6345. $value = null;
  6346. //Cleaning student answer list
  6347. $value = strip_tags($teacher_item);
  6348. $value = api_substr($value, 1, api_strlen($value) - 2);
  6349. $value = explode('/', $value);
  6350. if (!empty($value[0])) {
  6351. $value = trim($value[0]);
  6352. $value = str_replace('&nbsp;', '', $value);
  6353. $result .= $value;
  6354. }
  6355. }
  6356. }
  6357. return $result;
  6358. }
  6359. /**
  6360. * @return string
  6361. */
  6362. public function returnTimeLeftDiv()
  6363. {
  6364. $html = '<div id="clock_warning" style="display:none">';
  6365. $html .= Display::return_message(
  6366. get_lang('Time limit reached'),
  6367. 'warning'
  6368. );
  6369. $html .= ' ';
  6370. $html .= sprintf(
  6371. get_lang('Just a moment, please. You will be redirected in %s seconds...'),
  6372. '<span id="counter_to_redirect" class="red_alert"></span>'
  6373. );
  6374. $html .= '</div>';
  6375. $html .= '<div id="exercise_clock_warning" class="count_down"></div>';
  6376. return $html;
  6377. }
  6378. /**
  6379. * Get categories added in the exercise--category matrix.
  6380. *
  6381. * @return array
  6382. */
  6383. public function getCategoriesInExercise()
  6384. {
  6385. $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
  6386. if (!empty($this->id)) {
  6387. $sql = "SELECT * FROM $table
  6388. WHERE exercise_id = {$this->id} AND c_id = {$this->course_id} ";
  6389. $result = Database::query($sql);
  6390. $list = [];
  6391. if (Database::num_rows($result)) {
  6392. while ($row = Database::fetch_array($result, 'ASSOC')) {
  6393. $list[$row['category_id']] = $row;
  6394. }
  6395. return $list;
  6396. }
  6397. }
  6398. return [];
  6399. }
  6400. /**
  6401. * Get total number of question that will be parsed when using the category/exercise.
  6402. *
  6403. * @return int
  6404. */
  6405. public function getNumberQuestionExerciseCategory()
  6406. {
  6407. $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
  6408. if (!empty($this->id)) {
  6409. $sql = "SELECT SUM(count_questions) count_questions
  6410. FROM $table
  6411. WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
  6412. $result = Database::query($sql);
  6413. if (Database::num_rows($result)) {
  6414. $row = Database::fetch_array($result);
  6415. return (int) $row['count_questions'];
  6416. }
  6417. }
  6418. return 0;
  6419. }
  6420. /**
  6421. * Save categories in the TABLE_QUIZ_REL_CATEGORY table.
  6422. *
  6423. * @param array $categories
  6424. */
  6425. public function save_categories_in_exercise($categories)
  6426. {
  6427. if (!empty($categories) && !empty($this->id)) {
  6428. $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
  6429. $sql = "DELETE FROM $table
  6430. WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
  6431. Database::query($sql);
  6432. if (!empty($categories)) {
  6433. foreach ($categories as $categoryId => $countQuestions) {
  6434. $params = [
  6435. 'c_id' => $this->course_id,
  6436. 'exercise_id' => $this->id,
  6437. 'category_id' => $categoryId,
  6438. 'count_questions' => $countQuestions,
  6439. ];
  6440. Database::insert($table, $params);
  6441. }
  6442. }
  6443. }
  6444. }
  6445. /**
  6446. * @param array $questionList
  6447. * @param int $currentQuestion
  6448. * @param array $conditions
  6449. * @param string $link
  6450. *
  6451. * @return string
  6452. */
  6453. public function progressExercisePaginationBar(
  6454. $questionList,
  6455. $currentQuestion,
  6456. $conditions,
  6457. $link
  6458. ) {
  6459. $mediaQuestions = $this->getMediaList();
  6460. $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
  6461. $counter = 0;
  6462. $nextValue = 0;
  6463. $wasMedia = false;
  6464. $before = 0;
  6465. $counterNoMedias = 0;
  6466. foreach ($questionList as $questionId) {
  6467. $isCurrent = $currentQuestion == ($counterNoMedias + 1) ? true : false;
  6468. if (!empty($nextValue)) {
  6469. if ($wasMedia) {
  6470. $nextValue = $nextValue - $before + 1;
  6471. }
  6472. }
  6473. if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
  6474. $fixedValue = $counterNoMedias;
  6475. $html .= Display::progressPaginationBar(
  6476. $nextValue,
  6477. $mediaQuestions[$questionId],
  6478. $currentQuestion,
  6479. $fixedValue,
  6480. $conditions,
  6481. $link,
  6482. true,
  6483. true
  6484. );
  6485. $counter += count($mediaQuestions[$questionId]) - 1;
  6486. $before = count($questionList);
  6487. $wasMedia = true;
  6488. $nextValue += count($questionList);
  6489. } else {
  6490. $html .= Display::parsePaginationItem(
  6491. $questionId,
  6492. $isCurrent,
  6493. $conditions,
  6494. $link,
  6495. $counter
  6496. );
  6497. $counter++;
  6498. $nextValue++;
  6499. $wasMedia = false;
  6500. }
  6501. $counterNoMedias++;
  6502. }
  6503. $html .= '</ul></div>';
  6504. return $html;
  6505. }
  6506. /**
  6507. * Shows a list of numbers that represents the question to answer in a exercise.
  6508. *
  6509. * @param array $categories
  6510. * @param int $current
  6511. * @param array $conditions
  6512. * @param string $link
  6513. *
  6514. * @return string
  6515. */
  6516. public function progressExercisePaginationBarWithCategories(
  6517. $categories,
  6518. $current,
  6519. $conditions = [],
  6520. $link = null
  6521. ) {
  6522. $html = null;
  6523. $counterNoMedias = 0;
  6524. $nextValue = 0;
  6525. $wasMedia = false;
  6526. $before = 0;
  6527. if (!empty($categories)) {
  6528. $selectionType = $this->getQuestionSelectionType();
  6529. $useRootAsCategoryTitle = false;
  6530. // Grouping questions per parent category see BT#6540
  6531. if (in_array(
  6532. $selectionType,
  6533. [
  6534. EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
  6535. EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
  6536. ]
  6537. )) {
  6538. $useRootAsCategoryTitle = true;
  6539. }
  6540. // If the exercise is set to only show the titles of the categories
  6541. // at the root of the tree, then pre-order the categories tree by
  6542. // removing children and summing their questions into the parent
  6543. // categories
  6544. if ($useRootAsCategoryTitle) {
  6545. // The new categories list starts empty
  6546. $newCategoryList = [];
  6547. foreach ($categories as $category) {
  6548. $rootElement = $category['root'];
  6549. if (isset($category['parent_info'])) {
  6550. $rootElement = $category['parent_info']['id'];
  6551. }
  6552. //$rootElement = $category['id'];
  6553. // If the current category's ancestor was never seen
  6554. // before, then declare it and assign the current
  6555. // category to it.
  6556. if (!isset($newCategoryList[$rootElement])) {
  6557. $newCategoryList[$rootElement] = $category;
  6558. } else {
  6559. // If it was already seen, then merge the previous with
  6560. // the current category
  6561. $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
  6562. $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
  6563. $newCategoryList[$rootElement] = $category;
  6564. }
  6565. }
  6566. // Now use the newly built categories list, with only parents
  6567. $categories = $newCategoryList;
  6568. }
  6569. foreach ($categories as $category) {
  6570. $questionList = $category['question_list'];
  6571. // Check if in this category there questions added in a media
  6572. $mediaQuestionId = $category['media_question'];
  6573. $isMedia = false;
  6574. $fixedValue = null;
  6575. // Media exists!
  6576. if ($mediaQuestionId != 999) {
  6577. $isMedia = true;
  6578. $fixedValue = $counterNoMedias;
  6579. }
  6580. //$categoryName = $category['path']; << show the path
  6581. $categoryName = $category['name'];
  6582. if ($useRootAsCategoryTitle) {
  6583. if (isset($category['parent_info'])) {
  6584. $categoryName = $category['parent_info']['title'];
  6585. }
  6586. }
  6587. $html .= '<div class="row">';
  6588. $html .= '<div class="span2">'.$categoryName.'</div>';
  6589. $html .= '<div class="span8">';
  6590. if (!empty($nextValue)) {
  6591. if ($wasMedia) {
  6592. $nextValue = $nextValue - $before + 1;
  6593. }
  6594. }
  6595. $html .= Display::progressPaginationBar(
  6596. $nextValue,
  6597. $questionList,
  6598. $current,
  6599. $fixedValue,
  6600. $conditions,
  6601. $link,
  6602. $isMedia,
  6603. true
  6604. );
  6605. $html .= '</div>';
  6606. $html .= '</div>';
  6607. if ($mediaQuestionId == 999) {
  6608. $counterNoMedias += count($questionList);
  6609. } else {
  6610. $counterNoMedias++;
  6611. }
  6612. $nextValue += count($questionList);
  6613. $before = count($questionList);
  6614. if ($mediaQuestionId != 999) {
  6615. $wasMedia = true;
  6616. } else {
  6617. $wasMedia = false;
  6618. }
  6619. }
  6620. }
  6621. return $html;
  6622. }
  6623. /**
  6624. * Renders a question list.
  6625. *
  6626. * @param array $questionList (with media questions compressed)
  6627. * @param int $currentQuestion
  6628. * @param array $exerciseResult
  6629. * @param array $attemptList
  6630. * @param array $remindList
  6631. */
  6632. public function renderQuestionList(
  6633. $questionList,
  6634. $currentQuestion,
  6635. $exerciseResult,
  6636. $attemptList,
  6637. $remindList
  6638. ) {
  6639. $mediaQuestions = $this->getMediaList();
  6640. $i = 0;
  6641. // Normal question list render (medias compressed)
  6642. foreach ($questionList as $questionId) {
  6643. $i++;
  6644. // For sequential exercises
  6645. if ($this->type == ONE_PER_PAGE) {
  6646. // If it is not the right question, goes to the next loop iteration
  6647. if ($currentQuestion != $i) {
  6648. continue;
  6649. } else {
  6650. if (!in_array(
  6651. $this->getFeedbackType(),
  6652. [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
  6653. )) {
  6654. // if the user has already answered this question
  6655. if (isset($exerciseResult[$questionId])) {
  6656. echo Display::return_message(
  6657. get_lang('You already answered the question'),
  6658. 'normal'
  6659. );
  6660. break;
  6661. }
  6662. }
  6663. }
  6664. }
  6665. // The $questionList contains the media id we check
  6666. // if this questionId is a media question type
  6667. if (isset($mediaQuestions[$questionId]) &&
  6668. $mediaQuestions[$questionId] != 999
  6669. ) {
  6670. // The question belongs to a media
  6671. $mediaQuestionList = $mediaQuestions[$questionId];
  6672. $objQuestionTmp = Question::read($questionId);
  6673. $counter = 1;
  6674. if ($objQuestionTmp->type == MEDIA_QUESTION) {
  6675. echo $objQuestionTmp->show_media_content();
  6676. $countQuestionsInsideMedia = count($mediaQuestionList);
  6677. // Show questions that belongs to a media
  6678. if (!empty($mediaQuestionList)) {
  6679. // In order to parse media questions we use letters a, b, c, etc.
  6680. $letterCounter = 97;
  6681. foreach ($mediaQuestionList as $questionIdInsideMedia) {
  6682. $isLastQuestionInMedia = false;
  6683. if ($counter == $countQuestionsInsideMedia) {
  6684. $isLastQuestionInMedia = true;
  6685. }
  6686. $this->renderQuestion(
  6687. $questionIdInsideMedia,
  6688. $attemptList,
  6689. $remindList,
  6690. chr($letterCounter),
  6691. $currentQuestion,
  6692. $mediaQuestionList,
  6693. $isLastQuestionInMedia,
  6694. $questionList
  6695. );
  6696. $letterCounter++;
  6697. $counter++;
  6698. }
  6699. }
  6700. } else {
  6701. $this->renderQuestion(
  6702. $questionId,
  6703. $attemptList,
  6704. $remindList,
  6705. $i,
  6706. $currentQuestion,
  6707. null,
  6708. null,
  6709. $questionList
  6710. );
  6711. $i++;
  6712. }
  6713. } else {
  6714. // Normal question render.
  6715. $this->renderQuestion(
  6716. $questionId,
  6717. $attemptList,
  6718. $remindList,
  6719. $i,
  6720. $currentQuestion,
  6721. null,
  6722. null,
  6723. $questionList
  6724. );
  6725. }
  6726. // For sequential exercises.
  6727. if ($this->type == ONE_PER_PAGE) {
  6728. // quits the loop
  6729. break;
  6730. }
  6731. }
  6732. // end foreach()
  6733. if ($this->type == ALL_ON_ONE_PAGE) {
  6734. $exercise_actions = $this->show_button($questionId, $currentQuestion);
  6735. echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
  6736. }
  6737. }
  6738. /**
  6739. * @param int $questionId
  6740. * @param array $attemptList
  6741. * @param array $remindList
  6742. * @param int $i
  6743. * @param int $current_question
  6744. * @param array $questions_in_media
  6745. * @param bool $last_question_in_media
  6746. * @param array $realQuestionList
  6747. * @param bool $generateJS
  6748. */
  6749. public function renderQuestion(
  6750. $questionId,
  6751. $attemptList,
  6752. $remindList,
  6753. $i,
  6754. $current_question,
  6755. $questions_in_media = [],
  6756. $last_question_in_media = false,
  6757. $realQuestionList,
  6758. $generateJS = true
  6759. ) {
  6760. // With this option on the question is loaded via AJAX
  6761. //$generateJS = true;
  6762. //$this->loadQuestionAJAX = true;
  6763. if ($generateJS && $this->loadQuestionAJAX) {
  6764. $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
  6765. $params = [
  6766. 'questionId' => $questionId,
  6767. 'attemptList' => $attemptList,
  6768. 'remindList' => $remindList,
  6769. 'i' => $i,
  6770. 'current_question' => $current_question,
  6771. 'questions_in_media' => $questions_in_media,
  6772. 'last_question_in_media' => $last_question_in_media,
  6773. ];
  6774. $params = json_encode($params);
  6775. $script = '<script>
  6776. $(function(){
  6777. var params = '.$params.';
  6778. $.ajax({
  6779. type: "GET",
  6780. async: false,
  6781. data: params,
  6782. url: "'.$url.'",
  6783. success: function(return_value) {
  6784. $("#ajaxquestiondiv'.$questionId.'").html(return_value);
  6785. }
  6786. });
  6787. });
  6788. </script>
  6789. <div id="ajaxquestiondiv'.$questionId.'"></div>';
  6790. echo $script;
  6791. } else {
  6792. global $origin;
  6793. $question_obj = Question::read($questionId);
  6794. $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
  6795. $remind_highlight = null;
  6796. // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
  6797. // see #4542 no_remind_highlight class hide with jquery
  6798. if ($this->type == ALL_ON_ONE_PAGE && isset($_GET['reminder']) && $_GET['reminder'] == 2) {
  6799. $remind_highlight = 'no_remind_highlight';
  6800. if (in_array($question_obj->type, Question::question_type_no_review())) {
  6801. return null;
  6802. }
  6803. }
  6804. $attributes = ['id' => 'remind_list['.$questionId.']'];
  6805. // Showing the question
  6806. $exercise_actions = null;
  6807. echo '<a id="questionanchor'.$questionId.'"></a><br />';
  6808. echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
  6809. // Shows the question + possible answers
  6810. $showTitle = $this->getHideQuestionTitle() == 1 ? false : true;
  6811. echo $this->showQuestion(
  6812. $question_obj,
  6813. false,
  6814. $origin,
  6815. $i,
  6816. $showTitle,
  6817. false,
  6818. $user_choice,
  6819. false,
  6820. null,
  6821. false,
  6822. $this->getModelType(),
  6823. $this->categoryMinusOne
  6824. );
  6825. // Button save and continue
  6826. switch ($this->type) {
  6827. case ONE_PER_PAGE:
  6828. $exercise_actions .= $this->show_button(
  6829. $questionId,
  6830. $current_question,
  6831. null,
  6832. $remindList
  6833. );
  6834. break;
  6835. case ALL_ON_ONE_PAGE:
  6836. if (api_is_allowed_to_session_edit()) {
  6837. $button = [
  6838. Display::button(
  6839. 'save_now',
  6840. get_lang('Save and continue'),
  6841. ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
  6842. ),
  6843. '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
  6844. ];
  6845. $exercise_actions .= Display::div(
  6846. implode(PHP_EOL, $button),
  6847. ['class' => 'exercise_save_now_button']
  6848. );
  6849. }
  6850. break;
  6851. }
  6852. if (!empty($questions_in_media)) {
  6853. $count_of_questions_inside_media = count($questions_in_media);
  6854. if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
  6855. $button = [
  6856. Display::button(
  6857. 'save_now',
  6858. get_lang('Save and continue'),
  6859. ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
  6860. ),
  6861. '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
  6862. ];
  6863. $exercise_actions = Display::div(
  6864. implode(PHP_EOL, $button),
  6865. ['class' => 'exercise_save_now_button']
  6866. );
  6867. }
  6868. if ($last_question_in_media && $this->type == ONE_PER_PAGE) {
  6869. $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
  6870. }
  6871. }
  6872. // Checkbox review answers
  6873. if ($this->review_answers &&
  6874. !in_array($question_obj->type, Question::question_type_no_review())
  6875. ) {
  6876. $remind_question_div = Display::tag(
  6877. 'label',
  6878. Display::input(
  6879. 'checkbox',
  6880. 'remind_list['.$questionId.']',
  6881. '',
  6882. $attributes
  6883. ).get_lang('Revise question later'),
  6884. [
  6885. 'class' => 'checkbox',
  6886. 'for' => 'remind_list['.$questionId.']',
  6887. ]
  6888. );
  6889. $exercise_actions .= Display::div(
  6890. $remind_question_div,
  6891. ['class' => 'exercise_save_now_button']
  6892. );
  6893. }
  6894. echo Display::div(' ', ['class' => 'clear']);
  6895. $paginationCounter = null;
  6896. if ($this->type == ONE_PER_PAGE) {
  6897. if (empty($questions_in_media)) {
  6898. $paginationCounter = Display::paginationIndicator(
  6899. $current_question,
  6900. count($realQuestionList)
  6901. );
  6902. } else {
  6903. if ($last_question_in_media) {
  6904. $paginationCounter = Display::paginationIndicator(
  6905. $current_question,
  6906. count($realQuestionList)
  6907. );
  6908. }
  6909. }
  6910. }
  6911. echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
  6912. echo Display::div($exercise_actions, ['class' => 'form-actions']);
  6913. echo '</div>';
  6914. }
  6915. }
  6916. /**
  6917. * Returns an array of categories details for the questions of the current
  6918. * exercise.
  6919. *
  6920. * @return array
  6921. */
  6922. public function getQuestionWithCategories()
  6923. {
  6924. $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
  6925. $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
  6926. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  6927. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  6928. $sql = "SELECT DISTINCT cat.*
  6929. FROM $TBL_EXERCICE_QUESTION e
  6930. INNER JOIN $TBL_QUESTIONS q
  6931. ON (e.question_id = q.id AND e.c_id = q.c_id)
  6932. INNER JOIN $categoryRelTable catRel
  6933. ON (catRel.question_id = e.question_id AND catRel.c_id = e.c_id)
  6934. INNER JOIN $categoryTable cat
  6935. ON (cat.id = catRel.category_id AND cat.c_id = e.c_id)
  6936. WHERE
  6937. e.c_id = {$this->course_id} AND
  6938. e.exercice_id = ".intval($this->id);
  6939. $result = Database::query($sql);
  6940. $categoriesInExercise = [];
  6941. if (Database::num_rows($result)) {
  6942. $categoriesInExercise = Database::store_result($result, 'ASSOC');
  6943. }
  6944. return $categoriesInExercise;
  6945. }
  6946. /**
  6947. * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
  6948. */
  6949. public function get_max_score()
  6950. {
  6951. $out_max_score = 0;
  6952. // list of question's id !!! the array key start at 1 !!!
  6953. $questionList = $this->selectQuestionList(true);
  6954. // test is randomQuestions - see field random of test
  6955. if ($this->random > 0 && $this->randomByCat == 0) {
  6956. $numberRandomQuestions = $this->random;
  6957. $questionScoreList = [];
  6958. foreach ($questionList as $questionId) {
  6959. $tmpobj_question = Question::read($questionId);
  6960. if (is_object($tmpobj_question)) {
  6961. $questionScoreList[] = $tmpobj_question->weighting;
  6962. }
  6963. }
  6964. rsort($questionScoreList);
  6965. // add the first $numberRandomQuestions value of score array to get max_score
  6966. for ($i = 0; $i < min($numberRandomQuestions, count($questionScoreList)); $i++) {
  6967. $out_max_score += $questionScoreList[$i];
  6968. }
  6969. } elseif ($this->random > 0 && $this->randomByCat > 0) {
  6970. // test is random by category
  6971. // get the $numberRandomQuestions best score question of each category
  6972. $numberRandomQuestions = $this->random;
  6973. $tab_categories_scores = [];
  6974. foreach ($questionList as $questionId) {
  6975. $question_category_id = TestCategory::getCategoryForQuestion($questionId);
  6976. if (!is_array($tab_categories_scores[$question_category_id])) {
  6977. $tab_categories_scores[$question_category_id] = [];
  6978. }
  6979. $tmpobj_question = Question::read($questionId);
  6980. if (is_object($tmpobj_question)) {
  6981. $tab_categories_scores[$question_category_id][] = $tmpobj_question->weighting;
  6982. }
  6983. }
  6984. // here we've got an array with first key, the category_id, second key, score of question for this cat
  6985. foreach ($tab_categories_scores as $tab_scores) {
  6986. rsort($tab_scores);
  6987. for ($i = 0; $i < min($numberRandomQuestions, count($tab_scores)); $i++) {
  6988. $out_max_score += $tab_scores[$i];
  6989. }
  6990. }
  6991. } else {
  6992. // standard test, just add each question score
  6993. foreach ($questionList as $questionId) {
  6994. $question = Question::read($questionId, $this->course);
  6995. $out_max_score += $question->weighting;
  6996. }
  6997. }
  6998. return $out_max_score;
  6999. }
  7000. /**
  7001. * @return string
  7002. */
  7003. public function get_formated_title()
  7004. {
  7005. if (api_get_configuration_value('save_titles_as_html')) {
  7006. }
  7007. return api_html_entity_decode($this->selectTitle());
  7008. }
  7009. /**
  7010. * @param string $title
  7011. *
  7012. * @return string
  7013. */
  7014. public static function get_formated_title_variable($title)
  7015. {
  7016. return api_html_entity_decode($title);
  7017. }
  7018. /**
  7019. * @return string
  7020. */
  7021. public function format_title()
  7022. {
  7023. return api_htmlentities($this->title);
  7024. }
  7025. /**
  7026. * @param string $title
  7027. *
  7028. * @return string
  7029. */
  7030. public static function format_title_variable($title)
  7031. {
  7032. return api_htmlentities($title);
  7033. }
  7034. /**
  7035. * @param int $courseId
  7036. * @param int $sessionId
  7037. *
  7038. * @return array exercises
  7039. */
  7040. public function getExercisesByCourseSession($courseId, $sessionId)
  7041. {
  7042. $courseId = (int) $courseId;
  7043. $sessionId = (int) $sessionId;
  7044. $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
  7045. $sql = "SELECT * FROM $tbl_quiz cq
  7046. WHERE
  7047. cq.c_id = %s AND
  7048. (cq.session_id = %s OR cq.session_id = 0) AND
  7049. cq.active = 0
  7050. ORDER BY cq.id";
  7051. $sql = sprintf($sql, $courseId, $sessionId);
  7052. $result = Database::query($sql);
  7053. $rows = [];
  7054. while ($row = Database::fetch_array($result, 'ASSOC')) {
  7055. $rows[] = $row;
  7056. }
  7057. return $rows;
  7058. }
  7059. /**
  7060. * @param int $courseId
  7061. * @param int $sessionId
  7062. * @param array $quizId
  7063. *
  7064. * @return array exercises
  7065. */
  7066. public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
  7067. {
  7068. if (empty($quizId)) {
  7069. return [];
  7070. }
  7071. $sessionId = (int) $sessionId;
  7072. $courseId = (int) $courseId;
  7073. $ids = is_array($quizId) ? $quizId : [$quizId];
  7074. $ids = array_map('intval', $ids);
  7075. $ids = implode(',', $ids);
  7076. $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  7077. if ($sessionId != 0) {
  7078. $sql = "SELECT * FROM $track_exercises te
  7079. INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
  7080. WHERE
  7081. te.id = %s AND
  7082. te.session_id = %s AND
  7083. cq.id IN (%s)
  7084. ORDER BY cq.id";
  7085. $sql = sprintf($sql, $courseId, $sessionId, $ids);
  7086. } else {
  7087. $sql = "SELECT * FROM $track_exercises te
  7088. INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
  7089. WHERE
  7090. te.id = %s AND
  7091. cq.id IN (%s)
  7092. ORDER BY cq.id";
  7093. $sql = sprintf($sql, $courseId, $ids);
  7094. }
  7095. $result = Database::query($sql);
  7096. $rows = [];
  7097. while ($row = Database::fetch_array($result, 'ASSOC')) {
  7098. $rows[] = $row;
  7099. }
  7100. return $rows;
  7101. }
  7102. /**
  7103. * @param $exeId
  7104. * @param $exercise_stat_info
  7105. * @param $remindList
  7106. * @param $currentQuestion
  7107. *
  7108. * @return int|null
  7109. */
  7110. public static function getNextQuestionId(
  7111. $exeId,
  7112. $exercise_stat_info,
  7113. $remindList,
  7114. $currentQuestion
  7115. ) {
  7116. $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
  7117. if (isset($result[$exeId])) {
  7118. $result = $result[$exeId];
  7119. } else {
  7120. return null;
  7121. }
  7122. $data_tracking = $exercise_stat_info['data_tracking'];
  7123. $data_tracking = explode(',', $data_tracking);
  7124. // if this is the final question do nothing.
  7125. if ($currentQuestion == count($data_tracking)) {
  7126. return null;
  7127. }
  7128. $currentQuestion = $currentQuestion - 1;
  7129. if (!empty($result['question_list'])) {
  7130. $answeredQuestions = [];
  7131. foreach ($result['question_list'] as $question) {
  7132. if (!empty($question['answer'])) {
  7133. $answeredQuestions[] = $question['question_id'];
  7134. }
  7135. }
  7136. // Checking answered questions
  7137. $counterAnsweredQuestions = 0;
  7138. foreach ($data_tracking as $questionId) {
  7139. if (!in_array($questionId, $answeredQuestions)) {
  7140. if ($currentQuestion != $counterAnsweredQuestions) {
  7141. break;
  7142. }
  7143. }
  7144. $counterAnsweredQuestions++;
  7145. }
  7146. $counterRemindListQuestions = 0;
  7147. // Checking questions saved in the reminder list
  7148. if (!empty($remindList)) {
  7149. foreach ($data_tracking as $questionId) {
  7150. if (in_array($questionId, $remindList)) {
  7151. // Skip the current question
  7152. if ($currentQuestion != $counterRemindListQuestions) {
  7153. break;
  7154. }
  7155. }
  7156. $counterRemindListQuestions++;
  7157. }
  7158. if ($counterRemindListQuestions < $currentQuestion) {
  7159. return null;
  7160. }
  7161. if (!empty($counterRemindListQuestions)) {
  7162. if ($counterRemindListQuestions > $counterAnsweredQuestions) {
  7163. return $counterAnsweredQuestions;
  7164. } else {
  7165. return $counterRemindListQuestions;
  7166. }
  7167. }
  7168. }
  7169. return $counterAnsweredQuestions;
  7170. }
  7171. }
  7172. /**
  7173. * Gets the position of a questionId in the question list.
  7174. *
  7175. * @param $questionId
  7176. *
  7177. * @return int
  7178. */
  7179. public function getPositionInCompressedQuestionList($questionId)
  7180. {
  7181. $questionList = $this->getQuestionListWithMediasCompressed();
  7182. $mediaQuestions = $this->getMediaList();
  7183. $position = 1;
  7184. foreach ($questionList as $id) {
  7185. if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
  7186. $mediaQuestionList = $mediaQuestions[$id];
  7187. if (in_array($questionId, $mediaQuestionList)) {
  7188. return $position;
  7189. } else {
  7190. $position++;
  7191. }
  7192. } else {
  7193. if ($id == $questionId) {
  7194. return $position;
  7195. } else {
  7196. $position++;
  7197. }
  7198. }
  7199. }
  7200. return 1;
  7201. }
  7202. /**
  7203. * Get the correct answers in all attempts.
  7204. *
  7205. * @param int $learnPathId
  7206. * @param int $learnPathItemId
  7207. * @param bool $onlyCorrect
  7208. *
  7209. * @return array
  7210. */
  7211. public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
  7212. {
  7213. $attempts = Event::getExerciseResultsByUser(
  7214. api_get_user_id(),
  7215. $this->id,
  7216. api_get_course_int_id(),
  7217. api_get_session_id(),
  7218. $learnPathId,
  7219. $learnPathItemId,
  7220. 'DESC'
  7221. );
  7222. $list = [];
  7223. foreach ($attempts as $attempt) {
  7224. foreach ($attempt['question_list'] as $answers) {
  7225. foreach ($answers as $answer) {
  7226. $objAnswer = new Answer($answer['question_id']);
  7227. if ($onlyCorrect) {
  7228. switch ($objAnswer->getQuestionType()) {
  7229. case FILL_IN_BLANKS:
  7230. $isCorrect = FillBlanks::isCorrect($answer['answer']);
  7231. break;
  7232. case MATCHING:
  7233. case DRAGGABLE:
  7234. case MATCHING_DRAGGABLE:
  7235. $isCorrect = Matching::isCorrect(
  7236. $answer['position'],
  7237. $answer['answer'],
  7238. $answer['question_id']
  7239. );
  7240. break;
  7241. case ORAL_EXPRESSION:
  7242. $isCorrect = false;
  7243. break;
  7244. default:
  7245. $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
  7246. }
  7247. if ($isCorrect) {
  7248. $list[$answer['question_id']][] = $answer;
  7249. }
  7250. } else {
  7251. $list[$answer['question_id']][] = $answer;
  7252. }
  7253. }
  7254. }
  7255. if ($onlyCorrect === false) {
  7256. // Only take latest attempt
  7257. break;
  7258. }
  7259. }
  7260. return $list;
  7261. }
  7262. /**
  7263. * Get the correct answers in all attempts.
  7264. *
  7265. * @param int $learnPathId
  7266. * @param int $learnPathItemId
  7267. *
  7268. * @return array
  7269. */
  7270. public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
  7271. {
  7272. return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
  7273. }
  7274. /**
  7275. * @return bool
  7276. */
  7277. public function showPreviousButton()
  7278. {
  7279. $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
  7280. if ($allow === false) {
  7281. return true;
  7282. }
  7283. return $this->showPreviousButton;
  7284. }
  7285. /**
  7286. * @return int
  7287. */
  7288. public function getExerciseCategoryId()
  7289. {
  7290. if (empty($this->exerciseCategoryId)) {
  7291. return null;
  7292. }
  7293. return (int) $this->exerciseCategoryId;
  7294. }
  7295. /**
  7296. * @param int $value
  7297. */
  7298. public function setExerciseCategoryId($value)
  7299. {
  7300. if (!empty($value)) {
  7301. $this->exerciseCategoryId = (int) $value;
  7302. }
  7303. }
  7304. /**
  7305. * @param array $values
  7306. *
  7307. * @throws \Doctrine\DBAL\DBALException
  7308. */
  7309. public function setPageResultConfiguration($values)
  7310. {
  7311. $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
  7312. if ($pageConfig) {
  7313. $params = [
  7314. 'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
  7315. 'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
  7316. 'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : '',
  7317. ];
  7318. $type = Type::getType('array');
  7319. $platform = Database::getManager()->getConnection()->getDatabasePlatform();
  7320. $this->pageResultConfiguration = $type->convertToDatabaseValue($params, $platform);
  7321. }
  7322. }
  7323. /**
  7324. * @param array $defaults
  7325. */
  7326. public function setPageResultConfigurationDefaults(&$defaults)
  7327. {
  7328. $configuration = $this->getPageResultConfiguration();
  7329. if (!empty($configuration) && !empty($defaults)) {
  7330. $defaults = array_merge($defaults, $configuration);
  7331. }
  7332. }
  7333. /**
  7334. * @return array
  7335. */
  7336. public function getPageResultConfiguration()
  7337. {
  7338. $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
  7339. if ($pageConfig) {
  7340. /*$params = [
  7341. 'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
  7342. 'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
  7343. 'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : ''
  7344. ];*/
  7345. $type = Type::getType('array');
  7346. $platform = Database::getManager()->getConnection()->getDatabasePlatform();
  7347. $result = $type->convertToPHPValue($this->pageResultConfiguration, $platform);
  7348. return $result;
  7349. }
  7350. return [];
  7351. }
  7352. /**
  7353. * @param string $attribute
  7354. *
  7355. * @return mixed|null
  7356. */
  7357. public function getPageConfigurationAttribute($attribute)
  7358. {
  7359. $result = $this->getPageResultConfiguration();
  7360. if (!empty($result)) {
  7361. $value = isset($result[$attribute]) ? $result[$attribute] : null;
  7362. return $value;
  7363. }
  7364. return null;
  7365. }
  7366. /**
  7367. * @param bool $showPreviousButton
  7368. *
  7369. * @return Exercise
  7370. */
  7371. public function setShowPreviousButton($showPreviousButton)
  7372. {
  7373. $this->showPreviousButton = $showPreviousButton;
  7374. return $this;
  7375. }
  7376. /**
  7377. * @param array $notifications
  7378. */
  7379. public function setNotifications($notifications)
  7380. {
  7381. $this->notifications = $notifications;
  7382. }
  7383. /**
  7384. * @return array
  7385. */
  7386. public function getNotifications()
  7387. {
  7388. return $this->notifications;
  7389. }
  7390. /**
  7391. * @return bool
  7392. */
  7393. public function showExpectedChoice()
  7394. {
  7395. return api_get_configuration_value('show_exercise_expected_choice');
  7396. }
  7397. /**
  7398. * @return bool
  7399. */
  7400. public function showExpectedChoiceColumn()
  7401. {
  7402. if (!in_array($this->results_disabled, [
  7403. RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
  7404. ])
  7405. ) {
  7406. $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
  7407. if ($hide === 1) {
  7408. return false;
  7409. }
  7410. return true;
  7411. }
  7412. return false;
  7413. }
  7414. /**
  7415. * @param string $class
  7416. * @param string $scoreLabel
  7417. * @param string $result
  7418. * @param array
  7419. *
  7420. * @return string
  7421. */
  7422. public function getQuestionRibbon($class, $scoreLabel, $result, $array)
  7423. {
  7424. $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
  7425. if ($hide === 1) {
  7426. return '';
  7427. }
  7428. if ($this->showExpectedChoice()) {
  7429. $html = null;
  7430. $hideLabel = api_get_configuration_value('exercise_hide_label');
  7431. $label = '<div class="rib rib-'.$class.'">
  7432. <h3>'.$scoreLabel.'</h3>
  7433. </div>';
  7434. if (!empty($result)) {
  7435. $label .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
  7436. }
  7437. if ($hideLabel === true) {
  7438. $answerUsed = (int) $array['used'];
  7439. $answerMissing = (int) $array['missing'] - $answerUsed;
  7440. for ($i = 1; $i <= $answerUsed; $i++) {
  7441. $html .= '<span class="score-img">'.
  7442. Display::return_icon('attempt-check.png', null, null, ICON_SIZE_SMALL).
  7443. '</span>';
  7444. }
  7445. for ($i = 1; $i <= $answerMissing; $i++) {
  7446. $html .= '<span class="score-img">'.
  7447. Display::return_icon('attempt-nocheck.png', null, null, ICON_SIZE_SMALL).
  7448. '</span>';
  7449. }
  7450. $label = '<div class="score-title">'.get_lang('Correct answers').': '.$result.'</div>';
  7451. $label .= '<div class="score-limits">';
  7452. $label .= $html;
  7453. $label .= '</div>';
  7454. }
  7455. return '<div class="ribbon">
  7456. '.$label.'
  7457. </div>'
  7458. ;
  7459. } else {
  7460. $html = '<div class="ribbon">
  7461. <div class="rib rib-'.$class.'">
  7462. <h3>'.$scoreLabel.'</h3>
  7463. </div>';
  7464. if (!empty($result)) {
  7465. $html .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
  7466. }
  7467. $html .= '</div>';
  7468. return $html;
  7469. }
  7470. }
  7471. /**
  7472. * @return int
  7473. */
  7474. public function getAutoLaunch()
  7475. {
  7476. return $this->autolaunch;
  7477. }
  7478. /**
  7479. * Clean auto launch settings for all exercise in course/course-session.
  7480. */
  7481. public function enableAutoLaunch()
  7482. {
  7483. $table = Database::get_course_table(TABLE_QUIZ_TEST);
  7484. $sql = "UPDATE $table SET autolaunch = 1
  7485. WHERE iid = ".$this->iId;
  7486. Database::query($sql);
  7487. }
  7488. /**
  7489. * Clean auto launch settings for all exercise in course/course-session.
  7490. */
  7491. public function cleanCourseLaunchSettings()
  7492. {
  7493. $table = Database::get_course_table(TABLE_QUIZ_TEST);
  7494. $sql = "UPDATE $table SET autolaunch = 0
  7495. WHERE c_id = ".$this->course_id." AND session_id = ".$this->sessionId;
  7496. Database::query($sql);
  7497. }
  7498. /**
  7499. * Get the title without HTML tags.
  7500. *
  7501. * @return string
  7502. */
  7503. public function getUnformattedTitle()
  7504. {
  7505. return strip_tags(api_html_entity_decode($this->title));
  7506. }
  7507. /**
  7508. * @param int $start
  7509. * @param int $length
  7510. *
  7511. * @return array
  7512. */
  7513. public function getQuestionForTeacher($start = 0, $length = 10)
  7514. {
  7515. $start = (int) $start;
  7516. if ($start < 0) {
  7517. $start = 0;
  7518. }
  7519. $length = (int) $length;
  7520. $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  7521. $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
  7522. $sql = "SELECT DISTINCT e.question_id
  7523. FROM $quizRelQuestion e
  7524. INNER JOIN $question q
  7525. ON (e.question_id = q.iid AND e.c_id = q.c_id)
  7526. WHERE
  7527. e.c_id = {$this->course_id} AND
  7528. e.exercice_id = '".$this->id."'
  7529. ORDER BY question_order
  7530. LIMIT $start, $length
  7531. ";
  7532. $result = Database::query($sql);
  7533. $questionList = [];
  7534. while ($object = Database::fetch_object($result)) {
  7535. $questionList[] = $object->question_id;
  7536. }
  7537. return $questionList;
  7538. }
  7539. /**
  7540. * @param int $exerciseId
  7541. * @param array $courseInfo
  7542. * @param int $sessionId
  7543. *
  7544. * @throws \Doctrine\ORM\OptimisticLockException
  7545. *
  7546. * @return bool
  7547. */
  7548. public function generateStats($exerciseId, $courseInfo, $sessionId)
  7549. {
  7550. $allowStats = api_get_configuration_value('allow_gradebook_stats');
  7551. if (!$allowStats) {
  7552. return false;
  7553. }
  7554. if (empty($courseInfo)) {
  7555. return false;
  7556. }
  7557. $courseId = $courseInfo['real_id'];
  7558. $sessionId = (int) $sessionId;
  7559. $exerciseId = (int) $exerciseId;
  7560. $result = $this->read($exerciseId);
  7561. if (empty($result)) {
  7562. api_not_allowed(true);
  7563. }
  7564. $statusToFilter = empty($sessionId) ? STUDENT : 0;
  7565. $studentList = CourseManager::get_user_list_from_course_code(
  7566. $courseInfo['code'],
  7567. $sessionId,
  7568. null,
  7569. null,
  7570. $statusToFilter
  7571. );
  7572. if (empty($studentList)) {
  7573. Display::addFlash(Display::return_message(get_lang('No users in course')));
  7574. header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
  7575. exit;
  7576. }
  7577. $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  7578. $studentIdList = [];
  7579. if (!empty($studentList)) {
  7580. $studentIdList = array_column($studentList, 'user_id');
  7581. }
  7582. if ($this->exercise_was_added_in_lp == false) {
  7583. $sql = "SELECT * FROM $tblStats
  7584. WHERE
  7585. exe_exo_id = $exerciseId AND
  7586. orig_lp_id = 0 AND
  7587. orig_lp_item_id = 0 AND
  7588. status <> 'incomplete' AND
  7589. session_id = $sessionId AND
  7590. c_id = $courseId
  7591. ";
  7592. } else {
  7593. $lpId = null;
  7594. if (!empty($this->lpList)) {
  7595. // Taking only the first LP
  7596. $lpId = current($this->lpList);
  7597. $lpId = $lpId['lp_id'];
  7598. }
  7599. $sql = "SELECT *
  7600. FROM $tblStats
  7601. WHERE
  7602. exe_exo_id = $exerciseId AND
  7603. orig_lp_id = $lpId AND
  7604. status <> 'incomplete' AND
  7605. session_id = $sessionId AND
  7606. c_id = $courseId ";
  7607. }
  7608. $sql .= ' ORDER BY exe_id DESC';
  7609. $studentCount = 0;
  7610. $sum = 0;
  7611. $bestResult = 0;
  7612. $sumResult = 0;
  7613. $result = Database::query($sql);
  7614. while ($data = Database::fetch_array($result, 'ASSOC')) {
  7615. // Only take into account users in the current student list.
  7616. if (!empty($studentIdList)) {
  7617. if (!in_array($data['exe_user_id'], $studentIdList)) {
  7618. continue;
  7619. }
  7620. }
  7621. if (!isset($students[$data['exe_user_id']])) {
  7622. if ($data['exe_weighting'] != 0) {
  7623. $students[$data['exe_user_id']] = $data['exe_result'];
  7624. if ($data['exe_result'] > $bestResult) {
  7625. $bestResult = $data['exe_result'];
  7626. }
  7627. $sumResult += $data['exe_result'];
  7628. }
  7629. }
  7630. }
  7631. $count = count($studentList);
  7632. $average = $sumResult / $count;
  7633. $em = Database::getManager();
  7634. $links = AbstractLink::getGradebookLinksFromItem(
  7635. $this->id,
  7636. LINK_EXERCISE,
  7637. $courseInfo['code'],
  7638. $sessionId
  7639. );
  7640. if (empty($links)) {
  7641. $links = AbstractLink::getGradebookLinksFromItem(
  7642. $this->iId,
  7643. LINK_EXERCISE,
  7644. $courseInfo['code'],
  7645. $sessionId
  7646. );
  7647. }
  7648. if (!empty($links)) {
  7649. $repo = $em->getRepository('ChamiloCoreBundle:GradebookLink');
  7650. foreach ($links as $link) {
  7651. $linkId = $link['id'];
  7652. /** @var GradebookLink $exerciseLink */
  7653. $exerciseLink = $repo->find($linkId);
  7654. if ($exerciseLink) {
  7655. $exerciseLink
  7656. ->setUserScoreList($students)
  7657. ->setBestScore($bestResult)
  7658. ->setAverageScore($average)
  7659. ->setScoreWeight($this->get_max_score());
  7660. $em->persist($exerciseLink);
  7661. $em->flush();
  7662. }
  7663. }
  7664. }
  7665. }
  7666. /**
  7667. * Return an HTML table of exercises for on-screen printing, including
  7668. * action icons. If no exercise is present and the user can edit the
  7669. * course, show a "create test" button.
  7670. *
  7671. * @param int $categoryId
  7672. * @param string $keyword
  7673. *
  7674. * @return string
  7675. */
  7676. public static function exerciseGrid($categoryId, $keyword = '')
  7677. {
  7678. $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
  7679. $TBL_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
  7680. $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  7681. $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
  7682. $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
  7683. $categoryId = (int) $categoryId;
  7684. $keyword = Database::escape_string($keyword);
  7685. $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
  7686. $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
  7687. $autoLaunchAvailable = false;
  7688. if (api_get_course_setting('enable_exercise_auto_launch') == 1 &&
  7689. api_get_configuration_value('allow_exercise_auto_launch')
  7690. ) {
  7691. $autoLaunchAvailable = true;
  7692. }
  7693. $is_allowedToEdit = api_is_allowed_to_edit(null, true);
  7694. $courseInfo = api_get_course_info();
  7695. $sessionId = api_get_session_id();
  7696. $courseId = $courseInfo['real_id'];
  7697. $tableRows = [];
  7698. $uploadPath = DIR_HOTPOTATOES; //defined in main_api
  7699. $exercisePath = api_get_self();
  7700. $origin = api_get_origin();
  7701. $userInfo = api_get_user_info();
  7702. $charset = 'utf-8';
  7703. $token = Security::get_token();
  7704. $userId = api_get_user_id();
  7705. $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, $courseInfo);
  7706. $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
  7707. $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
  7708. // Condition for the session
  7709. $condition_session = api_get_session_condition($sessionId, true, true);
  7710. $content = '';
  7711. $column = 0;
  7712. if ($is_allowedToEdit) {
  7713. $column = 1;
  7714. }
  7715. $table = new SortableTableFromArrayConfig(
  7716. [],
  7717. $column,
  7718. self::PAGINATION_ITEMS_PER_PAGE,
  7719. 'exercises_cat_'.$categoryId
  7720. );
  7721. $limit = $table->per_page;
  7722. $page = $table->page_nr;
  7723. $from = $limit * ($page - 1);
  7724. $categoryCondition = '';
  7725. if (api_get_configuration_value('allow_exercise_categories')) {
  7726. if (!empty($categoryId)) {
  7727. $categoryCondition = " AND exercise_category_id = $categoryId ";
  7728. } else {
  7729. $categoryCondition = ' AND exercise_category_id IS NULL ';
  7730. }
  7731. }
  7732. $keywordCondition = '';
  7733. if (!empty($keyword)) {
  7734. $keywordCondition = " AND title LIKE '%$keyword%' ";
  7735. }
  7736. // Only for administrators
  7737. if ($is_allowedToEdit) {
  7738. $total_sql = "SELECT count(iid) as count
  7739. FROM $TBL_EXERCISES
  7740. WHERE
  7741. c_id = $courseId AND
  7742. active <> -1
  7743. $condition_session
  7744. $categoryCondition
  7745. $keywordCondition
  7746. ";
  7747. $sql = "SELECT * FROM $TBL_EXERCISES
  7748. WHERE
  7749. c_id = $courseId AND
  7750. active <> -1
  7751. $condition_session
  7752. $categoryCondition
  7753. $keywordCondition
  7754. ORDER BY title
  7755. LIMIT $from , $limit";
  7756. } else {
  7757. // Only for students
  7758. $total_sql = "SELECT count(iid) as count
  7759. FROM $TBL_EXERCISES
  7760. WHERE
  7761. c_id = $courseId AND
  7762. active = 1
  7763. $condition_session
  7764. $categoryCondition
  7765. $keywordCondition
  7766. ";
  7767. $sql = "SELECT * FROM $TBL_EXERCISES
  7768. WHERE c_id = $courseId AND
  7769. active = 1
  7770. $condition_session
  7771. $categoryCondition
  7772. $keywordCondition
  7773. ORDER BY title
  7774. LIMIT $from , $limit";
  7775. }
  7776. $result = Database::query($sql);
  7777. $result_total = Database::query($total_sql);
  7778. $total_exercises = 0;
  7779. if (Database :: num_rows($result_total)) {
  7780. $result_total = Database::fetch_array($result_total);
  7781. $total_exercises = $result_total['count'];
  7782. }
  7783. //get HotPotatoes files (active and inactive)
  7784. if ($is_allowedToEdit) {
  7785. $sql = "SELECT * FROM $TBL_DOCUMENT
  7786. WHERE
  7787. c_id = $courseId AND
  7788. path LIKE '".Database::escape_string($uploadPath.'/%/%')."'";
  7789. $res = Database::query($sql);
  7790. $hp_count = Database :: num_rows($res);
  7791. } else {
  7792. $sql = "SELECT * FROM $TBL_DOCUMENT d
  7793. INNER JOIN $TBL_ITEM_PROPERTY ip
  7794. ON (d.id = ip.ref AND d.c_id = ip.c_id)
  7795. WHERE
  7796. ip.tool = '".TOOL_DOCUMENT."' AND
  7797. d.path LIKE '".Database::escape_string($uploadPath.'/%/%')."' AND
  7798. ip.visibility = 1 AND
  7799. d.c_id = $courseId AND
  7800. ip.c_id = $courseId";
  7801. $res = Database::query($sql);
  7802. $hp_count = Database::num_rows($res);
  7803. }
  7804. $total = $total_exercises + $hp_count;
  7805. $exerciseList = [];
  7806. while ($row = Database::fetch_array($result, 'ASSOC')) {
  7807. $exerciseList[] = $row;
  7808. }
  7809. if (!empty($exerciseList) &&
  7810. api_get_setting('exercise_invisible_in_session') === 'true'
  7811. ) {
  7812. if (!empty($sessionId)) {
  7813. $changeDefaultVisibility = true;
  7814. if (api_get_setting('configure_exercise_visibility_in_course') === 'true') {
  7815. $changeDefaultVisibility = false;
  7816. if (api_get_course_setting('exercise_invisible_in_session') == 1) {
  7817. $changeDefaultVisibility = true;
  7818. }
  7819. }
  7820. if ($changeDefaultVisibility) {
  7821. // Check exercise
  7822. foreach ($exerciseList as $exercise) {
  7823. if ($exercise['session_id'] == 0) {
  7824. $visibilityInfo = api_get_item_property_info(
  7825. $courseId,
  7826. TOOL_QUIZ,
  7827. $exercise['iid'],
  7828. $sessionId
  7829. );
  7830. if (empty($visibilityInfo)) {
  7831. // Create a record for this
  7832. api_item_property_update(
  7833. $courseInfo,
  7834. TOOL_QUIZ,
  7835. $exercise['iid'],
  7836. 'invisible',
  7837. api_get_user_id(),
  7838. 0,
  7839. null,
  7840. '',
  7841. '',
  7842. $sessionId
  7843. );
  7844. }
  7845. }
  7846. }
  7847. }
  7848. }
  7849. }
  7850. if (!empty($exerciseList)) {
  7851. if ($origin !== 'learnpath') {
  7852. $visibilitySetting = api_get_configuration_value('show_hidden_exercise_added_to_lp');
  7853. //avoid sending empty parameters
  7854. $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
  7855. $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
  7856. foreach ($exerciseList as $row) {
  7857. $currentRow = [];
  7858. $my_exercise_id = $row['id'];
  7859. $attempt_text = '';
  7860. $actions = '';
  7861. $exercise = new Exercise();
  7862. $exercise->read($my_exercise_id, false);
  7863. if (empty($exercise->id)) {
  7864. continue;
  7865. }
  7866. $locked = $exercise->is_gradebook_locked;
  7867. // Validation when belongs to a session
  7868. $session_img = api_get_session_image($row['session_id'], $userInfo['status']);
  7869. $time_limits = false;
  7870. if (!empty($row['start_time']) || !empty($row['end_time'])) {
  7871. $time_limits = true;
  7872. }
  7873. $is_actived_time = false;
  7874. if ($time_limits) {
  7875. // check if start time
  7876. $start_time = false;
  7877. if (!empty($row['start_time'])) {
  7878. $start_time = api_strtotime($row['start_time'], 'UTC');
  7879. }
  7880. $end_time = false;
  7881. if (!empty($row['end_time'])) {
  7882. $end_time = api_strtotime($row['end_time'], 'UTC');
  7883. }
  7884. $now = time();
  7885. //If both "clocks" are enable
  7886. if ($start_time && $end_time) {
  7887. if ($now > $start_time && $end_time > $now) {
  7888. $is_actived_time = true;
  7889. }
  7890. } else {
  7891. //we check the start and end
  7892. if ($start_time) {
  7893. if ($now > $start_time) {
  7894. $is_actived_time = true;
  7895. }
  7896. }
  7897. if ($end_time) {
  7898. if ($end_time > $now) {
  7899. $is_actived_time = true;
  7900. }
  7901. }
  7902. }
  7903. }
  7904. // Blocking empty start times see BT#2800
  7905. global $_custom;
  7906. if (isset($_custom['exercises_hidden_when_no_start_date']) &&
  7907. $_custom['exercises_hidden_when_no_start_date']
  7908. ) {
  7909. if (empty($row['start_time'])) {
  7910. $time_limits = true;
  7911. $is_actived_time = false;
  7912. }
  7913. }
  7914. $cut_title = $exercise->getCutTitle();
  7915. $alt_title = '';
  7916. if ($cut_title != $row['title']) {
  7917. $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
  7918. }
  7919. // Teacher only
  7920. if ($is_allowedToEdit) {
  7921. $lp_blocked = null;
  7922. if ($exercise->exercise_was_added_in_lp == true) {
  7923. $lp_blocked = Display::div(
  7924. get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.'),
  7925. ['class' => 'lp_content_type_label']
  7926. );
  7927. }
  7928. // Get visibility in base course
  7929. $visibility = api_get_item_visibility(
  7930. $courseInfo,
  7931. TOOL_QUIZ,
  7932. $my_exercise_id,
  7933. 0
  7934. );
  7935. if (!empty($sessionId)) {
  7936. // If we are in a session, the test is invisible
  7937. // in the base course, it is included in a LP
  7938. // *and* the setting to show it is *not*
  7939. // specifically set to true, then hide it.
  7940. if ($visibility == 0) {
  7941. if (!$visibilitySetting) {
  7942. if ($exercise->exercise_was_added_in_lp == true) {
  7943. continue;
  7944. }
  7945. }
  7946. }
  7947. $visibility = api_get_item_visibility(
  7948. $courseInfo,
  7949. TOOL_QUIZ,
  7950. $my_exercise_id,
  7951. $sessionId
  7952. );
  7953. }
  7954. if ($row['active'] == 0 || $visibility == 0) {
  7955. $title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
  7956. } else {
  7957. $title = $cut_title;
  7958. }
  7959. $count_exercise_not_validated = (int) Event::count_exercise_result_not_validated(
  7960. $my_exercise_id,
  7961. $courseId,
  7962. $sessionId
  7963. );
  7964. /*$move = Display::return_icon(
  7965. 'all_directions.png',
  7966. get_lang('Move'),
  7967. ['class' => 'moved', 'style' => 'margin-bottom:-0.5em;']
  7968. );*/
  7969. $move = null;
  7970. $class_tip = '';
  7971. if (!empty($count_exercise_not_validated)) {
  7972. $results_text = $count_exercise_not_validated == 1 ? get_lang('Result not reviewed') : get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback not reviewed');
  7973. $title .= '<span class="exercise_tooltip" style="display: none;">'.$count_exercise_not_validated.' '.$results_text.' </span>';
  7974. $class_tip = 'link_tooltip';
  7975. }
  7976. $url = $move.'<a '.$alt_title.' class="'.$class_tip.'" id="tooltip_'.$row['id'].'" href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">
  7977. '.Display::return_icon('quiz.png', $row['title']).'
  7978. '.$title.' </a>'.PHP_EOL;
  7979. if (ExerciseLib::isQuizEmbeddable($row)) {
  7980. $embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
  7981. $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
  7982. }
  7983. $currentRow['title'] = $url.' '.$session_img.$lp_blocked;
  7984. // Count number exercise - teacher
  7985. $sql = "SELECT count(*) count FROM $TBL_EXERCISE_QUESTION
  7986. WHERE c_id = $courseId AND exercice_id = $my_exercise_id";
  7987. $sqlresult = Database::query($sql);
  7988. $rowi = (int) Database::result($sqlresult, 0, 0);
  7989. if ($sessionId == $row['session_id']) {
  7990. // Questions list
  7991. $actions = Display::url(
  7992. Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL),
  7993. 'admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
  7994. );
  7995. // Test settings
  7996. $settings = Display::url(
  7997. Display::return_icon('settings.png', get_lang('Configure'), '', ICON_SIZE_SMALL),
  7998. 'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
  7999. );
  8000. if ($limitTeacherAccess && !api_is_platform_admin()) {
  8001. $settings = '';
  8002. }
  8003. $actions .= $settings;
  8004. // Exercise results
  8005. $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
  8006. Display::return_icon('test_results.png', get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback'), '', ICON_SIZE_SMALL).'</a>';
  8007. if ($limitTeacherAccess) {
  8008. if (api_is_platform_admin()) {
  8009. $actions .= $resultsLink;
  8010. }
  8011. } else {
  8012. // Exercise results
  8013. $actions .= $resultsLink;
  8014. }
  8015. // Auto launch
  8016. if ($autoLaunchAvailable) {
  8017. $autoLaunch = $exercise->getAutoLaunch();
  8018. if (empty($autoLaunch)) {
  8019. $actions .= Display::url(
  8020. Display::return_icon(
  8021. 'launch_na.png',
  8022. get_lang('Enable'),
  8023. '',
  8024. ICON_SIZE_SMALL
  8025. ),
  8026. 'exercise.php?'.api_get_cidreq().'&choice=enable_launch&sec_token='.$token.'&exerciseId='.$row['id']
  8027. );
  8028. } else {
  8029. $actions .= Display::url(
  8030. Display::return_icon(
  8031. 'launch.png',
  8032. get_lang('Disable'),
  8033. '',
  8034. ICON_SIZE_SMALL
  8035. ),
  8036. 'exercise.php?'.api_get_cidreq().'&choice=disable_launch&sec_token='.$token.'&exerciseId='.$row['id']
  8037. );
  8038. }
  8039. }
  8040. // Export
  8041. $actions .= Display::url(
  8042. Display::return_icon('cd.png', get_lang('CopyTest')),
  8043. '',
  8044. [
  8045. 'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
  8046. 'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
  8047. ]
  8048. );
  8049. // Clean exercise
  8050. if ($locked == false) {
  8051. $clean = Display::url(
  8052. Display::return_icon(
  8053. 'clean.png',
  8054. get_lang('Clear all learners results for this exercise and feedback'),
  8055. '',
  8056. ICON_SIZE_SMALL
  8057. ),
  8058. '',
  8059. [
  8060. 'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('Are you sure to delete results and feedback'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
  8061. 'href' => 'exercise.php?'.api_get_cidreq().'&choice=clean_results&sec_token='.$token.'&exerciseId='.$row['id'],
  8062. ]
  8063. );
  8064. } else {
  8065. $clean = Display::return_icon(
  8066. 'clean_na.png',
  8067. get_lang('This option is not available because this activity is contained by an assessment, which is currently locked. To unlock the assessment, ask your platform administrator.'),
  8068. '',
  8069. ICON_SIZE_SMALL
  8070. );
  8071. }
  8072. if ($limitTeacherAccess && !api_is_platform_admin()) {
  8073. $clean = '';
  8074. }
  8075. $actions .= $clean;
  8076. // Visible / invisible
  8077. // Check if this exercise was added in a LP
  8078. if ($exercise->exercise_was_added_in_lp == true) {
  8079. $visibility = Display::return_icon(
  8080. 'invisible.png',
  8081. get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.'),
  8082. '',
  8083. ICON_SIZE_SMALL
  8084. );
  8085. } else {
  8086. if ($row['active'] == 0 || $visibility == 0) {
  8087. $visibility = Display::url(
  8088. Display::return_icon(
  8089. 'invisible.png',
  8090. get_lang('Activate'),
  8091. '',
  8092. ICON_SIZE_SMALL
  8093. ),
  8094. 'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
  8095. );
  8096. } else {
  8097. // else if not active
  8098. $visibility = Display::url(
  8099. Display::return_icon(
  8100. 'visible.png',
  8101. get_lang('Deactivate'),
  8102. '',
  8103. ICON_SIZE_SMALL
  8104. ),
  8105. 'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
  8106. );
  8107. }
  8108. }
  8109. if ($limitTeacherAccess && !api_is_platform_admin()) {
  8110. $visibility = '';
  8111. }
  8112. $actions .= $visibility;
  8113. // Export qti ...
  8114. $export = Display::url(
  8115. Display::return_icon(
  8116. 'export_qti2.png',
  8117. 'IMS/QTI',
  8118. '',
  8119. ICON_SIZE_SMALL
  8120. ),
  8121. 'exercise.php?action=exportqti2&exerciseId='.$row['id'].'&'.api_get_cidreq()
  8122. );
  8123. if ($limitTeacherAccess && !api_is_platform_admin()) {
  8124. $export = '';
  8125. }
  8126. $actions .= $export;
  8127. } else {
  8128. // not session
  8129. $actions = Display::return_icon(
  8130. 'edit_na.png',
  8131. get_lang('TestEditionNotAvailableInSession')
  8132. );
  8133. // Check if this exercise was added in a LP
  8134. if ($exercise->exercise_was_added_in_lp == true) {
  8135. $visibility = Display::return_icon(
  8136. 'invisible.png',
  8137. get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.'),
  8138. '',
  8139. ICON_SIZE_SMALL
  8140. );
  8141. } else {
  8142. if ($row['active'] == 0 || $visibility == 0) {
  8143. $visibility = Display::url(
  8144. Display::return_icon(
  8145. 'invisible.png',
  8146. get_lang('Activate'),
  8147. '',
  8148. ICON_SIZE_SMALL
  8149. ),
  8150. 'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
  8151. );
  8152. } else {
  8153. // else if not active
  8154. $visibility = Display::url(
  8155. Display::return_icon(
  8156. 'visible.png',
  8157. get_lang('Deactivate'),
  8158. '',
  8159. ICON_SIZE_SMALL
  8160. ),
  8161. 'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
  8162. );
  8163. }
  8164. }
  8165. if ($limitTeacherAccess && !api_is_platform_admin()) {
  8166. $visibility = '';
  8167. }
  8168. $actions .= $visibility;
  8169. $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
  8170. Display::return_icon('test_results.png', get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback'), '', ICON_SIZE_SMALL).'</a>';
  8171. $actions .= Display::url(
  8172. Display::return_icon('cd.gif', get_lang('CopyTest')),
  8173. '',
  8174. [
  8175. 'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
  8176. 'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
  8177. ]
  8178. );
  8179. }
  8180. // Delete
  8181. $delete = '';
  8182. if ($sessionId == $row['session_id']) {
  8183. if ($locked == false) {
  8184. $delete = Display::url(
  8185. Display::return_icon(
  8186. 'delete.png',
  8187. get_lang('Delete'),
  8188. '',
  8189. ICON_SIZE_SMALL
  8190. ),
  8191. '',
  8192. [
  8193. 'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('Are you sure to delete'), ENT_QUOTES, $charset))." ".addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
  8194. 'href' => 'exercise.php?'.api_get_cidreq().'&choice=delete&sec_token='.$token.'&exerciseId='.$row['id'],
  8195. ]
  8196. );
  8197. } else {
  8198. $delete = Display::return_icon(
  8199. 'delete_na.png',
  8200. get_lang('This option is not available because this activity is contained by an assessment, which is currently locked. To unlock the assessment, ask your platform administrator.'),
  8201. '',
  8202. ICON_SIZE_SMALL
  8203. );
  8204. }
  8205. }
  8206. if ($limitTeacherAccess && !api_is_platform_admin()) {
  8207. $delete = '';
  8208. }
  8209. $actions .= $delete;
  8210. // Number of questions
  8211. $random_label = null;
  8212. if ($row['random'] > 0 || $row['random'] == -1) {
  8213. // if random == -1 means use random questions with all questions
  8214. $random_number_of_question = $row['random'];
  8215. if ($random_number_of_question == -1) {
  8216. $random_number_of_question = $rowi;
  8217. }
  8218. if ($row['random_by_category'] > 0) {
  8219. $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
  8220. $my_exercise_id,
  8221. $random_number_of_question
  8222. );
  8223. $number_of_questions = $nbQuestionsTotal.' ';
  8224. $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('questions') : get_lang('question');
  8225. $number_of_questions .= ' - ';
  8226. $number_of_questions .= min(
  8227. TestCategory::getNumberMaxQuestionByCat($my_exercise_id), $random_number_of_question
  8228. ).' '.get_lang('QuestionByCategory');
  8229. } else {
  8230. $random_label = ' ('.get_lang('Random').') ';
  8231. $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
  8232. // Bug if we set a random value bigger than the real number of questions
  8233. if ($random_number_of_question > $rowi) {
  8234. $number_of_questions = $rowi.' '.$random_label;
  8235. }
  8236. }
  8237. } else {
  8238. $number_of_questions = $rowi;
  8239. }
  8240. $currentRow['count_questions'] = $number_of_questions;
  8241. } else {
  8242. // Student only.
  8243. $visibility = api_get_item_visibility(
  8244. $courseInfo,
  8245. TOOL_QUIZ,
  8246. $my_exercise_id,
  8247. $sessionId
  8248. );
  8249. if ($visibility == 0) {
  8250. continue;
  8251. }
  8252. $url = '<a '.$alt_title.' href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">'.
  8253. $cut_title.'</a>';
  8254. // Link of the exercise.
  8255. $currentRow['title'] = $url.' '.$session_img;
  8256. // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
  8257. // Don't remove this marker: note-query-exe-results
  8258. $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
  8259. WHERE
  8260. exe_exo_id = ".$row['id']." AND
  8261. exe_user_id = $userId AND
  8262. c_id = ".api_get_course_int_id()." AND
  8263. status <> 'incomplete' AND
  8264. orig_lp_id = 0 AND
  8265. orig_lp_item_id = 0 AND
  8266. session_id = '".api_get_session_id()."'
  8267. ORDER BY exe_id DESC";
  8268. $qryres = Database::query($sql);
  8269. $num = Database :: num_rows($qryres);
  8270. // Hide the results.
  8271. $my_result_disabled = $row['results_disabled'];
  8272. $attempt_text = '-';
  8273. // Time limits are on
  8274. if ($time_limits) {
  8275. // Exam is ready to be taken
  8276. if ($is_actived_time) {
  8277. // Show results
  8278. if (
  8279. in_array(
  8280. $my_result_disabled,
  8281. [
  8282. RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
  8283. RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
  8284. RESULT_DISABLE_SHOW_SCORE_ONLY,
  8285. RESULT_DISABLE_RANKING,
  8286. ]
  8287. )
  8288. ) {
  8289. // More than one attempt
  8290. if ($num > 0) {
  8291. $row_track = Database :: fetch_array($qryres);
  8292. $attempt_text = get_lang('LatestAttempt').' : ';
  8293. $attempt_text .= ExerciseLib::show_score(
  8294. $row_track['exe_result'],
  8295. $row_track['exe_weighting']
  8296. );
  8297. } else {
  8298. //No attempts
  8299. $attempt_text = get_lang('Not attempted');
  8300. }
  8301. } else {
  8302. $attempt_text = '-';
  8303. }
  8304. } else {
  8305. // Quiz not ready due to time limits
  8306. //@todo use the is_visible function
  8307. if (!empty($row['start_time']) && !empty($row['end_time'])) {
  8308. $today = time();
  8309. $start_time = api_strtotime($row['start_time'], 'UTC');
  8310. $end_time = api_strtotime($row['end_time'], 'UTC');
  8311. if ($today < $start_time) {
  8312. $attempt_text = sprintf(
  8313. get_lang('ExerciseWillBeActivatedFromXToY'),
  8314. api_convert_and_format_date($row['start_time']),
  8315. api_convert_and_format_date($row['end_time'])
  8316. );
  8317. } else {
  8318. if ($today > $end_time) {
  8319. $attempt_text = sprintf(
  8320. get_lang('ExerciseWasActivatedFromXToY'),
  8321. api_convert_and_format_date($row['start_time']),
  8322. api_convert_and_format_date($row['end_time'])
  8323. );
  8324. }
  8325. }
  8326. } else {
  8327. //$attempt_text = get_lang('ExamNotAvailableAtThisTime');
  8328. if (!empty($row['start_time'])) {
  8329. $attempt_text = sprintf(
  8330. get_lang('TestAvailableFromX'),
  8331. api_convert_and_format_date($row['start_time'])
  8332. );
  8333. }
  8334. if (!empty($row['end_time'])) {
  8335. $attempt_text = sprintf(
  8336. get_lang('TestAvailableUntilX'),
  8337. api_convert_and_format_date($row['end_time'])
  8338. );
  8339. }
  8340. }
  8341. }
  8342. } else {
  8343. // Normal behaviour.
  8344. // Show results.
  8345. if (
  8346. in_array(
  8347. $my_result_disabled,
  8348. [
  8349. RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
  8350. RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
  8351. RESULT_DISABLE_SHOW_SCORE_ONLY,
  8352. RESULT_DISABLE_RANKING,
  8353. ]
  8354. )
  8355. ) {
  8356. if ($num > 0) {
  8357. $row_track = Database :: fetch_array($qryres);
  8358. $attempt_text = get_lang('LatestAttempt').' : ';
  8359. $attempt_text .= ExerciseLib::show_score(
  8360. $row_track['exe_result'],
  8361. $row_track['exe_weighting']
  8362. );
  8363. } else {
  8364. $attempt_text = get_lang('Not attempted');
  8365. }
  8366. }
  8367. }
  8368. }
  8369. $currentRow['attempt'] = $attempt_text;
  8370. if ($is_allowedToEdit) {
  8371. $additionalActions = ExerciseLib::getAdditionalTeacherActions($row['id']);
  8372. if (!empty($additionalActions)) {
  8373. $actions .= $additionalActions.PHP_EOL;
  8374. }
  8375. $currentRow = [
  8376. $row['id'],
  8377. $currentRow['title'],
  8378. $currentRow['count_questions'],
  8379. $actions,
  8380. ];
  8381. } else {
  8382. $currentRow = [
  8383. $currentRow['title'],
  8384. $currentRow['attempt'],
  8385. ];
  8386. if ($isDrhOfCourse) {
  8387. $currentRow[] = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
  8388. Display::return_icon('test_results.png', get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback'), '', ICON_SIZE_SMALL).'</a>';
  8389. }
  8390. }
  8391. $tableRows[] = $currentRow;
  8392. }
  8393. }
  8394. }
  8395. // end exercise list
  8396. // Hotpotatoes results
  8397. if ($is_allowedToEdit) {
  8398. $sql = "SELECT d.iid, d.path as path, d.comment as comment
  8399. FROM $TBL_DOCUMENT d
  8400. WHERE
  8401. d.c_id = $courseId AND
  8402. (d.path LIKE '%htm%') AND
  8403. d.path LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
  8404. LIMIT $from , $limit"; // only .htm or .html files listed
  8405. } else {
  8406. $sql = "SELECT d.iid, d.path as path, d.comment as comment
  8407. FROM $TBL_DOCUMENT d
  8408. WHERE
  8409. d.c_id = $courseId AND
  8410. (d.path LIKE '%htm%') AND
  8411. d.path LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
  8412. LIMIT $from , $limit";
  8413. }
  8414. $result = Database::query($sql);
  8415. $attributes = [];
  8416. while ($row = Database::fetch_array($result, 'ASSOC')) {
  8417. $attributes[$row['iid']] = $row;
  8418. }
  8419. $nbrActiveTests = 0;
  8420. if (!empty($attributes)) {
  8421. foreach ($attributes as $item) {
  8422. $id = $item['iid'];
  8423. $path = $item['path'];
  8424. $title = GetQuizName($path, $documentPath);
  8425. if ($title == '') {
  8426. $title = basename($path);
  8427. }
  8428. // prof only
  8429. if ($is_allowedToEdit) {
  8430. $visibility = api_get_item_visibility(
  8431. ['real_id' => $courseId],
  8432. TOOL_DOCUMENT,
  8433. $id,
  8434. 0
  8435. );
  8436. if (!empty($sessionId)) {
  8437. if (0 == $visibility) {
  8438. continue;
  8439. }
  8440. $visibility = api_get_item_visibility(
  8441. ['real_id' => $courseId],
  8442. TOOL_DOCUMENT,
  8443. $id,
  8444. $sessionId
  8445. );
  8446. }
  8447. $title =
  8448. implode(PHP_EOL, [
  8449. Display::return_icon('hotpotatoes_s.png', 'HotPotatoes'),
  8450. Display::url(
  8451. $title,
  8452. 'showinframes.php?'.api_get_cidreq().'&'.http_build_query([
  8453. 'file' => $path,
  8454. 'uid' => $userId,
  8455. ]),
  8456. ['class' => $visibility == 0 ? 'text-muted' : null]
  8457. ),
  8458. ]);
  8459. $actions = Display::url(
  8460. Display::return_icon(
  8461. 'edit.png',
  8462. get_lang('Edit'),
  8463. '',
  8464. ICON_SIZE_SMALL
  8465. ),
  8466. 'adminhp.php?'.api_get_cidreq().'&hotpotatoesName='.$path
  8467. );
  8468. $actions .= '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
  8469. Display::return_icon('test_results.png', get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback'), '', ICON_SIZE_SMALL).
  8470. '</a>';
  8471. // if active
  8472. if ($visibility != 0) {
  8473. $nbrActiveTests = $nbrActiveTests + 1;
  8474. $actions .= ' <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=disable&file='.$path.'">'.
  8475. Display::return_icon('visible.png', get_lang('Deactivate'), '', ICON_SIZE_SMALL).'</a>';
  8476. } else { // else if not active
  8477. $actions .= ' <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=enable&file='.$path.'">'.
  8478. Display::return_icon('invisible.png', get_lang('Activate'), '', ICON_SIZE_SMALL).'</a>';
  8479. }
  8480. $actions .= '<a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=delete&file='.$path.'" onclick="javascript:if(!confirm(\''.addslashes(api_htmlentities(get_lang('Are you sure to delete'), ENT_QUOTES, $charset)).'\')) return false;">'.
  8481. Display::return_icon('delete.png', get_lang('Delete'), '', ICON_SIZE_SMALL).'</a>';
  8482. $currentRow = [
  8483. '',
  8484. $title,
  8485. '',
  8486. $actions,
  8487. ];
  8488. } else {
  8489. $visibility = api_get_item_visibility(
  8490. ['real_id' => $courseId],
  8491. TOOL_DOCUMENT,
  8492. $id,
  8493. $sessionId
  8494. );
  8495. if (0 == $visibility) {
  8496. continue;
  8497. }
  8498. // Student only
  8499. $attempt = ExerciseLib::getLatestHotPotatoResult(
  8500. $path,
  8501. $userId,
  8502. api_get_course_int_id(),
  8503. api_get_session_id()
  8504. );
  8505. $nbrActiveTests = $nbrActiveTests + 1;
  8506. $title = Display::url(
  8507. $title,
  8508. 'showinframes.php?'.api_get_cidreq().'&'.http_build_query(
  8509. [
  8510. 'file' => $path,
  8511. 'cid' => api_get_course_id(),
  8512. 'uid' => $userId,
  8513. ]
  8514. )
  8515. );
  8516. if (!empty($attempt)) {
  8517. $actions = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'&filter_by_user='.$userId.'">'.Display::return_icon('test_results.png', get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback'), '', ICON_SIZE_SMALL).'</a>';
  8518. $attemptText = get_lang('Latest attempt').' : ';
  8519. $attemptText .= ExerciseLib::show_score(
  8520. $attempt['exe_result'],
  8521. $attempt['exe_weighting']
  8522. ).' ';
  8523. $attemptText .= $actions;
  8524. } else {
  8525. // No attempts.
  8526. $attemptText = get_lang('Not attempted').' ';
  8527. }
  8528. $currentRow = [
  8529. $title,
  8530. $attemptText,
  8531. ];
  8532. if ($isDrhOfCourse) {
  8533. $currentRow[] = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
  8534. Display::return_icon('test_results.png', get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback'), '', ICON_SIZE_SMALL).'</a>';
  8535. }
  8536. }
  8537. $tableRows[] = $currentRow;
  8538. }
  8539. }
  8540. if (empty($tableRows) && empty($categoryId)) {
  8541. if ($is_allowedToEdit && $origin != 'learnpath') {
  8542. $content .= '<div id="no-data-view">';
  8543. $content .= '<h3>'.get_lang('Tests').'</h3>';
  8544. $content .= Display::return_icon('quiz.png', '', [], 64);
  8545. $content .= '<div class="controls">';
  8546. $content .= Display::url(
  8547. '<em class="fa fa-plus"></em> '.get_lang('Create a new test'),
  8548. 'exercise_admin.php?'.api_get_cidreq(),
  8549. ['class' => 'btn btn-primary']
  8550. );
  8551. $content .= '</div>';
  8552. $content .= '</div>';
  8553. }
  8554. } else {
  8555. if (empty($tableRows)) {
  8556. return '';
  8557. }
  8558. $table->setTableData($tableRows);
  8559. $table->setTotalNumberOfItems($total);
  8560. $table->set_additional_parameters([
  8561. 'cidReq' => api_get_course_id(),
  8562. 'id_session' => api_get_session_id(),
  8563. 'category_id' => $categoryId,
  8564. ]);
  8565. if ($is_allowedToEdit) {
  8566. $formActions = [];
  8567. $formActions['visible'] = get_lang('Activate');
  8568. $formActions['invisible'] = get_lang('Deactivate');
  8569. $formActions['delete'] = get_lang('Delete');
  8570. $table->set_form_actions($formActions);
  8571. }
  8572. $i = 0;
  8573. if ($is_allowedToEdit) {
  8574. $table->set_header($i++, '', false, 'width="18px"');
  8575. }
  8576. $table->set_header($i++, get_lang('Test name'), false);
  8577. if ($is_allowedToEdit) {
  8578. $table->set_header($i++, get_lang('Questions'), false);
  8579. $table->set_header($i++, get_lang('Detail'), false);
  8580. } else {
  8581. $table->set_header($i++, get_lang('Status'), false);
  8582. if ($isDrhOfCourse) {
  8583. $table->set_header($i++, get_lang('Detail'), false);
  8584. }
  8585. }
  8586. $content .= $table->return_table();
  8587. }
  8588. return $content;
  8589. }
  8590. /**
  8591. * @return int value in minutes
  8592. */
  8593. public function getResultAccess()
  8594. {
  8595. $extraFieldValue = new ExtraFieldValue('exercise');
  8596. $value = $extraFieldValue->get_values_by_handler_and_field_variable(
  8597. $this->iId,
  8598. 'results_available_for_x_minutes'
  8599. );
  8600. if (!empty($value) && isset($value['value'])) {
  8601. return (int) $value['value'];
  8602. }
  8603. return 0;
  8604. }
  8605. /**
  8606. * @param array $exerciseResultInfo
  8607. *
  8608. * @return bool
  8609. */
  8610. public function getResultAccessTimeDiff($exerciseResultInfo)
  8611. {
  8612. $value = $this->getResultAccess();
  8613. if (!empty($value)) {
  8614. $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
  8615. $endDate->add(new DateInterval('PT'.$value.'M'));
  8616. $now = time();
  8617. if ($endDate->getTimestamp() > $now) {
  8618. return (int) $endDate->getTimestamp() - $now;
  8619. }
  8620. }
  8621. return 0;
  8622. }
  8623. /**
  8624. * @param array $exerciseResultInfo
  8625. *
  8626. * @return bool
  8627. */
  8628. public function hasResultsAccess($exerciseResultInfo)
  8629. {
  8630. $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
  8631. if ($diff === 0) {
  8632. return false;
  8633. }
  8634. return true;
  8635. }
  8636. /**
  8637. * @return int
  8638. */
  8639. public function getResultsAccess()
  8640. {
  8641. $extraFieldValue = new ExtraFieldValue('exercise');
  8642. $value = $extraFieldValue->get_values_by_handler_and_field_variable(
  8643. $this->iId,
  8644. 'results_available_for_x_minutes'
  8645. );
  8646. if (!empty($value)) {
  8647. return (int) $value;
  8648. }
  8649. return 0;
  8650. }
  8651. /**
  8652. * @param Question $objQuestionTmp
  8653. * @param int $questionId
  8654. * @param bool $show_results
  8655. * @param array $question_result
  8656. */
  8657. public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
  8658. {
  8659. $id = (int) $objQuestionTmp->id;
  8660. $questionId = (int) $questionId;
  8661. $final_overlap = $question_result['extra']['final_overlap'];
  8662. $final_missing = $question_result['extra']['final_missing'];
  8663. $final_excess = $question_result['extra']['final_excess'];
  8664. $overlap_color = $question_result['extra']['overlap_color'];
  8665. $missing_color = $question_result['extra']['missing_color'];
  8666. $excess_color = $question_result['extra']['excess_color'];
  8667. $threadhold1 = $question_result['extra']['threadhold1'];
  8668. $threadhold2 = $question_result['extra']['threadhold2'];
  8669. $threadhold3 = $question_result['extra']['threadhold3'];
  8670. if ($show_results) {
  8671. if ($overlap_color) {
  8672. $overlap_color = 'green';
  8673. } else {
  8674. $overlap_color = 'red';
  8675. }
  8676. if ($missing_color) {
  8677. $missing_color = 'green';
  8678. } else {
  8679. $missing_color = 'red';
  8680. }
  8681. if ($excess_color) {
  8682. $excess_color = 'green';
  8683. } else {
  8684. $excess_color = 'red';
  8685. }
  8686. if (!is_numeric($final_overlap)) {
  8687. $final_overlap = 0;
  8688. }
  8689. if (!is_numeric($final_missing)) {
  8690. $final_missing = 0;
  8691. }
  8692. if (!is_numeric($final_excess)) {
  8693. $final_excess = 0;
  8694. }
  8695. if ($final_excess > 100) {
  8696. $final_excess = 100;
  8697. }
  8698. $table_resume = '
  8699. <table class="data_table">
  8700. <tr class="row_odd" >
  8701. <td>&nbsp;</td>
  8702. <td><b>'.get_lang('Requirements').'</b></td>
  8703. <td><b>'.get_lang('Your answer').'</b></td>
  8704. </tr>
  8705. <tr class="row_even">
  8706. <td><b>'.get_lang('Overlapping areaping area').'</b></td>
  8707. <td>'.get_lang('Minimumimum').' '.$threadhold1.'</td>
  8708. <td>
  8709. <div style="color:'.$overlap_color.'">
  8710. '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
  8711. </div>
  8712. </td>
  8713. </tr>
  8714. <tr>
  8715. <td><b>'.get_lang('Excessive areaive area').'</b></td>
  8716. <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
  8717. <td>
  8718. <div style="color:'.$excess_color.'">
  8719. '.(($final_excess < 0) ? 0 : intval($final_excess)).'
  8720. </div>
  8721. </td>
  8722. </tr>
  8723. <tr class="row_even">
  8724. <td><b>'.get_lang('Missing area area').'</b></td>
  8725. <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
  8726. <td>
  8727. <div style="color:'.$missing_color.'">
  8728. '.(($final_missing < 0) ? 0 : intval($final_missing)).'
  8729. </div>
  8730. </td>
  8731. </tr>
  8732. </table>
  8733. ';
  8734. $answerType = $objQuestionTmp->selectType();
  8735. if ($answerType != HOT_SPOT_DELINEATION) {
  8736. $item_list = explode('@@', $destination);
  8737. $try = $item_list[0];
  8738. $lp = $item_list[1];
  8739. $destinationid = $item_list[2];
  8740. $url = $item_list[3];
  8741. $table_resume = '';
  8742. } else {
  8743. if ($next == 0) {
  8744. $try = $try_hotspot;
  8745. $lp = $lp_hotspot;
  8746. $destinationid = $select_question_hotspot;
  8747. $url = $url_hotspot;
  8748. } else {
  8749. //show if no error
  8750. $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
  8751. $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
  8752. }
  8753. }
  8754. echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
  8755. if ($answerType == HOT_SPOT_DELINEATION) {
  8756. if ($organs_at_risk_hit > 0) {
  8757. $message = '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
  8758. $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
  8759. } else {
  8760. $message = '<p>'.get_lang('Your delineation :').'</p>';
  8761. $message .= $table_resume;
  8762. $message .= '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
  8763. }
  8764. $message .= '<p>'.$comment.'</p>';
  8765. echo $message;
  8766. } else {
  8767. echo '<p>'.$comment.'</p>';
  8768. }
  8769. // Showing the score
  8770. /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
  8771. WHERE exe_id = $id AND question_id = $questionId";
  8772. $resfree = Database::query($queryfree);
  8773. $questionScore = Database::result($resfree, 0, 'marks');
  8774. $totalScore += $questionScore;*/
  8775. $relPath = api_get_path(REL_PATH);
  8776. echo '</table></td></tr>';
  8777. echo "
  8778. <tr>
  8779. <td colspan=\"2\">
  8780. <div id=\"hotspot-solution\"></div>
  8781. <script>
  8782. $(function() {
  8783. new HotspotQuestion({
  8784. questionId: $questionId,
  8785. exerciseId: {$this->id},
  8786. exeId: $id,
  8787. selector: '#hotspot-solution',
  8788. for: 'solution',
  8789. relPath: '$relPath'
  8790. });
  8791. });
  8792. </script>
  8793. </td>
  8794. </tr>
  8795. </table>
  8796. ";
  8797. }
  8798. }
  8799. /**
  8800. * Clean exercise session variables.
  8801. */
  8802. public static function cleanSessionVariables()
  8803. {
  8804. Session::erase('objExercise');
  8805. Session::erase('exe_id');
  8806. Session::erase('calculatedAnswerId');
  8807. Session::erase('duration_time_previous');
  8808. Session::erase('duration_time');
  8809. Session::erase('objQuestion');
  8810. Session::erase('objAnswer');
  8811. Session::erase('questionList');
  8812. Session::erase('exerciseResult');
  8813. Session::erase('firstTime');
  8814. Session::erase('exerciseResultCoordinates');
  8815. Session::erase('hotspot_coord');
  8816. Session::erase('hotspot_dest');
  8817. Session::erase('hotspot_delineation_result');
  8818. }
  8819. /**
  8820. * Gets the question list ordered by the question_order setting (drag and drop).
  8821. *
  8822. * @param bool $adminView Optional.
  8823. *
  8824. * @return array
  8825. */
  8826. private function getQuestionOrderedList($adminView = false)
  8827. {
  8828. $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
  8829. $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
  8830. // Getting question_order to verify that the question
  8831. // list is correct and all question_order's were set
  8832. $sql = "SELECT DISTINCT count(e.question_order) as count
  8833. FROM $TBL_EXERCICE_QUESTION e
  8834. INNER JOIN $TBL_QUESTIONS q
  8835. ON (e.question_id = q.id AND e.c_id = q.c_id)
  8836. WHERE
  8837. e.c_id = {$this->course_id} AND
  8838. e.exercice_id = ".$this->id;
  8839. $result = Database::query($sql);
  8840. $row = Database::fetch_array($result);
  8841. $count_question_orders = $row['count'];
  8842. // Getting question list from the order (question list drag n drop interface).
  8843. $sql = "SELECT DISTINCT e.question_id, e.question_order
  8844. FROM $TBL_EXERCICE_QUESTION e
  8845. INNER JOIN $TBL_QUESTIONS q
  8846. ON (e.question_id = q.id AND e.c_id = q.c_id)
  8847. WHERE
  8848. e.c_id = {$this->course_id} AND
  8849. e.exercice_id = '".$this->id."'
  8850. ORDER BY question_order";
  8851. $result = Database::query($sql);
  8852. // Fills the array with the question ID for this exercise
  8853. // the key of the array is the question position
  8854. $temp_question_list = [];
  8855. $counter = 1;
  8856. $questionList = [];
  8857. while ($new_object = Database::fetch_object($result)) {
  8858. if (!$adminView) {
  8859. // Correct order.
  8860. $questionList[$new_object->question_order] = $new_object->question_id;
  8861. } else {
  8862. $questionList[$counter] = $new_object->question_id;
  8863. }
  8864. // Just in case we save the order in other array
  8865. $temp_question_list[$counter] = $new_object->question_id;
  8866. $counter++;
  8867. }
  8868. if (!empty($temp_question_list)) {
  8869. /* If both array don't match it means that question_order was not correctly set
  8870. for all questions using the default mysql order */
  8871. if (count($temp_question_list) != $count_question_orders) {
  8872. $questionList = $temp_question_list;
  8873. }
  8874. }
  8875. return $questionList;
  8876. }
  8877. /**
  8878. * Select N values from the questions per category array.
  8879. *
  8880. * @param array $categoriesAddedInExercise
  8881. * @param array $question_list
  8882. * @param array $questions_by_category per category
  8883. * @param bool $flatResult
  8884. * @param bool $randomizeQuestions
  8885. *
  8886. * @return array
  8887. */
  8888. private function pickQuestionsPerCategory(
  8889. $categoriesAddedInExercise,
  8890. $question_list,
  8891. &$questions_by_category,
  8892. $flatResult = true,
  8893. $randomizeQuestions = false
  8894. ) {
  8895. $addAll = true;
  8896. $categoryCountArray = [];
  8897. // Getting how many questions will be selected per category.
  8898. if (!empty($categoriesAddedInExercise)) {
  8899. $addAll = false;
  8900. // Parsing question according the category rel exercise settings
  8901. foreach ($categoriesAddedInExercise as $category_info) {
  8902. $category_id = $category_info['category_id'];
  8903. if (isset($questions_by_category[$category_id])) {
  8904. // How many question will be picked from this category.
  8905. $count = $category_info['count_questions'];
  8906. // -1 means all questions
  8907. $categoryCountArray[$category_id] = $count;
  8908. if ($count == -1) {
  8909. $categoryCountArray[$category_id] = 999;
  8910. }
  8911. }
  8912. }
  8913. }
  8914. if (!empty($questions_by_category)) {
  8915. $temp_question_list = [];
  8916. foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
  8917. if (isset($categoryCountArray) && !empty($categoryCountArray)) {
  8918. $numberOfQuestions = 0;
  8919. if (isset($categoryCountArray[$category_id])) {
  8920. $numberOfQuestions = $categoryCountArray[$category_id];
  8921. }
  8922. }
  8923. if ($addAll) {
  8924. $numberOfQuestions = 999;
  8925. }
  8926. if (!empty($numberOfQuestions)) {
  8927. $elements = TestCategory::getNElementsFromArray(
  8928. $categoryQuestionList,
  8929. $numberOfQuestions,
  8930. $randomizeQuestions
  8931. );
  8932. if (!empty($elements)) {
  8933. $temp_question_list[$category_id] = $elements;
  8934. $categoryQuestionList = $elements;
  8935. }
  8936. }
  8937. }
  8938. if (!empty($temp_question_list)) {
  8939. if ($flatResult) {
  8940. $temp_question_list = array_flatten($temp_question_list);
  8941. }
  8942. $question_list = $temp_question_list;
  8943. }
  8944. }
  8945. return $question_list;
  8946. }
  8947. /**
  8948. * Changes the exercise id.
  8949. *
  8950. * @param int $id - exercise id
  8951. */
  8952. private function updateId($id)
  8953. {
  8954. $this->id = $id;
  8955. }
  8956. /**
  8957. * Sends a notification when a user ends an examn.
  8958. *
  8959. * @param array $question_list_answers
  8960. * @param string $origin
  8961. * @param array $user_info
  8962. * @param string $url_email
  8963. * @param array $teachers
  8964. */
  8965. private function sendNotificationForOpenQuestions(
  8966. $question_list_answers,
  8967. $origin,
  8968. $user_info,
  8969. $url_email,
  8970. $teachers
  8971. ) {
  8972. // Email configuration settings
  8973. $courseCode = api_get_course_id();
  8974. $courseInfo = api_get_course_info($courseCode);
  8975. $sessionId = api_get_session_id();
  8976. $sessionData = '';
  8977. if (!empty($sessionId)) {
  8978. $sessionInfo = api_get_session_info($sessionId);
  8979. if (!empty($sessionInfo)) {
  8980. $sessionData = '<tr>'
  8981. .'<td><em>'.get_lang('Session name').'</em></td>'
  8982. .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
  8983. .'</tr>';
  8984. }
  8985. }
  8986. $msg = get_lang('A learner has answered an open question').'<br /><br />'
  8987. .get_lang('Attempt details').' : <br /><br />'
  8988. .'<table>'
  8989. .'<tr>'
  8990. .'<td><em>'.get_lang('Course name').'</em></td>'
  8991. .'<td>&nbsp;<b>#course#</b></td>'
  8992. .'</tr>'
  8993. .$sessionData
  8994. .'<tr>'
  8995. .'<td>'.get_lang('Test attempted').'</td>'
  8996. .'<td>&nbsp;#exercise#</td>'
  8997. .'</tr>'
  8998. .'<tr>'
  8999. .'<td>'.get_lang('Learner name').'</td>'
  9000. .'<td>&nbsp;#firstName# #lastName#</td>'
  9001. .'</tr>'
  9002. .'<tr>'
  9003. .'<td>'.get_lang('Learner e-mail').'</td>'
  9004. .'<td>&nbsp;#mail#</td>'
  9005. .'</tr>'
  9006. .'</table>';
  9007. $open_question_list = null;
  9008. foreach ($question_list_answers as $item) {
  9009. $question = $item['question'];
  9010. $answer = $item['answer'];
  9011. $answer_type = $item['answer_type'];
  9012. if (!empty($question) && !empty($answer) && $answer_type == FREE_ANSWER) {
  9013. $open_question_list .=
  9014. '<tr>'
  9015. .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>'
  9016. .'<td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>'
  9017. .'</tr>'
  9018. .'<tr>'
  9019. .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>'
  9020. .'<td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>'
  9021. .'</tr>';
  9022. }
  9023. }
  9024. if (!empty($open_question_list)) {
  9025. $msg .= '<p><br />'.get_lang('A learner has answered an open questionAre').' :</p>'.
  9026. '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
  9027. $msg .= $open_question_list;
  9028. $msg .= '</table><br />';
  9029. $msg = str_replace('#exercise#', $this->exercise, $msg);
  9030. $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
  9031. $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
  9032. $msg = str_replace('#mail#', $user_info['email'], $msg);
  9033. $msg = str_replace(
  9034. '#course#',
  9035. Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?id_session='.$sessionId),
  9036. $msg
  9037. );
  9038. if ($origin != 'learnpath') {
  9039. $msg .= '<br /><a href="#url#">'.get_lang('Click this link to check the answer and/or give feedback').'</a>';
  9040. }
  9041. $msg = str_replace('#url#', $url_email, $msg);
  9042. $subject = get_lang('A learner has answered an open question');
  9043. if (!empty($teachers)) {
  9044. foreach ($teachers as $user_id => $teacher_data) {
  9045. MessageManager::send_message_simple(
  9046. $user_id,
  9047. $subject,
  9048. $msg
  9049. );
  9050. }
  9051. }
  9052. }
  9053. }
  9054. /**
  9055. * Send notification for oral questions.
  9056. *
  9057. * @param array $question_list_answers
  9058. * @param string $origin
  9059. * @param int $exe_id
  9060. * @param array $user_info
  9061. * @param string $url_email
  9062. * @param array $teachers
  9063. */
  9064. private function sendNotificationForOralQuestions(
  9065. $question_list_answers,
  9066. $origin,
  9067. $exe_id,
  9068. $user_info,
  9069. $url_email,
  9070. $teachers
  9071. ) {
  9072. // Email configuration settings
  9073. $courseCode = api_get_course_id();
  9074. $courseInfo = api_get_course_info($courseCode);
  9075. $oral_question_list = null;
  9076. foreach ($question_list_answers as $item) {
  9077. $question = $item['question'];
  9078. $file = $item['generated_oral_file'];
  9079. $answer = $item['answer'];
  9080. if ($answer == 0) {
  9081. $answer = '';
  9082. }
  9083. $answer_type = $item['answer_type'];
  9084. if (!empty($question) && (!empty($answer) || !empty($file)) && $answer_type == ORAL_EXPRESSION) {
  9085. if (!empty($file)) {
  9086. $file = Display::url($file, $file);
  9087. }
  9088. $oral_question_list .= '<br />
  9089. <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
  9090. <tr>
  9091. <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
  9092. <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
  9093. </tr>
  9094. <tr>
  9095. <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
  9096. <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
  9097. </tr></table>';
  9098. }
  9099. }
  9100. if (!empty($oral_question_list)) {
  9101. $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
  9102. '.get_lang('Attempt details').' : <br /><br />
  9103. <table>
  9104. <tr>
  9105. <td><em>'.get_lang('Course name').'</em></td>
  9106. <td>&nbsp;<b>#course#</b></td>
  9107. </tr>
  9108. <tr>
  9109. <td>'.get_lang('Test attempted').'</td>
  9110. <td>&nbsp;#exercise#</td>
  9111. </tr>
  9112. <tr>
  9113. <td>'.get_lang('Learner name').'</td>
  9114. <td>&nbsp;#firstName# #lastName#</td>
  9115. </tr>
  9116. <tr>
  9117. <td>'.get_lang('Learner e-mail').'</td>
  9118. <td>&nbsp;#mail#</td>
  9119. </tr>
  9120. </table>';
  9121. $msg .= '<br />'.sprintf(get_lang('A learner has attempted one or more oral questionAreX'), $oral_question_list).'<br />';
  9122. $msg1 = str_replace("#exercise#", $this->exercise, $msg);
  9123. $msg = str_replace("#firstName#", $user_info['firstname'], $msg1);
  9124. $msg1 = str_replace("#lastName#", $user_info['lastname'], $msg);
  9125. $msg = str_replace("#mail#", $user_info['email'], $msg1);
  9126. $msg = str_replace("#course#", $courseInfo['name'], $msg1);
  9127. if (!in_array($origin, ['learnpath', 'embeddable'])) {
  9128. $msg .= '<br /><a href="#url#">'.get_lang('Click this link to check the answer and/or give feedback').'</a>';
  9129. }
  9130. $msg1 = str_replace("#url#", $url_email, $msg);
  9131. $mail_content = $msg1;
  9132. $subject = get_lang('A learner has attempted one or more oral question');
  9133. if (!empty($teachers)) {
  9134. foreach ($teachers as $user_id => $teacher_data) {
  9135. MessageManager::send_message_simple(
  9136. $user_id,
  9137. $subject,
  9138. $mail_content
  9139. );
  9140. }
  9141. }
  9142. }
  9143. }
  9144. /**
  9145. * Returns an array with the media list.
  9146. *
  9147. * @param array question list
  9148. *
  9149. * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
  9150. * <code>
  9151. * array (size=2)
  9152. * 999 =>
  9153. * array (size=3)
  9154. * 0 => int 7
  9155. * 1 => int 6
  9156. * 2 => int 3254
  9157. * 100 =>
  9158. * array (size=1)
  9159. * 0 => int 5
  9160. * </code>
  9161. */
  9162. private function setMediaList($questionList)
  9163. {
  9164. $mediaList = [];
  9165. /*
  9166. * Media feature is not activated in 1.11.x
  9167. if (!empty($questionList)) {
  9168. foreach ($questionList as $questionId) {
  9169. $objQuestionTmp = Question::read($questionId, $this->course_id);
  9170. // If a media question exists
  9171. if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
  9172. $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
  9173. } else {
  9174. // Always the last item
  9175. $mediaList[999][] = $objQuestionTmp->id;
  9176. }
  9177. }
  9178. }*/
  9179. $this->mediaList = $mediaList;
  9180. }
  9181. /**
  9182. * @param FormValidator $form
  9183. *
  9184. * @return HTML_QuickForm_group
  9185. */
  9186. private function setResultDisabledGroup(FormValidator $form)
  9187. {
  9188. $resultDisabledGroup = [];
  9189. $resultDisabledGroup[] = $form->createElement(
  9190. 'radio',
  9191. 'results_disabled',
  9192. null,
  9193. get_lang('Auto-evaluation mode: show score and expected answers'),
  9194. '0',
  9195. ['id' => 'result_disabled_0']
  9196. );
  9197. $resultDisabledGroup[] = $form->createElement(
  9198. 'radio',
  9199. 'results_disabled',
  9200. null,
  9201. get_lang('Exam mode: Do not show score nor answers'),
  9202. '1',
  9203. ['id' => 'result_disabled_1', 'onclick' => 'check_results_disabled()']
  9204. );
  9205. $resultDisabledGroup[] = $form->createElement(
  9206. 'radio',
  9207. 'results_disabled',
  9208. null,
  9209. get_lang('Practice mode: Show score only, by category if at least one is used'),
  9210. '2',
  9211. ['id' => 'result_disabled_2', 'onclick' => 'check_results_disabled()']
  9212. );
  9213. if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
  9214. $group = $form->addGroup(
  9215. $resultDisabledGroup,
  9216. null,
  9217. get_lang('ShowResults and feedback and feedback and feedback and feedback and feedback and feedbackToStudents')
  9218. );
  9219. return $group;
  9220. }
  9221. $resultDisabledGroup[] = $form->createElement(
  9222. 'radio',
  9223. 'results_disabled',
  9224. null,
  9225. get_lang('Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'),
  9226. '4',
  9227. ['id' => 'result_disabled_4']
  9228. );
  9229. $resultDisabledGroup[] = $form->createElement(
  9230. 'radio',
  9231. 'results_disabled',
  9232. null,
  9233. get_lang('Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'),
  9234. '5',
  9235. ['id' => 'result_disabled_5', 'onclick' => 'check_results_disabled()']
  9236. );
  9237. $resultDisabledGroup[] = $form->createElement(
  9238. 'radio',
  9239. 'results_disabled',
  9240. null,
  9241. get_lang('TestRankingMode'),
  9242. RESULT_DISABLE_RANKING,
  9243. ['id' => 'result_disabled_6']
  9244. );
  9245. $resultDisabledGroup[] = $form->createElement(
  9246. 'radio',
  9247. 'results_disabled',
  9248. null,
  9249. get_lang('TestShowOnlyGlobalScoreAndCorrect answers'),
  9250. RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
  9251. ['id' => 'result_disabled_7']
  9252. );
  9253. $resultDisabledGroup[] = $form->createElement(
  9254. 'radio',
  9255. 'results_disabled',
  9256. null,
  9257. get_lang('TestAutoEvaluationAndRankingMode'),
  9258. RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
  9259. ['id' => 'result_disabled_8']
  9260. );
  9261. $group = $form->addGroup(
  9262. $resultDisabledGroup,
  9263. null,
  9264. get_lang('ShowResults and feedback and feedback and feedback and feedback and feedback and feedbackToStudents')
  9265. );
  9266. return $group;
  9267. }
  9268. }