| 1 | package edu.ucsb.cs156.frontiers.services; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.databind.DeserializationFeature; | |
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 7 | import edu.ucsb.cs156.frontiers.entities.RosterStudent; | |
| 8 | import edu.ucsb.cs156.frontiers.models.CanvasGroup; | |
| 9 | import edu.ucsb.cs156.frontiers.models.CanvasGroupSet; | |
| 10 | import edu.ucsb.cs156.frontiers.models.CanvasStudent; | |
| 11 | import edu.ucsb.cs156.frontiers.utilities.CanonicalFormConverter; | |
| 12 | import edu.ucsb.cs156.frontiers.validators.HasLinkedCanvasCourse; | |
| 13 | import java.util.ArrayList; | |
| 14 | import java.util.List; | |
| 15 | import org.springframework.graphql.client.HttpSyncGraphQlClient; | |
| 16 | import org.springframework.stereotype.Service; | |
| 17 | import org.springframework.validation.annotation.Validated; | |
| 18 | import org.springframework.web.client.RestClient; | |
| 19 | ||
| 20 | /** | |
| 21 | * Service for interacting with the Canvas API. | |
| 22 | * | |
| 23 | * <p>Note that the Canvas API uses a GraphQL endpoint, which allows for more flexible queries | |
| 24 | * compared to traditional REST APIs. | |
| 25 | * | |
| 26 | * <p>For more information on the Canvas API, visit the official documentation at | |
| 27 | * https://canvas.instructure.com/doc/api/. | |
| 28 | * | |
| 29 | * <p>You can typically interact with Canvas API GraphQL endpoints interactively by appending | |
| 30 | * /graphiql to the URL of the Canvas instance. | |
| 31 | * | |
| 32 | * <p>For example, for UCSB Canvas, use: https://ucsb.instructure.com/graphiql | |
| 33 | */ | |
| 34 | @Service | |
| 35 | @Validated | |
| 36 | public class CanvasService { | |
| 37 | ||
| 38 | private HttpSyncGraphQlClient graphQlClient; | |
| 39 | private ObjectMapper mapper; | |
| 40 | ||
| 41 | private static final String CANVAS_GRAPHQL_URL = "https://ucsb.instructure.com/api/graphql"; | |
| 42 | ||
| 43 | public CanvasService(ObjectMapper mapper, RestClient.Builder builder) { | |
| 44 | this.graphQlClient = | |
| 45 | HttpSyncGraphQlClient.builder(builder.baseUrl(CANVAS_GRAPHQL_URL).build()).build(); | |
| 46 | this.mapper = mapper; | |
| 47 | this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 48 | } | |
| 49 | ||
| 50 | public List<CanvasGroupSet> getCanvasGroupSets(@HasLinkedCanvasCourse Course course) { | |
| 51 | String query = | |
| 52 | """ | |
| 53 | query GetGroupSets($courseId: ID!) { | |
| 54 | course(id: $courseId) { | |
| 55 | groupSets { | |
| 56 | _id | |
| 57 | name | |
| 58 | id | |
| 59 | } | |
| 60 | } | |
| 61 | } | |
| 62 | """; | |
| 63 | ||
| 64 | HttpSyncGraphQlClient authedClient = | |
| 65 | graphQlClient | |
| 66 | .mutate() | |
| 67 | .header("Authorization", "Bearer " + course.getCanvasApiToken()) | |
| 68 | .build(); | |
| 69 | ||
| 70 | List<CanvasGroupSet> groupSets = | |
| 71 | authedClient | |
| 72 | .document(query) | |
| 73 | .variable("courseId", course.getCanvasCourseId()) | |
| 74 | .retrieveSync("course.groupSets") | |
| 75 | .toEntityList(CanvasGroupSet.class); | |
| 76 |
1
1. getCanvasGroupSets : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroupSets → KILLED |
return groupSets; |
| 77 | } | |
| 78 | ||
| 79 | /** | |
| 80 | * Fetches the roster of students from Canvas for the given course. | |
| 81 | * | |
| 82 | * @param course the Course entity containing canvasApiToken and canvasCourseId | |
| 83 | * @return list of RosterStudent objects from Canvas | |
| 84 | */ | |
| 85 | public List<RosterStudent> getCanvasRoster(@HasLinkedCanvasCourse Course course) { | |
| 86 | String query = | |
| 87 | """ | |
| 88 | query GetRoster($courseId: ID!) { | |
| 89 | course(id: $courseId) { | |
| 90 | usersConnection(filter: {enrollmentTypes: StudentEnrollment}) { | |
| 91 | edges { | |
| 92 | node { | |
| 93 | firstName | |
| 94 | lastName | |
| 95 | sisId | |
| 96 | email | |
| 97 | integrationId | |
| 98 | } | |
| 99 | } | |
| 100 | } | |
| 101 | } | |
| 102 | } | |
| 103 | """; | |
| 104 | ||
| 105 | HttpSyncGraphQlClient authedClient = | |
| 106 | graphQlClient | |
| 107 | .mutate() | |
| 108 | .header("Authorization", "Bearer " + course.getCanvasApiToken()) | |
| 109 | .build(); | |
| 110 | ||
| 111 | List<CanvasStudent> students = | |
| 112 | authedClient | |
| 113 | .document(query) | |
| 114 | .variable("courseId", course.getCanvasCourseId()) | |
| 115 | .retrieveSync("course.usersConnection.edges") | |
| 116 | .toEntityList(JsonNode.class) | |
| 117 | .stream() | |
| 118 |
1
1. lambda$getCanvasRoster$0 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$0 → KILLED |
.map(node -> mapper.convertValue(node.get("node"), CanvasStudent.class)) |
| 119 | .toList(); | |
| 120 | ||
| 121 |
1
1. getCanvasRoster : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasRoster → KILLED |
return students.stream() |
| 122 | .map( | |
| 123 | student -> | |
| 124 |
1
1. lambda$getCanvasRoster$1 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$1 → KILLED |
RosterStudent.builder() |
| 125 | .firstName(student.getFirstName()) | |
| 126 | .lastName(student.getLastName()) | |
| 127 | .studentId(student.getStudentId()) | |
| 128 | .email(student.getEmail()) | |
| 129 | .build()) | |
| 130 | .toList(); | |
| 131 | } | |
| 132 | ||
| 133 | public List<CanvasGroup> getCanvasGroups( | |
| 134 | @HasLinkedCanvasCourse Course course, String groupSetId) { | |
| 135 | String query = | |
| 136 | """ | |
| 137 | query GetTeams($groupId: ID!) { | |
| 138 | node(id: $groupId) { | |
| 139 | ... on GroupSet { | |
| 140 | id | |
| 141 | name | |
| 142 | groups { | |
| 143 | name | |
| 144 | _id | |
| 145 | membersConnection { | |
| 146 | edges { | |
| 147 | node { | |
| 148 | user { | |
| 149 | email | |
| 150 | } | |
| 151 | } | |
| 152 | } | |
| 153 | } | |
| 154 | } | |
| 155 | } | |
| 156 | } | |
| 157 | } | |
| 158 | """; | |
| 159 | ||
| 160 | HttpSyncGraphQlClient authedClient = | |
| 161 | graphQlClient | |
| 162 | .mutate() | |
| 163 | .header("Authorization", "Bearer " + course.getCanvasApiToken()) | |
| 164 | .build(); | |
| 165 | ||
| 166 | List<JsonNode> groups = | |
| 167 | authedClient | |
| 168 | .document(query) | |
| 169 | .variable("groupId", groupSetId) | |
| 170 | .retrieveSync("node.groups") | |
| 171 | .toEntityList(JsonNode.class); | |
| 172 | ||
| 173 | List<CanvasGroup> parsedGroups = | |
| 174 | groups.stream() | |
| 175 | .map( | |
| 176 | group -> { | |
| 177 | CanvasGroup canvasGroup = | |
| 178 | CanvasGroup.builder() | |
| 179 | .name(group.get("name").asText()) | |
| 180 | .id(group.get("_id").asInt()) | |
| 181 | .members(new ArrayList<>()) | |
| 182 | .build(); | |
| 183 | group | |
| 184 | .get("membersConnection") | |
| 185 | .get("edges") | |
| 186 |
1
1. lambda$getCanvasGroups$3 : removed call to com/fasterxml/jackson/databind/JsonNode::forEach → KILLED |
.forEach( |
| 187 | edge -> { | |
| 188 | canvasGroup | |
| 189 | .getMembers() | |
| 190 | .add( | |
| 191 | CanonicalFormConverter.convertToValidEmail( | |
| 192 | edge.path("node").path("user").get("email").asText())); | |
| 193 | }); | |
| 194 |
1
1. lambda$getCanvasGroups$3 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasGroups$3 → KILLED |
return canvasGroup; |
| 195 | }) | |
| 196 | .toList(); | |
| 197 | ||
| 198 |
1
1. getCanvasGroups : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroups → KILLED |
return parsedGroups; |
| 199 | } | |
| 200 | } | |
Mutations | ||
| 76 |
1.1 |
|
| 118 |
1.1 |
|
| 121 |
1.1 |
|
| 124 |
1.1 |
|
| 186 |
1.1 |
|
| 194 |
1.1 |
|
| 198 |
1.1 |