ILIAS  release_7 Revision v7.30-3-g800a261c036
class.ilStudyProgrammeProgressDBRepository.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
6{
7 protected static $cache = [];
8 protected $db;
9
10 const TABLE = 'prg_usr_progress';
11
12 const FIELD_ID = 'id';
13 const FIELD_ASSIGNMENT_ID = 'assignment_id';
14 const FIELD_PRG_ID = 'prg_id';
15 const FIELD_USR_ID = 'usr_id';
16 const FIELD_POINTS = 'points';
17 const FIELD_POINTS_CUR = 'points_cur';
18 const FIELD_STATUS = 'status';
19 const FIELD_COMPLETION_BY = 'completion_by';
20 const FIELD_ASSIGNMENT_DATE = 'assignment_date';
21 const FIELD_LAST_CHANGE = 'last_change';
22 const FIELD_LAST_CHANGE_BY = 'last_change_by';
23 const FIELD_COMPLETION_DATE = 'completion_date';
24 const FIELD_DEADLINE = 'deadline';
25 const FIELD_VQ_DATE = 'vq_date';
26 const FIELD_INVALIDATED = 'invalidated';
27 const FIELD_MAIL_SENT_RISKYTOFAIL = 'sent_mail_risky_to_fail';
28 const FIELD_MAIL_SENT_WILLEXPIRE = 'sent_mail_expires';
29 const FIELD_IS_INDIVIDUAL = 'individual';
30
31 public function __construct(ilDBInterface $db)
32 {
33 $this->db = $db;
34 }
35
41 public function createFor(
44 int $acting_user = null
46 $id = $this->nextId();
47 $row = [
48 self::FIELD_ID => $id,
49 self::FIELD_ASSIGNMENT_ID => $ass->getId(),
50 self::FIELD_PRG_ID => $prg->getObjId(),
51 self::FIELD_USR_ID => $ass->getUserId(),
52 self::FIELD_POINTS => $prg->getAssessmentSettings()->getPoints(),
53 self::FIELD_POINTS_CUR => 0,
55 self::FIELD_COMPLETION_BY => null,
56 self::FIELD_LAST_CHANGE => ilUtil::now(),
57 self::FIELD_ASSIGNMENT_DATE => ilUtil::now(),
58 self::FIELD_LAST_CHANGE_BY => $acting_user,
59 self::FIELD_COMPLETION_DATE => null,
60 self::FIELD_DEADLINE => null,
61 self::FIELD_VQ_DATE => null,
62 self::FIELD_INVALIDATED => 0,
63 self::FIELD_IS_INDIVIDUAL => 0
64 ];
65 $this->insertRowDB($row);
66 return $this->buildByRow($row);
67 }
68
74 public function get(int $id) : ilStudyProgrammeProgress
75 {
76 foreach ($this->loadByFilter([self::FIELD_ID => $id]) as $row) {
77 return $this->buildByRow($row);
78 }
79 throw new ilException('invalid id ' . $id);
80 }
81
82
88 public function getByIds(
89 int $prg_id,
90 int $assignment_id
92 return $this->getByPrgIdAndAssignmentId($prg_id, $assignment_id);
93 }
94
102 public function getByPrgIdAndAssignmentId(int $prg_id, int $assignment_id)
103 {
104 $rows = $this->loadByFilter(
105 [
106 self::FIELD_PRG_ID => $prg_id,
107 self::FIELD_ASSIGNMENT_ID => $assignment_id
108 ]
109 );
110
111 foreach ($rows as $row) {
112 return $this->buildByRow($row);
113 }
114 }
115
117 {
118 $rows = $this->loadByFilter(
119 [
120 self::FIELD_PRG_ID => $assignment->getRootId(),
121 self::FIELD_ASSIGNMENT_ID => $assignment->getId(),
122 self::FIELD_USR_ID => $assignment->getUserId()
123 ]
124 );
125
126 foreach ($rows as $row) {
127 return $this->buildByRow($row);
128 }
129 }
130
136 public function getByPrgIdAndUserId(int $prg_id, int $usr_id) : array
137 {
138 $return = [];
139 foreach ($this->loadByFilter([self::FIELD_PRG_ID => $prg_id, self::FIELD_USR_ID => $usr_id]) as $row) {
140 $return[] = $this->buildByRow($row);
141 }
142 return $return;
143 }
144
150 public function getByPrgId(int $prg_id) : array
151 {
152 $return = [];
153 foreach ($this->loadByFilter([self::FIELD_PRG_ID => $prg_id]) as $row) {
154 $return[] = $this->buildByRow($row);
155 }
156 return $return;
157 }
158
163 public function getFirstByPrgId(int $prg_id)
164 {
165 foreach ($this->loadByFilter([self::FIELD_PRG_ID => $prg_id]) as $row) {
166 return $this->buildByRow($row);
167 }
168 }
169
175 public function getByAssignmentId(int $assignment_id) : array
176 {
177 $return = [];
178 foreach ($this->loadByFilter([self::FIELD_ASSIGNMENT_ID => $assignment_id]) as $row) {
179 $return[] = $this->buildByRow($row);
180 }
181 return $return;
182 }
183
189 public function getExpiredSuccessfull() : array
190 {
191 $return = [];
192 foreach ($this->loadExpiredSuccessful() as $row) {
193 $return[] = $this->buildByRow($row);
194 }
195 return $return;
196 }
197
203 public function getPassedDeadline() : array
204 {
205 $return = [];
206 foreach ($this->loadPassedDeadline() as $row) {
207 $return[] = $this->buildByRow($row);
208 }
209 return $return;
210 }
211
217 public function getRiskyToFailInstances() : array
218 {
219 $return = [];
220 foreach ($this->loadRiskyToFailInstance() as $row) {
221 $return[] = $this->buildByRow($row);
222 }
223 return $return;
224 }
225
229 public function update(ilStudyProgrammeProgress $progress)
230 {
231 $this->updateRowDB(
232 [
233 self::FIELD_ID => $progress->getId(),
234 self::FIELD_ASSIGNMENT_ID => $progress->getAssignmentId(),
235 self::FIELD_PRG_ID => $progress->getNodeId(),
236 self::FIELD_USR_ID => $progress->getUserId(),
237 self::FIELD_STATUS => $progress->getStatus(),
238 self::FIELD_POINTS => $progress->getAmountOfPoints(),
239 self::FIELD_POINTS_CUR => $progress->getCurrentAmountOfPoints(),
240 self::FIELD_COMPLETION_BY => $progress->getCompletionBy(),
241 self::FIELD_LAST_CHANGE_BY => $progress->getLastChangeBy(),
242 self::FIELD_LAST_CHANGE => $progress->getLastChange()->format(ilStudyProgrammeProgress::DATE_TIME_FORMAT),
243 self::FIELD_ASSIGNMENT_DATE => $progress->getAssignmentDate()->format(ilStudyProgrammeProgress::DATE_TIME_FORMAT),
244 self::FIELD_COMPLETION_DATE =>
245 $progress->getCompletionDate() ?
247 self::FIELD_DEADLINE => $progress->getDeadline() ? $progress->getDeadline()->format(ilStudyProgrammeProgress::DATE_FORMAT) : null,
248 self::FIELD_VQ_DATE => $progress->getValidityOfQualification() ? $progress->getValidityOfQualification()->format(ilStudyProgrammeProgress::DATE_TIME_FORMAT) : null,
249 self::FIELD_INVALIDATED => $progress->isInvalidated() ? 1 : 0,
250 self::FIELD_IS_INDIVIDUAL => $progress->hasIndividualModifications() ? 1 : 0
251 ]
252 );
253 }
254
258 public function delete(ilStudyProgrammeProgress $progress)
259 {
260 $this->deleteDB($progress->getId());
261 }
262
263 protected function insertRowDB(array $row)
264 {
265 $this->db->insert(
266 self::TABLE,
267 [
268 self::FIELD_ID => ['integer', $row[self::FIELD_ID]]
269 , self::FIELD_ASSIGNMENT_ID => ['integer', $row[self::FIELD_ASSIGNMENT_ID]]
270 , self::FIELD_PRG_ID => ['integer', $row[self::FIELD_PRG_ID]]
271 , self::FIELD_USR_ID => ['integer', $row[self::FIELD_USR_ID]]
272 , self::FIELD_STATUS => ['integer', $row[self::FIELD_STATUS]]
273 , self::FIELD_POINTS => ['integer', $row[self::FIELD_POINTS]]
274 , self::FIELD_POINTS_CUR => ['integer', $row[self::FIELD_POINTS_CUR]]
275 , self::FIELD_COMPLETION_BY => ['integer', $row[self::FIELD_COMPLETION_BY]]
276 , self::FIELD_LAST_CHANGE_BY => ['integer', $row[self::FIELD_LAST_CHANGE_BY]]
277 , self::FIELD_LAST_CHANGE => ['text', $row[self::FIELD_LAST_CHANGE]]
278 , self::FIELD_ASSIGNMENT_DATE => ['timestamp', $row[self::FIELD_ASSIGNMENT_DATE]]
279 , self::FIELD_COMPLETION_DATE => ['timestamp', $row[self::FIELD_COMPLETION_DATE]]
280 , self::FIELD_DEADLINE => ['text', $row[self::FIELD_DEADLINE]]
281 , self::FIELD_VQ_DATE => ['timestamp', $row[self::FIELD_VQ_DATE]]
282 , self::FIELD_INVALIDATED => ['timestamp', $row[self::FIELD_INVALIDATED]]
283 , self::FIELD_IS_INDIVIDUAL => ['integer', $row[self::FIELD_IS_INDIVIDUAL]]
284 ]
285 );
286 }
287
291 public function deleteForAssignmentId(int $assignment_id) : array
292 {
293 $progresses = $this->getByAssignmentId($assignment_id);
294
295 $query = 'DELETE FROM ' . self::TABLE . PHP_EOL
296 . ' WHERE ' . self::FIELD_ASSIGNMENT_ID . ' = '
297 . $this->db->quote($assignment_id, 'integer');
298
299 $this->db->manipulate($query);
300
301 return array_map(
302 function ($progress) {
303 return $progress->getNodeId();
304 },
305 $progresses
306 );
307 }
308
309 public function sentRiskyToFailFor(int $progress_id) : void
310 {
311 $where = [
312 self::FIELD_ID => [
313 'integer',
314 $progress_id
315 ]
316 ];
317
318 $values = [
319 self::FIELD_MAIL_SENT_RISKYTOFAIL => [
320 'timestamp',
321 date('Y-m-d H:i:s')
322 ]
323 ];
324
325 $this->db->update(self::TABLE, $values, $where);
326 }
327
328 public function sentExpiryInfoFor(int $progress_id) : void
329 {
330 $where = [
331 self::FIELD_ID => [
332 'integer',
333 $progress_id
334 ]
335 ];
336
337 $values = [
338 self::FIELD_MAIL_SENT_WILLEXPIRE => [
339 'timestamp',
340 date('Y-m-d H:i:s')
341 ]
342 ];
343
344 $this->db->update(self::TABLE, $values, $where);
345 }
346
347
348
349 protected function updateRowDB(array $data)
350 {
351 $where = [
352 self::FIELD_ID => [
353 'integer',
354 $data[self::FIELD_ID]
355 ]
356 ];
357
358 $values = [
359 self::FIELD_ASSIGNMENT_ID => [
360 'integer',
361 $data[self::FIELD_ASSIGNMENT_ID]
362 ],
363 self::FIELD_PRG_ID => [
364 'integer',
365 $data[self::FIELD_PRG_ID]
366 ],
367 self::FIELD_USR_ID => [
368 'integer',
369 $data[self::FIELD_USR_ID]
370 ],
371 self::FIELD_STATUS => [
372 'integer',
373 $data[self::FIELD_STATUS]
374 ],
375 self::FIELD_POINTS => [
376 'integer',
377 $data[self::FIELD_POINTS]
378 ],
379 self::FIELD_POINTS_CUR => [
380 'integer',
381 $data[self::FIELD_POINTS_CUR]
382 ],
383 self::FIELD_COMPLETION_BY => [
384 'integer',
385 $data[self::FIELD_COMPLETION_BY]
386 ],
387 self::FIELD_LAST_CHANGE_BY => [
388 'integer',
389 $data[self::FIELD_LAST_CHANGE_BY]
390 ],
391 self::FIELD_LAST_CHANGE => [
392 'text',
393 $data[self::FIELD_LAST_CHANGE]
394 ],
395 self::FIELD_ASSIGNMENT_DATE => [
396 'timestamp',
397 $data[self::FIELD_ASSIGNMENT_DATE]
398 ],
399 self::FIELD_COMPLETION_DATE => [
400 'timestamp',
401 $data[self::FIELD_COMPLETION_DATE]
402 ],
403 self::FIELD_DEADLINE => [
404 'text',
405 $data[self::FIELD_DEADLINE]
406 ],
407 self::FIELD_VQ_DATE => [
408 'timestamp',
409 $data[self::FIELD_VQ_DATE]
410 ],
411 self::FIELD_INVALIDATED => [
412 'integer',
413 $data[self::FIELD_INVALIDATED]
414 ],
415 self::FIELD_IS_INDIVIDUAL => [
416 'integer',
417 $data[self::FIELD_IS_INDIVIDUAL]
418 ]
419 ];
420
421 $this->db->update(self::TABLE, $values, $where);
422 }
423
427 protected function buildByRow(array $row) : ilStudyProgrammeProgress
428 {
429 $prgrs = (new ilStudyProgrammeProgress((int) $row[self::FIELD_ID]))
430 ->withAssignmentId((int) $row[self::FIELD_ASSIGNMENT_ID])
431 ->withNodeId((int) $row[self::FIELD_PRG_ID])
432 ->withUserId((int) $row[self::FIELD_USR_ID])
433 ->withStatus((int) $row[self::FIELD_STATUS])
434 ->withAmountOfPoints((int) $row[self::FIELD_POINTS])
435 ->withCurrentAmountOfPoints((int) $row[self::FIELD_POINTS_CUR])
436 ->withDeadline(
437 $row[self::FIELD_DEADLINE] ?
438 DateTimeImmutable::createFromFormat(ilStudyProgrammeProgress::DATE_FORMAT, $row[self::FIELD_DEADLINE]) :
439 null
440 )
441 ->withAssignmentDate(
442 DateTimeImmutable::createFromFormat(ilStudyProgrammeProgress::DATE_TIME_FORMAT, $row[self::FIELD_ASSIGNMENT_DATE])
443 )
444 ->withCompletion(
445 (int) $row[self::FIELD_COMPLETION_BY],
446 $row[self::FIELD_COMPLETION_DATE] ?
447 DateTimeImmutable::createFromFormat(ilStudyProgrammeProgress::DATE_TIME_FORMAT, $row[self::FIELD_COMPLETION_DATE]) :
448 null
449 )
450 ->withLastChange(
451 (int) $row[self::FIELD_LAST_CHANGE_BY],
452 $row[self::FIELD_LAST_CHANGE] ?
453 DateTimeImmutable::createFromFormat(ilStudyProgrammeProgress::DATE_TIME_FORMAT, $row[self::FIELD_LAST_CHANGE]) :
454 null
455 )
456 ->withValidityOfQualification(
457 $row[self::FIELD_VQ_DATE] ?
458 DateTimeImmutable::createFromFormat(ilStudyProgrammeProgress::DATE_TIME_FORMAT, $row[self::FIELD_VQ_DATE]) :
459 null
460 )
461 ->withIndividualModifications((bool) $row[self::FIELD_IS_INDIVIDUAL]);
462
463
464
465 if ((int) $row[self::FIELD_INVALIDATED] === 1) {
466 $prgrs = $prgrs->invalidate();
467 }
468
469 return $prgrs;
470 }
471
472 protected function loadByFilter(array $filter)
473 {
474 $q = $this->getSQLHeader()
475 . ' WHERE TRUE';
476 foreach ($filter as $field => $value) {
477 $q .= ' AND ' . $field . ' = ' . $this->db->quote($value, 'text');
478 }
479 $res = $this->db->query($q);
480 while ($rec = $this->db->fetchAssoc($res)) {
481 yield $rec;
482 }
483 }
484
485 protected function loadExpiredSuccessful()
486 {
487 $q = $this->getSQLHeader()
488 . ' WHERE ' . $this->db->in(
489 self::FIELD_STATUS,
490 [
493 ],
494 false,
495 'integer'
496 )
497 . ' AND ' . self::FIELD_VQ_DATE . ' IS NOT NULL'
498 . ' AND DATE(' . self::FIELD_VQ_DATE . ') < '
499 . $this->db->quote(
500 (new DateTimeImmutable())->format(ilStudyProgrammeProgress::DATE_FORMAT),
501 'text'
502 )
503 . ' AND ' . self::FIELD_INVALIDATED . ' != 1 OR ' . self::FIELD_INVALIDATED . ' IS NULL';
504
505 $res = $this->db->query($q);
506 while ($rec = $this->db->fetchAssoc($res)) {
507 yield $rec;
508 }
509 }
510
511 protected function loadPassedDeadline()
512 {
513 $q =
514 $this->getSQLHeader() . PHP_EOL
515 . 'WHERE ' . $this->db->in(
516 self::FIELD_STATUS,
517 [
520 ],
521 false,
522 'integer'
523 ) . PHP_EOL
524 . 'AND ' . self::FIELD_DEADLINE . ' IS NOT NULL' . PHP_EOL
525 . 'AND DATE(' . self::FIELD_DEADLINE . ') < ' . $this->db->quote(
526 (new DateTimeImmutable())->format(ilStudyProgrammeProgress::DATE_FORMAT),
527 'text'
528 ) . PHP_EOL
529 ;
530 $res = $this->db->query($q);
531 while ($rec = $this->db->fetchAssoc($res)) {
532 yield $rec;
533 }
534 }
535
536 protected function loadRiskyToFailInstance()
537 {
538 $q = $this->getSQLHeader()
539 . ' WHERE ' . $this->db->in(
540 self::FIELD_STATUS,
541 [
544 ],
545 true,
546 'integer'
547 )
548 . ' AND ' . self::FIELD_DEADLINE . ' IS NOT NULL'
549 . ' AND DATE(' . self::FIELD_DEADLINE . ') < '
550 . $this->db->quote(
551 (new DateTimeImmutable())->format(ilStudyProgrammeProgress::DATE_FORMAT),
552 'text'
553 )
554 . ' AND ' . self::FIELD_MAIL_SENT_RISKYTOFAIL . ' IS NULL'
555 ;
556 $res = $this->db->query($q);
557 while ($rec = $this->db->fetchAssoc($res)) {
558 yield $rec;
559 }
560 }
561
562 protected function getSQLHeader() : string
563 {
564 return 'SELECT ' . self::FIELD_ID
565 . ', ' . self::FIELD_ASSIGNMENT_ID
566 . ', ' . self::FIELD_PRG_ID
567 . ', ' . self::FIELD_USR_ID
568 . ', ' . self::FIELD_STATUS
569 . ', ' . self::FIELD_POINTS
570 . ', ' . self::FIELD_POINTS_CUR
571 . ', ' . self::FIELD_COMPLETION_BY
572 . ', ' . self::FIELD_LAST_CHANGE
573 . ', ' . self::FIELD_LAST_CHANGE_BY
574 . ', ' . self::FIELD_ASSIGNMENT_DATE
575 . ', ' . self::FIELD_COMPLETION_DATE
576 . ', ' . self::FIELD_DEADLINE
577 . ', ' . self::FIELD_VQ_DATE
578 . ', ' . self::FIELD_INVALIDATED
579 . ', ' . self::FIELD_IS_INDIVIDUAL
580 . ' FROM ' . self::TABLE;
581 }
582
587 public function getRiskyToFail(array $programmes_and_due) : array
588 {
589 $ret = [];
590 if (count($programmes_and_due) == 0) {
591 return $ret;
592 }
593
594 $where = [];
595 foreach ($programmes_and_due as $programme_obj_id => $due) {
596 $due = $due->format(ilStudyProgrammeProgress::DATE_FORMAT);
597 $where[] = '('
598 . self::FIELD_PRG_ID . '=' . $programme_obj_id
599 . ' AND ' . self::FIELD_DEADLINE . '<=' . $this->db->quote($due, 'text')
600 . ' AND ' . self::FIELD_MAIL_SENT_RISKYTOFAIL . ' IS NULL'
601 . ')';
602 }
603 $query = $this->getSQLHeader() . ' WHERE ' . implode(' OR ', $where);
604
605 $res = $this->db->query($query);
606 while ($rec = $this->db->fetchAssoc($res)) {
607 $ret[] = $this->buildByRow($rec);
608 }
609 return $ret;
610 }
611
616 public function getAboutToExpire(
617 array $programmes_and_due,
618 bool $discard_formerly_notified = true
619 ) : array {
620 $ret = [];
621 if (count($programmes_and_due) == 0) {
622 return $ret;
623 }
624
625 $where = [];
626 foreach ($programmes_and_due as $programme_obj_id => $due) {
628 $where_clause = '('
629 . self::FIELD_PRG_ID . '=' . $programme_obj_id
630 . ' AND ' . self::FIELD_VQ_DATE . '<=' . $this->db->quote($due, 'text');
631
632 if ($discard_formerly_notified) {
633 $where_clause .= ' AND ' . self::FIELD_MAIL_SENT_WILLEXPIRE . ' IS NULL';
634 }
635
636 $where_clause .= ')';
637 $where[] = $where_clause;
638 }
639
640 $query = $this->getSQLHeader() . ' WHERE ' . implode(' OR ', $where);
641 $res = $this->db->query($query);
642 while ($rec = $this->db->fetchAssoc($res)) {
643 $ret[] = $this->buildByRow($rec);
644 }
645 return $ret;
646 }
647
648 protected function nextId() : int
649 {
650 return (int) $this->db->nextId(self::TABLE);
651 }
652
654 string $assignment_table,
655 string $assignment_id_field
656 ) : void {
657 $query = 'DELETE FROM ' . self::TABLE . PHP_EOL
658 . 'WHERE ' . self::FIELD_ASSIGNMENT_ID . PHP_EOL
659 . 'NOT IN (' . PHP_EOL
660 . 'SELECT ' . $this->db->quoteIdentifier($assignment_id_field)
661 . ' FROM ' . $this->db->quoteIdentifier($assignment_table) . PHP_EOL
662 . ');' . PHP_EOL;
663 $this->db->manipulate($query);
664 }
665
666 public function deleteProgressesFor(int $prg_obj_id) : void
667 {
668 $query = 'DELETE FROM ' . self::TABLE . PHP_EOL
669 . ' WHERE ' . self::FIELD_PRG_ID . ' = '
670 . $this->db->quote($prg_obj_id, 'integer');
671
672 $this->db->manipulate($query);
673 }
674}
An exception for terminatinating execution or to throw for unit testing.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Represents one assignment of the user to a program tree.
getByPrgIdAndAssignmentId(int $prg_id, int $assignment_id)
Load progress belonging to a prg id and assignment.Will throw if the record does not exist yet.
deleteAllOrphanedProgresses(string $assignment_table, string $assignment_id_field)
getByPrgIdAndUserId(int $prg_id, int $usr_id)
Load progress objects belonging to a prg id and a user id.
getByAssignmentId(int $assignment_id)
Load progress objects belonging to an assignment id.Will throw if the record does not exist yet.
getByPrgId(int $prg_id)
Load progress objects belonging to a prg id.
createFor(ilStudyProgrammeSettings $prg, ilStudyProgrammeAssignment $ass, int $acting_user=null)
getExpiredSuccessfull()
Load all progress objects which are successfull and whose validity is expired.
getAboutToExpire(array $programmes_and_due, bool $discard_formerly_notified=true)
getByIds(int $prg_id, int $assignment_id)
Load progress belonging to a prg id and assignment.Will throw if the record does not exist yet.
update(ilStudyProgrammeProgress $progress)
Update record corresponding to progress.Will throw if the record does not exist yet.
Class ilStudyProgrammeProgress.
getId()
Get the id of the progress.
getAssignmentId()
Get the assignment this progress belongs to.
getAmountOfPoints()
Get the amount of points the user needs to achieve on the subnodes of this node.
getStatus()
Get the status the user has on this node.
getUserId()
Get the id of the user this progress is for.
getCompletionBy()
Get the id of object or user that lead to the successful completion of this node.
getLastChangeBy()
Get the id of the user/object who/which invoked the last change on this assignment.
getNodeId()
Get the obj_id of the program node this progress belongs to.
getObjId()
Get the id of the study program.
static now()
Return current timestamp in Y-m-d H:i:s format.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Covers the persistence of settings belonging to a study programme (SP).
$ret
Definition: parser.php:6
$query
foreach($_POST as $key=> $value) $res
$data
Definition: storeScorm.php:23
$rows
Definition: xhr_table.php:10