| 1 | package edu.ucsb.cs.scaffold.controller; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.core.JsonProcessingException; | |
| 4 | import com.fasterxml.jackson.core.type.TypeReference; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import edu.ucsb.cs.scaffold.entity.Concept; | |
| 7 | import edu.ucsb.cs.scaffold.entity.ConceptEdge; | |
| 8 | import edu.ucsb.cs.scaffold.entity.Course; | |
| 9 | import edu.ucsb.cs.scaffold.entity.PracticeProblem; | |
| 10 | import edu.ucsb.cs.scaffold.errors.EntityNotFoundException; | |
| 11 | import edu.ucsb.cs.scaffold.model.CreateConceptDTO; | |
| 12 | import edu.ucsb.cs.scaffold.model.CreateSubconceptDTO; | |
| 13 | import edu.ucsb.cs.scaffold.model.UpdateConceptDTO; | |
| 14 | import edu.ucsb.cs.scaffold.model.UpdateSubconceptDTO; | |
| 15 | import edu.ucsb.cs.scaffold.model.UserState; | |
| 16 | import edu.ucsb.cs.scaffold.repository.ConceptEdgeRepository; | |
| 17 | import edu.ucsb.cs.scaffold.repository.ConceptRepository; | |
| 18 | import edu.ucsb.cs.scaffold.repository.CourseRepository; | |
| 19 | import edu.ucsb.cs.scaffold.repository.PracticeProblemRepository; | |
| 20 | import edu.ucsb.cs.scaffold.repository.UserStateRepository; | |
| 21 | import edu.ucsb.cs.scaffold.services.ConceptGraphService; | |
| 22 | import edu.ucsb.cs.scaffold.services.MarkdownService; | |
| 23 | import io.swagger.v3.oas.annotations.Operation; | |
| 24 | import io.swagger.v3.oas.annotations.Parameter; | |
| 25 | import io.swagger.v3.oas.annotations.tags.Tag; | |
| 26 | import java.util.ArrayList; | |
| 27 | import java.util.Comparator; | |
| 28 | import java.util.HashMap; | |
| 29 | import java.util.HashSet; | |
| 30 | import java.util.LinkedHashMap; | |
| 31 | import java.util.List; | |
| 32 | import java.util.Map; | |
| 33 | import java.util.Objects; | |
| 34 | import java.util.stream.Collectors; | |
| 35 | import lombok.RequiredArgsConstructor; | |
| 36 | import org.springframework.security.access.prepost.PreAuthorize; | |
| 37 | import org.springframework.web.bind.annotation.DeleteMapping; | |
| 38 | import org.springframework.web.bind.annotation.GetMapping; | |
| 39 | import org.springframework.web.bind.annotation.PostMapping; | |
| 40 | import org.springframework.web.bind.annotation.PutMapping; | |
| 41 | import org.springframework.web.bind.annotation.RequestBody; | |
| 42 | import org.springframework.web.bind.annotation.RequestParam; | |
| 43 | import org.springframework.web.bind.annotation.RestController; | |
| 44 | ||
| 45 | @Tag(name = "Concepts") | |
| 46 | @RestController | |
| 47 | @RequiredArgsConstructor | |
| 48 | public class ConceptsController extends ApiController { | |
| 49 | ||
| 50 | public static final int MAX_RENDERED_CONCEPT_LABEL_LENGTH = Concept.MAX_RENDERED_LABEL_LENGTH; | |
| 51 | public static final int MAX_RENDERED_SUBCONCEPT_LABEL_LENGTH = | |
| 52 | Concept.MAX_RENDERED_SUBCONCEPT_LABEL_LENGTH; | |
| 53 | ||
| 54 | // Applied to every new top-level concept; users cannot assign a color at creation time for | |
| 55 | // now. Top-level concepts are assumed throughout the frontend (node styling, drag-out | |
| 56 | // detail edges) to always have a real color; a null/blank color silently breaks that | |
| 57 | // styling (e.g. an SVG edge with no stroke color renders with stroke: none and is | |
| 58 | // invisible). Matches the "Level 1" swatch in the frontend's concept-graph legend. | |
| 59 | public static final String DEFAULT_TOP_LEVEL_COLOR = "#c99ffe"; | |
| 60 | ||
| 61 | // The display order of subconcepts within a parent. sortOrder is author-controlled | |
| 62 | // (see reorderSubconcepts); the id tiebreaker makes the order deterministic even for | |
| 63 | // rows with equal or missing sortOrder (pre-backfill data, concurrent-create ties). | |
| 64 | private static final Comparator<Concept> SUBCONCEPT_DISPLAY_ORDER = | |
| 65 | Comparator.comparing(Concept::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())) | |
| 66 | .thenComparing(Concept::getId); | |
| 67 | ||
| 68 | private static final Comparator<Concept> COURSE_CONCEPT_TABLE_ORDER = | |
| 69 | Comparator.comparing(Concept::getLevel, Comparator.nullsLast(Comparator.naturalOrder())) | |
| 70 | .thenComparing(Concept::getX, Comparator.nullsLast(Comparator.naturalOrder())) | |
| 71 | .thenComparing(Concept::getId); | |
| 72 | ||
| 73 | private final ConceptRepository conceptRepository; | |
| 74 | private final PracticeProblemRepository practiceProblemRepository; | |
| 75 | private final ConceptEdgeRepository conceptEdgeRepository; | |
| 76 | private final CourseRepository courseRepository; | |
| 77 | private final UserStateRepository userStateRepository; | |
| 78 | private final MarkdownService markdownService; | |
| 79 | private final ConceptGraphService conceptGraphService; | |
| 80 | private final ObjectMapper objectMapper; | |
| 81 | ||
| 82 | @Operation(summary = "Get description/example/practiceUrl content for every concept in a course") | |
| 83 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 84 | @GetMapping("/api/concepts/content") | |
| 85 | public Map<Long, ConceptContentDTO> getContent( | |
| 86 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 87 | Map<Long, String> urlByConceptId = firstUrlByConceptId(courseId); | |
| 88 | ||
| 89 | Map<Long, ConceptContentDTO> result = new LinkedHashMap<>(); | |
| 90 | for (Concept concept : conceptRepository.findByCourseId(courseId)) { | |
| 91 |
1
1. getContent : negated conditional → KILLED |
Long parentId = concept.isSubconcept() ? concept.getParent().getId() : null; |
| 92 | result.put( | |
| 93 | concept.getId(), | |
| 94 | new ConceptContentDTO( | |
| 95 | concept.getId(), | |
| 96 | parentId, | |
| 97 | markdownService.toHtml(concept.getDescription()), | |
| 98 | markdownService.toHtml(concept.getExample()), | |
| 99 | urlByConceptId.get(concept.getId()))); | |
| 100 | } | |
| 101 |
1
1. getContent : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::getContent → KILLED |
return result; |
| 102 | } | |
| 103 | ||
| 104 | @Operation(summary = "Get the top-level concepts for a course in table order") | |
| 105 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 106 | @GetMapping("/api/concepts/course") | |
| 107 | public List<CourseConceptDTO> getCourseConcepts( | |
| 108 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 109 |
1
1. getCourseConcepts : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/ConceptsController::getCourseConcepts → KILLED |
return conceptRepository.findByCourseId(courseId).stream() |
| 110 |
2
1. lambda$getCourseConcepts$0 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getCourseConcepts$0 → KILLED 2. lambda$getCourseConcepts$0 : negated conditional → KILLED |
.filter(concept -> !concept.isSubconcept()) |
| 111 | .sorted(COURSE_CONCEPT_TABLE_ORDER) | |
| 112 | .map( | |
| 113 | concept -> | |
| 114 |
1
1. lambda$getCourseConcepts$1 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getCourseConcepts$1 → KILLED |
new CourseConceptDTO( |
| 115 | concept.getId(), | |
| 116 | concept.getLabel(), | |
| 117 | concept.getDescription(), | |
| 118 | concept.getExample(), | |
| 119 | concept.getLevel(), | |
| 120 | concept.getX(), | |
| 121 | concept.getY())) | |
| 122 | .toList(); | |
| 123 | } | |
| 124 | ||
| 125 | @Operation(summary = "Get the major concepts and their subconcepts for a course") | |
| 126 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 127 | @GetMapping("/api/concepts/graph") | |
| 128 | public List<MajorConceptDTO> getGraph( | |
| 129 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 130 | List<Concept> concepts = conceptRepository.findByCourseId(courseId); | |
| 131 | ||
| 132 | Map<Long, List<Concept>> subconceptsByParentId = | |
| 133 | concepts.stream() | |
| 134 | .filter(Concept::isSubconcept) | |
| 135 | .sorted(SUBCONCEPT_DISPLAY_ORDER) | |
| 136 | .collect( | |
| 137 | Collectors.groupingBy( | |
| 138 |
1
1. lambda$getGraph$2 : replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getGraph$2 → KILLED |
concept -> concept.getParent().getId(), |
| 139 | LinkedHashMap::new, | |
| 140 | Collectors.toList())); | |
| 141 | ||
| 142 | List<MajorConceptDTO> result = new ArrayList<>(); | |
| 143 | concepts.stream() | |
| 144 |
2
1. lambda$getGraph$3 : negated conditional → KILLED 2. lambda$getGraph$3 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getGraph$3 → KILLED |
.filter(concept -> !concept.isSubconcept()) |
| 145 | .sorted(Comparator.comparing(Concept::getId)) | |
| 146 |
1
1. getGraph : removed call to java/util/stream/Stream::forEach → KILLED |
.forEach( |
| 147 | major -> { | |
| 148 | List<SubconceptDTO> subconceptDtos = | |
| 149 | subconceptsByParentId.getOrDefault(major.getId(), List.of()).stream() | |
| 150 | .map( | |
| 151 | sub -> | |
| 152 |
1
1. lambda$getGraph$4 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getGraph$4 → KILLED |
new SubconceptDTO( |
| 153 | sub.getId(), | |
| 154 | sub.getParent().getId(), | |
| 155 | markdownService.toInlineHtml(sub.getLabel()))) | |
| 156 | .toList(); | |
| 157 | result.add( | |
| 158 | new MajorConceptDTO( | |
| 159 | major.getId(), | |
| 160 | markdownService.toInlineHtml(major.getLabel()), | |
| 161 | major.getColor(), | |
| 162 | subconceptDtos)); | |
| 163 | }); | |
| 164 |
1
1. getGraph : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/ConceptsController::getGraph → KILLED |
return result; |
| 165 | } | |
| 166 | ||
| 167 | @Operation(summary = "Get the graph positions of the top-level concepts in a course") | |
| 168 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 169 | @GetMapping("/api/concepts/positions") | |
| 170 | public Map<Long, PositionDTO> getPositions( | |
| 171 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 172 | Map<Long, PositionDTO> result = new LinkedHashMap<>(); | |
| 173 | for (Concept concept : conceptRepository.findByCourseId(courseId)) { | |
| 174 |
1
1. getPositions : negated conditional → KILLED |
if (!concept.isSubconcept()) { |
| 175 | result.put(concept.getId(), new PositionDTO(concept.getX(), concept.getY())); | |
| 176 | } | |
| 177 | } | |
| 178 |
1
1. getPositions : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::getPositions → KILLED |
return result; |
| 179 | } | |
| 180 | ||
| 181 | @Operation(summary = "Get all top-level concepts for a course") | |
| 182 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 183 | @GetMapping("/api/concepts/top-level") | |
| 184 | public List<TopLevelConceptDTO> getTopLevelConcepts( | |
| 185 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 186 |
1
1. getTopLevelConcepts : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/ConceptsController::getTopLevelConcepts → KILLED |
return conceptRepository.findByCourseId(courseId).stream() |
| 187 |
2
1. lambda$getTopLevelConcepts$6 : negated conditional → KILLED 2. lambda$getTopLevelConcepts$6 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getTopLevelConcepts$6 → KILLED |
.filter(concept -> !concept.isSubconcept()) |
| 188 | .sorted(Comparator.comparing(Concept::getId)) | |
| 189 | .map( | |
| 190 | concept -> | |
| 191 |
1
1. lambda$getTopLevelConcepts$7 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getTopLevelConcepts$7 → KILLED |
new TopLevelConceptDTO( |
| 192 | concept.getId(), | |
| 193 | concept.getLabel(), | |
| 194 | concept.getLevel(), | |
| 195 | concept.getX(), | |
| 196 | concept.getY())) | |
| 197 | .toList(); | |
| 198 | } | |
| 199 | ||
| 200 | @Operation(summary = "Get all subconcepts for a course in table-row format") | |
| 201 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 202 | @GetMapping("/api/concepts/subconcepts") | |
| 203 | public List<SubConceptTableRowDTO> getSubconcepts( | |
| 204 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 205 |
1
1. getSubconcepts : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/ConceptsController::getSubconcepts → KILLED |
return conceptRepository.findByCourseId(courseId).stream() |
| 206 | .filter(Concept::isSubconcept) | |
| 207 | .sorted( | |
| 208 | Comparator.comparing( | |
| 209 |
1
1. lambda$getSubconcepts$8 : replaced Integer return value with 0 for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getSubconcepts$8 → KILLED |
(Concept c) -> c.getParent().getLevel(), |
| 210 | Comparator.nullsLast(Comparator.naturalOrder())) | |
| 211 | .thenComparing( | |
| 212 |
1
1. lambda$getSubconcepts$9 : replaced Integer return value with 0 for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getSubconcepts$9 → KILLED |
c -> c.getParent().getX(), Comparator.nullsLast(Comparator.naturalOrder())) |
| 213 | .thenComparing( | |
| 214 | Concept::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())) | |
| 215 | .thenComparing(Concept::getId)) | |
| 216 | .map( | |
| 217 | concept -> | |
| 218 |
1
1. lambda$getSubconcepts$10 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$getSubconcepts$10 → KILLED |
new SubConceptTableRowDTO( |
| 219 | concept.getId(), | |
| 220 | concept.getLabel(), | |
| 221 | concept.getDescription(), | |
| 222 | concept.getExample(), | |
| 223 | concept.getParent().getId(), | |
| 224 | concept.getParent().getLabel(), | |
| 225 | concept.getParent().getLevel(), | |
| 226 | concept.getParent().getX(), | |
| 227 | concept.getSortOrder())) | |
| 228 | .toList(); | |
| 229 | } | |
| 230 | ||
| 231 | @Operation(summary = "Get the prerequisite edges between concepts in a course") | |
| 232 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 233 | @GetMapping("/api/concepts/edges") | |
| 234 | public List<EdgeDTO> getEdges( | |
| 235 | @Parameter(description = "id of the course") @RequestParam Long courseId) { | |
| 236 | List<EdgeDTO> result = new ArrayList<>(); | |
| 237 | for (ConceptEdge edge : conceptEdgeRepository.findByCourseId(courseId)) { | |
| 238 | result.add( | |
| 239 | new EdgeDTO( | |
| 240 | edge.getId(), edge.getSource().getId(), edge.getTarget().getId(), edge.getColor())); | |
| 241 | } | |
| 242 |
1
1. getEdges : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/ConceptsController::getEdges → KILLED |
return result; |
| 243 | } | |
| 244 | ||
| 245 | @Operation( | |
| 246 | summary = "Create a new top-level concept", | |
| 247 | description = | |
| 248 | """ | |
| 249 | Accepts a YAML (or JSON) request body. label, x, and y are required; color and | |
| 250 | level are not user-settable (every new top-level concept starts at level 1 with | |
| 251 | the default color, until a scaffold reset recomputes them). label, description, | |
| 252 | and example are Markdown; they are sanitized and canonicalized before being | |
| 253 | stored. YAML block scalars (|) make multi-line Markdown easy to enter through | |
| 254 | Swagger. | |
| 255 | """) | |
| 256 | @io.swagger.v3.oas.annotations.parameters.RequestBody( | |
| 257 | content = | |
| 258 | @io.swagger.v3.oas.annotations.media.Content( | |
| 259 | mediaType = "application/yaml", | |
| 260 | schema = | |
| 261 | @io.swagger.v3.oas.annotations.media.Schema( | |
| 262 | implementation = CreateConceptDTO.class), | |
| 263 | examples = | |
| 264 | @io.swagger.v3.oas.annotations.media.ExampleObject( | |
| 265 | value = | |
| 266 | """ | |
| 267 | courseId: 1 | |
| 268 | label: Arrays | |
| 269 | description: | | |
| 270 | An *array* is a fixed-size collection of elements. | |
| 271 | example: | | |
| 272 | ```java | |
| 273 | int[] arr = new int[5]; | |
| 274 | ``` | |
| 275 | x: 0 | |
| 276 | y: 0 | |
| 277 | """))) | |
| 278 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #dto.courseId)") | |
| 279 | @PostMapping( | |
| 280 | value = "/api/concept", | |
| 281 | consumes = {"application/yaml", "application/x-yaml", "application/json"}) | |
| 282 | public Concept postConcept(@RequestBody CreateConceptDTO dto) throws EntityNotFoundException { | |
| 283 |
1
1. postConcept : negated conditional → KILLED |
if (dto.getCourseId() == null) { |
| 284 | throw new IllegalArgumentException("courseId is required"); | |
| 285 | } | |
| 286 | Course course = | |
| 287 | courseRepository | |
| 288 | .findById(dto.getCourseId()) | |
| 289 |
1
1. lambda$postConcept$11 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$postConcept$11 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, dto.getCourseId())); |
| 290 | ||
| 291 | String cleanLabel = cleanAndValidateLabel(dto.getLabel()); | |
| 292 |
2
1. postConcept : negated conditional → KILLED 2. postConcept : negated conditional → KILLED |
if (dto.getX() == null || dto.getY() == null) { |
| 293 | throw new IllegalArgumentException("x and y are required for a top-level concept"); | |
| 294 | } | |
| 295 | ||
| 296 | Concept concept = | |
| 297 | Concept.builder() | |
| 298 | .course(course) | |
| 299 | .label(cleanLabel) | |
| 300 | .description(markdownService.clean(dto.getDescription())) | |
| 301 | .example(markdownService.clean(dto.getExample())) | |
| 302 | .color(DEFAULT_TOP_LEVEL_COLOR) | |
| 303 | .level(1) | |
| 304 | .x(dto.getX()) | |
| 305 | .y(dto.getY()) | |
| 306 | .build(); | |
| 307 |
1
1. postConcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::postConcept → KILLED |
return conceptRepository.save(concept); |
| 308 | } | |
| 309 | ||
| 310 | @Operation( | |
| 311 | summary = "Create a new subconcept of a top-level concept", | |
| 312 | description = | |
| 313 | """ | |
| 314 | Accepts a YAML (or JSON) request body. The parent must be an existing top-level | |
| 315 | concept in the same course, and the label must be unique among the parent's | |
| 316 | subconcepts. Subconcepts have no name, position, or color of their own. label, | |
| 317 | description, and example are Markdown; they are sanitized and canonicalized before | |
| 318 | being stored. | |
| 319 | """) | |
| 320 | @io.swagger.v3.oas.annotations.parameters.RequestBody( | |
| 321 | content = | |
| 322 | @io.swagger.v3.oas.annotations.media.Content( | |
| 323 | mediaType = "application/yaml", | |
| 324 | schema = | |
| 325 | @io.swagger.v3.oas.annotations.media.Schema( | |
| 326 | implementation = CreateSubconceptDTO.class), | |
| 327 | examples = | |
| 328 | @io.swagger.v3.oas.annotations.media.ExampleObject( | |
| 329 | value = | |
| 330 | """ | |
| 331 | courseId: 1 | |
| 332 | parentConceptId: 42 | |
| 333 | label: Accessing a value | |
| 334 | description: | | |
| 335 | Use square brackets with an index. | |
| 336 | example: | | |
| 337 | ```java | |
| 338 | int first = arr[0]; | |
| 339 | ``` | |
| 340 | """))) | |
| 341 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #dto.courseId)") | |
| 342 | @PostMapping( | |
| 343 | value = "/api/concept/subconcept", | |
| 344 | consumes = {"application/yaml", "application/x-yaml", "application/json"}) | |
| 345 | public Concept postSubconcept(@RequestBody CreateSubconceptDTO dto) | |
| 346 | throws EntityNotFoundException { | |
| 347 |
1
1. postSubconcept : negated conditional → KILLED |
if (dto.getCourseId() == null) { |
| 348 | throw new IllegalArgumentException("courseId is required"); | |
| 349 | } | |
| 350 |
1
1. postSubconcept : negated conditional → KILLED |
if (dto.getParentConceptId() == null) { |
| 351 | throw new IllegalArgumentException("parentConceptId is required"); | |
| 352 | } | |
| 353 | Course course = | |
| 354 | courseRepository | |
| 355 | .findById(dto.getCourseId()) | |
| 356 |
1
1. lambda$postSubconcept$12 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$postSubconcept$12 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, dto.getCourseId())); |
| 357 | ||
| 358 | String cleanLabel = cleanAndValidateSubconceptLabel(dto.getLabel()); | |
| 359 | ||
| 360 | Concept parent = | |
| 361 | conceptRepository | |
| 362 | .findById(dto.getParentConceptId()) | |
| 363 | .orElseThrow( | |
| 364 |
1
1. lambda$postSubconcept$13 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$postSubconcept$13 → KILLED |
() -> new EntityNotFoundException(Concept.class, dto.getParentConceptId())); |
| 365 |
1
1. postSubconcept : negated conditional → KILLED |
if (!parent.getCourse().getId().equals(dto.getCourseId())) { |
| 366 | throw new IllegalArgumentException( | |
| 367 | "parentConceptId %d belongs to a different course".formatted(dto.getParentConceptId())); | |
| 368 | } | |
| 369 |
1
1. postSubconcept : negated conditional → KILLED |
if (parent.isSubconcept()) { |
| 370 | throw new IllegalArgumentException( | |
| 371 | "parentConceptId %d is a subconcept; concepts can only be nested one level deep" | |
| 372 | .formatted(dto.getParentConceptId())); | |
| 373 | } | |
| 374 |
1
1. postSubconcept : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::rejectDuplicateLabelUnderParent → KILLED |
rejectDuplicateLabelUnderParent(parent, cleanLabel); |
| 375 | ||
| 376 | Concept concept = | |
| 377 | Concept.builder() | |
| 378 | .course(course) | |
| 379 | .label(cleanLabel) | |
| 380 | .description(markdownService.clean(dto.getDescription())) | |
| 381 | .example(markdownService.clean(dto.getExample())) | |
| 382 | .parent(parent) | |
| 383 | .sortOrder(nextSortOrder(parent.getId())) | |
| 384 | .build(); | |
| 385 |
1
1. postSubconcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::postSubconcept → KILLED |
return conceptRepository.save(concept); |
| 386 | } | |
| 387 | ||
| 388 | @Operation(summary = "Update the label, description, and example of a top-level concept") | |
| 389 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #conceptId)") | |
| 390 | @PutMapping("/api/concept/put") | |
| 391 | public Concept putConcept( | |
| 392 | @Parameter(name = "conceptId") @RequestParam Long conceptId, | |
| 393 | @RequestBody UpdateConceptDTO dto) | |
| 394 | throws EntityNotFoundException { | |
| 395 | ||
| 396 | Concept concept = | |
| 397 | conceptRepository | |
| 398 | .findById(conceptId) | |
| 399 |
1
1. lambda$putConcept$14 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$putConcept$14 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, conceptId)); |
| 400 | ||
| 401 |
1
1. putConcept : negated conditional → KILLED |
if (concept.isSubconcept()) { |
| 402 | throw new IllegalArgumentException( | |
| 403 | "concept %d is a subconcept; use PUT /api/concept/subconcept/put to update it" | |
| 404 | .formatted(conceptId)); | |
| 405 | } | |
| 406 | ||
| 407 |
1
1. putConcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setLabel → KILLED |
concept.setLabel(cleanAndValidateLabel(dto.getLabel())); |
| 408 |
1
1. putConcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setDescription → KILLED |
concept.setDescription(markdownService.clean(dto.getDescription())); |
| 409 |
1
1. putConcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setExample → KILLED |
concept.setExample(markdownService.clean(dto.getExample())); |
| 410 |
1
1. putConcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::putConcept → KILLED |
return conceptRepository.save(concept); |
| 411 | } | |
| 412 | ||
| 413 | @Operation(summary = "Update the label, description, and example of a subconcept") | |
| 414 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #conceptId)") | |
| 415 | @PutMapping("/api/concept/subconcept/put") | |
| 416 | public Concept putSubconcept( | |
| 417 | @Parameter(name = "conceptId") @RequestParam Long conceptId, | |
| 418 | @RequestBody UpdateSubconceptDTO dto) | |
| 419 | throws EntityNotFoundException { | |
| 420 | ||
| 421 | Concept concept = | |
| 422 | conceptRepository | |
| 423 | .findById(conceptId) | |
| 424 |
1
1. lambda$putSubconcept$15 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$putSubconcept$15 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, conceptId)); |
| 425 | ||
| 426 |
1
1. putSubconcept : negated conditional → KILLED |
if (!concept.isSubconcept()) { |
| 427 | throw new IllegalArgumentException( | |
| 428 | "concept %d is not a subconcept; use PUT /api/concept/put to update it" | |
| 429 | .formatted(conceptId)); | |
| 430 | } | |
| 431 | ||
| 432 | String cleanLabel = cleanAndValidateSubconceptLabel(dto.getLabel()); | |
| 433 |
1
1. putSubconcept : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::rejectDuplicateLabelUnderParent → KILLED |
rejectDuplicateLabelUnderParent(concept.getParent(), cleanLabel, concept.getId()); |
| 434 | ||
| 435 |
1
1. putSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setLabel → KILLED |
concept.setLabel(cleanLabel); |
| 436 |
1
1. putSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setDescription → KILLED |
concept.setDescription(markdownService.clean(dto.getDescription())); |
| 437 |
1
1. putSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setExample → KILLED |
concept.setExample(markdownService.clean(dto.getExample())); |
| 438 |
1
1. putSubconcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::putSubconcept → KILLED |
return conceptRepository.save(concept); |
| 439 | } | |
| 440 | ||
| 441 | @Operation(summary = "Add a practice problem URL to a concept") | |
| 442 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #conceptId)") | |
| 443 | @PostMapping("/api/concepts/practiceproblems/post") | |
| 444 | public PracticeProblem postPracticeProblem( | |
| 445 | @Parameter(name = "conceptId") @RequestParam Long conceptId, | |
| 446 | @Parameter(name = "url") @RequestParam String url) | |
| 447 | throws EntityNotFoundException { | |
| 448 | ||
| 449 | Concept concept = | |
| 450 | conceptRepository | |
| 451 | .findById(conceptId) | |
| 452 |
1
1. lambda$postPracticeProblem$16 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$postPracticeProblem$16 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, conceptId)); |
| 453 | ||
| 454 | String cleanUrl = url.strip(); | |
| 455 |
1
1. postPracticeProblem : negated conditional → KILLED |
if (cleanUrl.isEmpty()) { |
| 456 | throw new IllegalArgumentException("url may not be empty"); | |
| 457 | } | |
| 458 | Long courseId = concept.getCourse().getId(); | |
| 459 | if (practiceProblemRepository | |
| 460 | .findByCourseIdAndConceptIdAndUrl(courseId, conceptId, cleanUrl) | |
| 461 |
1
1. postPracticeProblem : negated conditional → KILLED |
.isPresent()) { |
| 462 | throw new IllegalArgumentException( | |
| 463 | "concept %d already has practice problem url %s".formatted(conceptId, cleanUrl)); | |
| 464 | } | |
| 465 | ||
| 466 | PracticeProblem practiceProblem = | |
| 467 | PracticeProblem.builder() | |
| 468 | .course(concept.getCourse()) | |
| 469 | .concept(concept) | |
| 470 | .url(cleanUrl) | |
| 471 | .build(); | |
| 472 |
1
1. postPracticeProblem : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::postPracticeProblem → KILLED |
return practiceProblemRepository.save(practiceProblem); |
| 473 | } | |
| 474 | ||
| 475 | @Operation( | |
| 476 | summary = "Designate an existing concept as a subconcept of a top-level concept", | |
| 477 | description = | |
| 478 | """ | |
| 479 | Sets the parent of concept conceptId to parentConceptId. The concept must have no | |
| 480 | subconcepts of its own and no prerequisite edges (delete those first). The parent | |
| 481 | must be a top-level concept in the same course. The concept keeps its x,y position | |
| 482 | (if any) and takes on the parent's color. | |
| 483 | """) | |
| 484 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #conceptId)") | |
| 485 | @PutMapping("/api/concepts/designate") | |
| 486 | public Concept designateSubconcept( | |
| 487 | @Parameter(name = "conceptId") @RequestParam Long conceptId, | |
| 488 | @Parameter(name = "parentConceptId") @RequestParam Long parentConceptId) | |
| 489 | throws EntityNotFoundException { | |
| 490 | ||
| 491 | Concept concept = | |
| 492 | conceptRepository | |
| 493 | .findById(conceptId) | |
| 494 |
1
1. lambda$designateSubconcept$17 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$designateSubconcept$17 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, conceptId)); |
| 495 | Concept parent = | |
| 496 | conceptRepository | |
| 497 | .findById(parentConceptId) | |
| 498 |
1
1. lambda$designateSubconcept$18 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$designateSubconcept$18 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, parentConceptId)); |
| 499 | ||
| 500 |
1
1. designateSubconcept : negated conditional → KILLED |
if (concept.getId().equals(parent.getId())) { |
| 501 | throw new IllegalArgumentException("a concept cannot be its own parent"); | |
| 502 | } | |
| 503 |
1
1. designateSubconcept : negated conditional → KILLED |
if (!parent.getCourse().getId().equals(concept.getCourse().getId())) { |
| 504 | throw new IllegalArgumentException( | |
| 505 | "parentConceptId %d belongs to a different course".formatted(parentConceptId)); | |
| 506 | } | |
| 507 |
1
1. designateSubconcept : negated conditional → KILLED |
if (parent.isSubconcept()) { |
| 508 | throw new IllegalArgumentException( | |
| 509 | "parentConceptId %d is a subconcept; concepts can only be nested one level deep" | |
| 510 | .formatted(parentConceptId)); | |
| 511 | } | |
| 512 |
1
1. designateSubconcept : negated conditional → KILLED |
if (!conceptRepository.findByParentId(conceptId).isEmpty()) { |
| 513 | throw new IllegalArgumentException( | |
| 514 | "concept %d has subconcepts of its own, so it cannot become a subconcept" | |
| 515 | .formatted(conceptId)); | |
| 516 | } | |
| 517 |
1
1. designateSubconcept : negated conditional → KILLED |
if (!conceptEdgeRepository.findBySourceIdOrTargetId(conceptId, conceptId).isEmpty()) { |
| 518 | throw new IllegalArgumentException( | |
| 519 | "concept %d has prerequisite edges; delete them before designating it as a subconcept" | |
| 520 | .formatted(conceptId)); | |
| 521 | } | |
| 522 |
1
1. designateSubconcept : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::rejectDuplicateLabelUnderParent → KILLED |
rejectDuplicateLabelUnderParent(parent, concept.getLabel()); |
| 523 | ||
| 524 |
1
1. designateSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setParent → KILLED |
concept.setParent(parent); |
| 525 |
1
1. designateSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setColor → KILLED |
concept.setColor(parent.getColor()); |
| 526 |
1
1. designateSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setSortOrder → KILLED |
concept.setSortOrder(nextSortOrder(parent.getId())); |
| 527 |
1
1. designateSubconcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::designateSubconcept → KILLED |
return conceptRepository.save(concept); |
| 528 | } | |
| 529 | ||
| 530 | @Operation( | |
| 531 | summary = "Split a subconcept off into its own top-level concept", | |
| 532 | description = | |
| 533 | """ | |
| 534 | Severs the concept's relationship with its parent, making it a top-level concept | |
| 535 | at the given x,y position. If the concept has no color, it inherits its former | |
| 536 | parent's color. | |
| 537 | """) | |
| 538 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #conceptId)") | |
| 539 | @PutMapping("/api/concepts/splitoff") | |
| 540 | public Concept splitOffSubconcept( | |
| 541 | @Parameter(name = "conceptId") @RequestParam Long conceptId, | |
| 542 | @Parameter(name = "x") @RequestParam Integer x, | |
| 543 | @Parameter(name = "y") @RequestParam Integer y) | |
| 544 | throws EntityNotFoundException { | |
| 545 | ||
| 546 | Concept concept = | |
| 547 | conceptRepository | |
| 548 | .findById(conceptId) | |
| 549 |
1
1. lambda$splitOffSubconcept$19 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$splitOffSubconcept$19 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, conceptId)); |
| 550 | ||
| 551 |
1
1. splitOffSubconcept : negated conditional → KILLED |
if (!concept.isSubconcept()) { |
| 552 | throw new IllegalArgumentException( | |
| 553 | "concept %d is already a top-level concept".formatted(conceptId)); | |
| 554 | } | |
| 555 | ||
| 556 |
1
1. splitOffSubconcept : negated conditional → KILLED |
if (concept.getColor() == null) { |
| 557 |
1
1. splitOffSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setColor → KILLED |
concept.setColor(concept.getParent().getColor()); |
| 558 | } | |
| 559 |
1
1. splitOffSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setParent → KILLED |
concept.setParent(null); |
| 560 |
1
1. splitOffSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setSortOrder → KILLED |
concept.setSortOrder(null); |
| 561 |
1
1. splitOffSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setX → KILLED |
concept.setX(x); |
| 562 |
1
1. splitOffSubconcept : removed call to edu/ucsb/cs/scaffold/entity/Concept::setY → KILLED |
concept.setY(y); |
| 563 |
1
1. splitOffSubconcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::splitOffSubconcept → KILLED |
return conceptRepository.save(concept); |
| 564 | } | |
| 565 | ||
| 566 | @Operation( | |
| 567 | summary = "Reorder the subconcepts of a top-level concept", | |
| 568 | description = | |
| 569 | """ | |
| 570 | Takes the complete ordered list of the parent's subconcept ids and rewrites every | |
| 571 | subconcept's sort position to its index in the list. Sending the whole permutation | |
| 572 | (rather than individual move operations) makes the update atomic and idempotent: | |
| 573 | concurrent reorders resolve to one complete, coherent ordering (last writer wins), | |
| 574 | and any sort-position ties left by concurrent subconcept creation are cleaned up as | |
| 575 | a side effect. The list must contain each current subconcept of the parent exactly | |
| 576 | once. Returns the subconcepts in their new order. | |
| 577 | """) | |
| 578 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #parentConceptId)") | |
| 579 | @PutMapping("/api/concepts/subconcepts/reorder") | |
| 580 | public List<SubconceptDTO> reorderSubconcepts( | |
| 581 | @Parameter(name = "parentConceptId") @RequestParam Long parentConceptId, | |
| 582 | @RequestBody List<Long> orderedSubconceptIds) | |
| 583 | throws EntityNotFoundException { | |
| 584 | ||
| 585 | Concept parent = | |
| 586 | conceptRepository | |
| 587 | .findById(parentConceptId) | |
| 588 |
1
1. lambda$reorderSubconcepts$20 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$reorderSubconcepts$20 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, parentConceptId)); |
| 589 |
1
1. reorderSubconcepts : negated conditional → KILLED |
if (parent.isSubconcept()) { |
| 590 | throw new IllegalArgumentException( | |
| 591 | "concept %d is a subconcept; only top-level concepts have subconcepts to reorder" | |
| 592 | .formatted(parentConceptId)); | |
| 593 | } | |
| 594 | ||
| 595 | List<Concept> subconcepts = conceptRepository.findByParentId(parentConceptId); | |
| 596 | Map<Long, Concept> subconceptById = new HashMap<>(); | |
| 597 | for (Concept subconcept : subconcepts) { | |
| 598 | subconceptById.put(subconcept.getId(), subconcept); | |
| 599 | } | |
| 600 |
1
1. reorderSubconcepts : negated conditional → KILLED |
if (new HashSet<>(orderedSubconceptIds).size() != orderedSubconceptIds.size() |
| 601 |
1
1. reorderSubconcepts : negated conditional → KILLED |
|| orderedSubconceptIds.size() != subconcepts.size() |
| 602 |
1
1. reorderSubconcepts : negated conditional → KILLED |
|| !subconceptById.keySet().containsAll(orderedSubconceptIds)) { |
| 603 | throw new IllegalArgumentException( | |
| 604 | "orderedSubconceptIds must contain the id of each subconcept of concept %d exactly once" | |
| 605 | .formatted(parentConceptId)); | |
| 606 | } | |
| 607 | ||
| 608 |
2
1. reorderSubconcepts : negated conditional → KILLED 2. reorderSubconcepts : changed conditional boundary → KILLED |
for (int i = 0; i < orderedSubconceptIds.size(); i++) { |
| 609 |
1
1. reorderSubconcepts : removed call to edu/ucsb/cs/scaffold/entity/Concept::setSortOrder → KILLED |
subconceptById.get(orderedSubconceptIds.get(i)).setSortOrder(i); |
| 610 | } | |
| 611 | conceptRepository.saveAll(subconcepts); | |
| 612 | ||
| 613 |
1
1. reorderSubconcepts : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/ConceptsController::reorderSubconcepts → KILLED |
return orderedSubconceptIds.stream() |
| 614 | .map(subconceptById::get) | |
| 615 | .map( | |
| 616 | sub -> | |
| 617 |
1
1. lambda$reorderSubconcepts$21 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$reorderSubconcepts$21 → KILLED |
new SubconceptDTO( |
| 618 | sub.getId(), | |
| 619 | sub.getParent().getId(), | |
| 620 | markdownService.toInlineHtml(sub.getLabel()))) | |
| 621 | .toList(); | |
| 622 | } | |
| 623 | ||
| 624 | @Operation(summary = "Create a prerequisite edge between two top-level concepts") | |
| 625 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #sourceConceptId)") | |
| 626 | @PostMapping("/api/concepts/edges/post") | |
| 627 | public ConceptEdge postConceptEdge( | |
| 628 | @Parameter(name = "sourceConceptId") @RequestParam Long sourceConceptId, | |
| 629 | @Parameter(name = "targetConceptId") @RequestParam Long targetConceptId) | |
| 630 | throws EntityNotFoundException { | |
| 631 | ||
| 632 | Concept source = | |
| 633 | conceptRepository | |
| 634 | .findById(sourceConceptId) | |
| 635 |
1
1. lambda$postConceptEdge$22 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$postConceptEdge$22 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, sourceConceptId)); |
| 636 | Concept target = | |
| 637 | conceptRepository | |
| 638 | .findById(targetConceptId) | |
| 639 |
1
1. lambda$postConceptEdge$23 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$postConceptEdge$23 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, targetConceptId)); |
| 640 | ||
| 641 |
1
1. postConceptEdge : negated conditional → KILLED |
if (source.getId().equals(target.getId())) { |
| 642 | throw new IllegalArgumentException("an edge cannot connect a concept to itself"); | |
| 643 | } | |
| 644 |
1
1. postConceptEdge : negated conditional → KILLED |
if (!source.getCourse().getId().equals(target.getCourse().getId())) { |
| 645 | throw new IllegalArgumentException("both concepts must belong to the same course"); | |
| 646 | } | |
| 647 |
2
1. postConceptEdge : negated conditional → KILLED 2. postConceptEdge : negated conditional → KILLED |
if (source.isSubconcept() || target.isSubconcept()) { |
| 648 | throw new IllegalArgumentException("edges may only connect top-level concepts"); | |
| 649 | } | |
| 650 | if (conceptEdgeRepository | |
| 651 | .findBySourceIdAndTargetId(sourceConceptId, targetConceptId) | |
| 652 |
1
1. postConceptEdge : negated conditional → KILLED |
.isPresent()) { |
| 653 | throw new IllegalArgumentException( | |
| 654 | "edge from concept %d to concept %d already exists" | |
| 655 | .formatted(sourceConceptId, targetConceptId)); | |
| 656 | } | |
| 657 |
1
1. postConceptEdge : negated conditional → KILLED |
if (conceptGraphService.wouldCreateCycle( |
| 658 | conceptEdgeRepository.findByCourseId(source.getCourse().getId()), | |
| 659 | sourceConceptId, | |
| 660 | targetConceptId)) { | |
| 661 | throw new IllegalArgumentException( | |
| 662 | "edge from concept %d to concept %d would create a cycle" | |
| 663 | .formatted(sourceConceptId, targetConceptId)); | |
| 664 | } | |
| 665 | ||
| 666 | ConceptEdge edge = | |
| 667 | ConceptEdge.builder().course(source.getCourse()).source(source).target(target).build(); | |
| 668 |
1
1. postConceptEdge : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::postConceptEdge → KILLED |
return conceptEdgeRepository.save(edge); |
| 669 | } | |
| 670 | ||
| 671 | @Operation(summary = "Delete a prerequisite edge") | |
| 672 | @PreAuthorize("@CourseSecurity.hasConceptEdgeManagementPermissions(#root, #id)") | |
| 673 | @DeleteMapping("/api/concepts/edges/delete") | |
| 674 | public Object deleteConceptEdge(@Parameter(name = "id") @RequestParam Long id) | |
| 675 | throws EntityNotFoundException { | |
| 676 | ConceptEdge edge = | |
| 677 | conceptEdgeRepository | |
| 678 | .findById(id) | |
| 679 |
1
1. lambda$deleteConceptEdge$24 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$deleteConceptEdge$24 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(ConceptEdge.class, id)); |
| 680 |
1
1. deleteConceptEdge : removed call to edu/ucsb/cs/scaffold/repository/ConceptEdgeRepository::delete → KILLED |
conceptEdgeRepository.delete(edge); |
| 681 |
1
1. deleteConceptEdge : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::deleteConceptEdge → KILLED |
return genericMessage("ConceptEdge with id %s deleted".formatted(id)); |
| 682 | } | |
| 683 | ||
| 684 | @Operation( | |
| 685 | summary = "Delete a concept; deleting a top-level concept also deletes its subconcepts") | |
| 686 | @PreAuthorize("@CourseSecurity.hasConceptManagementPermissions(#root, #conceptId)") | |
| 687 | @DeleteMapping("/api/concept/delete") | |
| 688 | public Object deleteConcept(@Parameter(name = "conceptId") @RequestParam Long conceptId) | |
| 689 | throws EntityNotFoundException { | |
| 690 | Concept concept = | |
| 691 | conceptRepository | |
| 692 | .findById(conceptId) | |
| 693 |
1
1. lambda$deleteConcept$25 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$deleteConcept$25 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Concept.class, conceptId)); |
| 694 | ||
| 695 | Course course = concept.getCourse(); | |
| 696 |
1
1. deleteConcept : negated conditional → KILLED |
if (concept.isSubconcept()) { |
| 697 |
1
1. deleteConcept : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::deleteConceptArtifacts → KILLED |
deleteConceptArtifacts(course.getId(), List.of(conceptId)); |
| 698 |
1
1. deleteConcept : removed call to edu/ucsb/cs/scaffold/repository/ConceptRepository::delete → KILLED |
conceptRepository.delete(concept); |
| 699 |
1
1. deleteConcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::deleteConcept → KILLED |
return genericMessage("Concept with id %s deleted".formatted(conceptId)); |
| 700 | } | |
| 701 | ||
| 702 | List<Concept> subconcepts = conceptRepository.findByParentId(conceptId); | |
| 703 | List<Long> conceptIdsToDelete = new ArrayList<>(); | |
| 704 | conceptIdsToDelete.add(conceptId); | |
| 705 | conceptIdsToDelete.addAll(subconcepts.stream().map(Concept::getId).toList()); | |
| 706 |
1
1. deleteConcept : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::deleteConceptArtifacts → KILLED |
deleteConceptArtifacts(course.getId(), conceptIdsToDelete); |
| 707 |
1
1. deleteConcept : removed call to edu/ucsb/cs/scaffold/repository/ConceptRepository::deleteAll → KILLED |
conceptRepository.deleteAll(subconcepts); |
| 708 |
1
1. deleteConcept : removed call to edu/ucsb/cs/scaffold/repository/ConceptRepository::delete → KILLED |
conceptRepository.delete(concept); |
| 709 |
1
1. deleteConcept : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::deleteConcept → KILLED |
return genericMessage("Concept with id %s deleted".formatted(conceptId)); |
| 710 | } | |
| 711 | ||
| 712 | @Operation( | |
| 713 | summary = "Recompute a course's concept-graph structure", | |
| 714 | description = | |
| 715 | """ | |
| 716 | Runs cycle detection, transitive reduction, longest-path leveling, and layout over | |
| 717 | a course's top-level concepts and prerequisite edges, persisting the results: | |
| 718 | edges found to be part of a cycle are colored red and excluded from the rest of | |
| 719 | the analysis (their endpoints keep whatever level/position they already had); | |
| 720 | edges made redundant by the graph's transitive structure are deleted; every other | |
| 721 | top-level concept is assigned a longest-path level (roots are level 1), a color | |
| 722 | from that level, and an x,y position (each level arranged left to right, sorted by | |
| 723 | prior x then id, centered at x=0, stacked above the previous level). | |
| 724 | """) | |
| 725 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 726 | @PostMapping("/api/course/scaffold/reset") | |
| 727 | public ScaffoldResetResponseDTO resetCourseScaffold( | |
| 728 | @Parameter(name = "courseId") @RequestParam Long courseId) throws EntityNotFoundException { | |
| 729 | courseRepository | |
| 730 | .findById(courseId) | |
| 731 |
1
1. lambda$resetCourseScaffold$26 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$26 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 732 | ||
| 733 | List<Concept> topLevelConcepts = | |
| 734 | conceptRepository.findByCourseId(courseId).stream() | |
| 735 |
2
1. lambda$resetCourseScaffold$27 : negated conditional → KILLED 2. lambda$resetCourseScaffold$27 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$27 → KILLED |
.filter(concept -> !concept.isSubconcept()) |
| 736 | .toList(); | |
| 737 | List<ConceptEdge> edges = conceptEdgeRepository.findByCourseId(courseId); | |
| 738 | ||
| 739 | Map<Long, Integer> priorXByConceptId = | |
| 740 | priorXByConceptId(topLevelConcepts, callerPrivatePositions(courseId)); | |
| 741 | ConceptGraphService.ResetResult result = | |
| 742 | conceptGraphService.reset(topLevelConcepts, edges, priorXByConceptId); | |
| 743 | ||
| 744 | for (ConceptEdge edge : edges) { | |
| 745 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/entity/ConceptEdge::setColor → KILLED |
edge.setColor( |
| 746 |
1
1. resetCourseScaffold : negated conditional → KILLED |
result.cycleEdgeIds().contains(edge.getId()) |
| 747 | ? ConceptGraphService.CYCLE_EDGE_COLOR | |
| 748 | : null); | |
| 749 | } | |
| 750 | conceptEdgeRepository.saveAll(edges); | |
| 751 | ||
| 752 | List<ConceptEdge> removedEdges = | |
| 753 |
2
1. lambda$resetCourseScaffold$28 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$28 → KILLED 2. lambda$resetCourseScaffold$28 : replaced boolean return with false for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$28 → KILLED |
edges.stream().filter(edge -> result.removedEdgeIds().contains(edge.getId())).toList(); |
| 754 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/repository/ConceptEdgeRepository::deleteAll → KILLED |
conceptEdgeRepository.deleteAll(removedEdges); |
| 755 | ||
| 756 | for (Concept concept : topLevelConcepts) { | |
| 757 | int level = result.levelByConceptId().getOrDefault(concept.getId(), 1); | |
| 758 | ConceptGraphService.Position position = result.positionByConceptId().get(concept.getId()); | |
| 759 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/entity/Concept::setLevel → KILLED |
concept.setLevel(level); |
| 760 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/entity/Concept::setColor → KILLED |
concept.setColor(conceptGraphService.colorForLevel(level)); |
| 761 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/entity/Concept::setX → KILLED |
concept.setX(position.x()); |
| 762 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/entity/Concept::setY → KILLED |
concept.setY(position.y()); |
| 763 | } | |
| 764 | conceptRepository.saveAll(topLevelConcepts); | |
| 765 |
1
1. resetCourseScaffold : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::clearPrivateTopLevelPositions → KILLED |
clearPrivateTopLevelPositions(courseId); |
| 766 | ||
| 767 | List<CycleEdgeDTO> cycleEdgeDtos = | |
| 768 | edges.stream() | |
| 769 |
2
1. lambda$resetCourseScaffold$29 : replaced boolean return with false for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$29 → KILLED 2. lambda$resetCourseScaffold$29 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$29 → KILLED |
.filter(edge -> result.cycleEdgeIds().contains(edge.getId())) |
| 770 | .map( | |
| 771 | edge -> | |
| 772 |
1
1. lambda$resetCourseScaffold$30 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$30 → KILLED |
new CycleEdgeDTO( |
| 773 | edge.getId(), edge.getSource().getId(), edge.getTarget().getId())) | |
| 774 | .toList(); | |
| 775 | List<RemovedEdgeDTO> removedEdgeDtos = | |
| 776 | removedEdges.stream() | |
| 777 | .map( | |
| 778 | edge -> | |
| 779 |
1
1. lambda$resetCourseScaffold$31 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$31 → KILLED |
new RemovedEdgeDTO( |
| 780 | edge.getId(), edge.getSource().getId(), edge.getTarget().getId())) | |
| 781 | .toList(); | |
| 782 | List<LevelAssignmentDTO> levelDtos = | |
| 783 | topLevelConcepts.stream() | |
| 784 | .sorted(Comparator.comparing(Concept::getId)) | |
| 785 | .map( | |
| 786 | concept -> | |
| 787 |
1
1. lambda$resetCourseScaffold$32 : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$resetCourseScaffold$32 → KILLED |
new LevelAssignmentDTO( |
| 788 | concept.getId(), | |
| 789 | concept.getLabel(), | |
| 790 | concept.getLevel(), | |
| 791 | concept.getColor(), | |
| 792 | concept.getX(), | |
| 793 | concept.getY())) | |
| 794 | .toList(); | |
| 795 | ||
| 796 | ScaffoldResetReportDTO report = | |
| 797 | new ScaffoldResetReportDTO(cycleEdgeDtos, removedEdgeDtos, levelDtos); | |
| 798 |
1
1. resetCourseScaffold : replaced return value with null for edu/ucsb/cs/scaffold/controller/ConceptsController::resetCourseScaffold → KILLED |
return new ScaffoldResetResponseDTO(report, getGraph(courseId), getEdges(courseId)); |
| 799 | } | |
| 800 | ||
| 801 | private String cleanAndValidateLabel(String label) { | |
| 802 |
1
1. cleanAndValidateLabel : replaced return value with "" for edu/ucsb/cs/scaffold/controller/ConceptsController::cleanAndValidateLabel → KILLED |
return cleanAndValidateLabel(label, MAX_RENDERED_CONCEPT_LABEL_LENGTH); |
| 803 | } | |
| 804 | ||
| 805 | private String cleanAndValidateSubconceptLabel(String label) { | |
| 806 |
1
1. cleanAndValidateSubconceptLabel : replaced return value with "" for edu/ucsb/cs/scaffold/controller/ConceptsController::cleanAndValidateSubconceptLabel → KILLED |
return cleanAndValidateLabel(label, MAX_RENDERED_SUBCONCEPT_LABEL_LENGTH); |
| 807 | } | |
| 808 | ||
| 809 | private String cleanAndValidateLabel(String label, int maxRenderedLength) { | |
| 810 | String cleanLabel = markdownService.cleanLabel(label); | |
| 811 |
2
1. cleanAndValidateLabel : negated conditional → KILLED 2. cleanAndValidateLabel : negated conditional → KILLED |
if (cleanLabel == null || cleanLabel.isEmpty()) { |
| 812 | throw new IllegalArgumentException("label may not be empty"); | |
| 813 | } | |
| 814 | int renderedLength = markdownService.renderedLength(cleanLabel); | |
| 815 |
2
1. cleanAndValidateLabel : negated conditional → KILLED 2. cleanAndValidateLabel : changed conditional boundary → KILLED |
if (renderedLength > maxRenderedLength) { |
| 816 | throw new IllegalArgumentException( | |
| 817 | "label renders to %d characters; the maximum is %d" | |
| 818 | .formatted(renderedLength, maxRenderedLength)); | |
| 819 | } | |
| 820 |
1
1. cleanAndValidateLabel : replaced return value with "" for edu/ucsb/cs/scaffold/controller/ConceptsController::cleanAndValidateLabel → KILLED |
return cleanLabel; |
| 821 | } | |
| 822 | ||
| 823 | /** | |
| 824 | * The sort position for a subconcept newly added to the given parent: one past the highest | |
| 825 | * existing position, so new subconcepts always append at the end of the author's ordering. | |
| 826 | * Subconcepts predating the sort_order column may have null positions; they are ignored here | |
| 827 | * (they display last, after positioned rows, until the author reorders). | |
| 828 | */ | |
| 829 | private int nextSortOrder(Long parentId) { | |
| 830 |
1
1. nextSortOrder : replaced int return with 0 for edu/ucsb/cs/scaffold/controller/ConceptsController::nextSortOrder → KILLED |
return conceptRepository.findByParentId(parentId).stream() |
| 831 | .map(Concept::getSortOrder) | |
| 832 | .filter(Objects::nonNull) | |
| 833 | .mapToInt(Integer::intValue) | |
| 834 | .max() | |
| 835 |
1
1. nextSortOrder : Replaced integer addition with subtraction → KILLED |
.orElse(-1) |
| 836 | + 1; | |
| 837 | } | |
| 838 | ||
| 839 | private void rejectDuplicateLabelUnderParent(Concept parent, String label) { | |
| 840 |
1
1. rejectDuplicateLabelUnderParent : removed call to edu/ucsb/cs/scaffold/controller/ConceptsController::rejectDuplicateLabelUnderParent → KILLED |
rejectDuplicateLabelUnderParent(parent, label, null); |
| 841 | } | |
| 842 | ||
| 843 | private void rejectDuplicateLabelUnderParent( | |
| 844 | Concept parent, String label, Long excludeConceptId) { | |
| 845 | Concept duplicate = | |
| 846 | conceptRepository.findByParentIdAndLabel(parent.getId(), label).orElse(null); | |
| 847 |
2
1. rejectDuplicateLabelUnderParent : negated conditional → KILLED 2. rejectDuplicateLabelUnderParent : negated conditional → KILLED |
if (duplicate != null && !Objects.equals(duplicate.getId(), excludeConceptId)) { |
| 848 | throw new IllegalArgumentException( | |
| 849 | "concept %d already has a subconcept with label %s".formatted(parent.getId(), label)); | |
| 850 | } | |
| 851 | } | |
| 852 | ||
| 853 | private void deleteConceptArtifacts(Long courseId, List<Long> conceptIds) { | |
| 854 | Map<Long, ConceptEdge> distinctEdges = new LinkedHashMap<>(); | |
| 855 | for (Long id : conceptIds) { | |
| 856 |
1
1. deleteConceptArtifacts : removed call to edu/ucsb/cs/scaffold/repository/PracticeProblemRepository::deleteAll → KILLED |
practiceProblemRepository.deleteAll( |
| 857 | practiceProblemRepository.findByCourseIdAndConceptId(courseId, id)); | |
| 858 | for (ConceptEdge edge : conceptEdgeRepository.findBySourceIdOrTargetId(id, id)) { | |
| 859 | distinctEdges.put(edge.getId(), edge); | |
| 860 | } | |
| 861 | } | |
| 862 |
1
1. deleteConceptArtifacts : removed call to edu/ucsb/cs/scaffold/repository/ConceptEdgeRepository::deleteAll → KILLED |
conceptEdgeRepository.deleteAll(distinctEdges.values()); |
| 863 | } | |
| 864 | ||
| 865 | /** | |
| 866 | * The requesting user's own private, unsaved top-level position overrides for the course (see | |
| 867 | * {@link UserState#getTopLevelPositions()}), keyed by the concept's numeric id as a string. Empty | |
| 868 | * if the user has never dragged a top-level node or has no saved state for the course. | |
| 869 | */ | |
| 870 | private Map<String, StoredPosition> callerPrivatePositions(Long courseId) { | |
| 871 | Long userId = getCurrentUser().getUser().getId(); | |
| 872 |
1
1. callerPrivatePositions : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::callerPrivatePositions → KILLED |
return userStateRepository |
| 873 | .findByUseridAndCourseId(userId, courseId) | |
| 874 |
1
1. lambda$callerPrivatePositions$33 : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::lambda$callerPrivatePositions$33 → KILLED |
.map(state -> parseTopLevelPositions(state.getTopLevelPositions())) |
| 875 | .orElseGet(Map::of); | |
| 876 | } | |
| 877 | ||
| 878 | private Map<String, StoredPosition> parseTopLevelPositions(String json) { | |
| 879 | try { | |
| 880 |
1
1. parseTopLevelPositions : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::parseTopLevelPositions → KILLED |
return objectMapper.readValue(json, new TypeReference<Map<String, StoredPosition>>() {}); |
| 881 | } catch (JsonProcessingException e) { | |
| 882 | throw new IllegalStateException("Unable to parse stored top-level position overrides", e); | |
| 883 | } | |
| 884 | } | |
| 885 | ||
| 886 | /** | |
| 887 | * The x value {@link ConceptGraphService#reset} should sort each concept by: the caller's private | |
| 888 | * override for that concept if they have dragged it, otherwise the concept's own persisted x. | |
| 889 | */ | |
| 890 | private Map<Long, Integer> priorXByConceptId( | |
| 891 | List<Concept> topLevelConcepts, Map<String, StoredPosition> privatePositions) { | |
| 892 | Map<Long, Integer> result = new HashMap<>(); | |
| 893 | for (Concept concept : topLevelConcepts) { | |
| 894 | StoredPosition override = privatePositions.get(String.valueOf(concept.getId())); | |
| 895 |
2
1. priorXByConceptId : negated conditional → KILLED 2. priorXByConceptId : negated conditional → KILLED |
Integer x = override != null && override.x() != null ? override.x() : concept.getX(); |
| 896 | result.put(concept.getId(), x); | |
| 897 | } | |
| 898 |
1
1. priorXByConceptId : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::priorXByConceptId → KILLED |
return result; |
| 899 | } | |
| 900 | ||
| 901 | /** | |
| 902 | * Clears every user's private top-level position overrides for the course. Called after a | |
| 903 | * successful reset, since a structural change (a concept moving to a different level) can make a | |
| 904 | * stale private override render in the wrong row. | |
| 905 | */ | |
| 906 | private void clearPrivateTopLevelPositions(Long courseId) { | |
| 907 | List<UserState> states = userStateRepository.findByCourseId(courseId); | |
| 908 | for (UserState state : states) { | |
| 909 |
1
1. clearPrivateTopLevelPositions : removed call to edu/ucsb/cs/scaffold/model/UserState::setTopLevelPositions → KILLED |
state.setTopLevelPositions("{}"); |
| 910 | } | |
| 911 | userStateRepository.saveAll(states); | |
| 912 | } | |
| 913 | ||
| 914 | private record StoredPosition(Integer x, Integer y) {} | |
| 915 | ||
| 916 | private Map<Long, String> firstUrlByConceptId(Long courseId) { | |
| 917 | Map<Long, String> result = new LinkedHashMap<>(); | |
| 918 | List<PracticeProblem> problems = | |
| 919 | practiceProblemRepository.findByCourseId(courseId).stream() | |
| 920 | .sorted(Comparator.comparing(PracticeProblem::getId)) | |
| 921 | .toList(); | |
| 922 | for (PracticeProblem problem : problems) { | |
| 923 | result.putIfAbsent(problem.getConcept().getId(), problem.getUrl()); | |
| 924 | } | |
| 925 |
1
1. firstUrlByConceptId : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/ConceptsController::firstUrlByConceptId → KILLED |
return result; |
| 926 | } | |
| 927 | ||
| 928 | public record ConceptContentDTO( | |
| 929 | Long id, Long parentId, String descriptionHtml, String exampleHtml, String practiceUrl) {} | |
| 930 | ||
| 931 | public record CourseConceptDTO( | |
| 932 | Long id, | |
| 933 | String label, | |
| 934 | String description, | |
| 935 | String example, | |
| 936 | Integer level, | |
| 937 | Integer x, | |
| 938 | Integer y) {} | |
| 939 | ||
| 940 | public record MajorConceptDTO( | |
| 941 | Long id, String labelHtml, String color, List<SubconceptDTO> subconcepts) {} | |
| 942 | ||
| 943 | public record SubconceptDTO(Long id, Long parentId, String labelHtml) {} | |
| 944 | ||
| 945 | public record TopLevelConceptDTO(Long id, String label, Integer level, Integer x, Integer y) {} | |
| 946 | ||
| 947 | public record SubConceptTableRowDTO( | |
| 948 | Long id, | |
| 949 | String label, | |
| 950 | String description, | |
| 951 | String example, | |
| 952 | Long parentId, | |
| 953 | String parentLabel, | |
| 954 | Integer parentLevel, | |
| 955 | Integer parentX, | |
| 956 | Integer sortOrder) {} | |
| 957 | ||
| 958 | public record PositionDTO(Integer x, Integer y) {} | |
| 959 | ||
| 960 | public record EdgeDTO(Long id, Long sourceId, Long targetId, String color) {} | |
| 961 | ||
| 962 | public record CycleEdgeDTO(Long edgeId, Long sourceId, Long targetId) {} | |
| 963 | ||
| 964 | public record RemovedEdgeDTO(Long edgeId, Long sourceId, Long targetId) {} | |
| 965 | ||
| 966 | public record LevelAssignmentDTO( | |
| 967 | Long id, String label, Integer level, String color, Integer x, Integer y) {} | |
| 968 | ||
| 969 | public record ScaffoldResetReportDTO( | |
| 970 | List<CycleEdgeDTO> cycleEdges, | |
| 971 | List<RemovedEdgeDTO> removedEdges, | |
| 972 | List<LevelAssignmentDTO> levels) {} | |
| 973 | ||
| 974 | public record ScaffoldResetResponseDTO( | |
| 975 | ScaffoldResetReportDTO report, List<MajorConceptDTO> graph, List<EdgeDTO> edges) {} | |
| 976 | } | |
Mutations | ||
| 91 |
1.1 |
|
| 101 |
1.1 |
|
| 109 |
1.1 |
|
| 110 |
1.1 2.2 |
|
| 114 |
1.1 |
|
| 138 |
1.1 |
|
| 144 |
1.1 2.2 |
|
| 146 |
1.1 |
|
| 152 |
1.1 |
|
| 164 |
1.1 |
|
| 174 |
1.1 |
|
| 178 |
1.1 |
|
| 186 |
1.1 |
|
| 187 |
1.1 2.2 |
|
| 191 |
1.1 |
|
| 205 |
1.1 |
|
| 209 |
1.1 |
|
| 212 |
1.1 |
|
| 218 |
1.1 |
|
| 242 |
1.1 |
|
| 283 |
1.1 |
|
| 289 |
1.1 |
|
| 292 |
1.1 2.2 |
|
| 307 |
1.1 |
|
| 347 |
1.1 |
|
| 350 |
1.1 |
|
| 356 |
1.1 |
|
| 364 |
1.1 |
|
| 365 |
1.1 |
|
| 369 |
1.1 |
|
| 374 |
1.1 |
|
| 385 |
1.1 |
|
| 399 |
1.1 |
|
| 401 |
1.1 |
|
| 407 |
1.1 |
|
| 408 |
1.1 |
|
| 409 |
1.1 |
|
| 410 |
1.1 |
|
| 424 |
1.1 |
|
| 426 |
1.1 |
|
| 433 |
1.1 |
|
| 435 |
1.1 |
|
| 436 |
1.1 |
|
| 437 |
1.1 |
|
| 438 |
1.1 |
|
| 452 |
1.1 |
|
| 455 |
1.1 |
|
| 461 |
1.1 |
|
| 472 |
1.1 |
|
| 494 |
1.1 |
|
| 498 |
1.1 |
|
| 500 |
1.1 |
|
| 503 |
1.1 |
|
| 507 |
1.1 |
|
| 512 |
1.1 |
|
| 517 |
1.1 |
|
| 522 |
1.1 |
|
| 524 |
1.1 |
|
| 525 |
1.1 |
|
| 526 |
1.1 |
|
| 527 |
1.1 |
|
| 549 |
1.1 |
|
| 551 |
1.1 |
|
| 556 |
1.1 |
|
| 557 |
1.1 |
|
| 559 |
1.1 |
|
| 560 |
1.1 |
|
| 561 |
1.1 |
|
| 562 |
1.1 |
|
| 563 |
1.1 |
|
| 588 |
1.1 |
|
| 589 |
1.1 |
|
| 600 |
1.1 |
|
| 601 |
1.1 |
|
| 602 |
1.1 |
|
| 608 |
1.1 2.2 |
|
| 609 |
1.1 |
|
| 613 |
1.1 |
|
| 617 |
1.1 |
|
| 635 |
1.1 |
|
| 639 |
1.1 |
|
| 641 |
1.1 |
|
| 644 |
1.1 |
|
| 647 |
1.1 2.2 |
|
| 652 |
1.1 |
|
| 657 |
1.1 |
|
| 668 |
1.1 |
|
| 679 |
1.1 |
|
| 680 |
1.1 |
|
| 681 |
1.1 |
|
| 693 |
1.1 |
|
| 696 |
1.1 |
|
| 697 |
1.1 |
|
| 698 |
1.1 |
|
| 699 |
1.1 |
|
| 706 |
1.1 |
|
| 707 |
1.1 |
|
| 708 |
1.1 |
|
| 709 |
1.1 |
|
| 731 |
1.1 |
|
| 735 |
1.1 2.2 |
|
| 745 |
1.1 |
|
| 746 |
1.1 |
|
| 753 |
1.1 2.2 |
|
| 754 |
1.1 |
|
| 759 |
1.1 |
|
| 760 |
1.1 |
|
| 761 |
1.1 |
|
| 762 |
1.1 |
|
| 765 |
1.1 |
|
| 769 |
1.1 2.2 |
|
| 772 |
1.1 |
|
| 779 |
1.1 |
|
| 787 |
1.1 |
|
| 798 |
1.1 |
|
| 802 |
1.1 |
|
| 806 |
1.1 |
|
| 811 |
1.1 2.2 |
|
| 815 |
1.1 2.2 |
|
| 820 |
1.1 |
|
| 830 |
1.1 |
|
| 835 |
1.1 |
|
| 840 |
1.1 |
|
| 847 |
1.1 2.2 |
|
| 856 |
1.1 |
|
| 862 |
1.1 |
|
| 872 |
1.1 |
|
| 874 |
1.1 |
|
| 880 |
1.1 |
|
| 895 |
1.1 2.2 |
|
| 898 |
1.1 |
|
| 909 |
1.1 |
|
| 925 |
1.1 |