Skip to content

Commit

Permalink
SAK-49998 Assignments: Improve performance and UX of Assignments by s…
Browse files Browse the repository at this point in the history
…tudent view
  • Loading branch information
stetsche committed Apr 24, 2024
1 parent 005540b commit a0a052a
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

package org.sakaiproject.assignment.api;

import java.util.List;

/**
* Store the constants used by Assignment tool and service
*
Expand Down Expand Up @@ -385,5 +387,8 @@ public enum IMSGradingProgress {
public static final String SAK_PROP_ALLOW_LINK_TO_EXISTING_GB_ITEM = "assignment.allowLinkToExistingGBItem";
public static final boolean SAK_PROP_ALLOW_LINK_TO_EXISTING_GB_ITEM_DFLT = true;

public static final String SAK_PROP_NON_SUBMITTER_PERMISSIONS = "assignment.submitter.remove.permission";
public static final List<String> SAK_PROP_NON_SUBMITTER_PERMISSIONS_DEFAULT = List.of(AssignmentServiceConstants.SECURE_ADD_ASSIGNMENT);

public static final String ASSIGNMENT_INPUT_ADD_SUBMISSION_TIME_SPENT = "value_ASSIGNMENT_INPUT_ADD_SUBMISSION_TIME_SPENT";
}
18 changes: 18 additions & 0 deletions assignment/api/src/resources/assignment.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1311,3 +1311,21 @@ tags_option_students_default=Default (hide tags to students)
tags_option_students_yes=Show tags to students and allow them to use the filter component

select=Select

datatables.aria.orderable=Activate to sort
datatables.aria.orderableRemove=Activate to remove sorting
datatables.aria.orderableReverse=Activate to invert sorting
datatables.aria.sortAscending=Activate to sort column ascending
datatables.aria.sortDescending=Activate to sort column descending
datatables.custom.first=First
datatables.custom.last=Last
datatables.custom.next=Next
datatables.custom.previous=Previous
datatables.emptyTable=No entries found
datatables.entries=entries
datatables.info=Showing _START_ to _END_ of _TOTAL_ entries
datatables.infoEmpty=Showing 0 to 0 of 0 entries
datatables.infoFiltered=(filtered from _MAX_ total entries)
datatables.lengthMenu=Show _MENU_ entries
datatables.search=Search:
datatables.zeroRecords=No matching entries found
18 changes: 18 additions & 0 deletions assignment/api/src/resources/assignment_ca.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1281,3 +1281,21 @@ tags_search=Cercar usant etiquetes
tags_option_students=Mostrar etiquetes als estudiants
tags_option_students_default=Predeterminat (ocultar etiquetes als estudiants).
tags_option_students_yes=Mostrar etiquetes als estudiantes i permetre'ls usar el filtre.

datatables.aria.orderable=Activar per ordenar
datatables.aria.orderableRemove=Activar per eliminar l\u0027ordenaci\u00f3
datatables.aria.orderableReverse=Activar per invertir l\u0027ordenaci\u00f3
datatables.aria.sortAscending=Activar per ordenar columna de forma ascendent
datatables.aria.sortDescending=Activar per ordenar columna de forma descendent
datatables.custom.first=Primer
datatables.custom.last=\u00daltim
datatables.custom.next=Seg\u00fcent
datatables.custom.previous=Anterior
datatables.emptyTable=No s\u0027han trobat entrades
datatables.entries=entrades
datatables.info=Mostrant _START_ a _END_ de _TOTAL_ entrades
datatables.infoEmpty=Mostrant 0 a 0 de 0 entrades
datatables.infoFiltered=(filtrat de _MAX_ total entrades)
datatables.lengthMenu=Mostrar _MENU_ entrades
datatables.search=Cercar:
datatables.zeroRecords=No s\u0027han trobat entrades coincidents
18 changes: 18 additions & 0 deletions assignment/api/src/resources/assignment_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1274,3 +1274,21 @@ tags_search=Buscar usando etiquetas
tags_option_students=Mostrar etiquetas a los estudiantes.
tags_option_students_default=Por defecto (ocultar etiquetas a los estudiantes).
tags_option_students_yes=Mostrar etiquetas a los estudiantes y permitirles el uso del filtro.

datatables.aria.orderable=Activar para ordenar
datatables.aria.orderableRemove=Activar para quitar ordenaci\u00f3n
datatables.aria.orderableReverse=Activar para invertir ordenaci\u00f3n
datatables.aria.sortAscending=Activar para ordenar columna de forma ascendente
datatables.aria.sortDescending=Activar para ordenar columna de forma descendente
datatables.custom.first=Primero
datatables.custom.last=\u00daltimo
datatables.custom.next=Siguiente
datatables.custom.previous=Anterior
datatables.emptyTable=No se encontraron entradas
datatables.entries=entradas
datatables.info=Mostrando _START_ a _END_ de _TOTAL_ entradas
datatables.infoEmpty=Mostrando 0 a 0 de 0 entradas
datatables.infoFiltered=(filtrado de _MAX_ total entradas)
datatables.lengthMenu=Mostrar _MENU_ entradas
datatables.search=Buscar:
datatables.zeroRecords=No se encontraron entradas coincidentes
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -1086,6 +1087,10 @@ public class AssignmentAction extends PagedResourceActionII {
* To know if grade_submission go from view_students_assignment view or not
**/
private static final String FROM_VIEW = "from_view";
/**
* Whether or not to reset the table state stored on the frontend
*/
private static final String RESET_TABLE_STATE = "reset_table_state";

/**
* Site property for export rubric in pdf
Expand Down Expand Up @@ -5703,18 +5708,27 @@ private String build_instructor_view_students_assignment_context(VelocityPortlet
// cleaning from view attribute
state.removeAttribute(FROM_VIEW);

// Null or true means we want to reset the tables state
boolean resetTableState = !Boolean.FALSE.equals(state.getAttribute(RESET_TABLE_STATE));
context.put("resetTableState", resetTableState);
state.setAttribute(RESET_TABLE_STATE, false);

String contextString = (String) state.getAttribute(STATE_CONTEXT_STRING);
Site site;
try {
site = siteService.getSite(contextString);
} catch (IdUnusedException e) {
throw new IllegalStateException("Can not build context for invalid site with id [" + contextString + "]");
}

initViewSubmissionListOption(state);
String allOrOneGroup = (String) state.getAttribute(VIEW_SUBMISSION_LIST_OPTION);
String search = (String) state.getAttribute(VIEW_SUBMISSION_SEARCH);
Boolean searchFilterOnly = (state.getAttribute(SUBMISSIONS_SEARCH_ONLY) != null && ((Boolean) state.getAttribute(SUBMISSIONS_SEARCH_ONLY)) ? Boolean.TRUE : Boolean.FALSE);

String accessPointUrl = serverConfigurationService.getAccessUrl() +
AssignmentReferenceReckoner.reckoner().context(contextString).reckon().getReference() +
"?contextString=" + contextString +
"&viewString=" + allOrOneGroup +
"&searchString=" + search +
"&searchFilterOnly=" + searchFilterOnly.toString() +
"&estimate=true";
context.put("accessPointUrl", accessPointUrl);
Expand All @@ -5730,53 +5744,55 @@ private String build_instructor_view_students_assignment_context(VelocityPortlet
}
context.put("hasAtLeastOneAnonAssignment", hasAtLeastOneAnonAssigment);

Map<String, User> studentMembers = assignments.stream()
// flatten to a single List<String>
.flatMap(a -> assignmentService.getSubmitterIdList(searchFilterOnly.toString(), allOrOneGroup, search, AssignmentReferenceReckoner.reckoner().assignment(a).reckon().getReference(), contextString).stream())
// collect into set for uniqueness
.collect(Collectors.toSet()).stream()
// convert to User
.map(s -> {
try {
return userDirectoryService.getUser(s);
} catch (UserNotDefinedException e) {
log.warn("User is not defined {}, {}", s, e.getMessage());
return null;
}
})
// filter nulls
.filter(Objects::nonNull)
// collect to Map<String, User>
.collect(Collectors.toMap(User::getId, Function.identity()));
List<String> nonSubmitterPermissions = serverConfigurationService.getStringList(AssignmentConstants.SAK_PROP_NON_SUBMITTER_PERMISSIONS,
AssignmentConstants.SAK_PROP_NON_SUBMITTER_PERMISSIONS_DEFAULT);

context.put("studentMembersMap", studentMembers);
context.put("studentMembers", new SortedIterator(studentMembers.values().iterator(), new AssignmentComparator(state, SORTED_USER_BY_SORTNAME, Boolean.TRUE.toString())));
context.put("viewGroup", state.getAttribute(VIEW_SUBMISSION_LIST_OPTION));
context.put("searchString", state.getAttribute(VIEW_SUBMISSION_SEARCH) != null ? state.getAttribute(VIEW_SUBMISSION_SEARCH) : "");
context.put("showSubmissionByFilterSearchOnly", state.getAttribute(SUBMISSIONS_SEARCH_ONLY) != null ? (Boolean) state.getAttribute(SUBMISSIONS_SEARCH_ONLY) : Boolean.FALSE);
Collection groups = getAllGroupsInSite(contextString);
context.put("groups", new SortedIterator(groups.iterator(), new AssignmentComparator(state, SORTED_BY_GROUP_TITLE, Boolean.TRUE.toString())));
Predicate<String> isNonSubmitter = (userId) -> nonSubmitterPermissions.stream()
.filter(permission -> securityService.unlock(userId, permission, site.getReference()))
.findAny()
.isEmpty();

AuthzGroup groupSelection = StringUtils.isBlank(allOrOneGroup) || AssignmentConstants.ALL.equals(allOrOneGroup)
? site
: site.getGroup(allOrOneGroup);

Map<String, User> studentMembers = groupSelection.getUsers().stream()
.filter(isNonSubmitter)
.map(this::getUser)
.flatMap(Optional::stream)
.collect(Collectors.toMap(User::getId, Function.identity()));

Comparator<Assignment> assignmentComparator = new AssignmentComparator(state, SORTED_BY_DEFAULT, Boolean.TRUE.toString());

Map<User, Iterator<Assignment>> showStudentAssignments = new HashMap<>();

Set<String> showStudentListSet = (Set<String>) state.getAttribute(STUDENT_LIST_SHOW_TABLE);
if (showStudentListSet != null) {
context.put("studentListShowSet", showStudentListSet);
for (String userId : showStudentListSet) {
User user = studentMembers.get(userId);
Set<String> expandedStudents = (Set<String>) state.getAttribute(STUDENT_LIST_SHOW_TABLE);
if (expandedStudents != null) {
context.put("studentListShowSet", expandedStudents);

// filter to obtain only grade-able assignments
List<Assignment> rv = assignments.stream()
.filter(a -> assignmentService.allowGradeSubmission(AssignmentReferenceReckoner.reckoner().assignment(a).reckon().getReference()))
.collect(Collectors.toList());
List<Assignment> gradableAssignments = assignments.stream()
.filter(a -> assignmentService.allowGradeSubmission(AssignmentReferenceReckoner.reckoner().assignment(a).reckon().getReference()))
.sorted(assignmentComparator)
.collect(Collectors.toList());

// sort the assignments into the default order before adding
Iterator assignmentSortFinal = new SortedIterator(rv.iterator(), new AssignmentComparator(state, SORTED_BY_DEFAULT, Boolean.TRUE.toString()));
for (String userId : expandedStudents) {
List<Assignment> userSubmittableAssignments = gradableAssignments.stream()
.filter(assignment -> assignmentService.canSubmit(assignment, userId))
.collect(Collectors.toList());

showStudentAssignments.put(user, assignmentSortFinal);
showStudentAssignments.put(studentMembers.get(userId), userSubmittableAssignments.iterator());
}
}

context.put("studentMembersMap", studentMembers);
context.put("studentMembers", new SortedIterator(studentMembers.values().iterator(), new AssignmentComparator(state, SORTED_USER_BY_SORTNAME, Boolean.TRUE.toString())));
context.put("viewGroup", state.getAttribute(VIEW_SUBMISSION_LIST_OPTION));
context.put("searchString", state.getAttribute(VIEW_SUBMISSION_SEARCH) != null ? state.getAttribute(VIEW_SUBMISSION_SEARCH) : "");
context.put("showSubmissionByFilterSearchOnly", state.getAttribute(SUBMISSIONS_SEARCH_ONLY) != null ? (Boolean) state.getAttribute(SUBMISSIONS_SEARCH_ONLY) : Boolean.FALSE);
Collection groups = getAllGroupsInSite(contextString);
context.put("groups", new SortedIterator(groups.iterator(), new AssignmentComparator(state, SORTED_BY_GROUP_TITLE, Boolean.TRUE.toString())));


context.put("studentAssignmentsTable", showStudentAssignments);
context.put("currentTime", Instant.now());

Expand Down Expand Up @@ -16757,4 +16773,12 @@ private String getRateSubmissionTimeSpent(AssignmentSubmission submission) {
}
return assignmentService.intToTime(assigmentRateSpent);
}

private Optional<User> getUser(String userId) {
try {
return Optional.of(userDirectoryService.getUser(userId));
} catch (UserNotDefinedException e) {
return Optional.empty();
}
}
}
72 changes: 72 additions & 0 deletions assignment/tool/src/webapp/js/assignmentsByStudent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
(() => {

// Include datatables dependencies
window.includeWebjarLibrary('datatables');
window.includeWebjarLibrary('datatables-rowgroup');

// Assignments By Students "global" namespace
window.ABS = {
datatablesConfig: {
dom: '<<".dt-header-row"<".dt-header-slot">lf><t><".dt-footer-row"ip>>',
stateSave: true,
columnDefs: [
{
sortable: false,
targets: "no-sort",
},
],
rowGroup: {
dataSrc(row) {
const dataCellHtml = row[0].display;
return parseDataCell(dataCellHtml).studentUserId;
},
startRender(rows, group) {
const firstRow = rows.data()[0];
const dataCellHtml = firstRow[0].display;

const data = parseDataCell(dataCellHtml);

return renderGrouping(data);
},
},
},
}

// Private functions

function parseDataCell(html) {
const template = document.createElement('template');
template.innerHTML = html;
const cell = template.content.children[0];

const expanded = cell.getAttribute("data-expanded") == "true";
const actionLink = cell.getAttribute("data-action-href");
const studentUserId = cell.getAttribute("data-user-id");
const studentName = cell.innerText.trim();

return {
actionLink,
expanded,
studentName,
studentUserId,
};
}

function renderGrouping({ studentName, actionLink, expanded }) {
const template = document.createElement('template');
template.innerHTML = `
<tr>
<td>
<a href="${actionLink}">
<span class="expand-icon fa ${expanded ? "fa-chevron-down" : "fa-chevron-right"}"
aria-hidden="true"></span>
<span>${studentName}</span>
</a>
</td>
</tr>
`;

return template.content;
}

})();
Loading

0 comments on commit a0a052a

Please sign in to comment.