LTI: Add scores resource for AG service

pull/3065/head
Angel Fernando Quiroz Campos 6 years ago
parent 27e82977c7
commit d43eb405a8
  1. 20
      plugin/ims_lti/src/ImsLti.php
  2. 11
      plugin/ims_lti/src/Service/LtiAssignmentGradesService.php
  3. 213
      plugin/ims_lti/src/Service/Resource/LtiScoresResource.php

@ -220,4 +220,24 @@ class ImsLti
return $clientId;
}
/**
* Validate the format ISO 8601 for date strings coming from JSON or JavaScript.
*
* @link https://www.myintervals.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ Pattern source.
*
* @param string $strDate
*
* @return bool
*/
public static function validateFormatDateIso8601($strDate)
{
$pattern = '/^([\+-]?\d{4}(?!\d{2}\b))((-?)('
.'(0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W'
.'([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))('
.'[T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?'
.'([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/';
return preg_match($pattern, $strDate) !== false;
}
}

@ -20,10 +20,12 @@ class LtiAssignmentGradesService extends LtiAdvantageService
const SCOPE_LINE_ITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
const SCOPE_LINE_ITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
const SCOPE_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
const SCOPE_SCORE_WRITE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
const TYPE_LINE_ITEM_CONTAINER = 'application/vnd.ims.lis.v2.lineitemcontainer+json';
const TYPE_LINE_ITEM = 'application/vnd.ims.lis.v2.lineitem+json';
const TYPE_RESULT_CONTAINER = 'application/vnd.ims.lis.v2.resultcontainer+json';
const TYPE_SCORE = 'application/vnd.ims.lis.v1.score+json';
/**
* @return array
@ -33,6 +35,7 @@ class LtiAssignmentGradesService extends LtiAdvantageService
$scopes = [
self::SCOPE_LINE_ITEM_READ,
self::SCOPE_RESULT_READ,
self::SCOPE_SCORE_WRITE,
];
$toolServices = $this->tool->getAdvantageServices();
@ -84,6 +87,14 @@ class LtiAssignmentGradesService extends LtiAdvantageService
);
}
if (isset($parts[4]) && 'scores' === $parts[4]) {
$resource = new LtiScoresResource(
$request->query->get('t'),
$parts[1],
$parts[3]
);
}
if (!$resource) {
throw new NotFoundHttpException('Line item resource not found.');
}

@ -0,0 +1,213 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\GradebookResult;
use Chamilo\CoreBundle\Entity\GradebookResultLog;
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* Class LtiScoresResource.
*/
class LtiScoresResource extends LtiAgsResource
{
const URL_TEMPLATE = '/context_id/lineitems/line_item_id/results';
const ACTIVITY_INITIALIZED = 'Initialized';
const ACTIVITY_STARTED = 'Started';
const ACTIVITY_IN_PROGRESS = 'InProgress';
const ACTIVITY_SUBMITTED = 'Submitted';
const ACTIVITY_COMPLETED = 'Completed';
const GRADING_FULLY_GRADED = 'FullyGraded';
const GRADING_PENDING = 'Pending';
const GRADING_PENDING_MANUAL = 'PendingManual';
const GRADING_FAILED = 'Failed';
const GRADING_NOT_READY = 'NotReady';
/**
* @var LineItem|null
*/
private $lineItem;
/**
* LtiScoresResource constructor.
*
* @param int $toolId
* @param int $courseId
* @param int $lineItemId
*
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/
public function __construct($toolId, $courseId, $lineItemId)
{
parent::__construct($toolId, $courseId);
$this->lineItem = Database::getManager()->find('ChamiloPluginBundle:ImsLti\LineItem', (int)$lineItemId);
}
/**
* @inheritDoc
*/
public function validate()
{
if (!$this->course) {
throw new NotFoundHttpException('Course not found.');
}
if (!$this->lineItem) {
throw new NotFoundHttpException('Line item not found');
}
if ($this->lineItem->getTool()->getId() !== $this->tool->getId()) {
throw new AccessDeniedHttpException('Line item not found for the tool.');
}
if (!$this->tool) {
throw new BadRequestHttpException('Tool not found.');
}
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
throw new AccessDeniedHttpException('Tool not found in course.');
}
if ($this->request->server->get('HTTP_ACCEPT') !== LtiAssignmentGradesService::TYPE_SCORE) {
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
}
$parentTool = $this->tool->getParent();
if ($parentTool) {
$advServices = $parentTool->getAdvantageServices();
if (LtiAssignmentGradesService::AGS_NONE === $advServices['ags']) {
throw new AccessDeniedHttpException('Assigment and grade service is not enabled for this tool.');
}
}
}
public function process()
{
switch ($this->request->getMethod()) {
case Request::METHOD_POST:
$this->processPost();
break;
default:
throw new MethodNotAllowedHttpException([Request::METHOD_POST]);
}
}
/**
* @throws Exception
*/
private function processPost()
{
$data = json_decode($this->request->getContent(), true);
if (empty($data) ||
!isset($data['userId']) ||
!isset($data['gradingProgress']) ||
!isset($data['activityProgress']) ||
!isset($data['timestamp']) ||
(isset($data['timestamp']) && !ImsLti::validateFormatDateIso8601($data['timestamp'])) ||
(isset($data['scoreGiven']) && !is_numeric($data['scoreGiven'])) ||
(isset($data['scoreGiven']) && !isset($data['scoreMaximum'])) ||
(isset($data['scoreMaximum']) && !is_numeric($data['scoreMaximum']))
) {
throw new BadRequestHttpException('Missing data to create score.');
}
$student = api_get_user_entity($data['userId']);
if (!$student) {
throw new BadRequestHttpException("User (id: {$data['userId']}) not found.");
}
$data['scoreMaximum'] = isset($data['scoreMaximum']) ? $data['scoreMaximum'] : 1;
$evaluation = $this->lineItem->getEvaluation();
$result = Database::getManager()
->getRepository('ChamiloCoreBundle:GradebookResult')
->findOneBy(
[
'userId' => $data['userId'],
'evaluationId' => $evaluation->getId(),
]
);
if ($result && $result->getCreatedAt() >= new DateTime($data['timestamp'])) {
throw new ConflictHttpException('The timestamp on record is later than the incoming score.');
}
if (isset($data['scoreGiven'])) {
if (self::GRADING_FULLY_GRADED !== $data['gradingProgress']) {
$data['scoreGiven'] = null;
} else {
$data['scoreGiven'] = (float) $data['scoreGiven'];
if ($data['scoreMaximum'] > 0 && $data['scoreMaximum'] != $evaluation->getMax()) {
$data['scoreGiven'] = $data['scoreGiven'] * $evaluation->getMax() / $data['scoreMaximum'];
}
}
}
if (!$result) {
$this->response->setStatusCode(Response::HTTP_CREATED);
}
$this->saveScore($data, $student, $result);
}
/**
* @param GradebookResult $result
* @param array $data
* @param User $student
*
* @throws OptimisticLockException
*/
private function saveScore(array $data, User $student, GradebookResult $result = null)
{
$em = Database::getManager();
$evaluation = $this->lineItem->getEvaluation();
if ($result) {
$resultLog = new GradebookResultLog();
$resultLog
->setCreatedAt(api_get_utc_datetime(null, false, true))
->setUserId($student->getId())
->setEvaluationId($evaluation->getId())
->setIdResult($result->getId())
->setScore($result->getScore());
$em->persist($resultLog);
} else {
$result = new GradebookResult();
$result
->setUserId($student->getId())
->setEvaluationId($evaluation->getId());
}
$result
->setCreatedAt(new DateTime($data['timestamp']))
->setScore($data['scoreGiven']);
$em->persist($result);
$em->flush();
}
}
Loading…
Cancel
Save