Actualización

This commit is contained in:
Xes
2025-04-10 12:36:07 +02:00
parent 1da7c3f3b9
commit 4aff98e77b
3147 changed files with 320647 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Class LtiAdvantageService.
*/
abstract class LtiAdvantageService
{
/**
* @var ImsLtiTool
*/
protected $tool;
/**
* LtiAdvantageService constructor.
*/
public function __construct(ImsLtiTool $tool)
{
$this->tool = $tool;
}
/**
* @return LtiAdvantageService
*/
public function setTool(ImsLtiTool $tool)
{
$this->tool = $tool;
return $this;
}
/**
* @return array
*/
abstract public function getAllowedScopes();
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LtiServiceResource
*/
abstract public static function getResource(Request $request, JsonResponse $response);
}

View File

@@ -0,0 +1,155 @@
<?php
/* For licensing terms, see /license.txt */
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class LtiAssignmentGradesService.
*/
class LtiAssignmentGradesService extends LtiAdvantageService
{
const AGS_NONE = 'none';
const AGS_SIMPLE = 'simple';
const AGS_FULL = 'full';
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
*/
public function getAllowedScopes()
{
$scopes = [
self::SCOPE_LINE_ITEM_READ,
self::SCOPE_RESULT_READ,
self::SCOPE_SCORE_WRITE,
];
$toolServices = $this->tool->getAdvantageServices();
if (self::AGS_FULL === $toolServices['ags']) {
$scopes[] = self::SCOPE_LINE_ITEM;
}
return $scopes;
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*
* @return LtiAdvantageServiceResource
*/
public static function getResource(Request $request, JsonResponse $response)
{
$parts = explode('/', $request->getPathInfo());
$parts = array_filter($parts);
$resource = null;
if (count($parts) === 2 && 'lineitems' === $parts[2]) {
$resource = new LtiLineItemsResource(
$request->query->get('t'),
$parts[1]
);
}
if (count($parts) === 3 && 'lineitems' === $parts[2]) {
$resource = new LtiLineItemResource(
$request->query->get('t'),
$parts[1],
$parts[3]
);
}
if (isset($parts[4]) && 'results' === $parts[4]) {
$resource = new LtiResultsResource(
$request->query->get('t'),
$parts[1],
$parts[3]
);
}
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.');
}
return $resource
->setRequest($request)
->setResponse($response);
}
/**
* @param int $contextId
* @param int $toolId
*
* @return string
*/
public static function getLineItemsUrl($contextId, $toolId, array $extraParams = [])
{
$base = api_get_path(WEB_PLUGIN_PATH).'ims_lti/ags2.php';
$resource = str_replace(
'context_id',
$contextId,
LtiLineItemsResource::URL_TEMPLATE
);
$params = array_merge($extraParams, ['t' => $toolId]);
$query = http_build_query($params);
return "$base$resource?$query";
}
/**
* @param int $contextId
* @param int $lineItemId
* @param int $toolId
*
* @return string
*/
public static function getLineItemUrl($contextId, $lineItemId, $toolId)
{
$base = api_get_path(WEB_PLUGIN_PATH).'ims_lti/ags2.php';
$resource = str_replace(
['context_id', 'line_item_id'],
[$contextId, $lineItemId],
LtiLineItemResource::URL_TEMPLATE
);
$query = http_build_query(['t' => $toolId]);
return "$base$resource?$query";
}
/**
* @param int $contextId
* @param int $lineItemId
* @param int $toolId
*
* @return string
*/
public static function getResultsUrl($contextId, $lineItemId, $toolId, array $extraParams = [])
{
$lineItemUrl = self::getLineItemUrl($contextId, $lineItemId, $toolId);
$query = http_build_query($extraParams);
return "$lineItemUrl/results?$query";
}
}

View File

@@ -0,0 +1,86 @@
<?php
/* For licensing terms, see /license.txt */
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class LtiNamesRoleProvisioningService.
*/
class LtiNamesRoleProvisioningService extends LtiAdvantageService
{
const NRPS_NONE = 'none';
const NRPS_CONTEXT_MEMBERSHIP = 'simple';
const SCOPE_CONTEXT_MEMBERSHIP_READ = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
const TYPE_MEMBERSHIP_CONTAINER = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json';
const USER_STATUS_ACTIVE = 'Active';
const USER_STATUS_INACTIVE = 'Inactive';
/**
* {@inheritDoc}
*/
public function getAllowedScopes()
{
return [
self::SCOPE_CONTEXT_MEMBERSHIP_READ,
];
}
/**
* {@inheritDoc}
*/
public static function getResource(Request $request, JsonResponse $response)
{
$parts = explode('/', $request->getPathInfo());
$parts = array_filter($parts);
$resource = null;
if (isset($parts[1], $parts[2]) &&
(int) $parts[1] > 0 && 'memberships' === $parts[2]
) {
$resource = new LtiContextMembershipResource(
$request->query->getInt('t'),
$parts[1],
$request->query->getInt('s')
);
}
if (!$resource) {
throw new NotFoundHttpException('Resource not found for Name and Role Provisioning.');
}
return $resource
->setRequest($request)
->setResponse($response);
}
/**
* @param int $toolId
* @param int $courseId
* @param int $sessionId
* @param array $extraParams
*
* @return string
*/
public static function getUrl($toolId, $courseId, $sessionId = 0, $extraParams = [])
{
$base = api_get_path(WEB_PLUGIN_PATH).'ims_lti/nrps2.php';
$resource = str_replace(
'context_id',
$courseId,
LtiContextMembershipResource::URL_TEMPLATE
);
$query = http_build_query(['s' => $sessionId, 't' => $toolId]);
if ($extraParams) {
$query .= '&'.http_build_query($extraParams);
}
return "$base$resource?$query";
}
}

View File

@@ -0,0 +1,123 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Chamilo\PluginBundle\Entity\ImsLti\Token;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
/**
* Class LtiAdvantageServiceResource.
*/
abstract class LtiAdvantageServiceResource
{
const URL_TEMPLATE = '/';
/**
* @var Request
*/
protected $request;
/**
* @var JsonResponse
*/
protected $response;
/**
* @var Course
*/
protected $course;
/**
* @var ImsLtiTool
*/
protected $tool;
/**
* LtiAdvantageServiceResource constructor.
*
* @param int $toolId
* @param int $courseId
*
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/
public function __construct($toolId, $courseId)
{
$this->course = api_get_course_entity((int) $courseId);
$this->tool = Database::getManager()->find('ChamiloPluginBundle:ImsLti\ImsLtiTool', (int) $toolId);
}
/**
* @return LtiAdvantageServiceResource
*/
public function setRequest(Request $request)
{
$this->request = $request;
return $this;
}
/**
* @return LtiAdvantageServiceResource
*/
public function setResponse(JsonResponse $response)
{
$this->response = $response;
return $this;
}
/**
* @throws HttpExceptionInterface
*/
abstract public function validate();
/**
* @throws MethodNotAllowedHttpException
*/
abstract public function process();
/**
* @throws HttpException
*/
protected function validateToken(array $allowedScopes)
{
$headers = getallheaders();
$authorization = isset($headers['Authorization']) ? $headers['Authorization'] : '';
if (substr($authorization, 0, 7) !== 'Bearer ') {
throw new BadRequestHttpException('Authorization is missing.');
}
$hash = trim(substr($authorization, 7));
/** @var Token $token */
$token = Database::getManager()
->getRepository('ChamiloPluginBundle:ImsLti\Token')
->findOneBy(['hash' => $hash]);
if (!$token) {
throw new BadRequestHttpException('Authorization token invalid.');
}
if ($token->getExpiresAt() < api_get_utc_datetime(null, false, true)->getTimestamp()) {
throw new BadRequestHttpException('Authorization token expired.');
}
$intersect = array_intersect(
$token->getScope(),
$allowedScopes
);
if (empty($intersect)) {
throw new BadRequestHttpException('Authorization token invalid for the scope.');
}
}
}

View File

@@ -0,0 +1,291 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\UserBundle\Entity\User;
use Doctrine\Common\Collections\Criteria;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* Class LtiContextMembershipResource.
*/
class LtiContextMembershipResource extends LtiAdvantageServiceResource
{
const URL_TEMPLATE = '/context_id/memberships';
/**
* @var bool
*/
private $inSession = false;
/**
* @var Session
*/
private $session;
/**
* LtiContextMembershipResorce constructor.
*
* @param int $toolId
* @param int $courseId
* @param int $sessionId
*
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*/
public function __construct($toolId, $courseId, $sessionId = 0)
{
$this->session = api_get_session_entity($sessionId);
parent::__construct($toolId, $courseId);
}
/**
* {@inheritDoc}
*/
public function validate()
{
if ($this->request->server->get('HTTP_ACCEPT') !== LtiNamesRoleProvisioningService::TYPE_MEMBERSHIP_CONTAINER) {
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
}
if (!$this->course) {
throw new BadRequestHttpException('Course not found.');
}
if (!$this->tool) {
throw new BadRequestHttpException('Tool not found.');
}
if (null === $this->tool->getCourse()) {
throw new BadRequestHttpException('Tool not enabled.');
}
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
throw new AccessDeniedHttpException('Tool not found in course.');
}
$sessionId = (int) $this->request->query->get('s');
$this->inSession = $sessionId > 0;
if ($this->inSession && !$this->session) {
throw new BadRequestHttpException('Session not found');
}
}
/**
* {@inheritDoc}
*/
public function process()
{
switch ($this->request->getMethod()) {
case Request::METHOD_GET:
$this->validateToken(
[LtiNamesRoleProvisioningService::SCOPE_CONTEXT_MEMBERSHIP_READ]
);
$this->processGet();
break;
default:
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
}
}
private function processGet()
{
$role = str_replace(
'http://purl.imsglobal.org/vocab/lis/v2/membership#',
'',
$this->request->query->get('role')
);
$limit = $this->request->query->getInt('limit');
$page = $this->request->query->getInt('page');
$status = -1;
if ('Instructor' === $role) {
$status = $this->session ? Session::COACH : User::COURSE_MANAGER;
} elseif ('Learner' === $role) {
$status = $this->session ? Session::STUDENT : User::STUDENT;
}
$members = $this->getMembers($status, $limit, $page);
$data = $this->getGetData($members);
$this->setLinkHeaderToGet($status, $limit, $page);
$this->response->headers->set('Content-Type', LtiNamesRoleProvisioningService::TYPE_MEMBERSHIP_CONTAINER);
$this->response->setData($data);
}
/**
* @param int $status If $status = -1 then get all statuses.
* @param int $limit
* @param int $page
*
* @return array
*/
private function getMembers($status, $limit, $page = 0)
{
if ($this->session) {
$subscriptions = $this->session->getUsersSubscriptionsInCourse($this->course);
// Add session admin as teacher in course
$adminSubscription = new SessionRelCourseRelUser();
$adminSubscription->setCourse($this->course);
$adminSubscription->setSession($this->session);
$adminSubscription->setStatus(Session::COACH);
$adminSubscription->setUser(
api_get_user_entity($this->session->getSessionAdminId())
);
$subscriptions->add($adminSubscription);
} else {
$subscriptions = $this->course->getUsers();
}
$criteria = Criteria::create();
if ($status > -1) {
$criteria->where(
Criteria::expr()->eq('status', $status)
);
}
if ($limit > 0) {
$criteria->setMaxResults($limit);
if ($page > 0) {
$criteria->setFirstResult($page * $limit);
}
}
return $subscriptions->matching($criteria)->toArray();
}
/**
* @return array
*/
private function getGetData(array $members)
{
$platformDomain = str_replace(['https://', 'http://'], '', api_get_setting('InstitutionUrl'));
$dataMembers = [];
$isSharingName = $this->tool->isSharingName();
$isSharingEmail = $this->tool->isSharingEmail();
$isSharingPicture = $this->tool->isSharingPicture();
foreach ($members as $member) {
/** @var User $user */
$user = $member->getUser();
$dataMember = [
'status' => $user->isActive()
? LtiNamesRoleProvisioningService::USER_STATUS_ACTIVE
: LtiNamesRoleProvisioningService::USER_STATUS_INACTIVE,
'user_id' => ImsLtiPlugin::getLaunchUserIdClaim($this->tool, $user),
'lis_person_sourcedid' => ImsLti::getPersonSourcedId($platformDomain, $user),
'lti11_legacy_user_id' => ImsLtiPlugin::generateToolUserId($user->getId()),
];
if ($isSharingName) {
$dataMember['name'] = $user->getFullname();
$dataMember['given_name'] = $user->getFirstname();
$dataMember['family_name'] = $user->getLastname();
}
if ($isSharingEmail) {
$dataMember['email'] = $user->getEmail();
}
if ($isSharingPicture) {
$dataMember['picture'] = UserManager::getUserPicture($user->getId());
}
if ($member instanceof CourseRelUser) {
$dataMember['roles'] = $member->getStatus() === User::COURSE_MANAGER
? ['http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor']
: ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'];
} elseif ($member instanceof SessionRelCourseRelUser) {
$dataMember['roles'] = $member->getStatus() === Session::STUDENT
? ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner']
: ['http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'];
}
$dataMembers[] = $dataMember;
}
return [
'id' => api_get_path(WEB_PLUGIN_PATH)
."ims_lti/nrps2.php/{$this->course->getId()}/memberships?"
.http_build_query(
[
't' => $this->tool->getId(),
's' => $this->session ? $this->session->getId() : null,
]
),
'context' => [
'id' => (string) $this->course->getId(),
'label' => $this->course->getCode(),
'title' => $this->course->getTitle(),
],
'members' => $dataMembers,
];
}
/**
* @param int $status
* @param int $limit
* @param int $page
*/
private function setLinkHeaderToGet($status, $limit, $page = 0)
{
if (!$limit) {
return;
}
if ($this->session) {
$subscriptions = $this->session->getUsersSubscriptionsInCourse($this->course);
} else {
$subscriptions = $this->course->getUsers();
}
$criteria = Criteria::create();
if ($status > -1) {
$criteria->where(
Criteria::expr()->eq('status', $status)
);
}
$count = $subscriptions->matching($criteria)->count();
if ($this->session) {
// +1 for session admin
$count++;
}
if ($page + 1 < ceil($count / $limit)) {
$url = LtiNamesRoleProvisioningService::getUrl(
$this->tool->getId(),
$this->course->getId(),
$this->session ? $this->session->getId() : 0,
[
'role' => $this->request->query->get('role'),
'limit' => $limit,
'page' => $page + 1,
]
);
$this->response->headers->set(
'Link',
'<'.$url.'>; rel="next"'
);
}
}
}

View File

@@ -0,0 +1,230 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
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\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* Class LtiLineItemResource.
*/
class LtiLineItemResource extends LtiAdvantageServiceResource
{
const URL_TEMPLATE = '/context_id/lineitems/line_item_id';
/**
* @var LineItem|null
*/
private $lineItem;
/**
* LtiLineItemResource 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);
}
/**
* @throws OptimisticLockException
*/
public function process()
{
switch ($this->request->getMethod()) {
case Request::METHOD_GET:
$this->validateToken(
[LtiAssignmentGradesService::SCOPE_LINE_ITEM]
);
$this->processGet();
break;
case Request::METHOD_PUT:
if (LtiAssignmentGradesService::AGS_FULL !== $this->tool->getAdvantageServices()['ags']) {
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
}
$this->validateToken(
[LtiAssignmentGradesService::SCOPE_LINE_ITEM]
);
$this->processPut();
break;
case Request::METHOD_DELETE:
if (LtiAssignmentGradesService::AGS_FULL !== $this->tool->getAdvantageServices()['ags']) {
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
}
$this->validateToken(
[LtiAssignmentGradesService::SCOPE_LINE_ITEM]
);
$this->processDelete();
break;
default:
throw new MethodNotAllowedHttpException([Request::METHOD_GET, Request::METHOD_PUT, Request::METHOD_DELETE]);
}
}
/**
* Validate the values for the resource URL.
*/
public function validate()
{
if (!$this->course) {
throw new BadRequestHttpException('Course not found.');
}
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_LINE_ITEM) {
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.');
}
}
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.');
}
}
private function processGet()
{
$data = $this->lineItem->toArray();
$data['id'] = LtiAssignmentGradesService::getLineItemUrl(
$this->course->getId(),
$this->lineItem->getId(),
$this->tool->getId()
);
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
$this->response->setData($data);
}
/**
* @throws OptimisticLockException
*/
private function processPut()
{
$data = json_decode($this->request->getContent(), true);
if (empty($data) || empty($data['label']) || empty($data['scoreMaximum'])) {
throw new BadRequestHttpException('Missing data to update line item.');
}
$this->updateLineItem($data);
$data['id'] = LtiAssignmentGradesService::getLineItemUrl(
$this->course->getId(),
$this->lineItem->getId(),
$this->tool->getId()
);
$data['scoreMaximum'] = $this->lineItem->getEvaluation()->getMax();
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
$this->response->setEncodingOptions(JSON_UNESCAPED_SLASHES);
$this->response->setData($data);
}
/**
* @throws OptimisticLockException
*/
private function updateLineItem(array $data)
{
$lineItemEvaluation = $this->lineItem->getEvaluation();
$evaluations = Evaluation::load($lineItemEvaluation->getId());
/** @var Evaluation $evaluation */
$evaluation = $evaluations[0];
$lineItemEvaluation->setName($data['label']);
if (isset($data['resourceId'])) {
$this->lineItem->setResourceId($data['resourceId']);
}
if (isset($data['tag'])) {
$this->lineItem->setTag($data['tag']);
}
if (!empty($data['startDateTime'])) {
$startDate = new DateTime($data['startDateTime']);
$this->lineItem->setStartDate($startDate);
}
if (!empty($data['endDateTime'])) {
$endDate = new DateTime($data['endDateTime']);
$this->lineItem->setEndDate($endDate);
}
if (!$evaluation->has_results()) {
$lineItemEvaluation->setMax($data['scoreMaximum']);
}
$em = Database::getManager();
$em->persist($this->lineItem);
$em->persist($lineItemEvaluation);
$em->flush();
}
/**
* @throws OptimisticLockException
*/
private function processDelete()
{
$this->deleteLineItem();
$this->response->setStatusCode(Response::HTTP_NO_CONTENT);
}
/**
* @throws OptimisticLockException
*/
private function deleteLineItem()
{
$lineItemEvaluation = $this->lineItem->getEvaluation();
$evaluations = Evaluation::load($lineItemEvaluation->getId());
/** @var Evaluation $evaluation */
$evaluation = $evaluations[0];
$em = Database::getManager();
$em->remove($this->lineItem);
$em->flush();
$evaluation->delete_with_results();
}
}

View File

@@ -0,0 +1,265 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
use Doctrine\Common\Collections\ArrayCollection;
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\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* Class LtiLineItemsResource.
*/
class LtiLineItemsResource extends LtiAdvantageServiceResource
{
const URL_TEMPLATE = '/context_id/lineitems';
public function validate()
{
if (!$this->course) {
throw new BadRequestHttpException('Course not found.');
}
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.');
}
$isMediaTypeAllowed = in_array(
$this->request->server->get('HTTP_ACCEPT'),
[
LtiAssignmentGradesService::TYPE_LINE_ITEM_CONTAINER,
LtiAssignmentGradesService::TYPE_LINE_ITEM,
]
);
if (!$isMediaTypeAllowed) {
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.');
}
}
}
/**
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
* @throws MethodNotAllowedHttpException
*/
public function process()
{
switch ($this->request->getMethod()) {
case Request::METHOD_POST:
if (LtiAssignmentGradesService::AGS_FULL !== $this->tool->getAdvantageServices()['ags']) {
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
}
$this->validateToken(
[
LtiAssignmentGradesService::SCOPE_LINE_ITEM,
]
);
$this->processPost();
break;
case Request::METHOD_GET:
$this->validateToken(
[
LtiAssignmentGradesService::SCOPE_LINE_ITEM,
LtiAssignmentGradesService::SCOPE_LINE_ITEM_READ,
]
);
$this->processGet();
break;
default:
throw new MethodNotAllowedHttpException([Request::METHOD_GET, Request::METHOD_POST]);
}
}
/**
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
*
* @return LineItem
*/
public function createLineItem(array $data)
{
$caterories = Category::load(null, null, $this->course->getCode());
/** @var Category $gradebookCategory */
$gradebookCategory = $caterories[0];
$em = Database::getManager();
$userId = 1;
$eval = new Evaluation();
$eval->set_user_id($userId);
$eval->set_category_id($gradebookCategory->get_id());
$eval->set_course_code($this->tool->getCourse()->getCode());
$eval->set_name($data['label']);
$eval->set_description(null);
$eval->set_weight(
$gradebookCategory->getRemainingWeight()
);
$eval->set_max($data['scoreMaximum']);
$eval->set_visible(1);
$eval->add();
$evaluation = $em->find('ChamiloCoreBundle:GradebookEvaluation', $eval->get_id());
$lineItem = new LineItem();
$lineItem
->setTool($this->tool)
->setEvaluation($evaluation)
->setResourceId(!empty($data['resourceId']) ? $data['resourceId'] : null)
->setTag(!empty($data['tag']) ? $data['tag'] : null);
if (!empty($data['startDateTime'])) {
$startDate = new DateTime($data['startDateTime']);
$lineItem->setStartDate($startDate);
}
if (!empty($data['endDateTime'])) {
$endDate = new DateTime($data['endDateTime']);
$lineItem->setEndDate($endDate);
}
$em->persist($lineItem);
$em->flush();
return $lineItem;
}
/**
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/
private function processPost()
{
$data = json_decode($this->request->getContent(), true);
if (empty($data) || empty($data['label']) || empty($data['scoreMaximum'])) {
throw new BadRequestHttpException('Missing data to create line item.');
}
$lineItem = $this->createLineItem($data);
$data['id'] = LtiAssignmentGradesService::getLineItemUrl(
$this->course->getId(),
$lineItem->getId(),
$this->tool->getId()
);
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
$this->response->setEncodingOptions(JSON_UNESCAPED_SLASHES);
$this->response->setStatusCode(Response::HTTP_CREATED);
$this->response->setData($data);
}
private function processGet()
{
$resourceLinkId = $this->request->query->get('resource_link_id');
$resourceId = $this->request->query->get('resource_id');
$tag = $this->request->query->get('tag');
$limit = $this->request->query->get('limit');
$page = $this->request->query->get('page');
$lineItems = $this->tool->getLineItems($resourceLinkId, $resourceId, $tag, $limit, $page);
$this->setLinkHeaderToGet($lineItems, $resourceLinkId, $resourceId, $tag, $limit, $page);
$data = $this->getGetData($lineItems);
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
$this->response->setData($data);
}
/**
* @param int $resourceLinkId
* @param int $resourceId
* @param int $tag
* @param int $limit
* @param int $page
*/
private function setLinkHeaderToGet(ArrayCollection $lineItems, $resourceLinkId, $resourceId, $tag, $limit, $page)
{
if (!$limit) {
return;
}
$links = [];
$links['first'] = 0;
$links['last'] = ceil($lineItems->count() / $limit);
$links['canonical'] = $page;
if ($page > 1) {
$links['prev'] = $page - 1;
}
if ($page + 1 <= $lineItems->count() / $limit) {
$links['next'] = $page + 1;
}
foreach ($links as $rel => $linkPage) {
$url = LtiAssignmentGradesService::getLineItemsUrl(
$this->course->getId(),
$this->tool->getId(),
[
'resource_link_id' => $resourceLinkId,
'resource_id' => $resourceId,
'tag' => $tag,
'limit' => $limit,
'page' => $linkPage,
]
);
$links[$rel] = '<'.$url.'>; rel="'.$rel.'"';
}
$this->response->headers->set(
'Link',
implode(', ', $links)
);
}
/**
* @return array
*/
private function getGetData(ArrayCollection $lineItems)
{
$data = [];
/** @var LineItem $lineItem */
foreach ($lineItems as $lineItem) {
$item = $lineItem->toArray();
$item['id'] = LtiAssignmentGradesService::getLineItemUrl(
$this->course->getId(),
$lineItem->getId(),
$this->tool->getId()
);
$data[] = $item;
}
return $data;
}
}

View File

@@ -0,0 +1,235 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\GradebookResult;
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* Class LtiResultsResource.
*/
class LtiResultsResource extends LtiAdvantageServiceResource
{
const URL_TEMPLATE = '/context_id/lineitems/line_item_id/results';
/**
* @var LineItem|null
*/
private $lineItem;
/**
* LtiResultsResource 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 BadRequestHttpException('Course not found.');
}
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_RESULT_CONTAINER) {
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.');
}
}
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.');
}
}
public function process()
{
switch ($this->request->getMethod()) {
case Request::METHOD_GET:
$this->validateToken(
[LtiAssignmentGradesService::SCOPE_RESULT_READ]
);
$this->processGet();
break;
default:
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
}
}
private function processGet()
{
$limit = $this->request->query->get('limit');
$page = $this->request->query->get('page');
$userId = $this->request->query->get('user_id');
$results = $this->getResults($limit, $userId, $page);
$data = $this->getGetData($results);
$this->setLinkHeaderToGet($userId, $limit, $page);
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_RESULT_CONTAINER);
$this->response->setData($data);
}
private function getResults($limit, $userId, $page = 0)
{
$em = Database::getManager();
$limit = (int) $limit;
$page = (int) $page;
$dql = 'SELECT r FROM ChamiloCoreBundle:GradebookResult r WHERE r.evaluationId = :id';
$parameters = ['id' => $this->lineItem->getEvaluation()->getId()];
if ($userId) {
$dql .= ' AND r.userId = :user';
$parameters['user'] = (int) $userId;
}
$query = $em->createQuery($dql);
if ($limit > 0) {
$query->setMaxResults($limit);
if ($page > 0) {
$query->setFirstResult($page * $limit);
}
}
return $query
->setParameters($parameters)
->getResult();
}
/**
* @param array|GradebookResult[] $results
*
* @return array
*/
private function getGetData(array $results)
{
$data = [];
foreach ($results as $result) {
$lineItemEndPoint = LtiAssignmentGradesService::getLineItemUrl(
$this->course->getId(),
$this->lineItem->getId(),
$this->tool->getId()
);
$data[] = [
'id' => "$lineItemEndPoint/results/{$result->getId()}",
'scoreOf' => $lineItemEndPoint,
'userId' => (string) $result->getUserId(),
'resultScore' => $result->getScore(),
'resultMaximum' => $this->lineItem->getEvaluation()->getMax(),
'comment' => null,
];
}
return $data;
}
/**
* @param int $userId
* @param int $limit
* @param int $page
*
* @throws \Doctrine\ORM\Query\QueryException
*/
private function setLinkHeaderToGet($userId, $limit, $page = 0)
{
$limit = (int) $limit;
$page = (int) $page;
if (!$limit) {
return;
}
$em = Database::getManager();
$dql = 'SELECT COUNT(r) FROM ChamiloCoreBundle:GradebookResult r WHERE r.evaluationId = :id';
$parameters = ['id' => $this->lineItem->getEvaluation()->getId()];
if ($userId) {
$dql .= ' AND r.userId = :user';
$parameters['user'] = (int) $userId;
}
$count = $em
->createQuery($dql)
->setParameters($parameters)
->getSingleScalarResult();
$links = [];
$links['first'] = 0;
$links['last'] = ceil($count / $limit);
$links['canonical'] = $page;
if ($page > 1) {
$links['prev'] = $page - 1;
}
if ($page + 1 < $links['last']) {
$links['next'] = $page + 1;
}
foreach ($links as $rel => $linkPage) {
$url = LtiAssignmentGradesService::getResultsUrl(
$this->course->getId(),
$this->lineItem->getId(),
$this->tool->getId(),
['user_id' => $userId, 'limit' => $limit, 'page' => $linkPage]
);
$links[$rel] = '<'.$url.'>; rel="'.$rel.'"';
}
$this->response->headers->set(
'Link',
implode(', ', $links)
);
}
}

View File

@@ -0,0 +1,214 @@
<?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 LtiAdvantageServiceResource
{
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->validateToken(
[LtiAssignmentGradesService::SCOPE_SCORE_WRITE]
);
$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
*
* @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();
}
}