From c1ac548e525bdf667ae8da1b6f4bee78c0b6561b Mon Sep 17 00:00:00 2001 From: Charles Severance Date: Tue, 16 Oct 2018 18:33:30 +0530 Subject: [PATCH] SAK-40727 - Add results support for tool-created line items (#6147) --- .../org/sakaiproject/lti13/LTI13Servlet.java | 156 ++++++++++++++++-- .../org/sakaiproject/lti13/LineItemUtil.java | 40 +++++ .../java/org/tsugi/ags2/objects/Result.java | 2 + 3 files changed, 184 insertions(+), 14 deletions(-) diff --git a/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java b/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java index 7ea6953863e9..30ade9102bee 100644 --- a/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java +++ b/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java @@ -89,13 +89,20 @@ import org.tsugi.lti13.objects.Endpoint; import org.sakaiproject.lti13.util.SakaiAccessToken; +import org.sakaiproject.service.gradebook.shared.AssessmentNotFoundException; import org.sakaiproject.service.gradebook.shared.Assignment; +import org.sakaiproject.service.gradebook.shared.CommentDefinition; +import org.sakaiproject.service.gradebook.shared.GradebookNotFoundException; +import org.sakaiproject.service.gradebook.shared.GradebookService; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; +import org.sakaiproject.tool.api.Session; +import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.cover.UserDirectoryService; import org.tsugi.ags2.objects.LineItem; +import org.tsugi.ags2.objects.Result; import org.tsugi.basiclti.XMLMap; import org.tsugi.lti13.objects.LaunchLIS; @@ -172,8 +179,15 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t return; } - // Handle lineitems created by the tool + // /imsblis/lti13/lineitem/{signed-placement}/results + if (parts.length == 6 && "lineitem".equals(parts[3]) && "results".equals(parts[5]) ) { + String signed_placement = parts[4]; + String lineItem = null; + handleLineItemsDetail(signed_placement, lineItem, true /*results */, request, response); + return; + } + // Handle lineitems created by the tool // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id} if (parts.length == 6 && "lineitems".equals(parts[3])) { String signed_placement = parts[4]; @@ -1271,7 +1285,7 @@ private void handleLineItemsGet(String signed_placement, boolean all, LineItem f /** * Provide the detail or results for a tool created lineitem * @param signed_placement - * @param lineItem + * @param lineItem - Can be null * @param results * @param request * @param response @@ -1280,13 +1294,15 @@ private void handleLineItemsDetail(String signed_placement, String lineItem, boo log.debug("signed_placement={}", signed_placement); // Make sure the lineItem id is a long - Long assignment_id; - try { - assignment_id = Long.parseLong(lineItem); - } catch (NumberFormatException e) { - LTI13Util.return400(response, "Bad value for assignment_id "+lineItem); - log.error("Bad value for assignment_id "+lineItem); - return; + Long assignment_id = null; + if ( lineItem != null ) { + try { + assignment_id = Long.parseLong(lineItem); + } catch (NumberFormatException e) { + LTI13Util.return400(response, "Bad value for assignment_id "+lineItem); + log.error("Bad value for assignment_id "+lineItem); + return; + } } // Load the access token, checking the the secret @@ -1314,6 +1330,7 @@ private void handleLineItemsDetail(String signed_placement, String lineItem, boo log.error("Could not load site associated with content={}", content.get(LTIService.LTI_ID)); return; } + String context_id = site.getId(); Map tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { @@ -1321,8 +1338,20 @@ private void handleLineItemsDetail(String signed_placement, String lineItem, boo return; } - String context_id = site.getId(); - Assignment a = LineItemUtil.getAssignmentByKeyDAO(context_id, sat.tool_id, assignment_id); + Assignment a; + + if ( assignment_id != null ) { + a = LineItemUtil.getAssignmentByKeyDAO(context_id, sat.tool_id, assignment_id); + } else { + String assignment_label = (String) content.get(LTIService.LTI_TITLE); + a = LineItemUtil.getAssignmentByLabelDAO(context_id, sat.tool_id, assignment_label); + } + + if ( a == null ) { + LTI13Util.return400(response, "Could not load assignment"); + log.error("Could not load assignment"); + return; + } // Return the line item metadata if ( ! results ) { @@ -1334,9 +1363,108 @@ private void handleLineItemsDetail(String signed_placement, String lineItem, boo return; } - // TODO support results - LTI13Util.return400(response, "results not implemented"); - return; + resultsForAssignment(signed_placement, site, a, assignment_id, request, response); + + } + + private void resultsForAssignment(String signed_placement, Site site, Assignment a, + Long assignment_id, HttpServletRequest request, HttpServletResponse response) + { + // TODO: Is the outer container an array or an object - the spec and swagger doc disagree + /* + [{ + "id": "https://lms.example.com/context/2923/lineitems/1/results/5323497", + "scoreOf": "https://lms.example.com/context/2923/lineitems/1", + "userId": "5323497", + "resultScore": 0.83, + "resultMaximum": 1, + "comment": "This is exceptional work." + }] + */ + response.setContentType(Result.MIME_TYPE_CONTAINER); + + // Look up the assignment so we can find the max points + GradebookService g = (GradebookService) ComponentManager + .get("org.sakaiproject.service.gradebook.GradebookService"); + Session sess = SessionManager.getCurrentSession(); + + // Indicate "who" is reading this grade - needs to be a real user account + String gb_user_id = ServerConfigurationService.getString( + "basiclti.outcomes.userid", "admin"); + String gb_user_eid = ServerConfigurationService.getString( + "basiclti.outcomes.usereid", gb_user_id); + sess.setUserId(gb_user_id); + sess.setUserEid(gb_user_eid); + + String context_id = site.getId(); + + SakaiBLTIUtil.pushAdvisor(); + try { + boolean success = false; + + List> lm = new ArrayList<>(); + + // Get users for each of the members. UserDirectoryService.getUsers will skip any undefined users. + Set members = site.getMembers(); + Map memberMap = new HashMap<>(); + List userIds = new ArrayList<>(); + for (Member member : members) { + userIds.add(member.getUserId()); + memberMap.put(member.getUserId(), member); + } + + List users = UserDirectoryService.getUsers(userIds); + boolean first = true; + PrintWriter out = response.getWriter(); + + out.println("{ \"results\" : ["); + for (User user : users) { + Result result = new Result(); + result.userId = user.getId(); + result.resultMaximum = a.getPoints(); + + if ( signed_placement != null ) { + if ( assignment_id != null ) { + result.id = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + assignment_id + "/results/" + user.getId(); + result.scoreOf = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + assignment_id; + } else { + result.id = getOurServerUrl() + LTI13_PATH + "lineitem/" + signed_placement + "/results/" + user.getId(); + result.scoreOf = getOurServerUrl() + LTI13_PATH + "lineitem/" + signed_placement; + } + } + + try { + CommentDefinition commentDef = g.getAssignmentScoreComment(context_id, a.getId(), user.getId()); + if (commentDef != null) { + result.comment = commentDef.getCommentText(); + } + } catch(AssessmentNotFoundException | GradebookNotFoundException e) { + e.printStackTrace(); // Unexpected + break; + } + + try { + String actualGrade = g.getAssignmentScoreString(context_id, a.getId(), user.getId()); + Double dGrade = new Double(actualGrade); + result.resultScore = dGrade; + } catch(NumberFormatException | AssessmentNotFoundException | GradebookNotFoundException e) { + result.resultScore = null; + } + + if (!first) { + out.println(","); + } + first = false; + out.print(JacksonUtil.prettyPrint(result)); + + } + out.println(""); + out.println("] }"); + } catch (Throwable t) { + t.printStackTrace(); + } finally { + SakaiBLTIUtil.popAdvisor(); + } } /** diff --git a/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LineItemUtil.java b/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LineItemUtil.java index 3e23311155a3..679b976f0659 100644 --- a/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LineItemUtil.java +++ b/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LineItemUtil.java @@ -40,6 +40,7 @@ import org.sakaiproject.service.gradebook.shared.Assignment; import org.sakaiproject.service.gradebook.shared.GradebookNotFoundException; import org.tsugi.ags2.objects.LineItem; +import org.tsugi.ags2.objects.Result; /** * Some Sakai Utility code for IMS Basic LTI This is mostly code to support the @@ -300,6 +301,45 @@ protected static Assignment getAssignmentByKeyDAO(String context_id, Long tool_i return null; } + /** + * Load a particular assignment by its internal Sakai GB key + * @param context_id + * @param tool_id + * @param assignment_id + * @return + */ + protected static Assignment getAssignmentByLabelDAO(String context_id, Long tool_id, String assignment_label) + { + GradebookService g = (GradebookService) ComponentManager + .get("org.sakaiproject.service.gradebook.GradebookService"); + Assignment retval = null; + + pushAdvisor(); + try { + List gradebookAssignments = g.getAssignments(context_id); + for (Iterator i = gradebookAssignments.iterator(); i.hasNext();) { + Assignment gAssignment = (Assignment) i.next(); + if (gAssignment.isExternallyMaintained()) { + continue; + } + if (assignment_label.equals(gAssignment.getName())) { + retval = gAssignment; + break; + } + } + } catch (GradebookNotFoundException e) { + log.error("Gradebook not found context_id={}", context_id); + retval = null; + } catch (Throwable e) { + log.error("Unexpected Throwable", e.getMessage()); + e.printStackTrace(); + retval = null; + } finally { + popAdvisor(); + } + return retval; + } + /** * Load a particular assignment by its internal Sakai GB key * @param context_id diff --git a/basiclti/tsugi-util/src/java/org/tsugi/ags2/objects/Result.java b/basiclti/tsugi-util/src/java/org/tsugi/ags2/objects/Result.java index 2e31dedccc4f..e446d1a5eab6 100644 --- a/basiclti/tsugi-util/src/java/org/tsugi/ags2/objects/Result.java +++ b/basiclti/tsugi-util/src/java/org/tsugi/ags2/objects/Result.java @@ -38,4 +38,6 @@ public class Result { // This is all output-only public Double resultScore; @JsonProperty("resultMaximum") public Double resultMaximum; + @JsonProperty("comment") + public String comment; }