AssessmentController.php 57.3 KB
Newer Older
mathieso's avatar
mathieso committed
1
<?php
2 3 4

namespace Drupal\skilling\Controller;

mathieso's avatar
mathieso committed
5
use DateTime;
6
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
7
use Drupal\Component\Utility\Bytes;
8
use Drupal\Core\Controller\ControllerBase;
9
use Drupal\Core\Datetime\DateFormatter;
10
use Drupal\Core\Entity\EntityStorageException;
mathieso's avatar
mathieso committed
11
use Drupal\Core\Extension\ModuleHandlerInterface;
12
use Drupal\Core\File\FileSystemInterface;
mathieso's avatar
mathieso committed
13
use Drupal\Core\Utility\Token;
mathieso's avatar
mathieso committed
14
use Drupal\skilling\Access\FilterUserInputInterface;
15
use Drupal\skilling\Access\SkillingAjaxSecurityInterface;
16
use Drupal\skilling\Access\SkillingCheckUserRelationships;
17
use Drupal\skilling\Assessment;
mathieso's avatar
mathieso committed
18
use Drupal\skilling\Badging;
mathieso's avatar
mathieso committed
19
use Drupal\skilling\Notice;
20
use Drupal\skilling\SkillingConstants;
21
use Drupal\skilling\SkillingCurrentUser;
mathieso's avatar
mathieso committed
22
use Drupal\skilling\SkillingParser\SkillingParser;
mathieso's avatar
mathieso committed
23
use Drupal\skilling\SkillingUserFactory;
mathieso's avatar
mathieso committed
24 25
use Drupal\skilling_history\History;
use Drupal\skilling_history\SkillingHistoryConstants;
26
use Drupal\user\Entity\User;
mathieso's avatar
mathieso committed
27
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
28 29 30 31
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
mathieso's avatar
mathieso committed
32
use Drupal\skilling\Utilities as SkillingUtilities;
mathieso's avatar
mathieso committed
33 34
use Symfony\Component\HttpFoundation\Session\Session;
use Drupal\Core\Access\CsrfTokenGenerator;
35
use Drupal\Core\Render\Renderer;
36 37
use Drupal\Core\Config\ConfigFactory;

38 39

/**
40
 * Ajax interaction with grading interface.
41 42
 */
class AssessmentController extends ControllerBase {
mathieso's avatar
mathieso committed
43

44
  const PATH_TO_ASSESSMENT_INTERFACE = 'libraries/feedback/feedback.html';
45

46
  /**
47
   * Entity type manager service.
48 49 50 51 52 53
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
mathieso's avatar
mathieso committed
54 55 56
   * The Skilling utilities service.
   *
   * @var \Drupal\Skilling\Utilities
mathieso's avatar
mathieso committed
57 58 59
   */
  protected $skillingUtilities;

60 61 62 63 64
  /**
   * Date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatter
   */
65 66
  protected $dateFormatter;

67 68 69 70 71
  /**
   * Current user service.
   *
   * @var \Drupal\skilling\SkillingCurrentUser
   */
72 73
  protected $currentUser;

74
  /**
75 76
   * Service with various helpers for assessment.
   *
77 78 79 80
   * @var \Drupal\skilling\Assessment
   */
  protected $assessmentService;

81 82 83
  /**
   * The history service.
   *
mathieso's avatar
mathieso committed
84
   * @var \Drupal\skilling_history\History
85
   */
mathieso's avatar
mathieso committed
86
  protected $historyService;
87

88 89 90 91 92 93 94
  /**
   * Service to check AJAX calls.
   *
   * @var \Drupal\skilling\Access\SkillingAjaxSecurityInterface
   */
  protected $ajaxSecurityService;

95 96 97 98 99 100 101
  /**
   * User relationship service.
   *
   * @var \Drupal\skilling\Access\SkillingCheckUserRelationships
   */
  protected $userRelationshipService;

mathieso's avatar
mathieso committed
102 103 104 105 106 107 108
  /**
   * Service to filter user input.
   *
   * @var \Drupal\skilling\Access\FilterUserInputInterface
   */
  protected $filterInputService;

mathieso's avatar
mathieso committed
109 110 111 112 113 114 115 116 117 118 119 120 121 122
  /**
   * Session service.
   *
   * @var \Symfony\Component\HttpFoundation\Session\Session
   */
  protected $sessionService;

  /**
   * CSRF token generator service.
   *
   * @var \Drupal\Core\Access\CsrfTokenGenerator
   */
  protected $csrfTokenGeneratorService;

mathieso's avatar
mathieso committed
123 124 125 126 127 128 129 130 131 132 133 134 135 136
  /**
   * The token service.
   *
   * @var \Drupal\Core\Utility\Token
   */
  protected $tokenService;

  /**
   * The user factory service.
   *
   * @var \Drupal\skilling\SkillingUserFactory
   */
  protected $skillingUserFactory;

mathieso's avatar
mathieso committed
137 138 139 140 141 142 143
  /**
   * The parser service.
   *
   * @var \Drupal\skilling\SkillingParser\SkillingParser
   */
  protected $skillingParser;

mathieso's avatar
mathieso committed
144 145 146 147 148 149
  /** The badging service.
   *
   * @var \Drupal\skilling\Badging
   */
  protected $badgingService;

mathieso's avatar
mathieso committed
150 151 152 153 154 155 156 157 158 159 160 161 162 163
  /**
   * Module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The notice service.
   *
   * @var \Drupal\skilling\Notice
   */
  protected $noticeService;

164 165 166 167
  /**
   * @var Renderer
   */
  protected $renderer;
mathieso's avatar
mathieso committed
168

169 170 171 172 173 174 175
  /**
   * Config factory service.
   *
   * @var \Drupal\Core\Config\ConfigFactory
   */
  protected $configFactory;

176 177 178 179 180
  /**
   * Overall evaluations graders can give.
   *
   * @var array
   */
181 182
  const VALID_EVALUATIONS = ['good', 'needs work', 'poor'];

mathieso's avatar
mathieso committed
183 184
  /**
   * Constructs a new AssessmentController object.
185 186
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
187
   *   Entity type manager service.
mathieso's avatar
mathieso committed
188
   * @param \Drupal\skilling\Utilities $skilling_utilities
189
   *   The Skilling utilities service.
190
   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
191
   *   Date formatter service.
192
   * @param \Drupal\skilling\SkillingCurrentUser $currentUser
193
   *   Current user service.
194
   * @param \Drupal\skilling\Assessment $assessment
195
   *   Service with various helpers for assessment.
mathieso's avatar
mathieso committed
196
   * @param \Drupal\skilling_history\History $historyService
197
   *   The history service.
198 199
   * @param \Drupal\skilling\Access\SkillingAjaxSecurityInterface $ajaxSecurityService
   *   AJAX security checking service.
200
   * @param \Drupal\skilling\Access\SkillingCheckUserRelationships $userRelationshipService
201
   *   User relationship service.
mathieso's avatar
mathieso committed
202 203
   * @param \Drupal\skilling\Access\FilterUserInputInterface $filterInputService
   *   Input filter service.
mathieso's avatar
mathieso committed
204 205 206 207
   * @param \Symfony\Component\HttpFoundation\Session\Session $session
   *   Session service.
   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrfTokenGeneratorService
   *   CSRF token generator service.
mathieso's avatar
mathieso committed
208 209 210 211
   * @param \Drupal\Core\Utility\Token $token
   *   The token service.
   * @param \Drupal\skilling\SkillingUserFactory $skillingUserFactory
   *   The user factory service.
mathieso's avatar
mathieso committed
212
   * @param \Drupal\skilling\SkillingParser\SkillingParser $skillingParser
mathieso's avatar
mathieso committed
213
   * @param \Drupal\skilling\Badging $badgingService
214 215
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   * @param \Drupal\skilling\Notice $noticeService
216
   * @param \Drupal\Core\Render\Renderer $renderer
217
   * @param \Drupal\Core\Config\ConfigFactory $configFactory
218
   */
mathieso's avatar
mathieso committed
219 220 221 222 223 224
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    SkillingUtilities $skilling_utilities,
    DateFormatter $date_formatter,
    SkillingCurrentUser $currentUser,
    Assessment $assessment,
mathieso's avatar
mathieso committed
225
    History $historyService,
mathieso's avatar
mathieso committed
226 227
    SkillingAjaxSecurityInterface $ajaxSecurityService,
    SkillingCheckUserRelationships $userRelationshipService,
mathieso's avatar
mathieso committed
228 229
    FilterUserInputInterface $filterInputService,
    Session $session,
mathieso's avatar
mathieso committed
230 231
    CsrfTokenGenerator $csrfTokenGeneratorService,
    Token $token,
mathieso's avatar
mathieso committed
232
    SkillingUserFactory $skillingUserFactory,
mathieso's avatar
mathieso committed
233
    SkillingParser $skillingParser,
mathieso's avatar
mathieso committed
234 235
    Badging $badgingService,
    ModuleHandlerInterface $moduleHandler,
236
    Notice $noticeService,
237 238
    Renderer $renderer,
    ConfigFactory $configFactory
mathieso's avatar
mathieso committed
239
  ) {
240
    $this->entityTypeManager = $entity_type_manager;
mathieso's avatar
mathieso committed
241
    $this->skillingUtilities = $skilling_utilities;
242
    $this->currentUser = $currentUser;
243
    $this->assessmentService = $assessment;
244
    $this->dateFormatter = $date_formatter;
mathieso's avatar
mathieso committed
245
    $this->historyService = $historyService;
246
    $this->ajaxSecurityService = $ajaxSecurityService;
247
    $this->userRelationshipService = $userRelationshipService;
mathieso's avatar
mathieso committed
248
    $this->filterInputService = $filterInputService;
mathieso's avatar
mathieso committed
249 250
    $this->sessionService = $session;
    $this->csrfTokenGeneratorService = $csrfTokenGeneratorService;
mathieso's avatar
mathieso committed
251 252
    $this->tokenService = $token;
    $this->skillingUserFactory = $skillingUserFactory;
mathieso's avatar
mathieso committed
253
    $this->skillingParser = $skillingParser;
mathieso's avatar
mathieso committed
254
    $this->badgingService = $badgingService;
mathieso's avatar
mathieso committed
255 256
    $this->moduleHandler = $moduleHandler;
    $this->noticeService = $noticeService;
257
    $this->renderer = $renderer;
258
    $this->configFactory = $configFactory;
259 260 261 262 263 264
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
265
    /* @noinspection PhpParamsInspection */
266
    return new static(
mathieso's avatar
mathieso committed
267 268
      $container->get('entity_type.manager'),
      $container->get('skilling.utilities'),
269
      $container->get('date.formatter'),
270
      $container->get('skilling.skilling_current_user'),
271
      $container->get('skilling.assessment'),
mathieso's avatar
mathieso committed
272
      $container->get('skilling_history.history'),
273
      $container->get('skilling.ajax_security'),
mathieso's avatar
mathieso committed
274
      $container->get('skilling.check_user_relationships'),
mathieso's avatar
mathieso committed
275 276
      $container->get('skilling.filter_user_input'),
      $container->get('session'),
mathieso's avatar
mathieso committed
277 278
      $container->get('csrf_token'),
      $container->get('token'),
mathieso's avatar
mathieso committed
279
      $container->get('skilling.skilling_user_factory'),
mathieso's avatar
mathieso committed
280
      $container->get('skilling.skillingparser'),
mathieso's avatar
mathieso committed
281 282
      $container->get('skilling.badging'),
      $container->get('module_handler'),
283
      $container->get('skilling.notice'),
284 285
      $container->get('renderer'),
      $container->get('config.factory')
286 287 288
    );
  }

289
  /**
mathieso's avatar
mathieso committed
290 291
   * Make a link to start the grading interface.
   *
292
   * @return array
mathieso's avatar
mathieso committed
293 294
   *   Render array.
   *
295 296 297 298 299 300 301 302 303
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function startGradingInterface() {
    $build = [];
    $build['start_grading_link'] = $this->computeAssessmentLink();
    return $build;
  }

304 305
  /**
   * Return render array element for grading link.
306
   *
307 308 309 310
   * MT array if there is nothing to show.
   *
   * @return array
   *   Render array element for grading link.
311
   *
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function computeAssessmentLink() {
    // Assume nothing to grade.
    $build = [];
    if (!$this->currentUser->isGrader()) {
      $build['problem'] = [
        '#markup' => $this->t('Sorry, only available to graders.'),
      ];
      return $build;
    }
    $waitingSubmissionCount = $this->assessmentService->computeNumSubmissionsWaitingAssessment();
    // Problem?
    if (is_null($waitingSubmissionCount)) {
      $build['problem'] = [
        '#markup' => $this->t('Sorry, could not count number of waiting submissions.'),
      ];
      $build['suggestion'] = [
        '#markup' => SkillingConstants::getErrorSuggestion(),
      ];
      return $build;
    }
    // Anything waiting?
    if ($waitingSubmissionCount === 0) {
      // No.
      $build = [
        '#markup' => $this->t('You have nothing to grade.'),
        '#cache' => ['max-age' => 0],
      ];
    }
    else {
      // User has things to grade.
345
      $assessmentInterfaceUrl
346
        = $this->skillingUtilities->getModuleUrlPath() . self::PATH_TO_ASSESSMENT_INTERFACE;
347 348
      $linkText = ($waitingSubmissionCount === 1)
        ? $this->t('one submission')
349
        : $this->t('@s submissions', ['@s' => $waitingSubmissionCount]);
mathieso's avatar
mathieso committed
350 351
      $sessionId = $this->sessionService->getId();
      $csrfToken = $this->csrfTokenGeneratorService->get();
352 353 354 355 356 357 358 359 360 361 362 363 364
      $build = [
        '#type' => 'container',
        '#attributes' => ['id' => 'start-assessing-wrapper'],
        // Not cachable.
        '#cache' => [
          'max-age' => 0,
        ],
        '#attached' => [
          'library' => 'skilling/start-grading',
          'drupalSettings' => [
            'sessionId' => $sessionId,
            'assessmentInterfaceUrl' => $assessmentInterfaceUrl,
            'csrfToken' => $csrfToken,
365
          ],
366
        ],
367
        'start_grading_link' => [
368 369 370 371
          '#markup' => $this->t(
            'You have <a href="#" id="start-grading">@lt</a> to grade.',
            ['@lt' => $linkText]
          ),
372
        ],
373 374 375 376 377
      ];
    }
    return $build;
  }

mathieso's avatar
mathieso committed
378
  /**
379 380 381 382 383 384 385 386
   * Load and return grader persona data.
   *
   * Security note: No client input apart from the GET. Server data sent
   * to client originates from graders (or defaults from installation).
   * Data entered through Drupal form system, using Stripped.
   *
   * Stripped is used again in this method, before sending data to the client.
   *
mathieso's avatar
mathieso committed
387 388
   * This is not a state-changing operation.
   *
mathieso's avatar
mathieso committed
389
   * @param \Symfony\Component\HttpFoundation\Request $request
390
   *   HTTP request.
mathieso's avatar
mathieso committed
391 392
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
393
   *   Response with persona data.
mathieso's avatar
mathieso committed
394 395 396
   *
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
mathieso's avatar
mathieso committed
397 398
   */
  public function getGraderPersona(Request $request) {
399
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
mathieso's avatar
mathieso committed
400 401 402 403 404
      $request,
      ['GET'],
      ['/skilling/get-grader-persona']
    );
    if (!$passedBasicSecurity) {
mathieso's avatar
mathieso committed
405 406
      throw new AccessDeniedException('Access denied');
    }
407
    if (!$this->currentUser->isGrader()) {
mathieso's avatar
mathieso committed
408 409 410 411 412
      throw new AccessDeniedException('Access denied');
    }
    $result = [
      'greetings' => [],
      'signatures' => [],
413 414 415
      'summaryGood' => [],
      'summaryNeedsWork' => [],
      'summaryPoor' => [],
mathieso's avatar
mathieso committed
416
    ];
417
    $drupalUser = $this->currentUser->getDrupalUser();
418
    /* @noinspection PhpUndefinedFieldInspection */
419
    $greetings = $drupalUser->field_feedback_greetings->getValue();
mathieso's avatar
mathieso committed
420
    foreach ($greetings as $greeting) {
421 422 423
      $text = $this->filterInputService->filterUserContent($greeting['value']);
      $text = $this->tokenService->replace($text);
      $result['greetings'][] = $text;
mathieso's avatar
mathieso committed
424
    }
425
    /* @noinspection PhpUndefinedFieldInspection */
426
    $signatures = $drupalUser->field_feedback_signatures->getValue();
mathieso's avatar
mathieso committed
427
    foreach ($signatures as $signature) {
428 429 430
      $text = $this->filterInputService->filterUserContent($signature['value']);
      $text = $this->tokenService->replace($text);
      $result['signatures'][] = $text;
mathieso's avatar
mathieso committed
431
    }
432
    /* @noinspection PhpUndefinedFieldInspection */
433
    $summariesGood = $drupalUser->field_feedback_summary_good->getValue();
mathieso's avatar
mathieso committed
434
    foreach ($summariesGood as $summaryGood) {
435 436 437
      $text = $this->filterInputService->filterUserContent($summaryGood['value']);
      $text = $this->tokenService->replace($text);
      $result['summaryGood'][] = $text;
mathieso's avatar
mathieso committed
438
    }
439
    /* @noinspection PhpUndefinedFieldInspection */
440
    $summariesNeedsWork = $drupalUser->field_feedback_summary_needs_wor->getValue();
mathieso's avatar
mathieso committed
441
    foreach ($summariesNeedsWork as $summaryNeedsWork) {
442 443 444
      $text = $this->filterInputService->filterUserContent($summaryNeedsWork['value']);
      $text = $this->tokenService->replace($text);
      $result['summaryNeedsWork'][] = $text;
mathieso's avatar
mathieso committed
445
    }
446
    /* @noinspection PhpUndefinedFieldInspection */
447
    $summariesPoor = $drupalUser->field_feedback_summary_poor->getValue();
mathieso's avatar
mathieso committed
448
    foreach ($summariesPoor as $summaryPoor) {
449 450 451
      $text = $this->filterInputService->filterUserContent($summaryPoor['value']);
      $text = $this->tokenService->replace($text);
      $result['summaryPoor'][] = $text;
mathieso's avatar
mathieso committed
452 453
    }
    return new JsonResponse($result);
454
  }
mathieso's avatar
mathieso committed
455

456 457 458
  /**
   * Get submissions for grader.
   *
459 460 461
   * Security note: No client input apart from the GET. Server data sent
   * to client computed in getUngradedSubmissionsForClasses().
   *
mathieso's avatar
mathieso committed
462 463
   * This is not a state-changing operation.
   *
464
   * @param \Symfony\Component\HttpFoundation\Request $request
465
   *   HTTP request.
466 467
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
468 469 470 471
   *   Submissions to grade.
   *
   * @see getUngradedSubmissionsForClasses
   *
mathieso's avatar
mathieso committed
472 473 474
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
   *
475
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
mathieso's avatar
mathieso committed
476
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
477 478
   */
  public function getSubmissionListForGrader(Request $request) {
479
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
mathieso's avatar
mathieso committed
480 481 482 483 484 485 486
      $request,
      ['GET'],
      ['/skilling/get-submissions-for-grader']
    );
    if (!$passedBasicSecurity) {
      throw new AccessDeniedException('Access denied');
    }
487
    if (!$this->currentUser->isGrader()) {
mathieso's avatar
mathieso committed
488 489
      throw new AccessDeniedException('Access denied');
    }
490
    $graderClassNids = $this->currentUser->getClassNidsForGrader();
491
    $result = $this->assessmentService->getUngradedSubmissionsForClasses(
492
      $graderClassNids
mathieso's avatar
mathieso committed
493 494
    );
    return new JsonResponse($result);
mathieso's avatar
mathieso committed
495 496
  }

497
  /**
498
   * Get class list for grader.
499
   *
500 501 502
   * Security note: No client input apart from the GET. Server data sent
   * to client computed in getClassesForGrader().
   *
mathieso's avatar
mathieso committed
503 504
   * This is not a state-changing operation.
   *
505
   * @param \Symfony\Component\HttpFoundation\Request $request
506
   *   HTTP request.
507 508
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
509 510 511 512
   *   Classes.
   *
   * @see getClassesForGrader
   *
mathieso's avatar
mathieso committed
513 514 515
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
   *
516
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
mathieso's avatar
mathieso committed
517
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
518
   */
519
  public function getClassListForGrader(Request $request) {
520
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
mathieso's avatar
mathieso committed
521 522 523 524 525 526 527
      $request,
      ['GET'],
      ['/skilling/get-class-list-for-grader']
    );
    if (!$passedBasicSecurity) {
      throw new AccessDeniedException('Access denied');
    }
528 529
    // Is the current user a grader?
    if (!$this->currentUser->isGrader()) {
mathieso's avatar
mathieso committed
530 531
      throw new AccessDeniedException('Access denied');
    }
532
    $graderClassNids = $this->currentUser->getClassNidsForGrader();
533
    $result = $this->assessmentService->getClassesForGrader($graderClassNids);
mathieso's avatar
mathieso committed
534
    return new JsonResponse($result);
535 536 537 538 539
  }

  /**
   * Get student list for grader.
   *
540 541 542 543 544 545 546 547 548 549 550
   * Security notes:
   *
   * This method get a list of student ids from the client.
   *
   * - Students ids are scanned to make sure they are all numeric.
   * - Each student id is checked to make sure the current user is a
   *   grader of that student.
   *
   * The method returns data about students. Names are returned only if the
   * current user has the instructor role, and is an instructor of that student.
   *
mathieso's avatar
mathieso committed
551 552
   * This is not a state-changing operation.
   *
553
   * @param \Symfony\Component\HttpFoundation\Request $request
554
   *   HTTP request.
555 556
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
557 558
   *   Student data.
   *
mathieso's avatar
mathieso committed
559 560 561
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
   *
562
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
563 564
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\skilling\Exception\SkillingException
565 566
   */
  public function getStudentListForGrader(Request $request) {
567
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
mathieso's avatar
mathieso committed
568 569 570
      $request,
      ['GET'],
      ['/skilling/get-student-list-for-grader']
571
    );
mathieso's avatar
mathieso committed
572 573
    if (!$passedBasicSecurity) {
      throw new AccessDeniedException('Access denied');
574
    }
575 576
    // Is the current user a grader?
    if (!$this->currentUser->isGrader()) {
mathieso's avatar
mathieso committed
577
      throw new AccessDeniedException('Access denied');
578 579
    }
    $studentIds = $request->query->get('studentIds');
580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
    // Should be array.
    if (!is_array($studentIds)) {
      throw new AccessDeniedException('Access denied');
    }
    // Every element should be numeric.
    $allNumeric = TRUE;
    foreach ($studentIds as $studentId) {
      if (!is_numeric($studentId)) {
        $allNumeric = FALSE;
        break;
      }
    }
    if (!$allNumeric) {
      throw new AccessDeniedException('Access denied');
    }
    // Make sure current user is grader of students with given ids.
    $isGraderOfStudent = TRUE;
    foreach ($studentIds as $studentId) {
      $grader = $this->userRelationshipService->isUserUidGraderOfUserUid(
        $this->currentUser->id(),
        $studentId
      );
      if (!$grader) {
        $isGraderOfStudent = FALSE;
        break;
      }
    }
    if (!$isGraderOfStudent) {
      throw new AccessDeniedException('Access denied');
    }
610
    $result = [];
mathieso's avatar
mathieso committed
611 612 613
    if (count($studentIds) > 0) {
      $students = $this->entityTypeManager->getStorage('user')
        ->loadMultiple($studentIds);
614
      // Load the published enrollments for the students.
mathieso's avatar
mathieso committed
615 616 617 618
      $enrollmentQuery = $this->entityTypeManager
        ->getStorage('node')
        ->getQuery()
        ->condition('type', 'enrollment')
619
        // Enrollment is published.
mathieso's avatar
mathieso committed
620
        ->condition('status', TRUE)
621
        // For the users.
mathieso's avatar
mathieso committed
622
        ->condition('field_user', $studentIds, 'IN')
623
        // Have student role.
mathieso's avatar
mathieso committed
624
        ->condition('field_class_roles', ['student'], 'IN')
625
        // User is not blocked.
mathieso's avatar
mathieso committed
626
        ->condition('field_user.entity.status', 1)
627
        // Class is published.
mathieso's avatar
mathieso committed
628 629 630 631
        ->condition('field_class.entity.status', 1);
      $enrollmentIds = $enrollmentQuery->execute();
      $enrollments = $this->entityTypeManager->getStorage('node')
        ->loadMultiple($enrollmentIds);
632 633
      // Make array with student id as key, value is array of
      // classes the student is enrolled in.
mathieso's avatar
mathieso committed
634 635 636
      $enrolledClasses = [];
      /** @var \Drupal\node\Entity\Node $enrollment */
      foreach ($enrollments as $enrollment) {
637
        /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
638
        $studentId = $enrollment->field_user->target_id;
639
        /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
640 641 642
        $classId = $enrollment->field_class->target_id;
        if (!isset($enrolledClasses[$studentId])) {
          $enrolledClasses[$studentId] = [];
643
        }
644 645 646 647 648
        // Does the grader have access to the class?
        $isGraderForClass = $this->currentUser->isGraderOfClassNid($classId);
        if ($isGraderForClass) {
          $enrolledClasses[$studentId][] = $classId;
        }
mathieso's avatar
mathieso committed
649
      }
650
      // Prep data for sending to client.
mathieso's avatar
mathieso committed
651 652
      /** @var \Drupal\user\Entity\User $student */
      foreach ($students as $student) {
653
        // Skip students there is no enrollment record for.
mathieso's avatar
mathieso committed
654
        // Should not happen because of logic above.
mathieso's avatar
mathieso committed
655
        if (isset($enrolledClasses[$student->id()])) {
656 657
          $isCanSeeNames = $this->getIsCanSeeNames($student);
          if ($isCanSeeNames) {
658 659
            // Filter content, just in case.
            /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
660
            $firstName = $this->filterInputService->filterUserContent($student->field_first_name->value);
661
            /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
662
            $lastName = $this->filterInputService->filterUserContent($student->field_last_name->value);
663 664
          }
          else {
665 666
            $firstName = $this->t('(Name withheld)');
            $lastName = $this->t('(Name withheld)');
667
          }
mathieso's avatar
mathieso committed
668 669
          $result[$student->id()] = [
            'studentId' => $student->id(),
670 671
            'firstName' => $firstName,
            'lastName' => $lastName,
mathieso's avatar
mathieso committed
672
            'classesStudentIsIn' => $enrolledClasses[$student->id()],
673
            'isCanSeeNames' => $isCanSeeNames,
mathieso's avatar
mathieso committed
674
          ];
675 676
        }
      }
mathieso's avatar
mathieso committed
677 678
    }
    return new JsonResponse($result);
679 680
  }

681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
  /**
   * Can the current user see the names of a student?
   *
   * @param \Drupal\user\Entity\User $student
   *   The student.
   *
   * @return bool
   *   True if allowed, else false.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\skilling\Exception\SkillingException
   */
  protected function getIsCanSeeNames(User $student) {
    // Only graders who are also instructors can see student names.
    $instructorOfStudent = $this->userRelationshipService->isUserUidInstructorOfUserUid(
      $this->currentUser->id(), $student->id()
    );
    $graderOfStudent = $this->userRelationshipService->isUserUidGraderOfUserUid(
      $this->currentUser->id(), $student->id()
    );
    // Can graders see names?
    $settings = $this->configFactory->get(SkillingConstants::SETTINGS_MAIN_KEY);
    $gradersSeeStudentNames = $settings->get(SkillingConstants::SETTING_KEY_GRADERS_SEE_STUDENT_NAMES);
    // Flag.
    $isCanSeeNames = FALSE;
    if ($instructorOfStudent) {
      $isCanSeeNames = TRUE;
    }
    if ($graderOfStudent && $gradersSeeStudentNames) {
      $isCanSeeNames = TRUE;
    }
    return $isCanSeeNames;
  }

716 717 718
  /**
   * Get grader data for grader interface.
   *
719 720 721 722
   * Security notes. No input from client, except for GET.
   *
   * Output to client includes data from graders. Filter it.
   *
mathieso's avatar
mathieso committed
723 724
   * This is not a state-changing operation.
   *
725
   * @param \Symfony\Component\HttpFoundation\Request $request
726
   *   HTTP request.
727 728
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
729
   *   Grader data.
mathieso's avatar
mathieso committed
730 731 732
   *
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
733 734
   */
  public function getGraderForGrader(Request $request) {
735
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
mathieso's avatar
mathieso committed
736 737 738 739 740 741 742
      $request,
      ['GET'],
      ['/skilling/get-grader-for-grader']
    );
    if (!$passedBasicSecurity) {
      throw new AccessDeniedException('Access denied');
    }
743 744
    // Is the current user a grader?
    if (!$this->currentUser->isGrader()) {
mathieso's avatar
mathieso committed
745 746
      throw new AccessDeniedException('Access denied');
    }
747
    $drupalUser = $this->currentUser->getDrupalUser();
748
    /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
749
    $result = [
750
      'graderId' => $drupalUser->id(),
mathieso's avatar
mathieso committed
751 752
      'firstName' => $this->filterInputService->filterUserContent($drupalUser->field_first_name->value),
      'lastName' => $this->filterInputService->filterUserContent($drupalUser->field_last_name->value),
mathieso's avatar
mathieso committed
753 754
    ];
    return new JsonResponse($result);
755 756 757 758 759
  }

  /**
   * Get exercises for grader.
   *
760 761 762 763 764 765 766 767 768 769
   * Security notes. The method gets ids from the client.
   * Make sure they are numeric, and all are ids for exercises.
   * Also check that the number of exercises retrieved from Drupal
   * is the same as the number of ids passed.
   *
   * All data returned from this method to the client comes from
   * authors. Still, in the context of the grading client, extra
   * tags added into exercise fields are unlikely to be functionally
   * relevant. Therefore, content is filtered using Stripped.
   *
mathieso's avatar
mathieso committed
770 771
   * This is not a state-changing operation.
   *
772
   * @param \Symfony\Component\HttpFoundation\Request $request
773
   *   HTTP request.
774 775
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
mathieso's avatar
mathieso committed
776 777 778 779
   *   Exercise data.
   *
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
780
   *
781
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
782
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
783 784
   */
  public function getExercisesForGrader(Request $request) {
785
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
786 787 788 789
      $request,
      ['GET'],
      ['/skilling/get-exercises-for-grader']
    );
mathieso's avatar
mathieso committed
790
    if (!$passedBasicSecurity) {
791 792
      throw new AccessDeniedException('Access denied');
    }
793
    if (!$this->currentUser->isGrader()) {
794 795 796
      throw new AccessDeniedException('Access denied');
    }
    $exerciseIds = $request->query->get('exerciseIds');
mathieso's avatar
mathieso committed
797
    if (!$exerciseIds) {
798 799
      throw new AccessDeniedException('Access denied');
    }
800 801 802 803 804 805 806 807 808 809 810
    // Every element should be numeric.
    $allNumeric = TRUE;
    foreach ($exerciseIds as $exerciseId) {
      if (!is_numeric($exerciseId)) {
        $allNumeric = FALSE;
        break;
      }
    }
    if (!$allNumeric) {
      throw new AccessDeniedException('Access denied');
    }
811
    $result = [];
mathieso's avatar
mathieso committed
812
    if (count($exerciseIds) > 0) {
813 814
      $exercises = $this->entityTypeManager->getStorage('node')
        ->loadMultiple($exerciseIds);
815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
      // Check that we got the expected number of exercises from the DB.
      if (count($exercises) !== count($exerciseIds)) {
        throw new AccessDeniedException('Access denied');
      }
      // Make sure that every entity is an exercise.
      $allExercises = TRUE;
      /** @var \Drupal\Node\NodeInterface $exercise */
      foreach ($exercises as $exercise) {
        if ($exercise->bundle() !== SkillingConstants::EXERCISE_CONTENT_TYPE) {
          $allExercises = FALSE;
        }
      }
      if (!$allExercises) {
        throw new AccessDeniedException('Access denied');
      }
830 831 832
      /** @var \Drupal\node\Entity\Node $exercise */
      foreach ($exercises as $exercise) {
        $rubricItemIds = [];
833
        /* @noinspection PhpUndefinedFieldInspection */
834 835 836
        foreach ($exercise->field_rubric_items as $rubricItem) {
          $rubricItemIds[] = $rubricItem->target_id;
        }
837 838 839 840 841
        $bodyToShow = $exercise->body->value;
        $titleToShow = $this->filterInputService->filterUserContent($exercise->title->value);
        if (!$exercise->isPublished()) {
          $titleToShow .= (string)($this->t(" (U)"));
        }
842
        /* @noinspection PhpUndefinedFieldInspection */
843 844
        $result[$exercise->id()] = [
          'exerciseId' => $exercise->id(),
845 846
          'published' => $exercise->isPublished(),
          'title' => $titleToShow,
mathieso's avatar
mathieso committed
847 848
          'description' => $this->filterInputService->filterUserContent($bodyToShow),
//          'description' => $this->filterInputService->filterUserContent($exercise->body->value),
mathieso's avatar
mathieso committed
849
          'notes' => $this->filterInputService->filterUserContent($exercise->field_notes->value),
850 851 852 853 854
          'rubricItemIds' => $rubricItemIds,
        ];
      }
    }
    return new JsonResponse($result);
855
  }
856

857
  /**
858 859 860 861 862 863 864 865 866
   * Get rubric items for grader.
   *
   * Security notes. This method gets an array of ids from the client.
   * Check that all are numeric, and correspond to rubric item ids. Also
   * check that the number of items retrieved from Drupal matches the number
   * that is expected.
   *
   * All of the content the method sends to the client originates from authors.
   * Apply Stripped to it anyway, because... reasons.
867
   *
mathieso's avatar
mathieso committed
868 869
   * This is not a state-changing operation.
   *
870
   * @param \Symfony\Component\HttpFoundation\Request $request
871
   *   HTTP request.
872 873
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
874 875
   *   Rubric item data.
   *
mathieso's avatar
mathieso committed
876 877 878
   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
   * for HTTP methods for state changing operations.
   *
879
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
880
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
881 882
   */
  public function getRubricItemsForGrader(Request $request) {
883
    $passedBasicSecurity = $this->ajaxSecurityService->securityCheckAjaxRequest(
884 885 886 887
      $request,
      ['GET'],
      ['/skilling/get-rubric-items-for-grader']
    );
mathieso's avatar
mathieso committed
888
    if (!$passedBasicSecurity) {
889 890
      throw new AccessDeniedException('Access denied');
    }
891
    if (!$this->currentUser->isGrader()) {
892 893
      throw new AccessDeniedException('Access denied');
    }
894
    // Get rubric item ids from request.
895
    $rubricItemIds = $request->query->get('rubricItemIds');
mathieso's avatar
mathieso committed
896
    if (!$rubricItemIds) {
897 898
      throw new AccessDeniedException('Access denied');
    }
899 900 901 902 903 904 905 906 907 908 909
    // Every element should be numeric.
    $allNumeric = TRUE;
    foreach ($rubricItemIds as $rubricItemId) {
      if (!is_numeric($rubricItemId)) {
        $allNumeric = FALSE;
        break;
      }
    }
    if (!$allNumeric) {
      throw new AccessDeniedException('Access denied');
    }
910 911
    $resultRIs = [];
    $resultRIOptions = [];
mathieso's avatar
mathieso committed
912
    if (count($rubricItemIds) > 0) {
913
      // Load rubric items from ids.
914 915
      $rubricItems = $this->entityTypeManager->getStorage('node')
        ->loadMultiple($rubricItemIds);
916 917 918 919 920 921 922 923 924 925 926 927 928 929 930
      // We should have a known number of items.
      if (count($rubricItems) !== count($rubricItems)) {
        throw new AccessDeniedException('Access denied');
      }
      // Make sure that every entity is a rubric item.
      $allRubricItems = TRUE;
      /** @var \Drupal\Node\NodeInterface $rubricItem */
      foreach ($rubricItems as $rubricItem) {
        if ($rubricItem->bundle() !== SkillingConstants::RUBRIC_ITEM_CONTENT_TYPE) {
          $allRubricItems = FALSE;
        }
      }
      if (!$allRubricItems) {
        throw new AccessDeniedException('Access denied');
      }
931 932
      /** @var \Drupal\node\Entity\Node $rubricItem */
      foreach ($rubricItems as $rubricItem) {
933
        // Load rubric item responses for the rubric item.
934
        $rubricItemResponseIds = [];
935
        /* @noinspection PhpUndefinedFieldInspection */
936 937 938
        $responseParagraphList = $rubricItem->field_rubric_item_responses->getValue();
        foreach ($responseParagraphList as $responseRef) {
          $rubricItemResponseIds[] = $responseRef['target_id'];
939
        }
940
        // Load the responses.
941
        $rubricItemResponses = $this->entityTypeManager
942
          ->getStorage('paragraph')
943
          ->loadMultiple($rubricItemResponseIds);
944 945
        // Make a rep of the rubric item for transfer.
        /* @noinspection PhpUndefinedFieldInspection */
946 947
        $ri = [
          'rubricItemId' => $rubricItem->id(),
mathieso's avatar
mathieso committed
948 949
          'title' => $this->filterInputService->filterUserContent($rubricItem->getTitle()),
          'notes' => $this->filterInputService->filterUserContent($rubricItem->field_notes->value),
950 951
          'responseOptionIds' => $rubricItemResponseIds,
        ];
952
        // Add RI to results.
953
        $resultRIs[$rubricItem->id()] = $ri;
mathieso's avatar
mathieso committed
954
        // Make a rep of the rubric item response option for transfer.
955
        /** @var \Drupal\paragraphs\ParagraphInterface $rubricItemResponse */
956
        foreach ($rubricItemResponses as $rubricItemResponse) {
957
          // Compute array of exercise ids the option applies to.
mathieso's avatar
mathieso committed
958 959
          // If MT, applies to all exercises that have the RI that has the
          // rubric item response option.
960
          $exerciseAppliesToIds = [];
961
          /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
962
          foreach ($rubricItemResponse->field_exercises as $appliesTo) {
963 964
            $exerciseAppliesToIds[] = $appliesTo->target_id;
          }
965
          /* @noinspection PhpUndefinedFieldInspection */
mathieso's avatar
mathieso committed
966
          $rubricItemResponseOption = [
967
            'responseOptionId' => $rubricItemResponse->id(),
mathieso's avatar
mathieso committed
968 969
            'response' => $this->filterInputService->filterUserContent($rubricItemResponse->field_response_to_student->value),
            'notes' => $this->filterInputService->filterUserContent($rubricItemResponse->field_notes->value),
mathieso's avatar
mathieso committed
970
            'completes' => $rubricItemResponse->field_completes_rubric_item->value,
971 972
            'appliesToExerciseIds' => $exerciseAppliesToIds,
          ];
mathieso's avatar
mathieso committed
973 974
          // Add rubric item response option to results.
          $resultRIOptions[$rubricItemResponse->id()] = $rubricItemResponseOption;
975 976 977
        } //End for each rubric item response.
      } //End for each rubric item.
    }