Skip to content

SAK-50907 Assignments add option to replicate the self-report rubric to the grading rubric #13229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assignment/api/src/resources/assignment.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,7 @@ youhavetorubric=This is a self-report assignment. You must use the following rub
youhavetorubricone=This is a self-report assignment. You must use the following rubric to grade your work before submitting. You must complete at least one criterion of the rubric in order to submit the task.
studentrubric=This is a self-report assignment. You can check the self-report of submitter before grading.
autoevaluation=Autoevaluation:
copyAutoevaluation=Copy student's autoevaluation to the grading rubric
instructor_grading=Instructor Grading:
reviewrubric=This is a self-report assignment. You can check your self-report before grading.
reviewrubricreport=This is your self-report.
Expand Down
1 change: 1 addition & 0 deletions assignment/api/src/resources/assignment_ca.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@ youhavetorubric=Aquesta tasca \u00fas autoavaluable. Has d'autoavaluar la teva t
youhavetorubricone=Aquesta tasca \u00fas autoavaluable. Has d'autoavaluar la teva tasca amb la r\u00FAbrica abans d'enviar-la. Has de completar almenys un criteri de la r\u00FAbrica per fer l'enviament.
studentrubric=Aquesta tasca \u00E9s autoavaluable. Pots revisar l'autoavaluaci\u00F3 de l'estudiant abans de puntuar la tasca.
autoevaluation=Autoavaluaci\u00f3:
copyAutoevaluation=Copiar l'autoavaluaci\u00f3 de l'estudiant a la r\u00fabrica de correcci\u00f3
instructor_grading=Correcci\u00f3 del profesor:
reviewrubric=Aquesta tasca \u00E9s autoavaluable. Pot revisar la autoavaluaci\u00F3 abans d'enviar-la.
reviewrubricreport=Aquesta \u00E9s la teva autoavaluaci\u00F3.
Expand Down
1 change: 1 addition & 0 deletions assignment/api/src/resources/assignment_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,7 @@ youhavetorubricone=Esta tarea es autoevaluable. Debes autoevaluar tu tarea con l
studentrubric=Esta tarea es autoevaluable. Puedes revisar la autoevaluaci\u00f3n del estudiante antes de puntuar la tarea.
autoevaluation=Autoevaluaci\u00f3n\:
instructor_grading=Correcci\u00f3n del docente\:
copyAutoevaluation=Copiar la autoevaluaci\u00f3n del estudiante a la r\u00fabrica de evaluaci\u00f3n
reviewrubric=Esta tarea es autoevaluable. Puedes revisar tu autoevaluaci\u00f3n antes de enviarla.
reviewrubricreport=Esta es tu autoevaluaci\u00f3n.

Expand Down
86 changes: 86 additions & 0 deletions assignment/tool/src/webapp/js/assignments.js
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,92 @@ ASN.disableTimesheetSetupSection = function()
ASN.toggleAutoAnnounceEstimate(false);
}

ASN.autocompleteRubricWithSelfReport = function(event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a brand new function, you should probably use a two space indentation. It's what we use in all the webcomponents and what we specify in our eslint config.

event.preventDefault();

const studentRubric = document.querySelector("sakai-rubric-student");
if (!studentRubric) {
console.error("sakai-rubric-student element not found");
return;
}

const gradingRubric = document.querySelector("sakai-rubric-grading");
if (!gradingRubric) {
console.error("sakai-rubric-grading element not found");
return;
}

const evaluationDetails = [];
const criterionRows = studentRubric.querySelectorAll(".criterion-row");
criterionRows.forEach(row => {
const criterionId = row.id.split("_").pop();
const selectedRating = row.querySelector(".rating-item.selected");
if (selectedRating) {
const selectedRatingId = selectedRating.id.split("-").pop();
evaluationDetails.push({ criterionId, selectedRatingId });
}
});
Comment on lines +1024 to +1033
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const evaluationDetails = [];
const criterionRows = studentRubric.querySelectorAll(".criterion-row");
criterionRows.forEach(row => {
const criterionId = row.id.split("_").pop();
const selectedRating = row.querySelector(".rating-item.selected");
if (selectedRating) {
const selectedRatingId = selectedRating.id.split("-").pop();
evaluationDetails.push({ criterionId, selectedRatingId });
}
});
const evaluationDetails = [...studentRubric.querySelectorAll(".criterion-row")].reduce(acc, row) => {
const selectedRatingId = row.querySelector(".rating-item.selected")?.id.split("-").pop();
selectedRatingId && acc.push({ criterionId: row.id.split("_").pop(), selectedRatingId });
return acc;
}, []);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm spreading the NodeList into an array and then reducing the array. I'm using optional chaining to remove some of the variables you had introduced.


if (evaluationDetails.length === 0) {
console.error("No evaluation details found in sakai-rubric-student");
return;
}

const selectedItems = gradingRubric.querySelectorAll(".rating-item.selected");
selectedItems.forEach(item => {
item.click();
});

let totalPoints = 0;
const decimalSeparator = 1.1.toLocaleString(portal.locale).substring(1, 2);
evaluationDetails.forEach(detail => {
const criterionRow = gradingRubric.querySelector(`#criterion_row_${detail.criterionId}`);
if (criterionRow) {
const ratingItem = criterionRow.querySelector(`.rating-item[data-rating-id="${detail.selectedRatingId}"]`);
if (ratingItem) {
ratingItem.click();
const pointsElement = ratingItem.querySelector(".points");
const pointsText = pointsElement.textContent.trim();
let points = parseFloat(pointsText.replace(",", "."));
const pointsInParentheses = pointsElement.querySelector("b");
if (pointsInParentheses) {
const pointsInParenthesesText = pointsInParentheses.textContent.trim().replace(/[()]/g, '');
points = parseFloat(pointsInParenthesesText.replace(",", "."));
}
totalPoints += points;
}
}
});
Comment on lines +1047 to +1064
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole block should go into the rubric code somewhere. It seems a bit fragile, especially selecting the bold tag to find the points in parentheses. You could move the code into rubrics and add a unit test.


const formattedTotalPoints = totalPoints.toLocaleString(portal.locale, { maximumFractionDigits: 2 }).replace(".", decimalSeparator);
const scoreGradeInput = document.querySelector("#grade");
if (scoreGradeInput) {
scoreGradeInput.value = formattedTotalPoints;
scoreGradeInput.dispatchEvent(new Event("keypress", { bubbles: true }));
}

const studentRubricAutocompleteConfirm = document.querySelector("#student-rubric-autocomplete-confirm");
if (studentRubricAutocompleteConfirm) {
studentRubricAutocompleteConfirm.classList.remove("d-none");
setTimeout(() => {
studentRubricAutocompleteConfirm.classList.add("d-none");
}, 2000);
}
};

document.addEventListener('rubric-student-rendered', function() {
const studentRubricButton = document.getElementById("student-rubric-autocomplete-button");
if (studentRubricButton) {
const rubricPreview = document.querySelector("sakai-rubric-criterion-preview");
if (rubricPreview) {
console.debug("sakai-rubric-criterion-preview element found, hiding button");
studentRubricButton.classList.add("d-none");
} else {
studentRubricButton.classList.remove("d-none");
}
}
});

$(document).ready(() => {
//peer-review and self-report
$('body').on('rubric-association-loaded', e => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,17 @@
evaluated-item-owner-id="$submitterId"
#end
></sakai-rubric-student>
<p class="help-block">
$tlang.getString("studentrubric")
</p>
<button id="student-rubric-autocomplete-button"
class="mb-2 btn-transparent text-start"
onclick="ASN.autocompleteRubricWithSelfReport(event)"
aria-haspopup="true"
title="$tlang.getString("copyAutoevaluation")">
<u>$tlang.getString("copyAutoevaluation")</u>
<i id="student-rubric-autocomplete-confirm" class="bi bi-check2 d-none"></i>
</button>
<hr class="itemSeparator" />
#end

Expand Down
1 change: 1 addition & 0 deletions webcomponents/bundle/src/main/bundle/grader.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ grading_rubric_tooltip=Grade this submission using a rubric
rubric=Rubric
autoevaluation=Autoevaluation:
openAutoevaluation=Check Autoevaluation
copyAutoevaluation=Copy student's autoevaluation to the grading rubric
studentrubric=This is a self-report assignment. You can check the self-report of submitter before grading.
add_feedback_tooltip=Write, or record, some feedback for this student
written_feedback_label=Write some feedback
Expand Down
1 change: 1 addition & 0 deletions webcomponents/bundle/src/main/bundle/grader_ca.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ grading_rubric_tooltip=Qualifica el lliurament usant una r\u00fabrica
rubric=R\u00fabrica
autoevaluation=Autoavaluaci\u00f3:
openAutoevaluation=Revisa l'autoavaluaci\u00f3
copyAutoevaluation=Copiar l'autoavaluaci\u00f3 de l'estudiant a la r\u00fabrica de correcci\u00f3
studentrubric=Aquesta tasca \u00E9s autoavaluable. Pots revisar l'autoavaluaci\u00F3 de l'estudiant abans de puntuar la tasca.
add_feedback_tooltip=Escriu, o enregistra, comentaris per a aquest estudiant
written_feedback_label=Escriu comentaris
Expand Down
1 change: 1 addition & 0 deletions webcomponents/bundle/src/main/bundle/grader_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ grading_rubric_tooltip=Evaluar este env\u00edo utilizando una r\u00fabrica
rubric=R\u00fabrica
autoevaluation=Autoevaluaci\u00f3n\:
openAutoevaluation=Revisa la autoevaluaci\u00f3n
copyAutoevaluation=Copiar la autoevaluaci\u00f3n del estudiante a la r\u00fabrica de evaluaci\u00f3n
studentrubric=Esta tarea es autoevaluable. Puedes revisar la autoevaluaci\u00f3n del estudiante antes de puntuar la tarea.
add_feedback_tooltip=Escribir o grabar alg\u00fan tipo de comentario o feedback para este/esta estudiante
written_feedback_label=Escribir comentarios/feedback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,17 @@ export class SakaiGrader extends graderRenderingMixin(gradableDataMixin(SakaiEle

_toggleStudentRubric() {
this._rubricStudentShowing = !this._rubricStudentShowing;

const studentRubricButton = document.getElementById("student-rubric-autocomplete-button");
if (studentRubricButton) {
const rubricPreview = document.querySelector("sakai-rubric-criterion-preview");
if (rubricPreview) {
console.debug("sakai-rubric-criterion-preview element found, hiding button");
studentRubricButton.classList.add("d-none");
} else {
studentRubricButton.classList.remove("d-none");
}
}
}

_togglePrivateNotesEditor() {
Expand Down Expand Up @@ -427,6 +438,77 @@ export class SakaiGrader extends graderRenderingMixin(gradableDataMixin(SakaiEle
this._addRubricParam(e, "criterion-comment");
}

_autocompleteRubricWithSelfReport() {
const studentRubric = document.querySelector("sakai-rubric-student");
if (!studentRubric) {
console.error("sakai-rubric-student element not found");
return;
}

const gradingRubric = document.querySelector("sakai-rubric-grading");
if (!gradingRubric) {
console.error("sakai-rubric-grading element not found");
return;
}

const evaluationDetails = [];
const criterionRows = studentRubric.querySelectorAll(".criterion-row");
criterionRows.forEach(row => {
const criterionId = row.id.split("_").pop();
const selectedRating = row.querySelector(".rating-item.selected");
if (selectedRating) {
const selectedRatingId = selectedRating.id.split("-").pop();
evaluationDetails.push({ criterionId, selectedRatingId });
}
});

if (evaluationDetails.length === 0) {
console.error("No evaluation details found in sakai-rubric-student");
return;
}

const selectedItems = gradingRubric.querySelectorAll(".rating-item.selected");
selectedItems.forEach(item => {
item.click();
});

let totalPoints = 0;
const decimalSeparator = 1.1.toLocaleString(portal.locale).substring(1, 2);
evaluationDetails.forEach(detail => {
const criterionRow = gradingRubric.querySelector(`#criterion_row_${detail.criterionId}`);
if (criterionRow) {
const ratingItem = criterionRow.querySelector(`.rating-item[data-rating-id="${detail.selectedRatingId}"]`);
if (ratingItem) {
ratingItem.click();
const pointsElement = ratingItem.querySelector(".points");
const pointsText = pointsElement.textContent.trim();
let points = parseFloat(pointsText.replace(",", "."));
const pointsInParentheses = pointsElement.querySelector("b");
if (pointsInParentheses) {
const pointsInParenthesesText = pointsInParentheses.textContent.trim().replace(/[()]/g, "");
points = parseFloat(pointsInParenthesesText.replace(",", "."));
}
totalPoints += points;
}
}
});

const formattedTotalPoints = totalPoints.toLocaleString(portal.locale, { maximumFractionDigits: 2 }).replace(".", decimalSeparator);
const scoreGradeInput = document.querySelector("#grade");
if (scoreGradeInput) {
scoreGradeInput.value = formattedTotalPoints;
scoreGradeInput.dispatchEvent(new Event("keypress", { bubbles: true }));
}

const studentRubricAutocompleteConfirm = document.querySelector("#student-rubric-autocomplete-confirm");
if (studentRubricAutocompleteConfirm) {
studentRubricAutocompleteConfirm.classList.remove("d-none");
setTimeout(() => {
studentRubricAutocompleteConfirm.classList.add("d-none");
}, 2000);
}
}

/**
* Bundle up and all the needed stuff, like the grade, rubric, instructor comments and attachments.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,16 @@ export const graderRenderingMixin = Base => class extends Base {
<div id="student-rubric-block" class="ms-2 ${this._rubricStudentShowing ? "d-block" : "d-none"}">
<h1>${this._i18n.autoevaluation}</h1>
<p>${this._i18n.studentrubric}</p>
<div>
<button id="student-rubric-autocomplete-button"
class="mb-2 btn-transparent text-start"
@click=${this._autocompleteRubricWithSelfReport}
aria-haspopup="true"
title="${this._i18n.copyAutoevaluation}">
<u>${this._i18n.copyAutoevaluation}</u>
<i id="student-rubric-autocomplete-confirm" class="bi bi-check2 d-none"></i>
</button>
</div>
<sakai-rubric-student
site-id="${portal.siteId}"
tool-id="${this.toolId}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class SakaiRubricStudent extends rubricsApiMixin(RubricsElement) {
console.debug("SakaiRubricStudent.render");
const isInstructor = this.instructor && this.instructor === "true";

return html`
const renderedContent = html`
<div class="rubric-details grading student-view">
${!this.dynamic ? html`
<h3>
Expand Down Expand Up @@ -138,6 +138,8 @@ export class SakaiRubricStudent extends rubricsApiMixin(RubricsElement) {
<div id="rubric-criteria-summary-${this.instanceSalt}" class="rubric-tab-content"></div>
</div>
`;
this.dispatchEvent(new CustomEvent("rubric-student-rendered", { bubbles: true, composed: true }));
return renderedContent;
}

_setRubric() {
Expand Down
Loading