Commit 675f7e7b authored by mathieso's avatar mathieso

Grading changes, and bug fixes.

parent f5005841
This diff is collapsed.
This diff is collapsed.
......@@ -24,23 +24,19 @@ function showSubmittedFile(event) {
src="${url}" type="text/plain; length=16"></iframe>
</div>`;
newWindow.document.body.innerHTML = htmlOutput;
return;
let submittedFileData = window.submissions[submissionId].subFileLink[fileItemIndex];
console.log(submittedFileData);
newWindow = window.open("", "",
"resizable=yes,status=yes,width=600,height=600");
newWindow.document.title = "Student file submission";
htmlOutput = `
<h3>Student attached file</h3>
<div style="font-family: Sans-Serif;">
<p><strong>Student:</strong> ${submittedFileData.studentName}</p>
<p><strong>Exercise:</strong> ${submittedFileData.exerciseName}</p>
<p><strong>File:</strong> ${submittedFileData.fileName}</p>
<iframe style="width: 100%;height: 100%;" id="student-submission" style="" src="${submittedFileData.submittedFileUrl}"></iframe>
</div>`;
newWindow.document.body.innerHTML = htmlOutput;
// return;
// let submittedFileData = window.submissions[submissionId].subFileLink[fileItemIndex];
// console.log(submittedFileData);
// newWindow = window.open("", "",
// "resizable=yes,status=yes,width=600,height=600");
// newWindow.document.title = "Student file submission";
// htmlOutput = `
// <h3>Student attached file</h3>
// <div style="font-family: Sans-Serif;">
// <p><strong>Student:</strong> ${submittedFileData.studentName}</p>
// <p><strong>Exercise:</strong> ${submittedFileData.exerciseName}</p>
// <p><strong>File:</strong> ${submittedFileData.fileName}</p>
// <iframe style="width: 100%;height: 100%;" id="student-submission" style="" src="${submittedFileData.submittedFileUrl}"></iframe>
// </div>`;
// newWindow.document.body.innerHTML = htmlOutput;
}
......@@ -11,7 +11,7 @@
v-show="isAnyOptionSelected"
@click.stop="clearChoice"
title="Clear your choice"
>&#x2718;</span>
>&#x21ba;</span>
<div class="pull-right">
<!--Button to collapse/expand rubric item option deets.
Used Bootstrap animations rather than Vue transitions, because I like the effect.
......@@ -98,8 +98,6 @@
},
data() {
return {
//Which response did the grader choose for this item?
// responseOptionIdChosen: 0,
//Max length in characters of an option summary.
maxOptionSummaryLength: 80,
/**
......@@ -115,7 +113,6 @@
},
computed: {
rubricItem(){
// console.log('rubricItem()');
return window.rubricItems[this.rubricItemId];
},
},
......@@ -157,10 +154,8 @@
* User clicked keep button on a new option.
*/
clickedKeepButton(optionIndex) {
// console.log('rubric item clickedKeepButton. optionIndex: ',optionIndex);
this.newOptions[optionIndex].keep
= ! this.newOptions[optionIndex].keep;
// console.log('this.newOptions[optionIndex].keep',this.newOptions[optionIndex].keep);
this.$nextTick(()=>{
this.$forceUpdate();
});
......@@ -170,7 +165,6 @@
* User clicked delete button on a new option.
*/
clickedDeleteButton(optionIndex) {
// console.log('rubric item clickedDeleteButton. optionIndex: ',optionIndex);
this.newOptions.splice(optionIndex, 1);
this.$nextTick(()=>{
this.$forceUpdate();
......@@ -235,11 +229,9 @@
*/
rubricItemComplete() {
let $collapseButton = $(this.$refs.collapseButton);
// console.log("$collapseButton", $collapseButton);
window.collapseButton = $collapseButton;
//Only collapse if not already collapsed.
if ( $collapseButton.find('span.glyphicon').hasClass('glyphicon-triangle-bottom') ) {
// console.log("click collapse");
$collapseButton.click();
}
},
......@@ -254,7 +246,6 @@
* User wants to forget choices.
*/
clearChoice(event) {
// console.log("clear event", event);
// Show the RI choices.
const target = $(event.target);
var $panel = target.closest('.panel');
......@@ -289,36 +280,6 @@
result = chosen.join("");
}
return '<small>' + result + '</small>';
// let chosen = [];
// let submissionRubricItemChoices = window.submissions[this.submissionId].rubricItemChoices;
// //Are there entries for this rubric item?
// if ( submissionRubricItemChoices[this.rubricItemId] !== undefined ) {
// //Yes.
// //Get the choices for this rubric item.
// let choiceIds = submissionRubricItemChoices[this.rubricItemId];
// choiceIds.forEach( choiceId => {
// let choiceResponse = window.responseOptions[choiceId].response;
// //Trim to max length.
// if ( choiceResponse.length > this.maxOptionSummaryLength ) {
// choiceResponse = choiceResponse.substring(0, this.maxOptionSummaryLength - 4) + '...';
// }
// chosen.push(choiceResponse);
// });
// } //End if there are choices.
// //See if there are any new items that need to be added.
// this.newOptions.forEach( newOption => {
// if ( newOption.chosen ) {
// chosen.push( newOption.title.trim() );
// }
// });
// let result = '';
// if ( chosen.length === 0 ) {
// result = '(Nothing chosen)';
// }
// else {
// result = chosen.join('<br>');
// }
// return '<small>' + result + '</small>';
},
}
}
......
......@@ -57,28 +57,32 @@
</div>
<div class="dropdown" id="exerciseCompletedContainer">
<button
class="btn btn-primary dropdown-toggle"
type="button" id="exerciseCompleted"
title="Has the student completed the exercise?"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"
:disabled="submissionGraded"
>
<span :class="exerciseCompletedClass()">
{{ exerciseCompletedLabel() }}
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="exerciseCompleted">
<li>
<a @click="exerciseCompletedClicked" data-completed="true" href="#"
>Complete</a>
</li>
<li>
<a @click="exerciseCompletedClicked" data-completed="false" href="#"
>Not complete</a>
</li>
</ul>
<span :class="exerciseCompletedClass()">
{{ exerciseCompletedLabel() }}
</span>
<!-- <button-->
<!-- class="btn btn-primary dropdown-toggle"-->
<!-- type="button"-->
<!-- id="exerciseCompleted"-->
<!-- title="Has the student completed the exercise?"-->
<!-- data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"-->
<!-- :disabled="submissionGraded"-->
<!-- >-->
<!-- <span :class="exerciseCompletedClass()">-->
<!-- {{ exerciseCompletedLabel() }}-->
<!-- </span>-->
<!-- <span class="caret"></span>-->
<!-- </button>-->
<!-- <ul class="dropdown-menu" aria-labelledby="exerciseCompleted">-->
<!-- <li>-->
<!-- <a @click="exerciseCompletedClicked" data-completed="true" href="#"-->
<!-- >Complete</a>-->
<!-- </li>-->
<!-- <li>-->
<!-- <a @click="exerciseCompletedClicked" data-completed="false" href="#"-->
<!-- >Not complete</a>-->
<!-- </li>-->
<!-- </ul>-->
</div>
</div>
......@@ -101,6 +105,13 @@
>Send message
</button>
<!--:disabled="sendMessageButtonDisabled()"-->
<!-- v-show="showThrobber"-->
<img src="src/images/ajax-loader.gif"
id="feedback-throbber"
title="Processing..."
alt="Processing..."
style="position: absolute; right: 8px; display: none;"
>
</div> <!-- End CP line 2 -->
</div> <!-- End CP -->
......@@ -148,6 +159,10 @@
submissionGraded: {
type: Boolean,
},
showThrobber: {
type: Boolean,
default: false
}
},
watch: {
graderTypedKey(){
......@@ -216,13 +231,6 @@
let label = complete ? 'Complete' : 'Not complete';
//Update data store.
window.submissions[this.submissionId].exerciseCompleted = complete;
//Update class of button.
$($('#exerciseCompletedContainer span').get(0))
.attr('class', 'completed-status');
// .attr('class', 'text-success');
//Update text on button.
$($('#exerciseCompletedContainer span').get(0)).text(label);
// thisOverallEvalClicked = this;
this.$forceUpdate();
},
//Update this component.
......@@ -289,11 +297,11 @@
let graded = numRubricItemsForExercise - this.numUngradedItems();
return graded + '/' + numRubricItemsForExercise;
},
//What class for the exercise complete dropdown button?
//What class for the exercise complete message?
exerciseCompletedClass(){
let result = 'completed-status';
// let result = (window.submissions[this.submissionId].exerciseCompleted === '')
// ? 'text-danger' : 'text-success';
// let result = 'completed-status';
let result = (window.submissions[this.submissionId].exerciseCompleted)
? 'text-success' : 'text-danger';
return result;
},
//What label for the exercise complete dropdown button?
......@@ -313,11 +321,11 @@
return result;
},
//Grader clicked exercise complete option.
exerciseCompletedClicked(){
window.submissions[this.submissionId].exerciseCompleted
= $(event.target).data('completed');
this.$forceUpdate();
},
// exerciseCompletedClicked(){
// window.submissions[this.submissionId].exerciseCompleted
// = $(event.target).data('completed');
// this.$forceUpdate();
// },
//Return create message button disabled state.
//Button is disabled until grader has chosen overall eval, and
//whether completed.
......@@ -345,7 +353,7 @@
// },
//User clicked the Create message button.
createMessage() {
console.log('createMessage');
// console.log('createMessage');
let message = '';
const student = window.students[
window.submissions[this.submissionId].studentId
......@@ -378,11 +386,11 @@
// Get the options for that RI.
// const rubricItemOptions = window.rubricItems[rubricItemId].responseOptionIds;
let graderChoices = window.submissions[this.submissionId].rubricItemChoices;
console.log("graderChoices", graderChoices);
// console.log("graderChoices", graderChoices);
const exerciseId = window.submissions[this.submissionId].exerciseId;
console.log("exerciseId", exerciseId);
// console.log("exerciseId", exerciseId);
const exerciseRubricItemIds = window.exercises[exerciseId].rubricItemIds;
console.log("exerciseRubricItemIds", exerciseRubricItemIds);
// console.log("exerciseRubricItemIds", exerciseRubricItemIds);
message += '<ul>';
// Run through the RIs in the order they appear for the exercise.
for (
......@@ -391,9 +399,9 @@
exerciseRubricItemIdIndex ++
) {
const rubricItemId = exerciseRubricItemIds[exerciseRubricItemIdIndex];
console.log("start outer loop rubricItemId", rubricItemId);
// console.log("start outer loop rubricItemId", rubricItemId);
let graderResponseOptionsChoicesForRI = graderChoices[rubricItemId];
console.log("graderResponseOptionsChoicesForRI", graderResponseOptionsChoicesForRI);
// console.log("graderResponseOptionsChoicesForRI", graderResponseOptionsChoicesForRI);
// Grader might not have chosen anything for the rubric item.
if (graderResponseOptionsChoicesForRI) {
for (
......@@ -402,7 +410,7 @@
roArrayIndex++
) {
const responseOptionId = graderResponseOptionsChoicesForRI[roArrayIndex];
console.log("responseOptionId", responseOptionId);
// console.log("responseOptionId", responseOptionId);
let responseText = window.responseOptions[responseOptionId].response;
if (isCanSeeNames) {
responseText = this.replaceStudentNameTokens(responseText, student);
......@@ -410,7 +418,7 @@
else {
responseText = this.killStudentNameTokens(responseText);
}
console.log("responseText", responseText);
// console.log("responseText", responseText);
message += `<li>${responseText}</li>`;
// Add response options to the output.
}
......@@ -538,6 +546,9 @@
//Erase the new options for the rubric item.
ri.newOptions = [];
}); //End for each rubric item.
//Show throbber.
// this.showThrobber = true;
$("#feedback-throbber").show();
//Send the new options to the server, if there are any.
//Server saves them, and sends back the new ids for the options.
saveNewResponseOptions(newOptions)
......@@ -597,9 +608,11 @@
} //End if there was data from the server.
//Finished with the new options.
//Now store the feedback.
console.log('afore save fb. submission:', submission);
// console.log('afore save fb. submission:', submission);
saveFeedback(submission.submissionId)
.done(function (dataReturned) {
// this.showThrobber = false;
$("#feedback-throbber").hide();
submission.feedbackDate = new Date();
// window.submissions[submission.submissionId].assessed = true;
//console.log('saveFeedback', dataReturned);
......
......@@ -14,6 +14,22 @@ X - initial valuies of new config flags and shit.
? Students can't see/edit their first/last name.
# Subm
Show submit new button when should not abe able to.
Also add check when showing new window.
Give perm to submissions admin view?
Edit submissions.
Does the instructor views access plugin work? Make one for graders and admin too
Add record selection thing?
# Exercises
Max days after no submission possible.
......
......@@ -2814,6 +2814,7 @@ function skilling_theme($existing, $type, $theme, $path) {
'challenge_image_url' => NULL,
'submission_links' => NULL,
'show_exercise_link' => NULL,
'submission_status' => NULL,
],
],
'insert_pattern' => [
......@@ -2990,6 +2991,7 @@ function skilling_theme($existing, $type, $theme, $path) {
'internal_name' => NULL,
'submission_links' => NULL,
'show_exercise_link' => NULL,
'submission_status' => NULL,
],
],
'student_submissions_for_exercise' => [
......@@ -2997,6 +2999,7 @@ function skilling_theme($existing, $type, $theme, $path) {
'exercise_nid' => NULL,
'internal_name' => NULL,
'submission_links' => NULL,
'submission_status' => NULL,
],
],
// List of lesson links for an exercise.
......
......@@ -10,7 +10,7 @@ services:
arguments: ['@token', '@entity_type.manager', '@skilling.utilities', '@skilling.skilling_current_user', '@skilling.skilling_user_factory', '@skilling.ajax_security']
skilling.submissions:
class: Drupal\skilling\Submissions
arguments: ['@entity_type.manager', '@skilling.skilling_current_user', '@skilling.utilities', '@skilling.current_class', '@config.factory', '@skilling.nid_bag', '@skilling.skillingparser', '@skilling.filter_user_input']
arguments: ['@entity_type.manager', '@skilling.skilling_current_user', '@skilling.utilities', '@skilling.current_class', '@config.factory', '@skilling.nid_bag', '@skilling.skillingparser', '@skilling.filter_user_input', '@skilling.class']
plugin.manager.skilling.custom_tag:
class: Drupal\skilling\Plugin\SkillingCustomTagManager
parent: default_plugin_manager
......
......@@ -405,11 +405,32 @@ class CurrentClass {
}
/**
* Get the exercise due paragraph items for a class.
* Published exercises only.
* Get exercise due record for an exercise for the current class.
* Return null if there is no record for the exercise.
*
* @param int $exerciseId
* Exercise id.
*
* @param NodeInterface $class
* The class to get exercise dues for.
* @return NodeInterface|null
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\skilling\Exception\SkillingException
*/
public function getExerciseDueForExercise($exerciseId) {
if (!$this->currentClass) {
throw new SkillingException(
Html::escape('No current class.'), __FILE__, __LINE__
);
}
return $this->skillingClass->getExerciseDueRecordForExercise(
$this->currentClass, $exerciseId
);
}
/**
* Get the exercise due paragraph items for the class.
* Published exercises only.
*
* @return array
* The exercise dues paragraph items, and exercise nids.
......@@ -417,60 +438,15 @@ class CurrentClass {
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\skilling\Exception\SkillingException
*/
public function getExerciseDues() {
$exerciseDues = [];
$exerciseNids = [];
$exerciseDueParaRefs = $this->currentClass->get(SkillingConstants::FIELD_EXERCISES_DUE)->getValue();
foreach ($exerciseDueParaRefs as $exerciseDueParaRef) {
$exerciseDueParagraph = $this->entityTypeManager->getStorage('paragraph')
->load($exerciseDueParaRef['target_id']);
if (!$exerciseDueParagraph) {
// In case the para does not exist.
continue;
}
$exerciseId = (int)$exerciseDueParagraph->get(SkillingConstants::FIELD_EXERCISE)
->getValue()[0]['target_id'];
/** @var NodeInterface $exercise */
$exercise = $this->entityTypeManager->getStorage('node')
->load($exerciseId);
if (!$exercise) {
// In case the exercise does not exist.
$exerciseDueParagraph->delete();
continue;
}
if ($exercise->isPublished()) {
// Check that something exists.
$dayDataieThing = $exerciseDueParagraph->get(SkillingConstants::FIELD_DAY)
->getValue();
if (isset($dayDataieThing[0])) {
$day = (int) $exerciseDueParagraph->get(SkillingConstants::FIELD_DAY)
->getValue()[0]['value'];
$required = (boolean) $exerciseDueParagraph->get(SkillingConstants::FIELD_REQUIRED)
->getValue()[0]['value'];
// Compute max subs allowed for exercise.
$exerMaxSubs = $exerciseDueParagraph->get(SkillingConstants::FIELD_MAX_SUBMISSIONS)
->getValue()[0]['value'];
if (is_null($exerMaxSubs)) {
$exerMaxSubs = $this->currentClass->get(SkillingConstants::FIELD_DEFAULT_MAX_SUBMISSIONS)
->getValue()[0]['value'];
}
else {
$exerMaxSubs = (integer) $exerMaxSubs;
}
// nulll or "2"
$exerciseDue = [
'exerciseId' => $exerciseId,
'day' => $day,
'required' => $required,
'maxSubs' => $exerMaxSubs,
];
$exerciseDues[] = $exerciseDue;
$exerciseNids[] = $exerciseId;
}
}
if (!$this->currentClass) {
throw new SkillingException(
Html::escape('No current class.'), __FILE__, __LINE__
);
}
return [$exerciseDues, $exerciseNids];
return $this->skillingClass->getExerciseDues($this->currentClass);
}
/**
......@@ -478,12 +454,25 @@ class CurrentClass {
* @return mixed
*/
public function getDefaultMaxSubmissions() {
$exerMaxSubs = $this->currentClass
->get(SkillingConstants::FIELD_DEFAULT_MAX_SUBMISSIONS)
->getValue()[0]['value'];
return $exerMaxSubs;
return $this->skillingClass->getDefaultMaxSubmissions($this->currentClass);
}
/**
* @param $exerciseId
*
* @return int|NULL
* @throws \Drupal\skilling\Exception\SkillingException
*/
public function getMaxSubmissionsAllowedForExercise($exerciseId) {
if (!$this->currentClass) {
throw new SkillingException(
Html::escape('No current class.'), __FILE__, __LINE__
);
}
return ;
}
/**
* Get the start date.
* @return mixed
......
......@@ -238,18 +238,14 @@ class ExerciseTag extends SkillingCustomTagBase {
$exerciseMaxSubsDisplay = null;
$whenDueDisplay = null;
$numberOfSubmissions = null;
$exerciseSubmissionStatus = null;
if (!is_null($this->currentClass->getCurrentClass())) {
// Submissions from the user.
$exerciseSubmissionStatus = $this->submissionsService->getSubmissionStatusForStudentExercise(
$this->currentUser, $exerciseId, $this->currentClass
);
$numberOfSubmissions = count($renderableSubmissionLinks['submissions']);
list($classExerciseDues, $classExerciseNids) = $this->currentClass->getExerciseDues();
// Find the exercise due data for the exercise.
$exerciseDue = NULL;
foreach ($classExerciseDues as $record) {
if ($record['exerciseId'] == $exercise->id()) {
$exerciseDue = $record;
break;
}
}
$exerciseDue = $this->currentClass->getExerciseDueForExercise($exerciseId);
if (is_null($exerciseDue)) {
// Exercise not in class timeline.
$whenDueDisplay = -1;
......@@ -298,12 +294,13 @@ class ExerciseTag extends SkillingCustomTagBase {
'#internal_name' => $exercise->field_internal_name->value,
'#due_date' => $whenDueDisplay,
'#required' => $isRequiredDisplay,
'#max_subs' => $exerciseMaxSubsDisplay,
'#number_subs' => $numberOfSubmissions,
'#max_subs' => (int)$exerciseMaxSubsDisplay,
'#number_subs' => (int)$numberOfSubmissions,
'#challenge' => $challenge,
'#challenge_image_url' => $challengeImageUrl,
'#submission_links' => $renderableSubmissionLinks,
'#show_exercise_link' => $showExerciseLink ? 'yes' : 'no',
'#submission_status' => $exerciseSubmissionStatus,
'#cache' => [
'max-age' => 0,
],
......
......@@ -23,7 +23,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* help = @Translation("Access based on whether the current user is an instructor of a class with node id in a contextual filter parameter."),
* )
*/
class ViewsInstructorAccess extends AccessPluginBase {
class ViewsInstructorOfClassAccess extends AccessPluginBase {
/**
* {@inheritdoc}
......@@ -77,6 +77,7 @@ class ViewsInstructorAccess extends AccessPluginBase {
* Returns whether the user has access to the view.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function access(AccountInterface $account) {
// This works, but is positional. Is there a better way?
......
......@@ -2,8 +2,10 @@
namespace Drupal\skilling;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\NodeInterface;
use Drupal\skilling\Exception\SkillingException;
use Drupal\skilling\Exception\SkillingWrongTypeException;
use Drupal\Core\Messenger\MessengerInterface;
......@@ -122,5 +124,159 @@ class SkillingClass {
}
/**
* Get the exercise due paragraph items for a class.
* Published exercises only.
*
* @param NodeInterface $class
* The class to get exercise dues for.
*
* @return array
* The exercise dues paragraph items, and exercise nids.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function getExerciseDues(NodeInterface $class) {
if (!$class) {
throw new SkillingException(
Html::escape('No class given.'), __FILE__, __LINE__
);
}
$exerciseDues = [];
$exerciseNids = [];
$exerciseDueParaRefs = $class->get(SkillingConstants::FIELD_EXERCISES_DUE)->getValue();
foreach ($exerciseDueParaRefs as $exerciseDueParaRef) {
$exerciseDueParagraph = $this->entityTypeManager->getStorage('paragraph')
->load($exerciseDueParaRef['target_id']);
if (!$exerciseDueParagraph) {
// In case the para does not exist.
continue;
}
$exerciseId = (int)$exerciseDueParagraph->get(SkillingConstants::FIELD_EXERCISE)
->getValue()[0]['target_id'];
/** @var NodeInterface $exercise */
$exercise = $this->entityTypeManager->getStorage('node')
->load($exerciseId);
if (!$exercise) {
// In case the exercise does not exist.
$exerciseDueParagraph->delete();
continue;
}
if ($exercise->isPublished()) {
// Check that something exists.
$dayDataieThing = $exerciseDueParagraph->get(SkillingConstants::FIELD_DAY)
->getValue();
if (isset($dayDataieThing[0])) {
$day = (int) $exerciseDueParagraph->get(SkillingConstants::FIELD_DAY)
->getValue()[0]['value'];
$required = (boolean) $exerciseDueParagraph->get(SkillingConstants::FIELD_REQUIRED)
->getValue()[0]['value'];
// Compute max subs allowed for exercise.
$exerMaxSubs = $exerciseDueParagraph->get(SkillingConstants::FIELD_MAX_SUBMISSIONS)
->getValue()[0]['value'];
if (is_null($exerMaxSubs)) {
$exerMaxSubs = $class->get(SkillingConstants::FIELD_DEFAULT_MAX_SUBMISSIONS)
->getValue()[0]['value'];
}
else {
$exerMaxSubs = (integer) $exerMaxSubs;
}
// nulll or "2"
$exerciseDue = [
'exerciseId' => $exerciseId,
'day' => $day,
'required' => $required,
'maxSubs' => $exerMaxSubs,
];
$exerciseDues[] = $exerciseDue;
$exerciseNids[] = $exerciseId;
}
}
}
return [$exerciseDues, $exerciseNids];
}
/**
* Get exercise due record for an exercise for the current class.
* Return null if there is no record for the exercise.
*
* @param \Drupal\node\NodeInterface $class
* @param int $exerci