lms.model.backend
1import logging 2import typing 3 4import edq.util.parse 5import quizcomp.model.quiz 6 7import lms.model.assignments 8import lms.model.config 9import lms.model.constants 10import lms.model.courses 11import lms.model.groups 12import lms.model.groupsets 13import lms.model.query 14import lms.model.scores 15import lms.model.users 16 17_logger = logging.getLogger(__name__) 18 19class APIBackend(): 20 """ 21 API backends provide a unified interface to an LMS. 22 23 Note that instead of using an abstract class, 24 methods will raise a NotImplementedError by default. 25 This will allow child backends to fill in as much functionality as they can, 26 while still leaving gaps where they are incomplete or impossible. 27 """ 28 29 _testing_override: typing.Union[bool, None] = None 30 """ A top-level override to control testing status. """ 31 32 def __init__(self, 33 config: lms.model.config.Config, 34 testing: typing.Union[bool, str] = False, 35 **kwargs: typing.Any) -> None: 36 self.config: lms.model.config.Config = config 37 """ The configuration options for this backend. """ 38 39 assert(self.config.backend_type is not None) 40 41 self.backend_type: lms.model.constants.BackendType = self.config.backend_type 42 """ 43 The backend type of this server. 44 This is set in config and compied for type checking. 45 """ 46 47 assert((self.config.server is not None) and (len(self.config.server) > 0)) 48 49 self.server: str = self.config.server 50 """ 51 The server to connect to. 52 This is set in config and compied for type checking. 53 """ 54 55 parsed_testing = edq.util.parse.soft_boolean(self.config.testing) 56 if (APIBackend._testing_override is not None): 57 parsed_testing = APIBackend._testing_override 58 59 if (parsed_testing is None): 60 parsed_testing = False 61 62 self.testing: bool = parsed_testing 63 """ True if the backend is being used for a test. """ 64 65 # Core Methods 66 67 def is_testing(self) -> bool: 68 """ Check if this backend is in testing mode. """ 69 70 return self.testing 71 72 def reset_connection(self) -> None: 73 """ 74 Inform the backend that their connection has been reset. 75 Note that this is not on the individual HTTP connection level, but instead on the server level. 76 For example, this is called when a testing server is reset (e.g., in a server runner). 77 """ 78 79 def get_standard_headers(self, write: bool = False) -> typing.Dict[str, str]: 80 """ 81 Get standard headers for this backend. 82 Children should take care to set the write header when performing a write operation. 83 """ 84 85 return { 86 lms.model.constants.HEADER_KEY_BACKEND: self.backend_type.value, 87 lms.model.constants.HEADER_KEY_WRITE: str(write).lower(), 88 } 89 90 def not_found(self, operation: str, identifiers: typing.Dict[str, typing.Any]) -> None: 91 """ 92 Called when the backend was unable to find some object. 93 This will only be called when a requested object is not found, 94 e.g., a user requested by ID is not found. 95 This is not called when a list naturally returns zero results, 96 or when a query does not match any items. 97 """ 98 99 _logger.warning("Object not found during operation: '%s'. Identifiers: %s.", operation, identifiers) 100 101 # API Methods 102 103 def courses_get(self, 104 course_queries: typing.Collection[lms.model.courses.CourseQuery], 105 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 106 """ 107 Get the specified courses associated with the given course. 108 """ 109 110 if (len(course_queries) == 0): 111 return [] 112 113 courses = self.courses_list(**kwargs) 114 115 matches = [] 116 for course in sorted(courses): 117 for query in course_queries: 118 if (query.match(course)): 119 matches.append(course) 120 break 121 122 return sorted(matches) 123 124 def courses_fetch(self, 125 course_id: str, 126 **kwargs: typing.Any) -> typing.Union[lms.model.courses.Course, None]: 127 """ 128 Fetch a single course associated with the context user. 129 Return None if no matching course is found. 130 131 By default, this will just do a list and choose the relevant record. 132 Specific backends may override this if there are performance concerns. 133 """ 134 135 courses = self.courses_list(**kwargs) 136 for course in courses: 137 if (course.id == course_id): 138 return course 139 140 return None 141 142 def courses_list(self, 143 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 144 """ 145 List the courses associated with the context user. 146 """ 147 148 raise NotImplementedError('courses_list') 149 150 def courses_assignments_get(self, 151 course_query: lms.model.courses.CourseQuery, 152 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 153 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 154 """ 155 Get the specified assignments associated with the given course. 156 """ 157 158 if (len(assignment_queries) == 0): 159 return [] 160 161 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 162 163 assignments = sorted(self.courses_assignments_list(resolved_course_query.get_id(), **kwargs)) 164 assignment_queries = sorted(assignment_queries) 165 166 matches = [] 167 for assignment in assignments: 168 for query in assignment_queries: 169 if (query.match(assignment)): 170 matches.append(assignment) 171 break 172 173 return matches 174 175 def courses_assignments_fetch(self, 176 course_id: str, 177 assignment_id: str, 178 **kwargs: typing.Any) -> typing.Union[lms.model.assignments.Assignment, None]: 179 """ 180 Fetch a single assignment associated with the given course. 181 Return None if no matching assignment is found. 182 183 By default, this will just do a list and choose the relevant record. 184 Specific backends may override this if there are performance concerns. 185 """ 186 187 assignments = self.courses_assignments_list(course_id, **kwargs) 188 for assignment in sorted(assignments): 189 if (assignment.id == assignment_id): 190 return assignment 191 192 return None 193 194 def courses_assignments_list(self, 195 course_id: str, 196 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 197 """ 198 List the assignments associated with the given course. 199 """ 200 201 raise NotImplementedError('courses_assignments_list') 202 203 def courses_assignments_resolve_and_list(self, 204 course_query: lms.model.courses.CourseQuery, 205 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 206 """ 207 List the assignments associated with the given course. 208 """ 209 210 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 211 return sorted(self.courses_assignments_list(resolved_course_query.get_id(), **kwargs)) 212 213 def courses_assignments_scores_get(self, 214 course_query: lms.model.courses.CourseQuery, 215 assignment_query: lms.model.assignments.AssignmentQuery, 216 user_queries: typing.Collection[lms.model.users.UserQuery], 217 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 218 """ 219 Get the scores associated with the given assignment query and user queries. 220 """ 221 222 if (len(user_queries) == 0): 223 return [] 224 225 scores = self.courses_assignments_scores_resolve_and_list(course_query, assignment_query, **kwargs) 226 227 matches = [] 228 for score in scores: 229 for user_query in user_queries: 230 if (user_query.match(score.user)): 231 matches.append(score) 232 233 return sorted(matches) 234 235 def courses_assignments_scores_fetch(self, 236 course_id: str, 237 assignment_id: str, 238 user_id: str, 239 **kwargs: typing.Any) -> typing.Union[lms.model.scores.AssignmentScore, None]: 240 """ 241 Fetch the score associated with the given assignment and user. 242 243 By default, this will just do a list and choose the relevant record. 244 Specific backends may override this if there are performance concerns. 245 """ 246 247 scores = self.courses_assignments_scores_list(course_id, assignment_id, **kwargs) 248 for score in scores: 249 if ((score.user is not None) and (score.user.id == user_id)): 250 return score 251 252 return None 253 254 def courses_assignments_scores_list(self, 255 course_id: str, 256 assignment_id: str, 257 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 258 """ 259 List the scores associated with the given assignment. 260 """ 261 262 raise NotImplementedError('courses_assignments_scores_list') 263 264 def courses_assignments_scores_resolve_and_list(self, 265 course_query: lms.model.courses.CourseQuery, 266 assignment_query: lms.model.assignments.AssignmentQuery, 267 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 268 """ 269 List the scores associated with the given assignment query. 270 In addition to resolving the assignment query, 271 users will also be resolved into their full version 272 (instead of the reduced version usually returned with scores). 273 """ 274 275 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 276 277 # Resolve the assignment query. 278 matched_assignments = self.courses_assignments_get(resolved_course_query, [assignment_query], **kwargs) 279 if (len(matched_assignments) == 0): 280 return [] 281 282 target_assignment = matched_assignments[0] 283 284 # List the scores. 285 scores = self.courses_assignments_scores_list(resolved_course_query.get_id(), target_assignment.id, **kwargs) 286 if (len(scores) == 0): 287 return [] 288 289 # Resolve the scores' queries. 290 291 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 292 users_map = {user.id: user for user in users} 293 294 for score in scores: 295 score.assignment = target_assignment.to_query() 296 297 if ((score.user is not None) and (score.user.id in users_map)): 298 score.user = users_map[score.user.id].to_query() 299 300 return sorted(scores) 301 302 def courses_assignments_scores_resolve_and_upload(self, 303 course_query: lms.model.courses.CourseQuery, 304 assignment_query: lms.model.assignments.AssignmentQuery, 305 scores: typing.Dict[lms.model.users.UserQuery, lms.model.scores.ScoreFragment], 306 **kwargs: typing.Any) -> int: 307 """ 308 Resolve queries and upload assignment scores (indexed by user query). 309 A None score (ScoreFragment.score) indicates that the score should be cleared. 310 Return the number of scores sent to the LMS. 311 """ 312 313 if (len(scores) == 0): 314 return 0 315 316 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 317 resolved_assignment_query = self.resolve_assignment_query(resolved_course_query.get_id(), assignment_query, **kwargs) 318 319 resolved_users = self.resolve_user_queries(resolved_course_query.get_id(), list(scores.keys()), warn_on_miss = True) 320 resolved_scores: typing.Dict[str, lms.model.scores.ScoreFragment] = {} 321 322 for (user, score) in scores.items(): 323 for resolved_user in resolved_users: 324 if (user.match(resolved_user)): 325 resolved_scores[resolved_user.get_id()] = score 326 continue 327 328 if (len(resolved_scores) == 0): 329 return 0 330 331 return self.courses_assignments_scores_upload( 332 resolved_course_query.get_id(), 333 resolved_assignment_query.get_id(), 334 resolved_scores, 335 **kwargs) 336 337 def courses_assignments_scores_upload(self, 338 course_id: str, 339 assignment_id: str, 340 scores: typing.Dict[str, lms.model.scores.ScoreFragment], 341 **kwargs: typing.Any) -> int: 342 """ 343 Upload assignment scores (indexed by user id). 344 A None score (ScoreFragment.score) indicates that the score should be cleared. 345 Return the number of scores sent to the LMS. 346 """ 347 348 raise NotImplementedError('courses_assignments_scores_upload') 349 350 def courses_gradebook_get(self, 351 course_query: lms.model.courses.CourseQuery, 352 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 353 user_queries: typing.Collection[lms.model.users.UserQuery], 354 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 355 """ 356 Get a gradebook with the specified users and assignments. 357 Specifying no users/assignments is the same as requesting all of them. 358 """ 359 360 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 361 362 resolved_assignment_queries = self.resolve_assignment_queries(resolved_course_query.get_id(), assignment_queries, empty_all = True, **kwargs) 363 assignment_ids = [query.get_id() for query in resolved_assignment_queries] 364 365 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, 366 empty_all = True, only_students = True, **kwargs) 367 user_ids = [query.get_id() for query in resolved_user_queries] 368 369 gradebook = self.courses_gradebook_fetch(resolved_course_query.get_id(), assignment_ids, user_ids, **kwargs) 370 371 # Resolve the gradebook's queries (so it can show names/emails instead of just IDs). 372 gradebook.update_queries(resolved_assignment_queries, resolved_user_queries) 373 374 return gradebook 375 376 def courses_gradebook_fetch(self, 377 course_id: str, 378 assignment_ids: typing.Collection[str], 379 user_ids: typing.Collection[str], 380 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 381 """ 382 Get a gradebook with the specified users and assignments. 383 If either the assignments or users is empty, an empty gradebook will be returned. 384 """ 385 386 raise NotImplementedError('courses_gradebook_fetch') 387 388 def courses_gradebook_list(self, 389 course_id: str, 390 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 391 """ 392 List the full gradebook associated with this course. 393 """ 394 395 return self.courses_gradebook_get(lms.model.courses.CourseQuery(id = course_id), [], [], **kwargs) 396 397 def courses_gradebook_resolve_and_list(self, 398 course_query: lms.model.courses.CourseQuery, 399 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 400 """ 401 List the full gradebook associated with this course. 402 """ 403 404 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 405 return self.courses_gradebook_list(resolved_course_query.get_id(), **kwargs) 406 407 def courses_gradebook_resolve_and_upload(self, 408 course_query: lms.model.courses.CourseQuery, 409 gradebook: lms.model.scores.Gradebook, 410 **kwargs: typing.Any) -> int: 411 """ 412 Resolve queries and upload a gradebook. 413 Missing scores in the gradebook are skipped, 414 a None score (ScoreFragment.score) indicates that the score should be cleared. 415 Return the number of scores sent to the LMS. 416 """ 417 418 if (len(gradebook) == 0): 419 return 0 420 421 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 422 423 assignments = self.courses_assignments_list(resolved_course_query.get_id(), **kwargs) 424 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 425 426 resolved_assignment_queries = [assignment.to_query() for assignment in assignments] 427 resolved_user_queries = [user.to_query() for user in users] 428 429 gradebook.update_queries(resolved_assignment_queries, resolved_user_queries) 430 431 return self.courses_gradebook_upload( 432 resolved_course_query.get_id(), 433 gradebook, 434 **kwargs) 435 436 def courses_gradebook_upload(self, 437 course_id: str, 438 gradebook: lms.model.scores.Gradebook, 439 **kwargs: typing.Any) -> int: 440 """ 441 Upload a gradebook. 442 All queries in the gradebook must be resolved (or at least have an ID). 443 Missing scores in the gradebook are skipped, 444 a None score (ScoreFragment.score) indicates that the score should be cleared. 445 Return the number of scores sent to the LMS. 446 """ 447 448 assignment_scores = gradebook.get_scores_by_assignment() 449 450 count = 0 451 for (assignment, user_scores) in assignment_scores.items(): 452 if (assignment.id is None): 453 raise ValueError(f"Assignment query for gradebook upload ({assignment}) does not have an ID.") 454 455 upload_scores = {} 456 for (user, score) in user_scores.items(): 457 if (user.id is None): 458 raise ValueError(f"User query for gradebook upload ({user}) does not have an ID.") 459 460 upload_scores[user.id] = score.to_fragment() 461 462 count += self.courses_assignments_scores_upload(course_id, assignment.id, upload_scores, **kwargs) 463 464 return count 465 466 def courses_groupsets_create(self, 467 course_id: str, 468 name: str, 469 **kwargs: typing.Any) -> lms.model.groupsets.GroupSet: 470 """ 471 Create a group set. 472 """ 473 474 raise NotImplementedError('courses_groupsets_create') 475 476 def courses_groupsets_resolve_and_create(self, 477 course_query: lms.model.courses.CourseQuery, 478 name: str, 479 **kwargs: typing.Any) -> lms.model.groupsets.GroupSet: 480 """ 481 Resolve references and create a group set. 482 """ 483 484 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 485 return self.courses_groupsets_create(resolved_course_query.get_id(), name, **kwargs) 486 487 def courses_groupsets_delete(self, 488 course_id: str, 489 groupset_id: str, 490 **kwargs: typing.Any) -> bool: 491 """ 492 Delete a group set. 493 """ 494 495 raise NotImplementedError('courses_groupsets_delete') 496 497 def courses_groupsets_resolve_and_delete(self, 498 course_query: lms.model.courses.CourseQuery, 499 groupset_query: lms.model.groupsets.GroupSetQuery, 500 **kwargs: typing.Any) -> bool: 501 """ 502 Resolve references and create a group set. 503 """ 504 505 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 506 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 507 return self.courses_groupsets_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 508 509 def courses_groupsets_get(self, 510 course_query: lms.model.courses.CourseQuery, 511 groupset_queries: typing.Collection[lms.model.groupsets.GroupSetQuery], 512 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 513 """ 514 Get the specified group sets associated with the given course. 515 """ 516 517 if (len(groupset_queries) == 0): 518 return [] 519 520 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 521 groupset_queries = sorted(groupset_queries) 522 groupsets = sorted(self.courses_groupsets_list(resolved_course_query.get_id(), **kwargs)) 523 524 matches = [] 525 for groupset in groupsets: 526 for query in groupset_queries: 527 if (query.match(groupset)): 528 matches.append(groupset) 529 break 530 531 return matches 532 533 def courses_groupsets_fetch(self, 534 course_id: str, 535 groupset_id: str, 536 **kwargs: typing.Any) -> typing.Union[lms.model.groupsets.GroupSet, None]: 537 """ 538 Fetch a single group set associated with the given course. 539 Return None if no matching group set is found. 540 541 By default, this will just do a list and choose the relevant record. 542 Specific backends may override this if there are performance concerns. 543 """ 544 545 groupsets = self.courses_groupsets_list(course_id, **kwargs) 546 for groupset in groupsets: 547 if (groupset.id == groupset_id): 548 return groupset 549 550 return None 551 552 def courses_groupsets_list(self, 553 course_id: str, 554 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 555 """ 556 List the group sets associated with the given course. 557 """ 558 559 raise NotImplementedError('courses_groupsets_list') 560 561 def courses_groupsets_resolve_and_list(self, 562 course_query: lms.model.courses.CourseQuery, 563 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 564 """ 565 List the group sets associated with the given course. 566 """ 567 568 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 569 return sorted(self.courses_groupsets_list(resolved_course_query.get_id(), **kwargs)) 570 571 def courses_groupsets_memberships_resolve_and_add(self, 572 course_query: lms.model.courses.CourseQuery, 573 groupset_query: lms.model.groupsets.GroupSetQuery, 574 memberships: typing.Collection[lms.model.groups.GroupMembership], 575 **kwargs: typing.Any) -> typing.Tuple[ 576 typing.List[lms.model.groups.Group], 577 typing.Dict[lms.model.groups.ResolvedGroupQuery, int] 578 ]: 579 """ 580 Resolve queries and add the specified users to the specified groups. 581 This may create groups. 582 583 Return: 584 - Created Groups 585 - Group Addition Counts 586 """ 587 588 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 589 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 590 591 found_group_memberships, missing_group_memberships, _ = self._resolve_group_memberships( 592 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 593 594 # Create missing groups. 595 created_groups = [] 596 for name in sorted(missing_group_memberships.keys()): 597 group = self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 598 created_groups.append(group) 599 600 # Merge in new group with existing structure. 601 query = group.to_query() 602 if (query not in found_group_memberships): 603 found_group_memberships[query] = [] 604 605 found_group_memberships[query] += missing_group_memberships[name] 606 607 # Add memberships. 608 counts = {} 609 for resolved_group_query in sorted(found_group_memberships.keys()): 610 resolved_user_queries = found_group_memberships[resolved_group_query] 611 612 count = self.courses_groups_memberships_resolve_and_add( 613 resolved_course_query, resolved_groupset_query, resolved_group_query, 614 resolved_user_queries, 615 **kwargs) 616 617 counts[resolved_group_query] = count 618 619 return (created_groups, counts) 620 621 def courses_groupsets_memberships_resolve_and_set(self, 622 course_query: lms.model.courses.CourseQuery, 623 groupset_query: lms.model.groupsets.GroupSetQuery, 624 memberships: typing.Collection[lms.model.groups.GroupMembership], 625 **kwargs: typing.Any) -> typing.Tuple[ 626 typing.List[lms.model.groups.Group], 627 typing.List[lms.model.groups.ResolvedGroupQuery], 628 typing.Dict[lms.model.groups.ResolvedGroupQuery, int], 629 typing.Dict[lms.model.groups.ResolvedGroupQuery, int], 630 ]: 631 """ 632 Resolve queries and set the specified group memberships. 633 This may create and delete groups. 634 635 Return: 636 - Created Groups 637 - Deleted Groups 638 - Group Addition Counts 639 - Group Subtraction Counts 640 """ 641 642 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 643 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 644 645 found_group_memberships, missing_group_memberships, unused_groups = self._resolve_group_memberships( 646 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 647 648 # Delete unused groups. 649 deleted_groups = [] 650 for group_query in sorted(unused_groups): 651 result = self.courses_groups_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query.get_id(), **kwargs) 652 if (result): 653 deleted_groups.append(group_query) 654 655 # Create missing groups. 656 created_groups = [] 657 for name in sorted(missing_group_memberships.keys()): 658 group = self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 659 created_groups.append(group) 660 661 # Merge in new group with existing structure. 662 query = group.to_query() 663 if (query not in found_group_memberships): 664 found_group_memberships[query] = [] 665 666 found_group_memberships[query] += missing_group_memberships[name] 667 668 # Set memberships. 669 add_counts = {} 670 sub_counts = {} 671 for resolved_group_query in sorted(found_group_memberships.keys()): 672 resolved_user_queries = found_group_memberships[resolved_group_query] 673 674 (add_count, sub_count, deleted) = self.courses_groups_memberships_resolve_and_set( 675 resolved_course_query, resolved_groupset_query, resolved_group_query, 676 resolved_user_queries, 677 delete_empty = True, 678 **kwargs) 679 680 if (deleted): 681 deleted_groups.append(resolved_group_query) 682 683 add_counts[resolved_group_query] = add_count 684 sub_counts[resolved_group_query] = sub_count 685 686 return (created_groups, deleted_groups, add_counts, sub_counts) 687 688 def courses_groupsets_memberships_resolve_and_subtract(self, 689 course_query: lms.model.courses.CourseQuery, 690 groupset_query: lms.model.groupsets.GroupSetQuery, 691 memberships: typing.Collection[lms.model.groups.GroupMembership], 692 **kwargs: typing.Any) -> typing.Dict[lms.model.groups.ResolvedGroupQuery, int]: 693 """ 694 Resolve queries and subtract the specified users to the specified groups. 695 This will not delete any groups. 696 697 Return: 698 - Group Subtraction Counts 699 """ 700 701 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 702 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 703 704 found_group_memberships, missing_group_memberships, _ = self._resolve_group_memberships( 705 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 706 707 # Warn about missing groups. 708 for name in sorted(missing_group_memberships.keys()): 709 _logger.warning("Group does not exist: '%s'.", name) 710 711 # Subtract memberships. 712 counts = {} 713 for resolved_group_query in sorted(found_group_memberships.keys()): 714 resolved_user_queries = found_group_memberships[resolved_group_query] 715 716 (count, _) = self.courses_groups_memberships_resolve_and_subtract( 717 resolved_course_query, resolved_groupset_query, resolved_group_query, 718 resolved_user_queries, 719 delete_empty = False, 720 **kwargs) 721 722 counts[resolved_group_query] = count 723 724 return counts 725 726 def courses_groupsets_memberships_list(self, 727 course_id: str, 728 groupset_id: str, 729 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 730 """ 731 List the membership of the group sets associated with the given course. 732 """ 733 734 raise NotImplementedError('courses_groupsets_memberships_list') 735 736 def courses_groupsets_memberships_resolve_and_list(self, 737 course_query: lms.model.courses.CourseQuery, 738 groupset_query: lms.model.groupsets.GroupSetQuery, 739 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 740 """ 741 List the membership of the group sets associated with the given course. 742 """ 743 744 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 745 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 746 747 memberships = self.courses_groupsets_memberships_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 748 749 # Resolve memberships. 750 751 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 752 groups = self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 753 754 users_map = {user.id: user.to_query() for user in users} 755 groups_map = {group.id: group.to_query() for group in groups} 756 757 for membership in memberships: 758 membership.update_queries(resolved_groupset_query, users = users_map, groups = groups_map) 759 760 return sorted(memberships) 761 762 def courses_groups_create(self, 763 course_id: str, 764 groupset_id: str, 765 name: str, 766 **kwargs: typing.Any) -> lms.model.groups.Group: 767 """ 768 Create a group. 769 """ 770 771 raise NotImplementedError('courses_groups_create') 772 773 def courses_groups_resolve_and_create(self, 774 course_query: lms.model.courses.CourseQuery, 775 groupset_query: lms.model.groupsets.GroupSetQuery, 776 name: str, 777 **kwargs: typing.Any) -> lms.model.groups.Group: 778 """ 779 Resolve references and create a group. 780 """ 781 782 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 783 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 784 return self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 785 786 def courses_groups_delete(self, 787 course_id: str, 788 groupset_id: str, 789 group_id: str, 790 **kwargs: typing.Any) -> bool: 791 """ 792 Delete a group. 793 """ 794 795 raise NotImplementedError('courses_groups_delete') 796 797 def courses_groups_resolve_and_delete(self, 798 course_query: lms.model.courses.CourseQuery, 799 groupset_query: lms.model.groupsets.GroupSetQuery, 800 group_query: lms.model.groups.GroupQuery, 801 **kwargs: typing.Any) -> bool: 802 """ 803 Resolve references and create a group. 804 """ 805 806 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 807 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 808 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 809 return self.courses_groups_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), resolved_group_query.get_id(), **kwargs) 810 811 def courses_groups_get(self, 812 course_query: lms.model.courses.CourseQuery, 813 groupset_query: lms.model.groupsets.GroupSetQuery, 814 group_queries: typing.Collection[lms.model.groups.GroupQuery], 815 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 816 """ 817 Get the specified groups associated with the given course. 818 """ 819 820 if (len(group_queries) == 0): 821 return [] 822 823 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 824 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 825 groups = self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 826 827 group_queries = sorted(group_queries) 828 groups = sorted(groups) 829 830 matches = [] 831 for group in groups: 832 for query in group_queries: 833 if (query.match(group)): 834 matches.append(group) 835 break 836 837 return matches 838 839 def courses_groups_fetch(self, 840 course_id: str, 841 groupset_id: str, 842 group_id: str, 843 **kwargs: typing.Any) -> typing.Union[lms.model.groups.Group, None]: 844 """ 845 Fetch a single group associated with the given course. 846 Return None if no matching group is found. 847 848 By default, this will just do a list and choose the relevant record. 849 Specific backends may override this if there are performance concerns. 850 """ 851 852 groups = self.courses_groups_list(course_id, groupset_id, **kwargs) 853 for group in groups: 854 if (group.id == group_id): 855 return group 856 857 return None 858 859 def courses_groups_list(self, 860 course_id: str, 861 groupset_id: str, 862 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 863 """ 864 List the groups associated with the given course. 865 """ 866 867 raise NotImplementedError('courses_groups_list') 868 869 def courses_groups_resolve_and_list(self, 870 course_query: lms.model.courses.CourseQuery, 871 groupset_query: lms.model.groupsets.GroupSetQuery, 872 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 873 """ 874 List the groups associated with the given course. 875 """ 876 877 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 878 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 879 return self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 880 881 def courses_groups_memberships_add(self, 882 course_id: str, 883 groupset_id: str, 884 group_id: str, 885 user_ids: typing.Collection[str], 886 **kwargs: typing.Any) -> int: 887 """ 888 Add the specified users to the group. 889 """ 890 891 raise NotImplementedError('courses_groups_memberships_add') 892 893 def courses_groups_memberships_resolve_and_add(self, 894 course_query: lms.model.courses.CourseQuery, 895 groupset_query: lms.model.groupsets.GroupSetQuery, 896 group_query: lms.model.groups.GroupQuery, 897 user_queries: typing.Collection[lms.model.users.UserQuery], 898 **kwargs: typing.Any) -> int: 899 """ 900 Resolve queries and add the specified users to the group. 901 """ 902 903 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 904 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 905 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 906 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 907 908 # Get users already in this group. 909 group_memberships = self.courses_groups_memberships_list( 910 resolved_course_query.get_id(), 911 resolved_groupset_query.get_id(), 912 resolved_group_query.get_id(), 913 **kwargs) 914 915 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 916 917 # Filter out users already in the group. 918 user_ids = [] 919 for query in sorted(resolved_user_queries): 920 if (query.get_id() in group_user_ids): 921 _logger.warning("User '%s' already in group '%s'.", query, resolved_group_query) 922 continue 923 924 user_ids.append(query.get_id()) 925 926 if (len(user_ids) == 0): 927 return 0 928 929 return self.courses_groups_memberships_add( 930 resolved_course_query.get_id(), 931 resolved_groupset_query.get_id(), 932 resolved_group_query.get_id(), 933 user_ids, 934 **kwargs) 935 936 def courses_groups_memberships_list(self, 937 course_id: str, 938 groupset_id: str, 939 group_id: str, 940 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 941 """ 942 List the membership of the group associated with the given group set. 943 """ 944 945 raise NotImplementedError('courses_groups_memberships_list') 946 947 def courses_groups_memberships_resolve_and_list(self, 948 course_query: lms.model.courses.CourseQuery, 949 groupset_query: lms.model.groupsets.GroupSetQuery, 950 group_query: lms.model.groups.GroupQuery, 951 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 952 """ 953 List the membership of the group associated with the given group set. 954 """ 955 956 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 957 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 958 959 groups = self.courses_groups_get(resolved_course_query, resolved_groupset_query, [group_query], **kwargs) 960 if (len(groups) == 0): 961 raise ValueError(f"Unable to find group: '{group_query}'.") 962 963 group = groups[0] 964 965 memberships = self.courses_groups_memberships_list( 966 resolved_course_query.get_id(), 967 resolved_groupset_query.get_id(), 968 group.id, 969 **kwargs) 970 971 # Resolve memberships. 972 973 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 974 users_map = {user.id: user.to_query() for user in users} 975 976 groups_map = {group.id: group.to_query()} 977 978 for membership in memberships: 979 membership.update_queries(resolved_groupset_query, users = users_map, groups = groups_map) 980 981 return sorted(memberships) 982 983 def courses_groups_memberships_resolve_and_set(self, 984 course_query: lms.model.courses.CourseQuery, 985 groupset_query: lms.model.groupsets.GroupSetQuery, 986 group_query: lms.model.groups.GroupQuery, 987 user_queries: typing.Collection[lms.model.users.UserQuery], 988 delete_empty: bool = False, 989 **kwargs: typing.Any) -> typing.Tuple[int, int, bool]: 990 """ 991 Resolve queries and set the specified users for the group. 992 This method can both add and subtract users from the group. 993 994 Returns: 995 - The count of users added. 996 - The count of users subtracted. 997 - If this group was deleted. 998 """ 999 1000 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1001 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 1002 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 1003 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 1004 1005 # Get users already in this group. 1006 group_memberships = self.courses_groups_memberships_list( 1007 resolved_course_query.get_id(), 1008 resolved_groupset_query.get_id(), 1009 resolved_group_query.get_id(), 1010 **kwargs) 1011 1012 group_user_queries = {membership.user for membership in group_memberships if membership.user is not None} 1013 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 1014 query_user_ids = {resolved_user_query.get_id() for resolved_user_query in resolved_user_queries} 1015 1016 # Collect users that need to be added. 1017 add_user_ids = [] 1018 for query_user_id in query_user_ids: 1019 if (query_user_id not in group_user_ids): 1020 add_user_ids.append(query_user_id) 1021 1022 # Collect users that need to be subtracted. 1023 sub_user_queries = [] 1024 for group_user_query in group_user_queries: 1025 if (group_user_query not in resolved_user_queries): 1026 sub_user_queries.append(group_user_query) 1027 1028 # Update the group. 1029 1030 add_count = 0 1031 if (len(add_user_ids) > 0): 1032 add_count = self.courses_groups_memberships_add( 1033 resolved_course_query.get_id(), 1034 resolved_groupset_query.get_id(), 1035 resolved_group_query.get_id(), 1036 add_user_ids, 1037 **kwargs) 1038 1039 sub_count = 0 1040 deleted = False 1041 if (len(sub_user_queries) > 0): 1042 sub_count, deleted = self.courses_groups_memberships_resolve_and_subtract( 1043 resolved_course_query, 1044 resolved_groupset_query, 1045 resolved_group_query, 1046 sub_user_queries, 1047 delete_empty = delete_empty, 1048 **kwargs) 1049 1050 return add_count, sub_count, deleted 1051 1052 def courses_groups_memberships_subtract(self, 1053 course_id: str, 1054 groupset_id: str, 1055 group_id: str, 1056 user_ids: typing.Collection[str], 1057 **kwargs: typing.Any) -> int: 1058 """ 1059 Subtract the specified users from the group. 1060 """ 1061 1062 raise NotImplementedError('courses_groups_memberships_subtract') 1063 1064 def courses_groups_memberships_resolve_and_subtract(self, 1065 course_query: lms.model.courses.CourseQuery, 1066 groupset_query: lms.model.groupsets.GroupSetQuery, 1067 group_query: lms.model.groups.GroupQuery, 1068 user_queries: typing.Collection[lms.model.users.UserQuery], 1069 delete_empty: bool = False, 1070 **kwargs: typing.Any) -> typing.Tuple[int, bool]: 1071 """ 1072 Resolve queries and subtract the specified users from the group. 1073 Return: 1074 - The number of users deleted. 1075 - If this group was deleted. 1076 """ 1077 1078 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1079 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 1080 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 1081 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 1082 1083 # Get users already in this group. 1084 group_memberships = self.courses_groups_memberships_list( 1085 resolved_course_query.get_id(), 1086 resolved_groupset_query.get_id(), 1087 resolved_group_query.get_id(), 1088 **kwargs) 1089 1090 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 1091 1092 # Filter out users not in the group. 1093 user_ids = [] 1094 for query in resolved_user_queries: 1095 if (query.get_id() not in group_user_ids): 1096 _logger.warning("User '%s' is not in group '%s'.", query, resolved_group_query) 1097 continue 1098 1099 user_ids.append(query.get_id()) 1100 1101 if (delete_empty and len(group_memberships) == 0): 1102 deleted = self.courses_groups_delete( 1103 resolved_course_query.get_id(), 1104 resolved_groupset_query.get_id(), 1105 resolved_group_query.get_id(), 1106 **kwargs) 1107 return 0, deleted 1108 1109 if (len(user_ids) == 0): 1110 return 0, False 1111 1112 count = self.courses_groups_memberships_subtract( 1113 resolved_course_query.get_id(), 1114 resolved_groupset_query.get_id(), 1115 resolved_group_query.get_id(), 1116 user_ids, 1117 **kwargs) 1118 1119 deleted = False 1120 if (delete_empty and (count == len(group_memberships))): 1121 deleted = self.courses_groups_delete( 1122 resolved_course_query.get_id(), 1123 resolved_groupset_query.get_id(), 1124 resolved_group_query.get_id(), 1125 **kwargs) 1126 1127 return count, deleted 1128 1129 def courses_quizzes_download(self, 1130 course_id: str, 1131 quiz_id: str, 1132 **kwargs: typing.Any) -> quizcomp.model.quiz.Quiz: 1133 """ 1134 Download a quiz from the LMS into a Quiz Composer quiz. 1135 This will only fetch the full quiz, not write it to disk. 1136 """ 1137 1138 raise NotImplementedError('courses_quizzes_download') 1139 1140 def courses_quizzes_resolve_and_download(self, 1141 course_query: lms.model.courses.CourseQuery, 1142 quiz_query: lms.model.assignments.AssignmentQuery, 1143 **kwargs: typing.Any) -> quizcomp.model.quiz.Quiz: 1144 """ 1145 Resolve queries and download a quiz from the LMS into a Quiz Composer quiz. 1146 This will only fetch the full quiz, not write it to disk. 1147 """ 1148 1149 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1150 resolved_quiz_query = self.resolve_quiz_query(resolved_course_query.get_id(), quiz_query, **kwargs) 1151 1152 return self.courses_quizzes_download(resolved_course_query.get_id(), resolved_quiz_query.get_id(), **kwargs) 1153 1154 def courses_quizzes_get(self, 1155 course_query: lms.model.courses.CourseQuery, 1156 quiz_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1157 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1158 """ 1159 Get the specified quizzes associated with the given course. 1160 """ 1161 1162 if (len(quiz_queries) == 0): 1163 return [] 1164 1165 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1166 1167 quizzes = sorted(self.courses_quizzes_list(resolved_course_query.get_id(), **kwargs)) 1168 quiz_queries = sorted(quiz_queries) 1169 1170 matches = [] 1171 for quiz in quizzes: 1172 for query in quiz_queries: 1173 if (query.match(quiz)): 1174 matches.append(quiz) 1175 break 1176 1177 return matches 1178 1179 def courses_quizzes_fetch(self, 1180 course_id: str, 1181 quiz_id: str, 1182 **kwargs: typing.Any) -> typing.Union[lms.model.assignments.Assignment, None]: 1183 """ 1184 Fetch a single quiz associated with the given course. 1185 Return None if no matching quiz is found. 1186 1187 By default, this will just do a list and choose the relevant record. 1188 Specific backends may override this if there are performance concerns. 1189 """ 1190 1191 quizzes = self.courses_quizzes_list(course_id, **kwargs) 1192 for quiz in sorted(quizzes): 1193 if (quiz.id == quiz_id): 1194 return quiz 1195 1196 return None 1197 1198 def courses_quizzes_list(self, 1199 course_id: str, 1200 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1201 """ 1202 List the quizzes associated with the given course. 1203 If specified, additional resources associated with the quiz (e.g., images) may also be fetched. 1204 """ 1205 1206 raise NotImplementedError('courses_quizzes_list') 1207 1208 def courses_quizzes_resolve_and_list(self, 1209 course_query: lms.model.courses.CourseQuery, 1210 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1211 """ 1212 List the quizzes associated with the given course. 1213 """ 1214 1215 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1216 return sorted(self.courses_quizzes_list(resolved_course_query.get_id(), **kwargs)) 1217 1218 def courses_quizzes_resolve_and_remove(self, 1219 course_query: lms.model.courses.CourseQuery, 1220 quiz_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1221 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1222 """ 1223 Resolve the course and remove any matching quiz. 1224 Return the quizzes that were removed. 1225 """ 1226 1227 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1228 1229 old_quizzes = self.courses_quizzes_get(resolved_course_query, quiz_queries) 1230 for old_quiz in old_quizzes: 1231 self.courses_quizzes_remove(resolved_course_query.get_id(), old_quiz.id) 1232 1233 return old_quizzes 1234 1235 def courses_quizzes_remove(self, 1236 course_id: str, 1237 quiz_id: str, 1238 **kwargs: typing.Any) -> None: 1239 """ 1240 Remove quiz from the LMS. 1241 """ 1242 1243 raise NotImplementedError('courses_quizzes_remove') 1244 1245 def courses_quizzes_resolve_and_upload(self, 1246 course_query: lms.model.courses.CourseQuery, 1247 quiz: quizcomp.model.quiz.Quiz, 1248 force: bool = False, 1249 **kwargs: typing.Any) -> lms.model.assignments.Assignment: 1250 """ 1251 Resolve the course and upload the quiz. 1252 If a quiz already exists with a matching query, then `force` will decide the behavior. 1253 If `force` is true then those matching quizzes will be removed, 1254 otherwise an exception will be raised. 1255 """ 1256 1257 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1258 1259 old_quizzes = self.courses_quizzes_get(resolved_course_query, [lms.model.assignments.AssignmentQuery(name = quiz.name)]) 1260 if ((not force) and (len(old_quizzes) > 0)): 1261 raise ValueError(f"Found {len(old_quizzes)} existing quizzes with a matching name ('{quiz.name}'), stopping upload.") 1262 1263 for old_quiz in old_quizzes: 1264 _logger.warning("Deleting existing quiz ('%s') before upload.", old_quiz.to_query()) 1265 self.courses_quizzes_remove(resolved_course_query.get_id(), old_quiz.id) 1266 1267 return self.courses_quizzes_upload(resolved_course_query.get_id(), quiz) 1268 1269 def courses_quizzes_upload(self, 1270 course_id: str, 1271 quiz: quizcomp.model.quiz.Quiz, 1272 **kwargs: typing.Any) -> lms.model.assignments.Assignment: 1273 """ 1274 Upload a single quiz to the LMS. 1275 1276 This method assumes that there is no quiz with a matching name. 1277 To handle that case, use courses_quizzes_resolve_and_upload() with `force = True`. 1278 """ 1279 1280 raise NotImplementedError('courses_quizzes_upload') 1281 1282 def courses_syllabus_fetch(self, 1283 course_id: str, 1284 **kwargs: typing.Any) -> typing.Union[str, None]: 1285 """ 1286 Get the syllabus for a course, or None if no syllabus exists. 1287 """ 1288 1289 raise NotImplementedError('courses_syllabus_fetch') 1290 1291 def courses_syllabus_get(self, 1292 course_query: lms.model.courses.CourseQuery, 1293 **kwargs: typing.Any) -> typing.Union[str, None]: 1294 """ 1295 Get the syllabus for a course query, or None if no syllabus exists. 1296 """ 1297 1298 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1299 1300 return self.courses_syllabus_fetch(resolved_course_query.get_id(), **kwargs) 1301 1302 def courses_users_get(self, 1303 course_query: lms.model.courses.CourseQuery, 1304 user_queries: typing.Collection[lms.model.users.UserQuery], 1305 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1306 """ 1307 Get the specified users associated with the given course. 1308 """ 1309 1310 if (len(user_queries) == 0): 1311 return [] 1312 1313 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1314 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 1315 1316 user_queries = sorted(user_queries) 1317 users = sorted(users) 1318 1319 matches = [] 1320 for user in users: 1321 for query in user_queries: 1322 if (query.match(user)): 1323 matches.append(user) 1324 break 1325 1326 return matches 1327 1328 def courses_users_fetch(self, 1329 course_id: str, 1330 user_id: str, 1331 **kwargs: typing.Any) -> typing.Union[lms.model.users.CourseUser, None]: 1332 """ 1333 Fetch a single user associated with the given course. 1334 Return None if no matching user is found. 1335 1336 By default, this will just do a list and choose the relevant record. 1337 Specific backends may override this if there are performance concerns. 1338 """ 1339 1340 users = self.courses_users_list(course_id, **kwargs) 1341 for user in sorted(users): 1342 if (user.id == user_id): 1343 return user 1344 1345 return None 1346 1347 def courses_users_list(self, 1348 course_id: str, 1349 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1350 """ 1351 List the users associated with the given course. 1352 """ 1353 1354 raise NotImplementedError('courses_users_list') 1355 1356 def courses_users_resolve_and_list(self, 1357 course_query: lms.model.courses.CourseQuery, 1358 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1359 """ 1360 List the users associated with the given course. 1361 """ 1362 1363 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1364 return list(sorted(self.courses_users_list(resolved_course_query.get_id(), **kwargs))) 1365 1366 def courses_users_scores_get(self, 1367 course_query: lms.model.courses.CourseQuery, 1368 user_query: lms.model.users.UserQuery, 1369 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1370 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1371 """ 1372 Get the scores associated with the given user query and assignment queries. 1373 """ 1374 1375 if (len(assignment_queries) == 0): 1376 return [] 1377 1378 scores = self.courses_users_scores_resolve_and_list(course_query, user_query, **kwargs) 1379 1380 scores = sorted(scores) 1381 assignment_queries = sorted(assignment_queries) 1382 1383 matches = [] 1384 for score in scores: 1385 for assignment_query in assignment_queries: 1386 if (assignment_query.match(score.assignment)): 1387 matches.append(score) 1388 1389 return matches 1390 1391 def courses_users_scores_fetch(self, 1392 course_id: str, 1393 user_id: str, 1394 assignment_id: str, 1395 **kwargs: typing.Any) -> typing.Union[lms.model.scores.AssignmentScore, None]: 1396 """ 1397 Fetch the score associated with the given user and assignment. 1398 1399 By default, this will just do a list and choose the relevant record. 1400 Specific backends may override this if there are performance concerns. 1401 """ 1402 1403 # The default implementation is the same as courses_assignments_scores_fetch(). 1404 return self.courses_assignments_scores_fetch(course_id, assignment_id, user_id, **kwargs) 1405 1406 def courses_users_scores_list(self, 1407 course_id: str, 1408 user_id: str, 1409 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1410 """ 1411 List the scores associated with the given user. 1412 """ 1413 1414 raise NotImplementedError('courses_users_scores_list') 1415 1416 def courses_users_scores_resolve_and_list(self, 1417 course_query: lms.model.courses.CourseQuery, 1418 user_query: lms.model.users.UserQuery, 1419 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1420 """ 1421 List the scores associated with the given user query. 1422 In addition to resolving the user query, 1423 assignments will also be resolved into their full version 1424 (instead of the reduced version usually returned with scores). 1425 """ 1426 1427 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1428 1429 # Resolve the user query. 1430 matched_users = self.courses_users_get(resolved_course_query, [user_query], **kwargs) 1431 if (len(matched_users) == 0): 1432 return [] 1433 1434 target_user = matched_users[0] 1435 1436 # List the scores. 1437 scores = self.courses_users_scores_list(resolved_course_query.get_id(), target_user.id, **kwargs) 1438 if (len(scores) == 0): 1439 return [] 1440 1441 # Resolve the scores' queries. 1442 1443 assignments = self.courses_assignments_list(resolved_course_query.get_id(), **kwargs) 1444 assignments_map = {assignment.id: assignment for assignment in assignments} 1445 1446 for score in scores: 1447 score.user = target_user.to_query() 1448 1449 if ((score.assignment is not None) and (score.assignment.id in assignments_map)): 1450 score.assignment = assignments_map[score.assignment.id].to_query() 1451 1452 return sorted(scores) 1453 1454 # Utility Methods 1455 1456 def parse_assignment_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.assignments.AssignmentQuery, None]: 1457 """ 1458 Attempt to parse an assignment query from a string. 1459 If there is no query, return a None. 1460 If the query is malformed, raise an exception. 1461 1462 By default, this method assumes that LMS IDs are ints. 1463 Child backends may override this to implement their specific behavior. 1464 """ 1465 1466 return lms.model.query.parse_int_query(lms.model.assignments.AssignmentQuery, text, check_email = False) 1467 1468 def parse_assignment_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.assignments.AssignmentQuery]: 1469 """ Parse a list of assignment queries. """ 1470 1471 queries = [] 1472 for text in texts: 1473 query = self.parse_assignment_query(text) 1474 if (query is not None): 1475 queries.append(query) 1476 1477 return queries 1478 1479 def parse_course_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.courses.CourseQuery, None]: 1480 """ 1481 Attempt to parse a course query from a string. 1482 If there is no query, return a None. 1483 If the query is malformed, raise an exception. 1484 1485 By default, this method assumes that LMS IDs are ints. 1486 Child backends may override this to implement their specific behavior. 1487 """ 1488 1489 return lms.model.query.parse_int_query(lms.model.courses.CourseQuery, text, check_email = False) 1490 1491 def parse_course_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.courses.CourseQuery]: 1492 """ Parse a list of course queries. """ 1493 1494 queries = [] 1495 for text in texts: 1496 query = self.parse_course_query(text) 1497 if (query is not None): 1498 queries.append(query) 1499 1500 return queries 1501 1502 def parse_groupset_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.groupsets.GroupSetQuery, None]: 1503 """ 1504 Attempt to parse a group set query from a string. 1505 If there is no query, return a None. 1506 If the query is malformed, raise an exception. 1507 1508 By default, this method assumes that LMS IDs are ints. 1509 Child backends may override this to implement their specific behavior. 1510 """ 1511 1512 return lms.model.query.parse_int_query(lms.model.groupsets.GroupSetQuery, text, check_email = False) 1513 1514 def parse_groupset_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.groupsets.GroupSetQuery]: 1515 """ Parse a list of group set queries. """ 1516 1517 queries = [] 1518 for text in texts: 1519 query = self.parse_groupset_query(text) 1520 if (query is not None): 1521 queries.append(query) 1522 1523 return queries 1524 1525 def parse_group_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.groups.GroupQuery, None]: 1526 """ 1527 Attempt to parse a group query from a string. 1528 If there is no query, return a None. 1529 If the query is malformed, raise an exception. 1530 1531 By default, this method assumes that LMS IDs are ints. 1532 Child backends may override this to implement their specific behavior. 1533 """ 1534 1535 return lms.model.query.parse_int_query(lms.model.groups.GroupQuery, text, check_email = False) 1536 1537 def parse_group_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.groups.GroupQuery]: 1538 """ Parse a list of group queries. """ 1539 1540 queries = [] 1541 for text in texts: 1542 query = self.parse_group_query(text) 1543 if (query is not None): 1544 queries.append(query) 1545 1546 return queries 1547 1548 def parse_user_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.users.UserQuery, None]: 1549 """ 1550 Attempt to parse a user query from a string. 1551 If there is no query, return a None. 1552 If the query is malformed, raise an exception. 1553 1554 By default, this method assumes that LMS IDs are ints. 1555 Child backends may override this to implement their specific behavior. 1556 """ 1557 1558 return lms.model.query.parse_int_query(lms.model.users.UserQuery, text, check_email = True) 1559 1560 def parse_user_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.users.UserQuery]: 1561 """ Parse a list of user queries. """ 1562 1563 queries = [] 1564 for text in texts: 1565 query = self.parse_user_query(text) 1566 if (query is not None): 1567 queries.append(query) 1568 1569 return queries 1570 1571 def resolve_assignment_query(self, 1572 course_id: str, 1573 assignment_query: lms.model.assignments.AssignmentQuery, 1574 **kwargs: typing.Any) -> lms.model.assignments.ResolvedAssignmentQuery: 1575 """ Resolve the assignment query or raise an exception. """ 1576 1577 # Shortcut already resolved queries. 1578 if (isinstance(assignment_query, lms.model.assignments.ResolvedAssignmentQuery)): 1579 return assignment_query 1580 1581 results = self.resolve_assignment_queries(course_id, [assignment_query], **kwargs) 1582 if (len(results) == 0): 1583 raise ValueError(f"Could not resolve assignment query: '{assignment_query}'.") 1584 1585 return results[0] 1586 1587 def resolve_assignment_queries(self, 1588 course_id: str, 1589 queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1590 **kwargs: typing.Any) -> typing.List[lms.model.assignments.ResolvedAssignmentQuery]: 1591 """ 1592 Resolve a list of assignment queries into a list of resolved assignment queries. 1593 See _resolve_queries(). 1594 """ 1595 1596 results = self._resolve_queries( 1597 queries, 1598 'assignment', 1599 self.courses_assignments_list(course_id, **kwargs), 1600 lms.model.assignments.ResolvedAssignmentQuery, 1601 **kwargs) 1602 1603 return typing.cast(typing.List[lms.model.assignments.ResolvedAssignmentQuery], results) 1604 1605 def resolve_course_query(self, 1606 query: lms.model.courses.CourseQuery, 1607 **kwargs: typing.Any) -> lms.model.courses.ResolvedCourseQuery: 1608 """ Resolve the course query or raise an exception. """ 1609 1610 # Shortcut already resolved queries. 1611 if (isinstance(query, lms.model.courses.ResolvedCourseQuery)): 1612 return query 1613 1614 results = self.resolve_course_queries([query], **kwargs) 1615 if (len(results) == 0): 1616 raise ValueError(f"Could not resolve course query: '{query}'.") 1617 1618 return results[0] 1619 1620 def resolve_course_queries(self, 1621 queries: typing.Collection[lms.model.courses.CourseQuery], 1622 **kwargs: typing.Any) -> typing.List[lms.model.courses.ResolvedCourseQuery]: 1623 """ 1624 Resolve a list of course queries into a list of resolved course queries. 1625 See _resolve_queries(). 1626 """ 1627 1628 results = self._resolve_queries( 1629 queries, 1630 'course', 1631 self.courses_list(**kwargs), 1632 lms.model.courses.ResolvedCourseQuery, 1633 **kwargs) 1634 1635 return typing.cast(typing.List[lms.model.courses.ResolvedCourseQuery], results) 1636 1637 def resolve_group_queries(self, 1638 course_id: str, 1639 groupset_id: str, 1640 queries: typing.Collection[lms.model.groups.GroupQuery], 1641 **kwargs: typing.Any) -> typing.List[lms.model.groups.ResolvedGroupQuery]: 1642 """ 1643 Resolve a list of group queries into a list of resolved group queries. 1644 See _resolve_queries(). 1645 """ 1646 1647 results = self._resolve_queries( 1648 queries, 1649 'group', 1650 self.courses_groups_list(course_id, groupset_id, **kwargs), 1651 lms.model.groups.ResolvedGroupQuery, 1652 **kwargs) 1653 1654 return typing.cast(typing.List[lms.model.groups.ResolvedGroupQuery], results) 1655 1656 def resolve_group_query(self, 1657 course_id: str, 1658 groupset_id: str, 1659 query: lms.model.groups.GroupQuery, 1660 **kwargs: typing.Any) -> lms.model.groups.ResolvedGroupQuery: 1661 """ Resolve the group query or raise an exception. """ 1662 1663 # Shortcut already resolved queries. 1664 if (isinstance(query, lms.model.groups.ResolvedGroupQuery)): 1665 return query 1666 1667 results = self.resolve_group_queries(course_id, groupset_id, [query], **kwargs) 1668 if (len(results) == 0): 1669 raise ValueError(f"Could not resolve group query: '{query}'.") 1670 1671 return results[0] 1672 1673 def resolve_groupset_queries(self, 1674 course_id: str, 1675 queries: typing.Collection[lms.model.groupsets.GroupSetQuery], 1676 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.ResolvedGroupSetQuery]: 1677 """ 1678 Resolve a list of group set queries into a list of resolved group set queries. 1679 See _resolve_queries(). 1680 """ 1681 1682 results = self._resolve_queries( 1683 queries, 1684 'group set', 1685 self.courses_groupsets_list(course_id, **kwargs), 1686 lms.model.groupsets.ResolvedGroupSetQuery, 1687 **kwargs) 1688 1689 return typing.cast(typing.List[lms.model.groupsets.ResolvedGroupSetQuery], results) 1690 1691 def resolve_groupset_query(self, 1692 course_id: str, 1693 groupset_query: lms.model.groupsets.GroupSetQuery, 1694 **kwargs: typing.Any) -> lms.model.groupsets.ResolvedGroupSetQuery: 1695 """ Resolve the group set query or raise an exception. """ 1696 1697 # Shortcut already resolved queries. 1698 if (isinstance(groupset_query, lms.model.groupsets.ResolvedGroupSetQuery)): 1699 return groupset_query 1700 1701 results = self.resolve_groupset_queries(course_id, [groupset_query], **kwargs) 1702 if (len(results) == 0): 1703 raise ValueError(f"Could not resolve group set query: '{groupset_query}'.") 1704 1705 return results[0] 1706 1707 def resolve_quiz_query(self, 1708 course_id: str, 1709 quiz_query: lms.model.assignments.AssignmentQuery, 1710 **kwargs: typing.Any) -> lms.model.assignments.ResolvedAssignmentQuery: 1711 """ Resolve the quiz query or raise an exception. """ 1712 1713 # Shortcut already resolved queries. 1714 if (isinstance(quiz_query, lms.model.assignments.ResolvedAssignmentQuery)): 1715 return quiz_query 1716 1717 results = self.resolve_quiz_queries(course_id, [quiz_query], **kwargs) 1718 if (len(results) == 0): 1719 raise ValueError(f"Could not resolve quiz query: '{quiz_query}'.") 1720 1721 return results[0] 1722 1723 def resolve_quiz_queries(self, 1724 course_id: str, 1725 queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1726 **kwargs: typing.Any) -> typing.List[lms.model.assignments.ResolvedAssignmentQuery]: 1727 """ 1728 Resolve a list of quiz queries into a list of resolved quiz queries. 1729 See _resolve_queries(). 1730 """ 1731 1732 results = self._resolve_queries( 1733 queries, 1734 'quiz', 1735 self.courses_quizzes_list(course_id, **kwargs), 1736 lms.model.assignments.ResolvedAssignmentQuery, 1737 **kwargs) 1738 1739 return typing.cast(typing.List[lms.model.assignments.ResolvedAssignmentQuery], results) 1740 1741 def resolve_user_queries(self, 1742 course_id: str, 1743 queries: typing.Collection[lms.model.users.UserQuery], 1744 only_students: bool = False, 1745 **kwargs: typing.Any) -> typing.List[lms.model.users.ResolvedUserQuery]: 1746 """ 1747 Resolve a list of user queries into a list of resolved user queries. 1748 See _resolve_queries(). 1749 """ 1750 1751 filter_func = None 1752 if (only_students): 1753 filter_func = lambda user: user.is_student() # pylint: disable=unnecessary-lambda-assignment 1754 1755 results = self._resolve_queries( 1756 queries, 1757 'user', 1758 self.courses_users_list(course_id, **kwargs), 1759 lms.model.users.ResolvedUserQuery, 1760 filter_func = filter_func, 1761 **kwargs) 1762 1763 return typing.cast(typing.List[lms.model.users.ResolvedUserQuery], results) 1764 1765 def _resolve_queries(self, 1766 queries: typing.Collection[lms.model.query.BaseQuery], 1767 label: str, 1768 items: typing.Collection, 1769 resolved_query_class: typing.Type, 1770 empty_all: bool = False, 1771 warn_on_miss: bool = False, 1772 filter_func: typing.Union[typing.Callable, None] = None, 1773 **kwargs: typing.Any) -> typing.List[lms.model.query.ResolvedBaseQuery]: 1774 """ 1775 Resolve a list of queries. 1776 The returned list may be shorter than the list of queries (if input queries are not matched). 1777 The queries will be deduplicated and sorted. 1778 1779 If |empty_all| is true and no queries are specified, then all items will be returned. 1780 1781 If |filter_func| is passed, then that function will be called with each raw item, 1782 and ones that return true will be kept. 1783 """ 1784 1785 if (filter_func is not None): 1786 items = list(filter(filter_func, items)) 1787 1788 if (empty_all and (len(queries) == 0)): 1789 return list(sorted({resolved_query_class(item) for item in items})) 1790 1791 matched_queries = [] # type: ignore[var-annotated] 1792 for query in queries: 1793 match = False 1794 for item in items: 1795 if (query.match(item)): 1796 matched_query = resolved_query_class(item) 1797 1798 if (match): 1799 raise ValueError( 1800 f"Ambiguous {label} query ('{query}')" 1801 f" matches multiple {label}s ['{matched_queries[-1]}', '{matched_query}'].") 1802 1803 matched_queries.append(matched_query) 1804 match = True 1805 1806 if ((not match) and warn_on_miss): 1807 _logger.warning("Could not resolve %s query '%s'.", label, query) 1808 1809 return list(sorted(set(matched_queries))) 1810 1811 def _resolve_group_memberships(self, 1812 course_id: str, 1813 groupset_id: str, 1814 memberships: typing.Collection[lms.model.groups.GroupMembership], 1815 **kwargs: typing.Any) -> typing.Tuple[ 1816 typing.Dict[lms.model.groups.ResolvedGroupQuery, typing.List[lms.model.users.ResolvedUserQuery]], 1817 typing.Dict[str, typing.List[lms.model.users.ResolvedUserQuery]], 1818 typing.List[lms.model.groups.ResolvedGroupQuery]]: 1819 """ 1820 Resolve a list of group memberships. 1821 This method will resolved each query and split up the memberships by the appropriate group. 1822 If a group does not exist, the memberships will be split by apparent group name. 1823 1824 Returns: 1825 - Memberships in Found Groups (keyed by resolved group query) 1826 - Memberships in Missing Groups (keyed by apparent group name) 1827 - Groups not involved in any of the returned memberships. 1828 1829 The returned dicts will be the found groups (keyed by resolved query) and then the missing groups (keyed by apparent group name). 1830 """ 1831 1832 found_group_memberships: typing.Dict[lms.model.groups.ResolvedGroupQuery, typing.List[lms.model.users.ResolvedUserQuery]] = {} 1833 missing_group_memberships: typing.Dict[str, typing.List[lms.model.users.ResolvedUserQuery]] = {} 1834 1835 users = self.courses_users_list(course_id, **kwargs) 1836 resolved_user_queries = [user.to_query() for user in sorted(users)] 1837 1838 groups = self.courses_groups_list(course_id, groupset_id, **kwargs) 1839 resolved_group_queries = [group.to_query() for group in sorted(groups)] 1840 1841 for (i, membership) in enumerate(memberships): 1842 # Resolve user. 1843 1844 resolved_user_query = None 1845 for possible_user_query in resolved_user_queries: 1846 if (membership.user.match(possible_user_query)): 1847 resolved_user_query = possible_user_query 1848 break 1849 1850 if (resolved_user_query is None): 1851 _logger.warning("Could not resolve user '%s' for membership entry at index %d.", membership.user, i) 1852 continue 1853 1854 # Resolve group. 1855 1856 resolved_group_query = None 1857 for possible_group_query in resolved_group_queries: 1858 if (membership.group.match(possible_group_query)): 1859 resolved_group_query = possible_group_query 1860 break 1861 1862 # Add to the correct collection. 1863 1864 if (resolved_group_query is None): 1865 if ((membership.group.name is None) or (len(membership.group.name) == 0)): 1866 _logger.warning(("Membership entry at index %d has a group with no name." 1867 + " Ensure that non-existent groups all have names."), i) 1868 continue 1869 1870 if (membership.group.name not in missing_group_memberships): 1871 missing_group_memberships[membership.group.name] = [] 1872 1873 missing_group_memberships[membership.group.name].append(resolved_user_query) 1874 else: 1875 if (resolved_group_query not in found_group_memberships): 1876 found_group_memberships[resolved_group_query] = [] 1877 1878 found_group_memberships[resolved_group_query].append(resolved_user_query) 1879 1880 unused_groups = sorted(list(set(resolved_group_queries) - set(found_group_memberships.keys()))) 1881 1882 return (found_group_memberships, missing_group_memberships, unused_groups)
20class APIBackend(): 21 """ 22 API backends provide a unified interface to an LMS. 23 24 Note that instead of using an abstract class, 25 methods will raise a NotImplementedError by default. 26 This will allow child backends to fill in as much functionality as they can, 27 while still leaving gaps where they are incomplete or impossible. 28 """ 29 30 _testing_override: typing.Union[bool, None] = None 31 """ A top-level override to control testing status. """ 32 33 def __init__(self, 34 config: lms.model.config.Config, 35 testing: typing.Union[bool, str] = False, 36 **kwargs: typing.Any) -> None: 37 self.config: lms.model.config.Config = config 38 """ The configuration options for this backend. """ 39 40 assert(self.config.backend_type is not None) 41 42 self.backend_type: lms.model.constants.BackendType = self.config.backend_type 43 """ 44 The backend type of this server. 45 This is set in config and compied for type checking. 46 """ 47 48 assert((self.config.server is not None) and (len(self.config.server) > 0)) 49 50 self.server: str = self.config.server 51 """ 52 The server to connect to. 53 This is set in config and compied for type checking. 54 """ 55 56 parsed_testing = edq.util.parse.soft_boolean(self.config.testing) 57 if (APIBackend._testing_override is not None): 58 parsed_testing = APIBackend._testing_override 59 60 if (parsed_testing is None): 61 parsed_testing = False 62 63 self.testing: bool = parsed_testing 64 """ True if the backend is being used for a test. """ 65 66 # Core Methods 67 68 def is_testing(self) -> bool: 69 """ Check if this backend is in testing mode. """ 70 71 return self.testing 72 73 def reset_connection(self) -> None: 74 """ 75 Inform the backend that their connection has been reset. 76 Note that this is not on the individual HTTP connection level, but instead on the server level. 77 For example, this is called when a testing server is reset (e.g., in a server runner). 78 """ 79 80 def get_standard_headers(self, write: bool = False) -> typing.Dict[str, str]: 81 """ 82 Get standard headers for this backend. 83 Children should take care to set the write header when performing a write operation. 84 """ 85 86 return { 87 lms.model.constants.HEADER_KEY_BACKEND: self.backend_type.value, 88 lms.model.constants.HEADER_KEY_WRITE: str(write).lower(), 89 } 90 91 def not_found(self, operation: str, identifiers: typing.Dict[str, typing.Any]) -> None: 92 """ 93 Called when the backend was unable to find some object. 94 This will only be called when a requested object is not found, 95 e.g., a user requested by ID is not found. 96 This is not called when a list naturally returns zero results, 97 or when a query does not match any items. 98 """ 99 100 _logger.warning("Object not found during operation: '%s'. Identifiers: %s.", operation, identifiers) 101 102 # API Methods 103 104 def courses_get(self, 105 course_queries: typing.Collection[lms.model.courses.CourseQuery], 106 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 107 """ 108 Get the specified courses associated with the given course. 109 """ 110 111 if (len(course_queries) == 0): 112 return [] 113 114 courses = self.courses_list(**kwargs) 115 116 matches = [] 117 for course in sorted(courses): 118 for query in course_queries: 119 if (query.match(course)): 120 matches.append(course) 121 break 122 123 return sorted(matches) 124 125 def courses_fetch(self, 126 course_id: str, 127 **kwargs: typing.Any) -> typing.Union[lms.model.courses.Course, None]: 128 """ 129 Fetch a single course associated with the context user. 130 Return None if no matching course is found. 131 132 By default, this will just do a list and choose the relevant record. 133 Specific backends may override this if there are performance concerns. 134 """ 135 136 courses = self.courses_list(**kwargs) 137 for course in courses: 138 if (course.id == course_id): 139 return course 140 141 return None 142 143 def courses_list(self, 144 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 145 """ 146 List the courses associated with the context user. 147 """ 148 149 raise NotImplementedError('courses_list') 150 151 def courses_assignments_get(self, 152 course_query: lms.model.courses.CourseQuery, 153 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 154 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 155 """ 156 Get the specified assignments associated with the given course. 157 """ 158 159 if (len(assignment_queries) == 0): 160 return [] 161 162 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 163 164 assignments = sorted(self.courses_assignments_list(resolved_course_query.get_id(), **kwargs)) 165 assignment_queries = sorted(assignment_queries) 166 167 matches = [] 168 for assignment in assignments: 169 for query in assignment_queries: 170 if (query.match(assignment)): 171 matches.append(assignment) 172 break 173 174 return matches 175 176 def courses_assignments_fetch(self, 177 course_id: str, 178 assignment_id: str, 179 **kwargs: typing.Any) -> typing.Union[lms.model.assignments.Assignment, None]: 180 """ 181 Fetch a single assignment associated with the given course. 182 Return None if no matching assignment is found. 183 184 By default, this will just do a list and choose the relevant record. 185 Specific backends may override this if there are performance concerns. 186 """ 187 188 assignments = self.courses_assignments_list(course_id, **kwargs) 189 for assignment in sorted(assignments): 190 if (assignment.id == assignment_id): 191 return assignment 192 193 return None 194 195 def courses_assignments_list(self, 196 course_id: str, 197 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 198 """ 199 List the assignments associated with the given course. 200 """ 201 202 raise NotImplementedError('courses_assignments_list') 203 204 def courses_assignments_resolve_and_list(self, 205 course_query: lms.model.courses.CourseQuery, 206 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 207 """ 208 List the assignments associated with the given course. 209 """ 210 211 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 212 return sorted(self.courses_assignments_list(resolved_course_query.get_id(), **kwargs)) 213 214 def courses_assignments_scores_get(self, 215 course_query: lms.model.courses.CourseQuery, 216 assignment_query: lms.model.assignments.AssignmentQuery, 217 user_queries: typing.Collection[lms.model.users.UserQuery], 218 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 219 """ 220 Get the scores associated with the given assignment query and user queries. 221 """ 222 223 if (len(user_queries) == 0): 224 return [] 225 226 scores = self.courses_assignments_scores_resolve_and_list(course_query, assignment_query, **kwargs) 227 228 matches = [] 229 for score in scores: 230 for user_query in user_queries: 231 if (user_query.match(score.user)): 232 matches.append(score) 233 234 return sorted(matches) 235 236 def courses_assignments_scores_fetch(self, 237 course_id: str, 238 assignment_id: str, 239 user_id: str, 240 **kwargs: typing.Any) -> typing.Union[lms.model.scores.AssignmentScore, None]: 241 """ 242 Fetch the score associated with the given assignment and user. 243 244 By default, this will just do a list and choose the relevant record. 245 Specific backends may override this if there are performance concerns. 246 """ 247 248 scores = self.courses_assignments_scores_list(course_id, assignment_id, **kwargs) 249 for score in scores: 250 if ((score.user is not None) and (score.user.id == user_id)): 251 return score 252 253 return None 254 255 def courses_assignments_scores_list(self, 256 course_id: str, 257 assignment_id: str, 258 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 259 """ 260 List the scores associated with the given assignment. 261 """ 262 263 raise NotImplementedError('courses_assignments_scores_list') 264 265 def courses_assignments_scores_resolve_and_list(self, 266 course_query: lms.model.courses.CourseQuery, 267 assignment_query: lms.model.assignments.AssignmentQuery, 268 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 269 """ 270 List the scores associated with the given assignment query. 271 In addition to resolving the assignment query, 272 users will also be resolved into their full version 273 (instead of the reduced version usually returned with scores). 274 """ 275 276 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 277 278 # Resolve the assignment query. 279 matched_assignments = self.courses_assignments_get(resolved_course_query, [assignment_query], **kwargs) 280 if (len(matched_assignments) == 0): 281 return [] 282 283 target_assignment = matched_assignments[0] 284 285 # List the scores. 286 scores = self.courses_assignments_scores_list(resolved_course_query.get_id(), target_assignment.id, **kwargs) 287 if (len(scores) == 0): 288 return [] 289 290 # Resolve the scores' queries. 291 292 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 293 users_map = {user.id: user for user in users} 294 295 for score in scores: 296 score.assignment = target_assignment.to_query() 297 298 if ((score.user is not None) and (score.user.id in users_map)): 299 score.user = users_map[score.user.id].to_query() 300 301 return sorted(scores) 302 303 def courses_assignments_scores_resolve_and_upload(self, 304 course_query: lms.model.courses.CourseQuery, 305 assignment_query: lms.model.assignments.AssignmentQuery, 306 scores: typing.Dict[lms.model.users.UserQuery, lms.model.scores.ScoreFragment], 307 **kwargs: typing.Any) -> int: 308 """ 309 Resolve queries and upload assignment scores (indexed by user query). 310 A None score (ScoreFragment.score) indicates that the score should be cleared. 311 Return the number of scores sent to the LMS. 312 """ 313 314 if (len(scores) == 0): 315 return 0 316 317 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 318 resolved_assignment_query = self.resolve_assignment_query(resolved_course_query.get_id(), assignment_query, **kwargs) 319 320 resolved_users = self.resolve_user_queries(resolved_course_query.get_id(), list(scores.keys()), warn_on_miss = True) 321 resolved_scores: typing.Dict[str, lms.model.scores.ScoreFragment] = {} 322 323 for (user, score) in scores.items(): 324 for resolved_user in resolved_users: 325 if (user.match(resolved_user)): 326 resolved_scores[resolved_user.get_id()] = score 327 continue 328 329 if (len(resolved_scores) == 0): 330 return 0 331 332 return self.courses_assignments_scores_upload( 333 resolved_course_query.get_id(), 334 resolved_assignment_query.get_id(), 335 resolved_scores, 336 **kwargs) 337 338 def courses_assignments_scores_upload(self, 339 course_id: str, 340 assignment_id: str, 341 scores: typing.Dict[str, lms.model.scores.ScoreFragment], 342 **kwargs: typing.Any) -> int: 343 """ 344 Upload assignment scores (indexed by user id). 345 A None score (ScoreFragment.score) indicates that the score should be cleared. 346 Return the number of scores sent to the LMS. 347 """ 348 349 raise NotImplementedError('courses_assignments_scores_upload') 350 351 def courses_gradebook_get(self, 352 course_query: lms.model.courses.CourseQuery, 353 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 354 user_queries: typing.Collection[lms.model.users.UserQuery], 355 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 356 """ 357 Get a gradebook with the specified users and assignments. 358 Specifying no users/assignments is the same as requesting all of them. 359 """ 360 361 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 362 363 resolved_assignment_queries = self.resolve_assignment_queries(resolved_course_query.get_id(), assignment_queries, empty_all = True, **kwargs) 364 assignment_ids = [query.get_id() for query in resolved_assignment_queries] 365 366 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, 367 empty_all = True, only_students = True, **kwargs) 368 user_ids = [query.get_id() for query in resolved_user_queries] 369 370 gradebook = self.courses_gradebook_fetch(resolved_course_query.get_id(), assignment_ids, user_ids, **kwargs) 371 372 # Resolve the gradebook's queries (so it can show names/emails instead of just IDs). 373 gradebook.update_queries(resolved_assignment_queries, resolved_user_queries) 374 375 return gradebook 376 377 def courses_gradebook_fetch(self, 378 course_id: str, 379 assignment_ids: typing.Collection[str], 380 user_ids: typing.Collection[str], 381 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 382 """ 383 Get a gradebook with the specified users and assignments. 384 If either the assignments or users is empty, an empty gradebook will be returned. 385 """ 386 387 raise NotImplementedError('courses_gradebook_fetch') 388 389 def courses_gradebook_list(self, 390 course_id: str, 391 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 392 """ 393 List the full gradebook associated with this course. 394 """ 395 396 return self.courses_gradebook_get(lms.model.courses.CourseQuery(id = course_id), [], [], **kwargs) 397 398 def courses_gradebook_resolve_and_list(self, 399 course_query: lms.model.courses.CourseQuery, 400 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 401 """ 402 List the full gradebook associated with this course. 403 """ 404 405 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 406 return self.courses_gradebook_list(resolved_course_query.get_id(), **kwargs) 407 408 def courses_gradebook_resolve_and_upload(self, 409 course_query: lms.model.courses.CourseQuery, 410 gradebook: lms.model.scores.Gradebook, 411 **kwargs: typing.Any) -> int: 412 """ 413 Resolve queries and upload a gradebook. 414 Missing scores in the gradebook are skipped, 415 a None score (ScoreFragment.score) indicates that the score should be cleared. 416 Return the number of scores sent to the LMS. 417 """ 418 419 if (len(gradebook) == 0): 420 return 0 421 422 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 423 424 assignments = self.courses_assignments_list(resolved_course_query.get_id(), **kwargs) 425 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 426 427 resolved_assignment_queries = [assignment.to_query() for assignment in assignments] 428 resolved_user_queries = [user.to_query() for user in users] 429 430 gradebook.update_queries(resolved_assignment_queries, resolved_user_queries) 431 432 return self.courses_gradebook_upload( 433 resolved_course_query.get_id(), 434 gradebook, 435 **kwargs) 436 437 def courses_gradebook_upload(self, 438 course_id: str, 439 gradebook: lms.model.scores.Gradebook, 440 **kwargs: typing.Any) -> int: 441 """ 442 Upload a gradebook. 443 All queries in the gradebook must be resolved (or at least have an ID). 444 Missing scores in the gradebook are skipped, 445 a None score (ScoreFragment.score) indicates that the score should be cleared. 446 Return the number of scores sent to the LMS. 447 """ 448 449 assignment_scores = gradebook.get_scores_by_assignment() 450 451 count = 0 452 for (assignment, user_scores) in assignment_scores.items(): 453 if (assignment.id is None): 454 raise ValueError(f"Assignment query for gradebook upload ({assignment}) does not have an ID.") 455 456 upload_scores = {} 457 for (user, score) in user_scores.items(): 458 if (user.id is None): 459 raise ValueError(f"User query for gradebook upload ({user}) does not have an ID.") 460 461 upload_scores[user.id] = score.to_fragment() 462 463 count += self.courses_assignments_scores_upload(course_id, assignment.id, upload_scores, **kwargs) 464 465 return count 466 467 def courses_groupsets_create(self, 468 course_id: str, 469 name: str, 470 **kwargs: typing.Any) -> lms.model.groupsets.GroupSet: 471 """ 472 Create a group set. 473 """ 474 475 raise NotImplementedError('courses_groupsets_create') 476 477 def courses_groupsets_resolve_and_create(self, 478 course_query: lms.model.courses.CourseQuery, 479 name: str, 480 **kwargs: typing.Any) -> lms.model.groupsets.GroupSet: 481 """ 482 Resolve references and create a group set. 483 """ 484 485 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 486 return self.courses_groupsets_create(resolved_course_query.get_id(), name, **kwargs) 487 488 def courses_groupsets_delete(self, 489 course_id: str, 490 groupset_id: str, 491 **kwargs: typing.Any) -> bool: 492 """ 493 Delete a group set. 494 """ 495 496 raise NotImplementedError('courses_groupsets_delete') 497 498 def courses_groupsets_resolve_and_delete(self, 499 course_query: lms.model.courses.CourseQuery, 500 groupset_query: lms.model.groupsets.GroupSetQuery, 501 **kwargs: typing.Any) -> bool: 502 """ 503 Resolve references and create a group set. 504 """ 505 506 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 507 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 508 return self.courses_groupsets_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 509 510 def courses_groupsets_get(self, 511 course_query: lms.model.courses.CourseQuery, 512 groupset_queries: typing.Collection[lms.model.groupsets.GroupSetQuery], 513 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 514 """ 515 Get the specified group sets associated with the given course. 516 """ 517 518 if (len(groupset_queries) == 0): 519 return [] 520 521 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 522 groupset_queries = sorted(groupset_queries) 523 groupsets = sorted(self.courses_groupsets_list(resolved_course_query.get_id(), **kwargs)) 524 525 matches = [] 526 for groupset in groupsets: 527 for query in groupset_queries: 528 if (query.match(groupset)): 529 matches.append(groupset) 530 break 531 532 return matches 533 534 def courses_groupsets_fetch(self, 535 course_id: str, 536 groupset_id: str, 537 **kwargs: typing.Any) -> typing.Union[lms.model.groupsets.GroupSet, None]: 538 """ 539 Fetch a single group set associated with the given course. 540 Return None if no matching group set is found. 541 542 By default, this will just do a list and choose the relevant record. 543 Specific backends may override this if there are performance concerns. 544 """ 545 546 groupsets = self.courses_groupsets_list(course_id, **kwargs) 547 for groupset in groupsets: 548 if (groupset.id == groupset_id): 549 return groupset 550 551 return None 552 553 def courses_groupsets_list(self, 554 course_id: str, 555 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 556 """ 557 List the group sets associated with the given course. 558 """ 559 560 raise NotImplementedError('courses_groupsets_list') 561 562 def courses_groupsets_resolve_and_list(self, 563 course_query: lms.model.courses.CourseQuery, 564 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 565 """ 566 List the group sets associated with the given course. 567 """ 568 569 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 570 return sorted(self.courses_groupsets_list(resolved_course_query.get_id(), **kwargs)) 571 572 def courses_groupsets_memberships_resolve_and_add(self, 573 course_query: lms.model.courses.CourseQuery, 574 groupset_query: lms.model.groupsets.GroupSetQuery, 575 memberships: typing.Collection[lms.model.groups.GroupMembership], 576 **kwargs: typing.Any) -> typing.Tuple[ 577 typing.List[lms.model.groups.Group], 578 typing.Dict[lms.model.groups.ResolvedGroupQuery, int] 579 ]: 580 """ 581 Resolve queries and add the specified users to the specified groups. 582 This may create groups. 583 584 Return: 585 - Created Groups 586 - Group Addition Counts 587 """ 588 589 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 590 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 591 592 found_group_memberships, missing_group_memberships, _ = self._resolve_group_memberships( 593 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 594 595 # Create missing groups. 596 created_groups = [] 597 for name in sorted(missing_group_memberships.keys()): 598 group = self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 599 created_groups.append(group) 600 601 # Merge in new group with existing structure. 602 query = group.to_query() 603 if (query not in found_group_memberships): 604 found_group_memberships[query] = [] 605 606 found_group_memberships[query] += missing_group_memberships[name] 607 608 # Add memberships. 609 counts = {} 610 for resolved_group_query in sorted(found_group_memberships.keys()): 611 resolved_user_queries = found_group_memberships[resolved_group_query] 612 613 count = self.courses_groups_memberships_resolve_and_add( 614 resolved_course_query, resolved_groupset_query, resolved_group_query, 615 resolved_user_queries, 616 **kwargs) 617 618 counts[resolved_group_query] = count 619 620 return (created_groups, counts) 621 622 def courses_groupsets_memberships_resolve_and_set(self, 623 course_query: lms.model.courses.CourseQuery, 624 groupset_query: lms.model.groupsets.GroupSetQuery, 625 memberships: typing.Collection[lms.model.groups.GroupMembership], 626 **kwargs: typing.Any) -> typing.Tuple[ 627 typing.List[lms.model.groups.Group], 628 typing.List[lms.model.groups.ResolvedGroupQuery], 629 typing.Dict[lms.model.groups.ResolvedGroupQuery, int], 630 typing.Dict[lms.model.groups.ResolvedGroupQuery, int], 631 ]: 632 """ 633 Resolve queries and set the specified group memberships. 634 This may create and delete groups. 635 636 Return: 637 - Created Groups 638 - Deleted Groups 639 - Group Addition Counts 640 - Group Subtraction Counts 641 """ 642 643 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 644 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 645 646 found_group_memberships, missing_group_memberships, unused_groups = self._resolve_group_memberships( 647 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 648 649 # Delete unused groups. 650 deleted_groups = [] 651 for group_query in sorted(unused_groups): 652 result = self.courses_groups_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query.get_id(), **kwargs) 653 if (result): 654 deleted_groups.append(group_query) 655 656 # Create missing groups. 657 created_groups = [] 658 for name in sorted(missing_group_memberships.keys()): 659 group = self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 660 created_groups.append(group) 661 662 # Merge in new group with existing structure. 663 query = group.to_query() 664 if (query not in found_group_memberships): 665 found_group_memberships[query] = [] 666 667 found_group_memberships[query] += missing_group_memberships[name] 668 669 # Set memberships. 670 add_counts = {} 671 sub_counts = {} 672 for resolved_group_query in sorted(found_group_memberships.keys()): 673 resolved_user_queries = found_group_memberships[resolved_group_query] 674 675 (add_count, sub_count, deleted) = self.courses_groups_memberships_resolve_and_set( 676 resolved_course_query, resolved_groupset_query, resolved_group_query, 677 resolved_user_queries, 678 delete_empty = True, 679 **kwargs) 680 681 if (deleted): 682 deleted_groups.append(resolved_group_query) 683 684 add_counts[resolved_group_query] = add_count 685 sub_counts[resolved_group_query] = sub_count 686 687 return (created_groups, deleted_groups, add_counts, sub_counts) 688 689 def courses_groupsets_memberships_resolve_and_subtract(self, 690 course_query: lms.model.courses.CourseQuery, 691 groupset_query: lms.model.groupsets.GroupSetQuery, 692 memberships: typing.Collection[lms.model.groups.GroupMembership], 693 **kwargs: typing.Any) -> typing.Dict[lms.model.groups.ResolvedGroupQuery, int]: 694 """ 695 Resolve queries and subtract the specified users to the specified groups. 696 This will not delete any groups. 697 698 Return: 699 - Group Subtraction Counts 700 """ 701 702 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 703 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 704 705 found_group_memberships, missing_group_memberships, _ = self._resolve_group_memberships( 706 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 707 708 # Warn about missing groups. 709 for name in sorted(missing_group_memberships.keys()): 710 _logger.warning("Group does not exist: '%s'.", name) 711 712 # Subtract memberships. 713 counts = {} 714 for resolved_group_query in sorted(found_group_memberships.keys()): 715 resolved_user_queries = found_group_memberships[resolved_group_query] 716 717 (count, _) = self.courses_groups_memberships_resolve_and_subtract( 718 resolved_course_query, resolved_groupset_query, resolved_group_query, 719 resolved_user_queries, 720 delete_empty = False, 721 **kwargs) 722 723 counts[resolved_group_query] = count 724 725 return counts 726 727 def courses_groupsets_memberships_list(self, 728 course_id: str, 729 groupset_id: str, 730 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 731 """ 732 List the membership of the group sets associated with the given course. 733 """ 734 735 raise NotImplementedError('courses_groupsets_memberships_list') 736 737 def courses_groupsets_memberships_resolve_and_list(self, 738 course_query: lms.model.courses.CourseQuery, 739 groupset_query: lms.model.groupsets.GroupSetQuery, 740 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 741 """ 742 List the membership of the group sets associated with the given course. 743 """ 744 745 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 746 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 747 748 memberships = self.courses_groupsets_memberships_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 749 750 # Resolve memberships. 751 752 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 753 groups = self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 754 755 users_map = {user.id: user.to_query() for user in users} 756 groups_map = {group.id: group.to_query() for group in groups} 757 758 for membership in memberships: 759 membership.update_queries(resolved_groupset_query, users = users_map, groups = groups_map) 760 761 return sorted(memberships) 762 763 def courses_groups_create(self, 764 course_id: str, 765 groupset_id: str, 766 name: str, 767 **kwargs: typing.Any) -> lms.model.groups.Group: 768 """ 769 Create a group. 770 """ 771 772 raise NotImplementedError('courses_groups_create') 773 774 def courses_groups_resolve_and_create(self, 775 course_query: lms.model.courses.CourseQuery, 776 groupset_query: lms.model.groupsets.GroupSetQuery, 777 name: str, 778 **kwargs: typing.Any) -> lms.model.groups.Group: 779 """ 780 Resolve references and create a group. 781 """ 782 783 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 784 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 785 return self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 786 787 def courses_groups_delete(self, 788 course_id: str, 789 groupset_id: str, 790 group_id: str, 791 **kwargs: typing.Any) -> bool: 792 """ 793 Delete a group. 794 """ 795 796 raise NotImplementedError('courses_groups_delete') 797 798 def courses_groups_resolve_and_delete(self, 799 course_query: lms.model.courses.CourseQuery, 800 groupset_query: lms.model.groupsets.GroupSetQuery, 801 group_query: lms.model.groups.GroupQuery, 802 **kwargs: typing.Any) -> bool: 803 """ 804 Resolve references and create a group. 805 """ 806 807 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 808 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 809 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 810 return self.courses_groups_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), resolved_group_query.get_id(), **kwargs) 811 812 def courses_groups_get(self, 813 course_query: lms.model.courses.CourseQuery, 814 groupset_query: lms.model.groupsets.GroupSetQuery, 815 group_queries: typing.Collection[lms.model.groups.GroupQuery], 816 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 817 """ 818 Get the specified groups associated with the given course. 819 """ 820 821 if (len(group_queries) == 0): 822 return [] 823 824 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 825 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 826 groups = self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 827 828 group_queries = sorted(group_queries) 829 groups = sorted(groups) 830 831 matches = [] 832 for group in groups: 833 for query in group_queries: 834 if (query.match(group)): 835 matches.append(group) 836 break 837 838 return matches 839 840 def courses_groups_fetch(self, 841 course_id: str, 842 groupset_id: str, 843 group_id: str, 844 **kwargs: typing.Any) -> typing.Union[lms.model.groups.Group, None]: 845 """ 846 Fetch a single group associated with the given course. 847 Return None if no matching group is found. 848 849 By default, this will just do a list and choose the relevant record. 850 Specific backends may override this if there are performance concerns. 851 """ 852 853 groups = self.courses_groups_list(course_id, groupset_id, **kwargs) 854 for group in groups: 855 if (group.id == group_id): 856 return group 857 858 return None 859 860 def courses_groups_list(self, 861 course_id: str, 862 groupset_id: str, 863 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 864 """ 865 List the groups associated with the given course. 866 """ 867 868 raise NotImplementedError('courses_groups_list') 869 870 def courses_groups_resolve_and_list(self, 871 course_query: lms.model.courses.CourseQuery, 872 groupset_query: lms.model.groupsets.GroupSetQuery, 873 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 874 """ 875 List the groups associated with the given course. 876 """ 877 878 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 879 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 880 return self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 881 882 def courses_groups_memberships_add(self, 883 course_id: str, 884 groupset_id: str, 885 group_id: str, 886 user_ids: typing.Collection[str], 887 **kwargs: typing.Any) -> int: 888 """ 889 Add the specified users to the group. 890 """ 891 892 raise NotImplementedError('courses_groups_memberships_add') 893 894 def courses_groups_memberships_resolve_and_add(self, 895 course_query: lms.model.courses.CourseQuery, 896 groupset_query: lms.model.groupsets.GroupSetQuery, 897 group_query: lms.model.groups.GroupQuery, 898 user_queries: typing.Collection[lms.model.users.UserQuery], 899 **kwargs: typing.Any) -> int: 900 """ 901 Resolve queries and add the specified users to the group. 902 """ 903 904 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 905 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 906 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 907 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 908 909 # Get users already in this group. 910 group_memberships = self.courses_groups_memberships_list( 911 resolved_course_query.get_id(), 912 resolved_groupset_query.get_id(), 913 resolved_group_query.get_id(), 914 **kwargs) 915 916 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 917 918 # Filter out users already in the group. 919 user_ids = [] 920 for query in sorted(resolved_user_queries): 921 if (query.get_id() in group_user_ids): 922 _logger.warning("User '%s' already in group '%s'.", query, resolved_group_query) 923 continue 924 925 user_ids.append(query.get_id()) 926 927 if (len(user_ids) == 0): 928 return 0 929 930 return self.courses_groups_memberships_add( 931 resolved_course_query.get_id(), 932 resolved_groupset_query.get_id(), 933 resolved_group_query.get_id(), 934 user_ids, 935 **kwargs) 936 937 def courses_groups_memberships_list(self, 938 course_id: str, 939 groupset_id: str, 940 group_id: str, 941 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 942 """ 943 List the membership of the group associated with the given group set. 944 """ 945 946 raise NotImplementedError('courses_groups_memberships_list') 947 948 def courses_groups_memberships_resolve_and_list(self, 949 course_query: lms.model.courses.CourseQuery, 950 groupset_query: lms.model.groupsets.GroupSetQuery, 951 group_query: lms.model.groups.GroupQuery, 952 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 953 """ 954 List the membership of the group associated with the given group set. 955 """ 956 957 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 958 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 959 960 groups = self.courses_groups_get(resolved_course_query, resolved_groupset_query, [group_query], **kwargs) 961 if (len(groups) == 0): 962 raise ValueError(f"Unable to find group: '{group_query}'.") 963 964 group = groups[0] 965 966 memberships = self.courses_groups_memberships_list( 967 resolved_course_query.get_id(), 968 resolved_groupset_query.get_id(), 969 group.id, 970 **kwargs) 971 972 # Resolve memberships. 973 974 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 975 users_map = {user.id: user.to_query() for user in users} 976 977 groups_map = {group.id: group.to_query()} 978 979 for membership in memberships: 980 membership.update_queries(resolved_groupset_query, users = users_map, groups = groups_map) 981 982 return sorted(memberships) 983 984 def courses_groups_memberships_resolve_and_set(self, 985 course_query: lms.model.courses.CourseQuery, 986 groupset_query: lms.model.groupsets.GroupSetQuery, 987 group_query: lms.model.groups.GroupQuery, 988 user_queries: typing.Collection[lms.model.users.UserQuery], 989 delete_empty: bool = False, 990 **kwargs: typing.Any) -> typing.Tuple[int, int, bool]: 991 """ 992 Resolve queries and set the specified users for the group. 993 This method can both add and subtract users from the group. 994 995 Returns: 996 - The count of users added. 997 - The count of users subtracted. 998 - If this group was deleted. 999 """ 1000 1001 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1002 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 1003 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 1004 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 1005 1006 # Get users already in this group. 1007 group_memberships = self.courses_groups_memberships_list( 1008 resolved_course_query.get_id(), 1009 resolved_groupset_query.get_id(), 1010 resolved_group_query.get_id(), 1011 **kwargs) 1012 1013 group_user_queries = {membership.user for membership in group_memberships if membership.user is not None} 1014 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 1015 query_user_ids = {resolved_user_query.get_id() for resolved_user_query in resolved_user_queries} 1016 1017 # Collect users that need to be added. 1018 add_user_ids = [] 1019 for query_user_id in query_user_ids: 1020 if (query_user_id not in group_user_ids): 1021 add_user_ids.append(query_user_id) 1022 1023 # Collect users that need to be subtracted. 1024 sub_user_queries = [] 1025 for group_user_query in group_user_queries: 1026 if (group_user_query not in resolved_user_queries): 1027 sub_user_queries.append(group_user_query) 1028 1029 # Update the group. 1030 1031 add_count = 0 1032 if (len(add_user_ids) > 0): 1033 add_count = self.courses_groups_memberships_add( 1034 resolved_course_query.get_id(), 1035 resolved_groupset_query.get_id(), 1036 resolved_group_query.get_id(), 1037 add_user_ids, 1038 **kwargs) 1039 1040 sub_count = 0 1041 deleted = False 1042 if (len(sub_user_queries) > 0): 1043 sub_count, deleted = self.courses_groups_memberships_resolve_and_subtract( 1044 resolved_course_query, 1045 resolved_groupset_query, 1046 resolved_group_query, 1047 sub_user_queries, 1048 delete_empty = delete_empty, 1049 **kwargs) 1050 1051 return add_count, sub_count, deleted 1052 1053 def courses_groups_memberships_subtract(self, 1054 course_id: str, 1055 groupset_id: str, 1056 group_id: str, 1057 user_ids: typing.Collection[str], 1058 **kwargs: typing.Any) -> int: 1059 """ 1060 Subtract the specified users from the group. 1061 """ 1062 1063 raise NotImplementedError('courses_groups_memberships_subtract') 1064 1065 def courses_groups_memberships_resolve_and_subtract(self, 1066 course_query: lms.model.courses.CourseQuery, 1067 groupset_query: lms.model.groupsets.GroupSetQuery, 1068 group_query: lms.model.groups.GroupQuery, 1069 user_queries: typing.Collection[lms.model.users.UserQuery], 1070 delete_empty: bool = False, 1071 **kwargs: typing.Any) -> typing.Tuple[int, bool]: 1072 """ 1073 Resolve queries and subtract the specified users from the group. 1074 Return: 1075 - The number of users deleted. 1076 - If this group was deleted. 1077 """ 1078 1079 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1080 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 1081 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 1082 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 1083 1084 # Get users already in this group. 1085 group_memberships = self.courses_groups_memberships_list( 1086 resolved_course_query.get_id(), 1087 resolved_groupset_query.get_id(), 1088 resolved_group_query.get_id(), 1089 **kwargs) 1090 1091 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 1092 1093 # Filter out users not in the group. 1094 user_ids = [] 1095 for query in resolved_user_queries: 1096 if (query.get_id() not in group_user_ids): 1097 _logger.warning("User '%s' is not in group '%s'.", query, resolved_group_query) 1098 continue 1099 1100 user_ids.append(query.get_id()) 1101 1102 if (delete_empty and len(group_memberships) == 0): 1103 deleted = self.courses_groups_delete( 1104 resolved_course_query.get_id(), 1105 resolved_groupset_query.get_id(), 1106 resolved_group_query.get_id(), 1107 **kwargs) 1108 return 0, deleted 1109 1110 if (len(user_ids) == 0): 1111 return 0, False 1112 1113 count = self.courses_groups_memberships_subtract( 1114 resolved_course_query.get_id(), 1115 resolved_groupset_query.get_id(), 1116 resolved_group_query.get_id(), 1117 user_ids, 1118 **kwargs) 1119 1120 deleted = False 1121 if (delete_empty and (count == len(group_memberships))): 1122 deleted = self.courses_groups_delete( 1123 resolved_course_query.get_id(), 1124 resolved_groupset_query.get_id(), 1125 resolved_group_query.get_id(), 1126 **kwargs) 1127 1128 return count, deleted 1129 1130 def courses_quizzes_download(self, 1131 course_id: str, 1132 quiz_id: str, 1133 **kwargs: typing.Any) -> quizcomp.model.quiz.Quiz: 1134 """ 1135 Download a quiz from the LMS into a Quiz Composer quiz. 1136 This will only fetch the full quiz, not write it to disk. 1137 """ 1138 1139 raise NotImplementedError('courses_quizzes_download') 1140 1141 def courses_quizzes_resolve_and_download(self, 1142 course_query: lms.model.courses.CourseQuery, 1143 quiz_query: lms.model.assignments.AssignmentQuery, 1144 **kwargs: typing.Any) -> quizcomp.model.quiz.Quiz: 1145 """ 1146 Resolve queries and download a quiz from the LMS into a Quiz Composer quiz. 1147 This will only fetch the full quiz, not write it to disk. 1148 """ 1149 1150 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1151 resolved_quiz_query = self.resolve_quiz_query(resolved_course_query.get_id(), quiz_query, **kwargs) 1152 1153 return self.courses_quizzes_download(resolved_course_query.get_id(), resolved_quiz_query.get_id(), **kwargs) 1154 1155 def courses_quizzes_get(self, 1156 course_query: lms.model.courses.CourseQuery, 1157 quiz_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1158 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1159 """ 1160 Get the specified quizzes associated with the given course. 1161 """ 1162 1163 if (len(quiz_queries) == 0): 1164 return [] 1165 1166 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1167 1168 quizzes = sorted(self.courses_quizzes_list(resolved_course_query.get_id(), **kwargs)) 1169 quiz_queries = sorted(quiz_queries) 1170 1171 matches = [] 1172 for quiz in quizzes: 1173 for query in quiz_queries: 1174 if (query.match(quiz)): 1175 matches.append(quiz) 1176 break 1177 1178 return matches 1179 1180 def courses_quizzes_fetch(self, 1181 course_id: str, 1182 quiz_id: str, 1183 **kwargs: typing.Any) -> typing.Union[lms.model.assignments.Assignment, None]: 1184 """ 1185 Fetch a single quiz associated with the given course. 1186 Return None if no matching quiz is found. 1187 1188 By default, this will just do a list and choose the relevant record. 1189 Specific backends may override this if there are performance concerns. 1190 """ 1191 1192 quizzes = self.courses_quizzes_list(course_id, **kwargs) 1193 for quiz in sorted(quizzes): 1194 if (quiz.id == quiz_id): 1195 return quiz 1196 1197 return None 1198 1199 def courses_quizzes_list(self, 1200 course_id: str, 1201 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1202 """ 1203 List the quizzes associated with the given course. 1204 If specified, additional resources associated with the quiz (e.g., images) may also be fetched. 1205 """ 1206 1207 raise NotImplementedError('courses_quizzes_list') 1208 1209 def courses_quizzes_resolve_and_list(self, 1210 course_query: lms.model.courses.CourseQuery, 1211 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1212 """ 1213 List the quizzes associated with the given course. 1214 """ 1215 1216 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1217 return sorted(self.courses_quizzes_list(resolved_course_query.get_id(), **kwargs)) 1218 1219 def courses_quizzes_resolve_and_remove(self, 1220 course_query: lms.model.courses.CourseQuery, 1221 quiz_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1222 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1223 """ 1224 Resolve the course and remove any matching quiz. 1225 Return the quizzes that were removed. 1226 """ 1227 1228 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1229 1230 old_quizzes = self.courses_quizzes_get(resolved_course_query, quiz_queries) 1231 for old_quiz in old_quizzes: 1232 self.courses_quizzes_remove(resolved_course_query.get_id(), old_quiz.id) 1233 1234 return old_quizzes 1235 1236 def courses_quizzes_remove(self, 1237 course_id: str, 1238 quiz_id: str, 1239 **kwargs: typing.Any) -> None: 1240 """ 1241 Remove quiz from the LMS. 1242 """ 1243 1244 raise NotImplementedError('courses_quizzes_remove') 1245 1246 def courses_quizzes_resolve_and_upload(self, 1247 course_query: lms.model.courses.CourseQuery, 1248 quiz: quizcomp.model.quiz.Quiz, 1249 force: bool = False, 1250 **kwargs: typing.Any) -> lms.model.assignments.Assignment: 1251 """ 1252 Resolve the course and upload the quiz. 1253 If a quiz already exists with a matching query, then `force` will decide the behavior. 1254 If `force` is true then those matching quizzes will be removed, 1255 otherwise an exception will be raised. 1256 """ 1257 1258 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1259 1260 old_quizzes = self.courses_quizzes_get(resolved_course_query, [lms.model.assignments.AssignmentQuery(name = quiz.name)]) 1261 if ((not force) and (len(old_quizzes) > 0)): 1262 raise ValueError(f"Found {len(old_quizzes)} existing quizzes with a matching name ('{quiz.name}'), stopping upload.") 1263 1264 for old_quiz in old_quizzes: 1265 _logger.warning("Deleting existing quiz ('%s') before upload.", old_quiz.to_query()) 1266 self.courses_quizzes_remove(resolved_course_query.get_id(), old_quiz.id) 1267 1268 return self.courses_quizzes_upload(resolved_course_query.get_id(), quiz) 1269 1270 def courses_quizzes_upload(self, 1271 course_id: str, 1272 quiz: quizcomp.model.quiz.Quiz, 1273 **kwargs: typing.Any) -> lms.model.assignments.Assignment: 1274 """ 1275 Upload a single quiz to the LMS. 1276 1277 This method assumes that there is no quiz with a matching name. 1278 To handle that case, use courses_quizzes_resolve_and_upload() with `force = True`. 1279 """ 1280 1281 raise NotImplementedError('courses_quizzes_upload') 1282 1283 def courses_syllabus_fetch(self, 1284 course_id: str, 1285 **kwargs: typing.Any) -> typing.Union[str, None]: 1286 """ 1287 Get the syllabus for a course, or None if no syllabus exists. 1288 """ 1289 1290 raise NotImplementedError('courses_syllabus_fetch') 1291 1292 def courses_syllabus_get(self, 1293 course_query: lms.model.courses.CourseQuery, 1294 **kwargs: typing.Any) -> typing.Union[str, None]: 1295 """ 1296 Get the syllabus for a course query, or None if no syllabus exists. 1297 """ 1298 1299 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1300 1301 return self.courses_syllabus_fetch(resolved_course_query.get_id(), **kwargs) 1302 1303 def courses_users_get(self, 1304 course_query: lms.model.courses.CourseQuery, 1305 user_queries: typing.Collection[lms.model.users.UserQuery], 1306 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1307 """ 1308 Get the specified users associated with the given course. 1309 """ 1310 1311 if (len(user_queries) == 0): 1312 return [] 1313 1314 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1315 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 1316 1317 user_queries = sorted(user_queries) 1318 users = sorted(users) 1319 1320 matches = [] 1321 for user in users: 1322 for query in user_queries: 1323 if (query.match(user)): 1324 matches.append(user) 1325 break 1326 1327 return matches 1328 1329 def courses_users_fetch(self, 1330 course_id: str, 1331 user_id: str, 1332 **kwargs: typing.Any) -> typing.Union[lms.model.users.CourseUser, None]: 1333 """ 1334 Fetch a single user associated with the given course. 1335 Return None if no matching user is found. 1336 1337 By default, this will just do a list and choose the relevant record. 1338 Specific backends may override this if there are performance concerns. 1339 """ 1340 1341 users = self.courses_users_list(course_id, **kwargs) 1342 for user in sorted(users): 1343 if (user.id == user_id): 1344 return user 1345 1346 return None 1347 1348 def courses_users_list(self, 1349 course_id: str, 1350 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1351 """ 1352 List the users associated with the given course. 1353 """ 1354 1355 raise NotImplementedError('courses_users_list') 1356 1357 def courses_users_resolve_and_list(self, 1358 course_query: lms.model.courses.CourseQuery, 1359 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1360 """ 1361 List the users associated with the given course. 1362 """ 1363 1364 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1365 return list(sorted(self.courses_users_list(resolved_course_query.get_id(), **kwargs))) 1366 1367 def courses_users_scores_get(self, 1368 course_query: lms.model.courses.CourseQuery, 1369 user_query: lms.model.users.UserQuery, 1370 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1371 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1372 """ 1373 Get the scores associated with the given user query and assignment queries. 1374 """ 1375 1376 if (len(assignment_queries) == 0): 1377 return [] 1378 1379 scores = self.courses_users_scores_resolve_and_list(course_query, user_query, **kwargs) 1380 1381 scores = sorted(scores) 1382 assignment_queries = sorted(assignment_queries) 1383 1384 matches = [] 1385 for score in scores: 1386 for assignment_query in assignment_queries: 1387 if (assignment_query.match(score.assignment)): 1388 matches.append(score) 1389 1390 return matches 1391 1392 def courses_users_scores_fetch(self, 1393 course_id: str, 1394 user_id: str, 1395 assignment_id: str, 1396 **kwargs: typing.Any) -> typing.Union[lms.model.scores.AssignmentScore, None]: 1397 """ 1398 Fetch the score associated with the given user and assignment. 1399 1400 By default, this will just do a list and choose the relevant record. 1401 Specific backends may override this if there are performance concerns. 1402 """ 1403 1404 # The default implementation is the same as courses_assignments_scores_fetch(). 1405 return self.courses_assignments_scores_fetch(course_id, assignment_id, user_id, **kwargs) 1406 1407 def courses_users_scores_list(self, 1408 course_id: str, 1409 user_id: str, 1410 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1411 """ 1412 List the scores associated with the given user. 1413 """ 1414 1415 raise NotImplementedError('courses_users_scores_list') 1416 1417 def courses_users_scores_resolve_and_list(self, 1418 course_query: lms.model.courses.CourseQuery, 1419 user_query: lms.model.users.UserQuery, 1420 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1421 """ 1422 List the scores associated with the given user query. 1423 In addition to resolving the user query, 1424 assignments will also be resolved into their full version 1425 (instead of the reduced version usually returned with scores). 1426 """ 1427 1428 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1429 1430 # Resolve the user query. 1431 matched_users = self.courses_users_get(resolved_course_query, [user_query], **kwargs) 1432 if (len(matched_users) == 0): 1433 return [] 1434 1435 target_user = matched_users[0] 1436 1437 # List the scores. 1438 scores = self.courses_users_scores_list(resolved_course_query.get_id(), target_user.id, **kwargs) 1439 if (len(scores) == 0): 1440 return [] 1441 1442 # Resolve the scores' queries. 1443 1444 assignments = self.courses_assignments_list(resolved_course_query.get_id(), **kwargs) 1445 assignments_map = {assignment.id: assignment for assignment in assignments} 1446 1447 for score in scores: 1448 score.user = target_user.to_query() 1449 1450 if ((score.assignment is not None) and (score.assignment.id in assignments_map)): 1451 score.assignment = assignments_map[score.assignment.id].to_query() 1452 1453 return sorted(scores) 1454 1455 # Utility Methods 1456 1457 def parse_assignment_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.assignments.AssignmentQuery, None]: 1458 """ 1459 Attempt to parse an assignment query from a string. 1460 If there is no query, return a None. 1461 If the query is malformed, raise an exception. 1462 1463 By default, this method assumes that LMS IDs are ints. 1464 Child backends may override this to implement their specific behavior. 1465 """ 1466 1467 return lms.model.query.parse_int_query(lms.model.assignments.AssignmentQuery, text, check_email = False) 1468 1469 def parse_assignment_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.assignments.AssignmentQuery]: 1470 """ Parse a list of assignment queries. """ 1471 1472 queries = [] 1473 for text in texts: 1474 query = self.parse_assignment_query(text) 1475 if (query is not None): 1476 queries.append(query) 1477 1478 return queries 1479 1480 def parse_course_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.courses.CourseQuery, None]: 1481 """ 1482 Attempt to parse a course query from a string. 1483 If there is no query, return a None. 1484 If the query is malformed, raise an exception. 1485 1486 By default, this method assumes that LMS IDs are ints. 1487 Child backends may override this to implement their specific behavior. 1488 """ 1489 1490 return lms.model.query.parse_int_query(lms.model.courses.CourseQuery, text, check_email = False) 1491 1492 def parse_course_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.courses.CourseQuery]: 1493 """ Parse a list of course queries. """ 1494 1495 queries = [] 1496 for text in texts: 1497 query = self.parse_course_query(text) 1498 if (query is not None): 1499 queries.append(query) 1500 1501 return queries 1502 1503 def parse_groupset_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.groupsets.GroupSetQuery, None]: 1504 """ 1505 Attempt to parse a group set query from a string. 1506 If there is no query, return a None. 1507 If the query is malformed, raise an exception. 1508 1509 By default, this method assumes that LMS IDs are ints. 1510 Child backends may override this to implement their specific behavior. 1511 """ 1512 1513 return lms.model.query.parse_int_query(lms.model.groupsets.GroupSetQuery, text, check_email = False) 1514 1515 def parse_groupset_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.groupsets.GroupSetQuery]: 1516 """ Parse a list of group set queries. """ 1517 1518 queries = [] 1519 for text in texts: 1520 query = self.parse_groupset_query(text) 1521 if (query is not None): 1522 queries.append(query) 1523 1524 return queries 1525 1526 def parse_group_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.groups.GroupQuery, None]: 1527 """ 1528 Attempt to parse a group query from a string. 1529 If there is no query, return a None. 1530 If the query is malformed, raise an exception. 1531 1532 By default, this method assumes that LMS IDs are ints. 1533 Child backends may override this to implement their specific behavior. 1534 """ 1535 1536 return lms.model.query.parse_int_query(lms.model.groups.GroupQuery, text, check_email = False) 1537 1538 def parse_group_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.groups.GroupQuery]: 1539 """ Parse a list of group queries. """ 1540 1541 queries = [] 1542 for text in texts: 1543 query = self.parse_group_query(text) 1544 if (query is not None): 1545 queries.append(query) 1546 1547 return queries 1548 1549 def parse_user_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.users.UserQuery, None]: 1550 """ 1551 Attempt to parse a user query from a string. 1552 If there is no query, return a None. 1553 If the query is malformed, raise an exception. 1554 1555 By default, this method assumes that LMS IDs are ints. 1556 Child backends may override this to implement their specific behavior. 1557 """ 1558 1559 return lms.model.query.parse_int_query(lms.model.users.UserQuery, text, check_email = True) 1560 1561 def parse_user_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.users.UserQuery]: 1562 """ Parse a list of user queries. """ 1563 1564 queries = [] 1565 for text in texts: 1566 query = self.parse_user_query(text) 1567 if (query is not None): 1568 queries.append(query) 1569 1570 return queries 1571 1572 def resolve_assignment_query(self, 1573 course_id: str, 1574 assignment_query: lms.model.assignments.AssignmentQuery, 1575 **kwargs: typing.Any) -> lms.model.assignments.ResolvedAssignmentQuery: 1576 """ Resolve the assignment query or raise an exception. """ 1577 1578 # Shortcut already resolved queries. 1579 if (isinstance(assignment_query, lms.model.assignments.ResolvedAssignmentQuery)): 1580 return assignment_query 1581 1582 results = self.resolve_assignment_queries(course_id, [assignment_query], **kwargs) 1583 if (len(results) == 0): 1584 raise ValueError(f"Could not resolve assignment query: '{assignment_query}'.") 1585 1586 return results[0] 1587 1588 def resolve_assignment_queries(self, 1589 course_id: str, 1590 queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1591 **kwargs: typing.Any) -> typing.List[lms.model.assignments.ResolvedAssignmentQuery]: 1592 """ 1593 Resolve a list of assignment queries into a list of resolved assignment queries. 1594 See _resolve_queries(). 1595 """ 1596 1597 results = self._resolve_queries( 1598 queries, 1599 'assignment', 1600 self.courses_assignments_list(course_id, **kwargs), 1601 lms.model.assignments.ResolvedAssignmentQuery, 1602 **kwargs) 1603 1604 return typing.cast(typing.List[lms.model.assignments.ResolvedAssignmentQuery], results) 1605 1606 def resolve_course_query(self, 1607 query: lms.model.courses.CourseQuery, 1608 **kwargs: typing.Any) -> lms.model.courses.ResolvedCourseQuery: 1609 """ Resolve the course query or raise an exception. """ 1610 1611 # Shortcut already resolved queries. 1612 if (isinstance(query, lms.model.courses.ResolvedCourseQuery)): 1613 return query 1614 1615 results = self.resolve_course_queries([query], **kwargs) 1616 if (len(results) == 0): 1617 raise ValueError(f"Could not resolve course query: '{query}'.") 1618 1619 return results[0] 1620 1621 def resolve_course_queries(self, 1622 queries: typing.Collection[lms.model.courses.CourseQuery], 1623 **kwargs: typing.Any) -> typing.List[lms.model.courses.ResolvedCourseQuery]: 1624 """ 1625 Resolve a list of course queries into a list of resolved course queries. 1626 See _resolve_queries(). 1627 """ 1628 1629 results = self._resolve_queries( 1630 queries, 1631 'course', 1632 self.courses_list(**kwargs), 1633 lms.model.courses.ResolvedCourseQuery, 1634 **kwargs) 1635 1636 return typing.cast(typing.List[lms.model.courses.ResolvedCourseQuery], results) 1637 1638 def resolve_group_queries(self, 1639 course_id: str, 1640 groupset_id: str, 1641 queries: typing.Collection[lms.model.groups.GroupQuery], 1642 **kwargs: typing.Any) -> typing.List[lms.model.groups.ResolvedGroupQuery]: 1643 """ 1644 Resolve a list of group queries into a list of resolved group queries. 1645 See _resolve_queries(). 1646 """ 1647 1648 results = self._resolve_queries( 1649 queries, 1650 'group', 1651 self.courses_groups_list(course_id, groupset_id, **kwargs), 1652 lms.model.groups.ResolvedGroupQuery, 1653 **kwargs) 1654 1655 return typing.cast(typing.List[lms.model.groups.ResolvedGroupQuery], results) 1656 1657 def resolve_group_query(self, 1658 course_id: str, 1659 groupset_id: str, 1660 query: lms.model.groups.GroupQuery, 1661 **kwargs: typing.Any) -> lms.model.groups.ResolvedGroupQuery: 1662 """ Resolve the group query or raise an exception. """ 1663 1664 # Shortcut already resolved queries. 1665 if (isinstance(query, lms.model.groups.ResolvedGroupQuery)): 1666 return query 1667 1668 results = self.resolve_group_queries(course_id, groupset_id, [query], **kwargs) 1669 if (len(results) == 0): 1670 raise ValueError(f"Could not resolve group query: '{query}'.") 1671 1672 return results[0] 1673 1674 def resolve_groupset_queries(self, 1675 course_id: str, 1676 queries: typing.Collection[lms.model.groupsets.GroupSetQuery], 1677 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.ResolvedGroupSetQuery]: 1678 """ 1679 Resolve a list of group set queries into a list of resolved group set queries. 1680 See _resolve_queries(). 1681 """ 1682 1683 results = self._resolve_queries( 1684 queries, 1685 'group set', 1686 self.courses_groupsets_list(course_id, **kwargs), 1687 lms.model.groupsets.ResolvedGroupSetQuery, 1688 **kwargs) 1689 1690 return typing.cast(typing.List[lms.model.groupsets.ResolvedGroupSetQuery], results) 1691 1692 def resolve_groupset_query(self, 1693 course_id: str, 1694 groupset_query: lms.model.groupsets.GroupSetQuery, 1695 **kwargs: typing.Any) -> lms.model.groupsets.ResolvedGroupSetQuery: 1696 """ Resolve the group set query or raise an exception. """ 1697 1698 # Shortcut already resolved queries. 1699 if (isinstance(groupset_query, lms.model.groupsets.ResolvedGroupSetQuery)): 1700 return groupset_query 1701 1702 results = self.resolve_groupset_queries(course_id, [groupset_query], **kwargs) 1703 if (len(results) == 0): 1704 raise ValueError(f"Could not resolve group set query: '{groupset_query}'.") 1705 1706 return results[0] 1707 1708 def resolve_quiz_query(self, 1709 course_id: str, 1710 quiz_query: lms.model.assignments.AssignmentQuery, 1711 **kwargs: typing.Any) -> lms.model.assignments.ResolvedAssignmentQuery: 1712 """ Resolve the quiz query or raise an exception. """ 1713 1714 # Shortcut already resolved queries. 1715 if (isinstance(quiz_query, lms.model.assignments.ResolvedAssignmentQuery)): 1716 return quiz_query 1717 1718 results = self.resolve_quiz_queries(course_id, [quiz_query], **kwargs) 1719 if (len(results) == 0): 1720 raise ValueError(f"Could not resolve quiz query: '{quiz_query}'.") 1721 1722 return results[0] 1723 1724 def resolve_quiz_queries(self, 1725 course_id: str, 1726 queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1727 **kwargs: typing.Any) -> typing.List[lms.model.assignments.ResolvedAssignmentQuery]: 1728 """ 1729 Resolve a list of quiz queries into a list of resolved quiz queries. 1730 See _resolve_queries(). 1731 """ 1732 1733 results = self._resolve_queries( 1734 queries, 1735 'quiz', 1736 self.courses_quizzes_list(course_id, **kwargs), 1737 lms.model.assignments.ResolvedAssignmentQuery, 1738 **kwargs) 1739 1740 return typing.cast(typing.List[lms.model.assignments.ResolvedAssignmentQuery], results) 1741 1742 def resolve_user_queries(self, 1743 course_id: str, 1744 queries: typing.Collection[lms.model.users.UserQuery], 1745 only_students: bool = False, 1746 **kwargs: typing.Any) -> typing.List[lms.model.users.ResolvedUserQuery]: 1747 """ 1748 Resolve a list of user queries into a list of resolved user queries. 1749 See _resolve_queries(). 1750 """ 1751 1752 filter_func = None 1753 if (only_students): 1754 filter_func = lambda user: user.is_student() # pylint: disable=unnecessary-lambda-assignment 1755 1756 results = self._resolve_queries( 1757 queries, 1758 'user', 1759 self.courses_users_list(course_id, **kwargs), 1760 lms.model.users.ResolvedUserQuery, 1761 filter_func = filter_func, 1762 **kwargs) 1763 1764 return typing.cast(typing.List[lms.model.users.ResolvedUserQuery], results) 1765 1766 def _resolve_queries(self, 1767 queries: typing.Collection[lms.model.query.BaseQuery], 1768 label: str, 1769 items: typing.Collection, 1770 resolved_query_class: typing.Type, 1771 empty_all: bool = False, 1772 warn_on_miss: bool = False, 1773 filter_func: typing.Union[typing.Callable, None] = None, 1774 **kwargs: typing.Any) -> typing.List[lms.model.query.ResolvedBaseQuery]: 1775 """ 1776 Resolve a list of queries. 1777 The returned list may be shorter than the list of queries (if input queries are not matched). 1778 The queries will be deduplicated and sorted. 1779 1780 If |empty_all| is true and no queries are specified, then all items will be returned. 1781 1782 If |filter_func| is passed, then that function will be called with each raw item, 1783 and ones that return true will be kept. 1784 """ 1785 1786 if (filter_func is not None): 1787 items = list(filter(filter_func, items)) 1788 1789 if (empty_all and (len(queries) == 0)): 1790 return list(sorted({resolved_query_class(item) for item in items})) 1791 1792 matched_queries = [] # type: ignore[var-annotated] 1793 for query in queries: 1794 match = False 1795 for item in items: 1796 if (query.match(item)): 1797 matched_query = resolved_query_class(item) 1798 1799 if (match): 1800 raise ValueError( 1801 f"Ambiguous {label} query ('{query}')" 1802 f" matches multiple {label}s ['{matched_queries[-1]}', '{matched_query}'].") 1803 1804 matched_queries.append(matched_query) 1805 match = True 1806 1807 if ((not match) and warn_on_miss): 1808 _logger.warning("Could not resolve %s query '%s'.", label, query) 1809 1810 return list(sorted(set(matched_queries))) 1811 1812 def _resolve_group_memberships(self, 1813 course_id: str, 1814 groupset_id: str, 1815 memberships: typing.Collection[lms.model.groups.GroupMembership], 1816 **kwargs: typing.Any) -> typing.Tuple[ 1817 typing.Dict[lms.model.groups.ResolvedGroupQuery, typing.List[lms.model.users.ResolvedUserQuery]], 1818 typing.Dict[str, typing.List[lms.model.users.ResolvedUserQuery]], 1819 typing.List[lms.model.groups.ResolvedGroupQuery]]: 1820 """ 1821 Resolve a list of group memberships. 1822 This method will resolved each query and split up the memberships by the appropriate group. 1823 If a group does not exist, the memberships will be split by apparent group name. 1824 1825 Returns: 1826 - Memberships in Found Groups (keyed by resolved group query) 1827 - Memberships in Missing Groups (keyed by apparent group name) 1828 - Groups not involved in any of the returned memberships. 1829 1830 The returned dicts will be the found groups (keyed by resolved query) and then the missing groups (keyed by apparent group name). 1831 """ 1832 1833 found_group_memberships: typing.Dict[lms.model.groups.ResolvedGroupQuery, typing.List[lms.model.users.ResolvedUserQuery]] = {} 1834 missing_group_memberships: typing.Dict[str, typing.List[lms.model.users.ResolvedUserQuery]] = {} 1835 1836 users = self.courses_users_list(course_id, **kwargs) 1837 resolved_user_queries = [user.to_query() for user in sorted(users)] 1838 1839 groups = self.courses_groups_list(course_id, groupset_id, **kwargs) 1840 resolved_group_queries = [group.to_query() for group in sorted(groups)] 1841 1842 for (i, membership) in enumerate(memberships): 1843 # Resolve user. 1844 1845 resolved_user_query = None 1846 for possible_user_query in resolved_user_queries: 1847 if (membership.user.match(possible_user_query)): 1848 resolved_user_query = possible_user_query 1849 break 1850 1851 if (resolved_user_query is None): 1852 _logger.warning("Could not resolve user '%s' for membership entry at index %d.", membership.user, i) 1853 continue 1854 1855 # Resolve group. 1856 1857 resolved_group_query = None 1858 for possible_group_query in resolved_group_queries: 1859 if (membership.group.match(possible_group_query)): 1860 resolved_group_query = possible_group_query 1861 break 1862 1863 # Add to the correct collection. 1864 1865 if (resolved_group_query is None): 1866 if ((membership.group.name is None) or (len(membership.group.name) == 0)): 1867 _logger.warning(("Membership entry at index %d has a group with no name." 1868 + " Ensure that non-existent groups all have names."), i) 1869 continue 1870 1871 if (membership.group.name not in missing_group_memberships): 1872 missing_group_memberships[membership.group.name] = [] 1873 1874 missing_group_memberships[membership.group.name].append(resolved_user_query) 1875 else: 1876 if (resolved_group_query not in found_group_memberships): 1877 found_group_memberships[resolved_group_query] = [] 1878 1879 found_group_memberships[resolved_group_query].append(resolved_user_query) 1880 1881 unused_groups = sorted(list(set(resolved_group_queries) - set(found_group_memberships.keys()))) 1882 1883 return (found_group_memberships, missing_group_memberships, unused_groups)
API backends provide a unified interface to an LMS.
Note that instead of using an abstract class, methods will raise a NotImplementedError by default. This will allow child backends to fill in as much functionality as they can, while still leaving gaps where they are incomplete or impossible.
33 def __init__(self, 34 config: lms.model.config.Config, 35 testing: typing.Union[bool, str] = False, 36 **kwargs: typing.Any) -> None: 37 self.config: lms.model.config.Config = config 38 """ The configuration options for this backend. """ 39 40 assert(self.config.backend_type is not None) 41 42 self.backend_type: lms.model.constants.BackendType = self.config.backend_type 43 """ 44 The backend type of this server. 45 This is set in config and compied for type checking. 46 """ 47 48 assert((self.config.server is not None) and (len(self.config.server) > 0)) 49 50 self.server: str = self.config.server 51 """ 52 The server to connect to. 53 This is set in config and compied for type checking. 54 """ 55 56 parsed_testing = edq.util.parse.soft_boolean(self.config.testing) 57 if (APIBackend._testing_override is not None): 58 parsed_testing = APIBackend._testing_override 59 60 if (parsed_testing is None): 61 parsed_testing = False 62 63 self.testing: bool = parsed_testing 64 """ True if the backend is being used for a test. """
The backend type of this server. This is set in config and compied for type checking.
68 def is_testing(self) -> bool: 69 """ Check if this backend is in testing mode. """ 70 71 return self.testing
Check if this backend is in testing mode.
73 def reset_connection(self) -> None: 74 """ 75 Inform the backend that their connection has been reset. 76 Note that this is not on the individual HTTP connection level, but instead on the server level. 77 For example, this is called when a testing server is reset (e.g., in a server runner). 78 """
Inform the backend that their connection has been reset. Note that this is not on the individual HTTP connection level, but instead on the server level. For example, this is called when a testing server is reset (e.g., in a server runner).
80 def get_standard_headers(self, write: bool = False) -> typing.Dict[str, str]: 81 """ 82 Get standard headers for this backend. 83 Children should take care to set the write header when performing a write operation. 84 """ 85 86 return { 87 lms.model.constants.HEADER_KEY_BACKEND: self.backend_type.value, 88 lms.model.constants.HEADER_KEY_WRITE: str(write).lower(), 89 }
Get standard headers for this backend. Children should take care to set the write header when performing a write operation.
91 def not_found(self, operation: str, identifiers: typing.Dict[str, typing.Any]) -> None: 92 """ 93 Called when the backend was unable to find some object. 94 This will only be called when a requested object is not found, 95 e.g., a user requested by ID is not found. 96 This is not called when a list naturally returns zero results, 97 or when a query does not match any items. 98 """ 99 100 _logger.warning("Object not found during operation: '%s'. Identifiers: %s.", operation, identifiers)
Called when the backend was unable to find some object. This will only be called when a requested object is not found, e.g., a user requested by ID is not found. This is not called when a list naturally returns zero results, or when a query does not match any items.
104 def courses_get(self, 105 course_queries: typing.Collection[lms.model.courses.CourseQuery], 106 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 107 """ 108 Get the specified courses associated with the given course. 109 """ 110 111 if (len(course_queries) == 0): 112 return [] 113 114 courses = self.courses_list(**kwargs) 115 116 matches = [] 117 for course in sorted(courses): 118 for query in course_queries: 119 if (query.match(course)): 120 matches.append(course) 121 break 122 123 return sorted(matches)
Get the specified courses associated with the given course.
125 def courses_fetch(self, 126 course_id: str, 127 **kwargs: typing.Any) -> typing.Union[lms.model.courses.Course, None]: 128 """ 129 Fetch a single course associated with the context user. 130 Return None if no matching course is found. 131 132 By default, this will just do a list and choose the relevant record. 133 Specific backends may override this if there are performance concerns. 134 """ 135 136 courses = self.courses_list(**kwargs) 137 for course in courses: 138 if (course.id == course_id): 139 return course 140 141 return None
Fetch a single course associated with the context user. Return None if no matching course is found.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
143 def courses_list(self, 144 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 145 """ 146 List the courses associated with the context user. 147 """ 148 149 raise NotImplementedError('courses_list')
List the courses associated with the context user.
151 def courses_assignments_get(self, 152 course_query: lms.model.courses.CourseQuery, 153 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 154 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 155 """ 156 Get the specified assignments associated with the given course. 157 """ 158 159 if (len(assignment_queries) == 0): 160 return [] 161 162 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 163 164 assignments = sorted(self.courses_assignments_list(resolved_course_query.get_id(), **kwargs)) 165 assignment_queries = sorted(assignment_queries) 166 167 matches = [] 168 for assignment in assignments: 169 for query in assignment_queries: 170 if (query.match(assignment)): 171 matches.append(assignment) 172 break 173 174 return matches
Get the specified assignments associated with the given course.
176 def courses_assignments_fetch(self, 177 course_id: str, 178 assignment_id: str, 179 **kwargs: typing.Any) -> typing.Union[lms.model.assignments.Assignment, None]: 180 """ 181 Fetch a single assignment associated with the given course. 182 Return None if no matching assignment is found. 183 184 By default, this will just do a list and choose the relevant record. 185 Specific backends may override this if there are performance concerns. 186 """ 187 188 assignments = self.courses_assignments_list(course_id, **kwargs) 189 for assignment in sorted(assignments): 190 if (assignment.id == assignment_id): 191 return assignment 192 193 return None
Fetch a single assignment associated with the given course. Return None if no matching assignment is found.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
195 def courses_assignments_list(self, 196 course_id: str, 197 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 198 """ 199 List the assignments associated with the given course. 200 """ 201 202 raise NotImplementedError('courses_assignments_list')
List the assignments associated with the given course.
204 def courses_assignments_resolve_and_list(self, 205 course_query: lms.model.courses.CourseQuery, 206 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 207 """ 208 List the assignments associated with the given course. 209 """ 210 211 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 212 return sorted(self.courses_assignments_list(resolved_course_query.get_id(), **kwargs))
List the assignments associated with the given course.
214 def courses_assignments_scores_get(self, 215 course_query: lms.model.courses.CourseQuery, 216 assignment_query: lms.model.assignments.AssignmentQuery, 217 user_queries: typing.Collection[lms.model.users.UserQuery], 218 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 219 """ 220 Get the scores associated with the given assignment query and user queries. 221 """ 222 223 if (len(user_queries) == 0): 224 return [] 225 226 scores = self.courses_assignments_scores_resolve_and_list(course_query, assignment_query, **kwargs) 227 228 matches = [] 229 for score in scores: 230 for user_query in user_queries: 231 if (user_query.match(score.user)): 232 matches.append(score) 233 234 return sorted(matches)
Get the scores associated with the given assignment query and user queries.
236 def courses_assignments_scores_fetch(self, 237 course_id: str, 238 assignment_id: str, 239 user_id: str, 240 **kwargs: typing.Any) -> typing.Union[lms.model.scores.AssignmentScore, None]: 241 """ 242 Fetch the score associated with the given assignment and user. 243 244 By default, this will just do a list and choose the relevant record. 245 Specific backends may override this if there are performance concerns. 246 """ 247 248 scores = self.courses_assignments_scores_list(course_id, assignment_id, **kwargs) 249 for score in scores: 250 if ((score.user is not None) and (score.user.id == user_id)): 251 return score 252 253 return None
Fetch the score associated with the given assignment and user.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
255 def courses_assignments_scores_list(self, 256 course_id: str, 257 assignment_id: str, 258 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 259 """ 260 List the scores associated with the given assignment. 261 """ 262 263 raise NotImplementedError('courses_assignments_scores_list')
List the scores associated with the given assignment.
265 def courses_assignments_scores_resolve_and_list(self, 266 course_query: lms.model.courses.CourseQuery, 267 assignment_query: lms.model.assignments.AssignmentQuery, 268 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 269 """ 270 List the scores associated with the given assignment query. 271 In addition to resolving the assignment query, 272 users will also be resolved into their full version 273 (instead of the reduced version usually returned with scores). 274 """ 275 276 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 277 278 # Resolve the assignment query. 279 matched_assignments = self.courses_assignments_get(resolved_course_query, [assignment_query], **kwargs) 280 if (len(matched_assignments) == 0): 281 return [] 282 283 target_assignment = matched_assignments[0] 284 285 # List the scores. 286 scores = self.courses_assignments_scores_list(resolved_course_query.get_id(), target_assignment.id, **kwargs) 287 if (len(scores) == 0): 288 return [] 289 290 # Resolve the scores' queries. 291 292 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 293 users_map = {user.id: user for user in users} 294 295 for score in scores: 296 score.assignment = target_assignment.to_query() 297 298 if ((score.user is not None) and (score.user.id in users_map)): 299 score.user = users_map[score.user.id].to_query() 300 301 return sorted(scores)
List the scores associated with the given assignment query. In addition to resolving the assignment query, users will also be resolved into their full version (instead of the reduced version usually returned with scores).
303 def courses_assignments_scores_resolve_and_upload(self, 304 course_query: lms.model.courses.CourseQuery, 305 assignment_query: lms.model.assignments.AssignmentQuery, 306 scores: typing.Dict[lms.model.users.UserQuery, lms.model.scores.ScoreFragment], 307 **kwargs: typing.Any) -> int: 308 """ 309 Resolve queries and upload assignment scores (indexed by user query). 310 A None score (ScoreFragment.score) indicates that the score should be cleared. 311 Return the number of scores sent to the LMS. 312 """ 313 314 if (len(scores) == 0): 315 return 0 316 317 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 318 resolved_assignment_query = self.resolve_assignment_query(resolved_course_query.get_id(), assignment_query, **kwargs) 319 320 resolved_users = self.resolve_user_queries(resolved_course_query.get_id(), list(scores.keys()), warn_on_miss = True) 321 resolved_scores: typing.Dict[str, lms.model.scores.ScoreFragment] = {} 322 323 for (user, score) in scores.items(): 324 for resolved_user in resolved_users: 325 if (user.match(resolved_user)): 326 resolved_scores[resolved_user.get_id()] = score 327 continue 328 329 if (len(resolved_scores) == 0): 330 return 0 331 332 return self.courses_assignments_scores_upload( 333 resolved_course_query.get_id(), 334 resolved_assignment_query.get_id(), 335 resolved_scores, 336 **kwargs)
Resolve queries and upload assignment scores (indexed by user query). A None score (ScoreFragment.score) indicates that the score should be cleared. Return the number of scores sent to the LMS.
338 def courses_assignments_scores_upload(self, 339 course_id: str, 340 assignment_id: str, 341 scores: typing.Dict[str, lms.model.scores.ScoreFragment], 342 **kwargs: typing.Any) -> int: 343 """ 344 Upload assignment scores (indexed by user id). 345 A None score (ScoreFragment.score) indicates that the score should be cleared. 346 Return the number of scores sent to the LMS. 347 """ 348 349 raise NotImplementedError('courses_assignments_scores_upload')
Upload assignment scores (indexed by user id). A None score (ScoreFragment.score) indicates that the score should be cleared. Return the number of scores sent to the LMS.
351 def courses_gradebook_get(self, 352 course_query: lms.model.courses.CourseQuery, 353 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 354 user_queries: typing.Collection[lms.model.users.UserQuery], 355 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 356 """ 357 Get a gradebook with the specified users and assignments. 358 Specifying no users/assignments is the same as requesting all of them. 359 """ 360 361 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 362 363 resolved_assignment_queries = self.resolve_assignment_queries(resolved_course_query.get_id(), assignment_queries, empty_all = True, **kwargs) 364 assignment_ids = [query.get_id() for query in resolved_assignment_queries] 365 366 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, 367 empty_all = True, only_students = True, **kwargs) 368 user_ids = [query.get_id() for query in resolved_user_queries] 369 370 gradebook = self.courses_gradebook_fetch(resolved_course_query.get_id(), assignment_ids, user_ids, **kwargs) 371 372 # Resolve the gradebook's queries (so it can show names/emails instead of just IDs). 373 gradebook.update_queries(resolved_assignment_queries, resolved_user_queries) 374 375 return gradebook
Get a gradebook with the specified users and assignments. Specifying no users/assignments is the same as requesting all of them.
377 def courses_gradebook_fetch(self, 378 course_id: str, 379 assignment_ids: typing.Collection[str], 380 user_ids: typing.Collection[str], 381 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 382 """ 383 Get a gradebook with the specified users and assignments. 384 If either the assignments or users is empty, an empty gradebook will be returned. 385 """ 386 387 raise NotImplementedError('courses_gradebook_fetch')
Get a gradebook with the specified users and assignments. If either the assignments or users is empty, an empty gradebook will be returned.
389 def courses_gradebook_list(self, 390 course_id: str, 391 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 392 """ 393 List the full gradebook associated with this course. 394 """ 395 396 return self.courses_gradebook_get(lms.model.courses.CourseQuery(id = course_id), [], [], **kwargs)
List the full gradebook associated with this course.
398 def courses_gradebook_resolve_and_list(self, 399 course_query: lms.model.courses.CourseQuery, 400 **kwargs: typing.Any) -> lms.model.scores.Gradebook: 401 """ 402 List the full gradebook associated with this course. 403 """ 404 405 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 406 return self.courses_gradebook_list(resolved_course_query.get_id(), **kwargs)
List the full gradebook associated with this course.
408 def courses_gradebook_resolve_and_upload(self, 409 course_query: lms.model.courses.CourseQuery, 410 gradebook: lms.model.scores.Gradebook, 411 **kwargs: typing.Any) -> int: 412 """ 413 Resolve queries and upload a gradebook. 414 Missing scores in the gradebook are skipped, 415 a None score (ScoreFragment.score) indicates that the score should be cleared. 416 Return the number of scores sent to the LMS. 417 """ 418 419 if (len(gradebook) == 0): 420 return 0 421 422 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 423 424 assignments = self.courses_assignments_list(resolved_course_query.get_id(), **kwargs) 425 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 426 427 resolved_assignment_queries = [assignment.to_query() for assignment in assignments] 428 resolved_user_queries = [user.to_query() for user in users] 429 430 gradebook.update_queries(resolved_assignment_queries, resolved_user_queries) 431 432 return self.courses_gradebook_upload( 433 resolved_course_query.get_id(), 434 gradebook, 435 **kwargs)
Resolve queries and upload a gradebook. Missing scores in the gradebook are skipped, a None score (ScoreFragment.score) indicates that the score should be cleared. Return the number of scores sent to the LMS.
437 def courses_gradebook_upload(self, 438 course_id: str, 439 gradebook: lms.model.scores.Gradebook, 440 **kwargs: typing.Any) -> int: 441 """ 442 Upload a gradebook. 443 All queries in the gradebook must be resolved (or at least have an ID). 444 Missing scores in the gradebook are skipped, 445 a None score (ScoreFragment.score) indicates that the score should be cleared. 446 Return the number of scores sent to the LMS. 447 """ 448 449 assignment_scores = gradebook.get_scores_by_assignment() 450 451 count = 0 452 for (assignment, user_scores) in assignment_scores.items(): 453 if (assignment.id is None): 454 raise ValueError(f"Assignment query for gradebook upload ({assignment}) does not have an ID.") 455 456 upload_scores = {} 457 for (user, score) in user_scores.items(): 458 if (user.id is None): 459 raise ValueError(f"User query for gradebook upload ({user}) does not have an ID.") 460 461 upload_scores[user.id] = score.to_fragment() 462 463 count += self.courses_assignments_scores_upload(course_id, assignment.id, upload_scores, **kwargs) 464 465 return count
Upload a gradebook. All queries in the gradebook must be resolved (or at least have an ID). Missing scores in the gradebook are skipped, a None score (ScoreFragment.score) indicates that the score should be cleared. Return the number of scores sent to the LMS.
467 def courses_groupsets_create(self, 468 course_id: str, 469 name: str, 470 **kwargs: typing.Any) -> lms.model.groupsets.GroupSet: 471 """ 472 Create a group set. 473 """ 474 475 raise NotImplementedError('courses_groupsets_create')
Create a group set.
477 def courses_groupsets_resolve_and_create(self, 478 course_query: lms.model.courses.CourseQuery, 479 name: str, 480 **kwargs: typing.Any) -> lms.model.groupsets.GroupSet: 481 """ 482 Resolve references and create a group set. 483 """ 484 485 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 486 return self.courses_groupsets_create(resolved_course_query.get_id(), name, **kwargs)
Resolve references and create a group set.
488 def courses_groupsets_delete(self, 489 course_id: str, 490 groupset_id: str, 491 **kwargs: typing.Any) -> bool: 492 """ 493 Delete a group set. 494 """ 495 496 raise NotImplementedError('courses_groupsets_delete')
Delete a group set.
498 def courses_groupsets_resolve_and_delete(self, 499 course_query: lms.model.courses.CourseQuery, 500 groupset_query: lms.model.groupsets.GroupSetQuery, 501 **kwargs: typing.Any) -> bool: 502 """ 503 Resolve references and create a group set. 504 """ 505 506 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 507 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 508 return self.courses_groupsets_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs)
Resolve references and create a group set.
510 def courses_groupsets_get(self, 511 course_query: lms.model.courses.CourseQuery, 512 groupset_queries: typing.Collection[lms.model.groupsets.GroupSetQuery], 513 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 514 """ 515 Get the specified group sets associated with the given course. 516 """ 517 518 if (len(groupset_queries) == 0): 519 return [] 520 521 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 522 groupset_queries = sorted(groupset_queries) 523 groupsets = sorted(self.courses_groupsets_list(resolved_course_query.get_id(), **kwargs)) 524 525 matches = [] 526 for groupset in groupsets: 527 for query in groupset_queries: 528 if (query.match(groupset)): 529 matches.append(groupset) 530 break 531 532 return matches
Get the specified group sets associated with the given course.
534 def courses_groupsets_fetch(self, 535 course_id: str, 536 groupset_id: str, 537 **kwargs: typing.Any) -> typing.Union[lms.model.groupsets.GroupSet, None]: 538 """ 539 Fetch a single group set associated with the given course. 540 Return None if no matching group set is found. 541 542 By default, this will just do a list and choose the relevant record. 543 Specific backends may override this if there are performance concerns. 544 """ 545 546 groupsets = self.courses_groupsets_list(course_id, **kwargs) 547 for groupset in groupsets: 548 if (groupset.id == groupset_id): 549 return groupset 550 551 return None
Fetch a single group set associated with the given course. Return None if no matching group set is found.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
553 def courses_groupsets_list(self, 554 course_id: str, 555 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 556 """ 557 List the group sets associated with the given course. 558 """ 559 560 raise NotImplementedError('courses_groupsets_list')
List the group sets associated with the given course.
562 def courses_groupsets_resolve_and_list(self, 563 course_query: lms.model.courses.CourseQuery, 564 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSet]: 565 """ 566 List the group sets associated with the given course. 567 """ 568 569 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 570 return sorted(self.courses_groupsets_list(resolved_course_query.get_id(), **kwargs))
List the group sets associated with the given course.
572 def courses_groupsets_memberships_resolve_and_add(self, 573 course_query: lms.model.courses.CourseQuery, 574 groupset_query: lms.model.groupsets.GroupSetQuery, 575 memberships: typing.Collection[lms.model.groups.GroupMembership], 576 **kwargs: typing.Any) -> typing.Tuple[ 577 typing.List[lms.model.groups.Group], 578 typing.Dict[lms.model.groups.ResolvedGroupQuery, int] 579 ]: 580 """ 581 Resolve queries and add the specified users to the specified groups. 582 This may create groups. 583 584 Return: 585 - Created Groups 586 - Group Addition Counts 587 """ 588 589 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 590 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 591 592 found_group_memberships, missing_group_memberships, _ = self._resolve_group_memberships( 593 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 594 595 # Create missing groups. 596 created_groups = [] 597 for name in sorted(missing_group_memberships.keys()): 598 group = self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 599 created_groups.append(group) 600 601 # Merge in new group with existing structure. 602 query = group.to_query() 603 if (query not in found_group_memberships): 604 found_group_memberships[query] = [] 605 606 found_group_memberships[query] += missing_group_memberships[name] 607 608 # Add memberships. 609 counts = {} 610 for resolved_group_query in sorted(found_group_memberships.keys()): 611 resolved_user_queries = found_group_memberships[resolved_group_query] 612 613 count = self.courses_groups_memberships_resolve_and_add( 614 resolved_course_query, resolved_groupset_query, resolved_group_query, 615 resolved_user_queries, 616 **kwargs) 617 618 counts[resolved_group_query] = count 619 620 return (created_groups, counts)
Resolve queries and add the specified users to the specified groups. This may create groups.
Return:
- Created Groups
- Group Addition Counts
622 def courses_groupsets_memberships_resolve_and_set(self, 623 course_query: lms.model.courses.CourseQuery, 624 groupset_query: lms.model.groupsets.GroupSetQuery, 625 memberships: typing.Collection[lms.model.groups.GroupMembership], 626 **kwargs: typing.Any) -> typing.Tuple[ 627 typing.List[lms.model.groups.Group], 628 typing.List[lms.model.groups.ResolvedGroupQuery], 629 typing.Dict[lms.model.groups.ResolvedGroupQuery, int], 630 typing.Dict[lms.model.groups.ResolvedGroupQuery, int], 631 ]: 632 """ 633 Resolve queries and set the specified group memberships. 634 This may create and delete groups. 635 636 Return: 637 - Created Groups 638 - Deleted Groups 639 - Group Addition Counts 640 - Group Subtraction Counts 641 """ 642 643 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 644 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 645 646 found_group_memberships, missing_group_memberships, unused_groups = self._resolve_group_memberships( 647 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 648 649 # Delete unused groups. 650 deleted_groups = [] 651 for group_query in sorted(unused_groups): 652 result = self.courses_groups_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query.get_id(), **kwargs) 653 if (result): 654 deleted_groups.append(group_query) 655 656 # Create missing groups. 657 created_groups = [] 658 for name in sorted(missing_group_memberships.keys()): 659 group = self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs) 660 created_groups.append(group) 661 662 # Merge in new group with existing structure. 663 query = group.to_query() 664 if (query not in found_group_memberships): 665 found_group_memberships[query] = [] 666 667 found_group_memberships[query] += missing_group_memberships[name] 668 669 # Set memberships. 670 add_counts = {} 671 sub_counts = {} 672 for resolved_group_query in sorted(found_group_memberships.keys()): 673 resolved_user_queries = found_group_memberships[resolved_group_query] 674 675 (add_count, sub_count, deleted) = self.courses_groups_memberships_resolve_and_set( 676 resolved_course_query, resolved_groupset_query, resolved_group_query, 677 resolved_user_queries, 678 delete_empty = True, 679 **kwargs) 680 681 if (deleted): 682 deleted_groups.append(resolved_group_query) 683 684 add_counts[resolved_group_query] = add_count 685 sub_counts[resolved_group_query] = sub_count 686 687 return (created_groups, deleted_groups, add_counts, sub_counts)
Resolve queries and set the specified group memberships. This may create and delete groups.
Return:
- Created Groups
- Deleted Groups
- Group Addition Counts
- Group Subtraction Counts
689 def courses_groupsets_memberships_resolve_and_subtract(self, 690 course_query: lms.model.courses.CourseQuery, 691 groupset_query: lms.model.groupsets.GroupSetQuery, 692 memberships: typing.Collection[lms.model.groups.GroupMembership], 693 **kwargs: typing.Any) -> typing.Dict[lms.model.groups.ResolvedGroupQuery, int]: 694 """ 695 Resolve queries and subtract the specified users to the specified groups. 696 This will not delete any groups. 697 698 Return: 699 - Group Subtraction Counts 700 """ 701 702 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 703 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 704 705 found_group_memberships, missing_group_memberships, _ = self._resolve_group_memberships( 706 resolved_course_query.get_id(), resolved_groupset_query.get_id(), memberships, **kwargs) 707 708 # Warn about missing groups. 709 for name in sorted(missing_group_memberships.keys()): 710 _logger.warning("Group does not exist: '%s'.", name) 711 712 # Subtract memberships. 713 counts = {} 714 for resolved_group_query in sorted(found_group_memberships.keys()): 715 resolved_user_queries = found_group_memberships[resolved_group_query] 716 717 (count, _) = self.courses_groups_memberships_resolve_and_subtract( 718 resolved_course_query, resolved_groupset_query, resolved_group_query, 719 resolved_user_queries, 720 delete_empty = False, 721 **kwargs) 722 723 counts[resolved_group_query] = count 724 725 return counts
Resolve queries and subtract the specified users to the specified groups. This will not delete any groups.
Return:
- Group Subtraction Counts
727 def courses_groupsets_memberships_list(self, 728 course_id: str, 729 groupset_id: str, 730 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 731 """ 732 List the membership of the group sets associated with the given course. 733 """ 734 735 raise NotImplementedError('courses_groupsets_memberships_list')
List the membership of the group sets associated with the given course.
737 def courses_groupsets_memberships_resolve_and_list(self, 738 course_query: lms.model.courses.CourseQuery, 739 groupset_query: lms.model.groupsets.GroupSetQuery, 740 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 741 """ 742 List the membership of the group sets associated with the given course. 743 """ 744 745 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 746 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 747 748 memberships = self.courses_groupsets_memberships_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 749 750 # Resolve memberships. 751 752 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 753 groups = self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 754 755 users_map = {user.id: user.to_query() for user in users} 756 groups_map = {group.id: group.to_query() for group in groups} 757 758 for membership in memberships: 759 membership.update_queries(resolved_groupset_query, users = users_map, groups = groups_map) 760 761 return sorted(memberships)
List the membership of the group sets associated with the given course.
763 def courses_groups_create(self, 764 course_id: str, 765 groupset_id: str, 766 name: str, 767 **kwargs: typing.Any) -> lms.model.groups.Group: 768 """ 769 Create a group. 770 """ 771 772 raise NotImplementedError('courses_groups_create')
Create a group.
774 def courses_groups_resolve_and_create(self, 775 course_query: lms.model.courses.CourseQuery, 776 groupset_query: lms.model.groupsets.GroupSetQuery, 777 name: str, 778 **kwargs: typing.Any) -> lms.model.groups.Group: 779 """ 780 Resolve references and create a group. 781 """ 782 783 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 784 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 785 return self.courses_groups_create(resolved_course_query.get_id(), resolved_groupset_query.get_id(), name, **kwargs)
Resolve references and create a group.
787 def courses_groups_delete(self, 788 course_id: str, 789 groupset_id: str, 790 group_id: str, 791 **kwargs: typing.Any) -> bool: 792 """ 793 Delete a group. 794 """ 795 796 raise NotImplementedError('courses_groups_delete')
Delete a group.
798 def courses_groups_resolve_and_delete(self, 799 course_query: lms.model.courses.CourseQuery, 800 groupset_query: lms.model.groupsets.GroupSetQuery, 801 group_query: lms.model.groups.GroupQuery, 802 **kwargs: typing.Any) -> bool: 803 """ 804 Resolve references and create a group. 805 """ 806 807 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 808 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 809 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 810 return self.courses_groups_delete(resolved_course_query.get_id(), resolved_groupset_query.get_id(), resolved_group_query.get_id(), **kwargs)
Resolve references and create a group.
812 def courses_groups_get(self, 813 course_query: lms.model.courses.CourseQuery, 814 groupset_query: lms.model.groupsets.GroupSetQuery, 815 group_queries: typing.Collection[lms.model.groups.GroupQuery], 816 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 817 """ 818 Get the specified groups associated with the given course. 819 """ 820 821 if (len(group_queries) == 0): 822 return [] 823 824 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 825 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 826 groups = self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs) 827 828 group_queries = sorted(group_queries) 829 groups = sorted(groups) 830 831 matches = [] 832 for group in groups: 833 for query in group_queries: 834 if (query.match(group)): 835 matches.append(group) 836 break 837 838 return matches
Get the specified groups associated with the given course.
840 def courses_groups_fetch(self, 841 course_id: str, 842 groupset_id: str, 843 group_id: str, 844 **kwargs: typing.Any) -> typing.Union[lms.model.groups.Group, None]: 845 """ 846 Fetch a single group associated with the given course. 847 Return None if no matching group is found. 848 849 By default, this will just do a list and choose the relevant record. 850 Specific backends may override this if there are performance concerns. 851 """ 852 853 groups = self.courses_groups_list(course_id, groupset_id, **kwargs) 854 for group in groups: 855 if (group.id == group_id): 856 return group 857 858 return None
Fetch a single group associated with the given course. Return None if no matching group is found.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
860 def courses_groups_list(self, 861 course_id: str, 862 groupset_id: str, 863 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 864 """ 865 List the groups associated with the given course. 866 """ 867 868 raise NotImplementedError('courses_groups_list')
List the groups associated with the given course.
870 def courses_groups_resolve_and_list(self, 871 course_query: lms.model.courses.CourseQuery, 872 groupset_query: lms.model.groupsets.GroupSetQuery, 873 **kwargs: typing.Any) -> typing.List[lms.model.groups.Group]: 874 """ 875 List the groups associated with the given course. 876 """ 877 878 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 879 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 880 return self.courses_groups_list(resolved_course_query.get_id(), resolved_groupset_query.get_id(), **kwargs)
List the groups associated with the given course.
882 def courses_groups_memberships_add(self, 883 course_id: str, 884 groupset_id: str, 885 group_id: str, 886 user_ids: typing.Collection[str], 887 **kwargs: typing.Any) -> int: 888 """ 889 Add the specified users to the group. 890 """ 891 892 raise NotImplementedError('courses_groups_memberships_add')
Add the specified users to the group.
894 def courses_groups_memberships_resolve_and_add(self, 895 course_query: lms.model.courses.CourseQuery, 896 groupset_query: lms.model.groupsets.GroupSetQuery, 897 group_query: lms.model.groups.GroupQuery, 898 user_queries: typing.Collection[lms.model.users.UserQuery], 899 **kwargs: typing.Any) -> int: 900 """ 901 Resolve queries and add the specified users to the group. 902 """ 903 904 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 905 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 906 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 907 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 908 909 # Get users already in this group. 910 group_memberships = self.courses_groups_memberships_list( 911 resolved_course_query.get_id(), 912 resolved_groupset_query.get_id(), 913 resolved_group_query.get_id(), 914 **kwargs) 915 916 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 917 918 # Filter out users already in the group. 919 user_ids = [] 920 for query in sorted(resolved_user_queries): 921 if (query.get_id() in group_user_ids): 922 _logger.warning("User '%s' already in group '%s'.", query, resolved_group_query) 923 continue 924 925 user_ids.append(query.get_id()) 926 927 if (len(user_ids) == 0): 928 return 0 929 930 return self.courses_groups_memberships_add( 931 resolved_course_query.get_id(), 932 resolved_groupset_query.get_id(), 933 resolved_group_query.get_id(), 934 user_ids, 935 **kwargs)
Resolve queries and add the specified users to the group.
937 def courses_groups_memberships_list(self, 938 course_id: str, 939 groupset_id: str, 940 group_id: str, 941 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 942 """ 943 List the membership of the group associated with the given group set. 944 """ 945 946 raise NotImplementedError('courses_groups_memberships_list')
List the membership of the group associated with the given group set.
948 def courses_groups_memberships_resolve_and_list(self, 949 course_query: lms.model.courses.CourseQuery, 950 groupset_query: lms.model.groupsets.GroupSetQuery, 951 group_query: lms.model.groups.GroupQuery, 952 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.GroupSetMembership]: 953 """ 954 List the membership of the group associated with the given group set. 955 """ 956 957 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 958 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 959 960 groups = self.courses_groups_get(resolved_course_query, resolved_groupset_query, [group_query], **kwargs) 961 if (len(groups) == 0): 962 raise ValueError(f"Unable to find group: '{group_query}'.") 963 964 group = groups[0] 965 966 memberships = self.courses_groups_memberships_list( 967 resolved_course_query.get_id(), 968 resolved_groupset_query.get_id(), 969 group.id, 970 **kwargs) 971 972 # Resolve memberships. 973 974 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 975 users_map = {user.id: user.to_query() for user in users} 976 977 groups_map = {group.id: group.to_query()} 978 979 for membership in memberships: 980 membership.update_queries(resolved_groupset_query, users = users_map, groups = groups_map) 981 982 return sorted(memberships)
List the membership of the group associated with the given group set.
984 def courses_groups_memberships_resolve_and_set(self, 985 course_query: lms.model.courses.CourseQuery, 986 groupset_query: lms.model.groupsets.GroupSetQuery, 987 group_query: lms.model.groups.GroupQuery, 988 user_queries: typing.Collection[lms.model.users.UserQuery], 989 delete_empty: bool = False, 990 **kwargs: typing.Any) -> typing.Tuple[int, int, bool]: 991 """ 992 Resolve queries and set the specified users for the group. 993 This method can both add and subtract users from the group. 994 995 Returns: 996 - The count of users added. 997 - The count of users subtracted. 998 - If this group was deleted. 999 """ 1000 1001 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1002 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 1003 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 1004 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 1005 1006 # Get users already in this group. 1007 group_memberships = self.courses_groups_memberships_list( 1008 resolved_course_query.get_id(), 1009 resolved_groupset_query.get_id(), 1010 resolved_group_query.get_id(), 1011 **kwargs) 1012 1013 group_user_queries = {membership.user for membership in group_memberships if membership.user is not None} 1014 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 1015 query_user_ids = {resolved_user_query.get_id() for resolved_user_query in resolved_user_queries} 1016 1017 # Collect users that need to be added. 1018 add_user_ids = [] 1019 for query_user_id in query_user_ids: 1020 if (query_user_id not in group_user_ids): 1021 add_user_ids.append(query_user_id) 1022 1023 # Collect users that need to be subtracted. 1024 sub_user_queries = [] 1025 for group_user_query in group_user_queries: 1026 if (group_user_query not in resolved_user_queries): 1027 sub_user_queries.append(group_user_query) 1028 1029 # Update the group. 1030 1031 add_count = 0 1032 if (len(add_user_ids) > 0): 1033 add_count = self.courses_groups_memberships_add( 1034 resolved_course_query.get_id(), 1035 resolved_groupset_query.get_id(), 1036 resolved_group_query.get_id(), 1037 add_user_ids, 1038 **kwargs) 1039 1040 sub_count = 0 1041 deleted = False 1042 if (len(sub_user_queries) > 0): 1043 sub_count, deleted = self.courses_groups_memberships_resolve_and_subtract( 1044 resolved_course_query, 1045 resolved_groupset_query, 1046 resolved_group_query, 1047 sub_user_queries, 1048 delete_empty = delete_empty, 1049 **kwargs) 1050 1051 return add_count, sub_count, deleted
Resolve queries and set the specified users for the group. This method can both add and subtract users from the group.
Returns:
- The count of users added.
- The count of users subtracted.
- If this group was deleted.
1053 def courses_groups_memberships_subtract(self, 1054 course_id: str, 1055 groupset_id: str, 1056 group_id: str, 1057 user_ids: typing.Collection[str], 1058 **kwargs: typing.Any) -> int: 1059 """ 1060 Subtract the specified users from the group. 1061 """ 1062 1063 raise NotImplementedError('courses_groups_memberships_subtract')
Subtract the specified users from the group.
1065 def courses_groups_memberships_resolve_and_subtract(self, 1066 course_query: lms.model.courses.CourseQuery, 1067 groupset_query: lms.model.groupsets.GroupSetQuery, 1068 group_query: lms.model.groups.GroupQuery, 1069 user_queries: typing.Collection[lms.model.users.UserQuery], 1070 delete_empty: bool = False, 1071 **kwargs: typing.Any) -> typing.Tuple[int, bool]: 1072 """ 1073 Resolve queries and subtract the specified users from the group. 1074 Return: 1075 - The number of users deleted. 1076 - If this group was deleted. 1077 """ 1078 1079 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1080 resolved_groupset_query = self.resolve_groupset_query(resolved_course_query.get_id(), groupset_query, **kwargs) 1081 resolved_group_query = self.resolve_group_query(resolved_course_query.get_id(), resolved_groupset_query.get_id(), group_query, **kwargs) 1082 resolved_user_queries = self.resolve_user_queries(resolved_course_query.get_id(), user_queries, warn_on_miss = True, **kwargs) 1083 1084 # Get users already in this group. 1085 group_memberships = self.courses_groups_memberships_list( 1086 resolved_course_query.get_id(), 1087 resolved_groupset_query.get_id(), 1088 resolved_group_query.get_id(), 1089 **kwargs) 1090 1091 group_user_ids = {membership.user.id for membership in group_memberships if membership.user.id is not None} 1092 1093 # Filter out users not in the group. 1094 user_ids = [] 1095 for query in resolved_user_queries: 1096 if (query.get_id() not in group_user_ids): 1097 _logger.warning("User '%s' is not in group '%s'.", query, resolved_group_query) 1098 continue 1099 1100 user_ids.append(query.get_id()) 1101 1102 if (delete_empty and len(group_memberships) == 0): 1103 deleted = self.courses_groups_delete( 1104 resolved_course_query.get_id(), 1105 resolved_groupset_query.get_id(), 1106 resolved_group_query.get_id(), 1107 **kwargs) 1108 return 0, deleted 1109 1110 if (len(user_ids) == 0): 1111 return 0, False 1112 1113 count = self.courses_groups_memberships_subtract( 1114 resolved_course_query.get_id(), 1115 resolved_groupset_query.get_id(), 1116 resolved_group_query.get_id(), 1117 user_ids, 1118 **kwargs) 1119 1120 deleted = False 1121 if (delete_empty and (count == len(group_memberships))): 1122 deleted = self.courses_groups_delete( 1123 resolved_course_query.get_id(), 1124 resolved_groupset_query.get_id(), 1125 resolved_group_query.get_id(), 1126 **kwargs) 1127 1128 return count, deleted
Resolve queries and subtract the specified users from the group. Return: - The number of users deleted. - If this group was deleted.
1130 def courses_quizzes_download(self, 1131 course_id: str, 1132 quiz_id: str, 1133 **kwargs: typing.Any) -> quizcomp.model.quiz.Quiz: 1134 """ 1135 Download a quiz from the LMS into a Quiz Composer quiz. 1136 This will only fetch the full quiz, not write it to disk. 1137 """ 1138 1139 raise NotImplementedError('courses_quizzes_download')
Download a quiz from the LMS into a Quiz Composer quiz. This will only fetch the full quiz, not write it to disk.
1141 def courses_quizzes_resolve_and_download(self, 1142 course_query: lms.model.courses.CourseQuery, 1143 quiz_query: lms.model.assignments.AssignmentQuery, 1144 **kwargs: typing.Any) -> quizcomp.model.quiz.Quiz: 1145 """ 1146 Resolve queries and download a quiz from the LMS into a Quiz Composer quiz. 1147 This will only fetch the full quiz, not write it to disk. 1148 """ 1149 1150 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1151 resolved_quiz_query = self.resolve_quiz_query(resolved_course_query.get_id(), quiz_query, **kwargs) 1152 1153 return self.courses_quizzes_download(resolved_course_query.get_id(), resolved_quiz_query.get_id(), **kwargs)
Resolve queries and download a quiz from the LMS into a Quiz Composer quiz. This will only fetch the full quiz, not write it to disk.
1155 def courses_quizzes_get(self, 1156 course_query: lms.model.courses.CourseQuery, 1157 quiz_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1158 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1159 """ 1160 Get the specified quizzes associated with the given course. 1161 """ 1162 1163 if (len(quiz_queries) == 0): 1164 return [] 1165 1166 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1167 1168 quizzes = sorted(self.courses_quizzes_list(resolved_course_query.get_id(), **kwargs)) 1169 quiz_queries = sorted(quiz_queries) 1170 1171 matches = [] 1172 for quiz in quizzes: 1173 for query in quiz_queries: 1174 if (query.match(quiz)): 1175 matches.append(quiz) 1176 break 1177 1178 return matches
Get the specified quizzes associated with the given course.
1180 def courses_quizzes_fetch(self, 1181 course_id: str, 1182 quiz_id: str, 1183 **kwargs: typing.Any) -> typing.Union[lms.model.assignments.Assignment, None]: 1184 """ 1185 Fetch a single quiz associated with the given course. 1186 Return None if no matching quiz is found. 1187 1188 By default, this will just do a list and choose the relevant record. 1189 Specific backends may override this if there are performance concerns. 1190 """ 1191 1192 quizzes = self.courses_quizzes_list(course_id, **kwargs) 1193 for quiz in sorted(quizzes): 1194 if (quiz.id == quiz_id): 1195 return quiz 1196 1197 return None
Fetch a single quiz associated with the given course. Return None if no matching quiz is found.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
1199 def courses_quizzes_list(self, 1200 course_id: str, 1201 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1202 """ 1203 List the quizzes associated with the given course. 1204 If specified, additional resources associated with the quiz (e.g., images) may also be fetched. 1205 """ 1206 1207 raise NotImplementedError('courses_quizzes_list')
List the quizzes associated with the given course. If specified, additional resources associated with the quiz (e.g., images) may also be fetched.
1209 def courses_quizzes_resolve_and_list(self, 1210 course_query: lms.model.courses.CourseQuery, 1211 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1212 """ 1213 List the quizzes associated with the given course. 1214 """ 1215 1216 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1217 return sorted(self.courses_quizzes_list(resolved_course_query.get_id(), **kwargs))
List the quizzes associated with the given course.
1219 def courses_quizzes_resolve_and_remove(self, 1220 course_query: lms.model.courses.CourseQuery, 1221 quiz_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1222 **kwargs: typing.Any) -> typing.List[lms.model.assignments.Assignment]: 1223 """ 1224 Resolve the course and remove any matching quiz. 1225 Return the quizzes that were removed. 1226 """ 1227 1228 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1229 1230 old_quizzes = self.courses_quizzes_get(resolved_course_query, quiz_queries) 1231 for old_quiz in old_quizzes: 1232 self.courses_quizzes_remove(resolved_course_query.get_id(), old_quiz.id) 1233 1234 return old_quizzes
Resolve the course and remove any matching quiz. Return the quizzes that were removed.
1236 def courses_quizzes_remove(self, 1237 course_id: str, 1238 quiz_id: str, 1239 **kwargs: typing.Any) -> None: 1240 """ 1241 Remove quiz from the LMS. 1242 """ 1243 1244 raise NotImplementedError('courses_quizzes_remove')
Remove quiz from the LMS.
1246 def courses_quizzes_resolve_and_upload(self, 1247 course_query: lms.model.courses.CourseQuery, 1248 quiz: quizcomp.model.quiz.Quiz, 1249 force: bool = False, 1250 **kwargs: typing.Any) -> lms.model.assignments.Assignment: 1251 """ 1252 Resolve the course and upload the quiz. 1253 If a quiz already exists with a matching query, then `force` will decide the behavior. 1254 If `force` is true then those matching quizzes will be removed, 1255 otherwise an exception will be raised. 1256 """ 1257 1258 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1259 1260 old_quizzes = self.courses_quizzes_get(resolved_course_query, [lms.model.assignments.AssignmentQuery(name = quiz.name)]) 1261 if ((not force) and (len(old_quizzes) > 0)): 1262 raise ValueError(f"Found {len(old_quizzes)} existing quizzes with a matching name ('{quiz.name}'), stopping upload.") 1263 1264 for old_quiz in old_quizzes: 1265 _logger.warning("Deleting existing quiz ('%s') before upload.", old_quiz.to_query()) 1266 self.courses_quizzes_remove(resolved_course_query.get_id(), old_quiz.id) 1267 1268 return self.courses_quizzes_upload(resolved_course_query.get_id(), quiz)
Resolve the course and upload the quiz.
If a quiz already exists with a matching query, then force will decide the behavior.
If force is true then those matching quizzes will be removed,
otherwise an exception will be raised.
1270 def courses_quizzes_upload(self, 1271 course_id: str, 1272 quiz: quizcomp.model.quiz.Quiz, 1273 **kwargs: typing.Any) -> lms.model.assignments.Assignment: 1274 """ 1275 Upload a single quiz to the LMS. 1276 1277 This method assumes that there is no quiz with a matching name. 1278 To handle that case, use courses_quizzes_resolve_and_upload() with `force = True`. 1279 """ 1280 1281 raise NotImplementedError('courses_quizzes_upload')
Upload a single quiz to the LMS.
This method assumes that there is no quiz with a matching name.
To handle that case, use courses_quizzes_resolve_and_upload() with force = True.
1283 def courses_syllabus_fetch(self, 1284 course_id: str, 1285 **kwargs: typing.Any) -> typing.Union[str, None]: 1286 """ 1287 Get the syllabus for a course, or None if no syllabus exists. 1288 """ 1289 1290 raise NotImplementedError('courses_syllabus_fetch')
Get the syllabus for a course, or None if no syllabus exists.
1292 def courses_syllabus_get(self, 1293 course_query: lms.model.courses.CourseQuery, 1294 **kwargs: typing.Any) -> typing.Union[str, None]: 1295 """ 1296 Get the syllabus for a course query, or None if no syllabus exists. 1297 """ 1298 1299 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1300 1301 return self.courses_syllabus_fetch(resolved_course_query.get_id(), **kwargs)
Get the syllabus for a course query, or None if no syllabus exists.
1303 def courses_users_get(self, 1304 course_query: lms.model.courses.CourseQuery, 1305 user_queries: typing.Collection[lms.model.users.UserQuery], 1306 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1307 """ 1308 Get the specified users associated with the given course. 1309 """ 1310 1311 if (len(user_queries) == 0): 1312 return [] 1313 1314 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1315 users = self.courses_users_list(resolved_course_query.get_id(), **kwargs) 1316 1317 user_queries = sorted(user_queries) 1318 users = sorted(users) 1319 1320 matches = [] 1321 for user in users: 1322 for query in user_queries: 1323 if (query.match(user)): 1324 matches.append(user) 1325 break 1326 1327 return matches
Get the specified users associated with the given course.
1329 def courses_users_fetch(self, 1330 course_id: str, 1331 user_id: str, 1332 **kwargs: typing.Any) -> typing.Union[lms.model.users.CourseUser, None]: 1333 """ 1334 Fetch a single user associated with the given course. 1335 Return None if no matching user is found. 1336 1337 By default, this will just do a list and choose the relevant record. 1338 Specific backends may override this if there are performance concerns. 1339 """ 1340 1341 users = self.courses_users_list(course_id, **kwargs) 1342 for user in sorted(users): 1343 if (user.id == user_id): 1344 return user 1345 1346 return None
Fetch a single user associated with the given course. Return None if no matching user is found.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
1348 def courses_users_list(self, 1349 course_id: str, 1350 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1351 """ 1352 List the users associated with the given course. 1353 """ 1354 1355 raise NotImplementedError('courses_users_list')
List the users associated with the given course.
1357 def courses_users_resolve_and_list(self, 1358 course_query: lms.model.courses.CourseQuery, 1359 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 1360 """ 1361 List the users associated with the given course. 1362 """ 1363 1364 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1365 return list(sorted(self.courses_users_list(resolved_course_query.get_id(), **kwargs)))
List the users associated with the given course.
1367 def courses_users_scores_get(self, 1368 course_query: lms.model.courses.CourseQuery, 1369 user_query: lms.model.users.UserQuery, 1370 assignment_queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1371 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1372 """ 1373 Get the scores associated with the given user query and assignment queries. 1374 """ 1375 1376 if (len(assignment_queries) == 0): 1377 return [] 1378 1379 scores = self.courses_users_scores_resolve_and_list(course_query, user_query, **kwargs) 1380 1381 scores = sorted(scores) 1382 assignment_queries = sorted(assignment_queries) 1383 1384 matches = [] 1385 for score in scores: 1386 for assignment_query in assignment_queries: 1387 if (assignment_query.match(score.assignment)): 1388 matches.append(score) 1389 1390 return matches
Get the scores associated with the given user query and assignment queries.
1392 def courses_users_scores_fetch(self, 1393 course_id: str, 1394 user_id: str, 1395 assignment_id: str, 1396 **kwargs: typing.Any) -> typing.Union[lms.model.scores.AssignmentScore, None]: 1397 """ 1398 Fetch the score associated with the given user and assignment. 1399 1400 By default, this will just do a list and choose the relevant record. 1401 Specific backends may override this if there are performance concerns. 1402 """ 1403 1404 # The default implementation is the same as courses_assignments_scores_fetch(). 1405 return self.courses_assignments_scores_fetch(course_id, assignment_id, user_id, **kwargs)
Fetch the score associated with the given user and assignment.
By default, this will just do a list and choose the relevant record. Specific backends may override this if there are performance concerns.
1407 def courses_users_scores_list(self, 1408 course_id: str, 1409 user_id: str, 1410 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1411 """ 1412 List the scores associated with the given user. 1413 """ 1414 1415 raise NotImplementedError('courses_users_scores_list')
List the scores associated with the given user.
1417 def courses_users_scores_resolve_and_list(self, 1418 course_query: lms.model.courses.CourseQuery, 1419 user_query: lms.model.users.UserQuery, 1420 **kwargs: typing.Any) -> typing.List[lms.model.scores.AssignmentScore]: 1421 """ 1422 List the scores associated with the given user query. 1423 In addition to resolving the user query, 1424 assignments will also be resolved into their full version 1425 (instead of the reduced version usually returned with scores). 1426 """ 1427 1428 resolved_course_query = self.resolve_course_query(course_query, **kwargs) 1429 1430 # Resolve the user query. 1431 matched_users = self.courses_users_get(resolved_course_query, [user_query], **kwargs) 1432 if (len(matched_users) == 0): 1433 return [] 1434 1435 target_user = matched_users[0] 1436 1437 # List the scores. 1438 scores = self.courses_users_scores_list(resolved_course_query.get_id(), target_user.id, **kwargs) 1439 if (len(scores) == 0): 1440 return [] 1441 1442 # Resolve the scores' queries. 1443 1444 assignments = self.courses_assignments_list(resolved_course_query.get_id(), **kwargs) 1445 assignments_map = {assignment.id: assignment for assignment in assignments} 1446 1447 for score in scores: 1448 score.user = target_user.to_query() 1449 1450 if ((score.assignment is not None) and (score.assignment.id in assignments_map)): 1451 score.assignment = assignments_map[score.assignment.id].to_query() 1452 1453 return sorted(scores)
List the scores associated with the given user query. In addition to resolving the user query, assignments will also be resolved into their full version (instead of the reduced version usually returned with scores).
1457 def parse_assignment_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.assignments.AssignmentQuery, None]: 1458 """ 1459 Attempt to parse an assignment query from a string. 1460 If there is no query, return a None. 1461 If the query is malformed, raise an exception. 1462 1463 By default, this method assumes that LMS IDs are ints. 1464 Child backends may override this to implement their specific behavior. 1465 """ 1466 1467 return lms.model.query.parse_int_query(lms.model.assignments.AssignmentQuery, text, check_email = False)
Attempt to parse an assignment query from a string. If there is no query, return a None. If the query is malformed, raise an exception.
By default, this method assumes that LMS IDs are ints. Child backends may override this to implement their specific behavior.
1469 def parse_assignment_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.assignments.AssignmentQuery]: 1470 """ Parse a list of assignment queries. """ 1471 1472 queries = [] 1473 for text in texts: 1474 query = self.parse_assignment_query(text) 1475 if (query is not None): 1476 queries.append(query) 1477 1478 return queries
Parse a list of assignment queries.
1480 def parse_course_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.courses.CourseQuery, None]: 1481 """ 1482 Attempt to parse a course query from a string. 1483 If there is no query, return a None. 1484 If the query is malformed, raise an exception. 1485 1486 By default, this method assumes that LMS IDs are ints. 1487 Child backends may override this to implement their specific behavior. 1488 """ 1489 1490 return lms.model.query.parse_int_query(lms.model.courses.CourseQuery, text, check_email = False)
Attempt to parse a course query from a string. If there is no query, return a None. If the query is malformed, raise an exception.
By default, this method assumes that LMS IDs are ints. Child backends may override this to implement their specific behavior.
1492 def parse_course_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.courses.CourseQuery]: 1493 """ Parse a list of course queries. """ 1494 1495 queries = [] 1496 for text in texts: 1497 query = self.parse_course_query(text) 1498 if (query is not None): 1499 queries.append(query) 1500 1501 return queries
Parse a list of course queries.
1503 def parse_groupset_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.groupsets.GroupSetQuery, None]: 1504 """ 1505 Attempt to parse a group set query from a string. 1506 If there is no query, return a None. 1507 If the query is malformed, raise an exception. 1508 1509 By default, this method assumes that LMS IDs are ints. 1510 Child backends may override this to implement their specific behavior. 1511 """ 1512 1513 return lms.model.query.parse_int_query(lms.model.groupsets.GroupSetQuery, text, check_email = False)
Attempt to parse a group set query from a string. If there is no query, return a None. If the query is malformed, raise an exception.
By default, this method assumes that LMS IDs are ints. Child backends may override this to implement their specific behavior.
1515 def parse_groupset_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.groupsets.GroupSetQuery]: 1516 """ Parse a list of group set queries. """ 1517 1518 queries = [] 1519 for text in texts: 1520 query = self.parse_groupset_query(text) 1521 if (query is not None): 1522 queries.append(query) 1523 1524 return queries
Parse a list of group set queries.
1526 def parse_group_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.groups.GroupQuery, None]: 1527 """ 1528 Attempt to parse a group query from a string. 1529 If there is no query, return a None. 1530 If the query is malformed, raise an exception. 1531 1532 By default, this method assumes that LMS IDs are ints. 1533 Child backends may override this to implement their specific behavior. 1534 """ 1535 1536 return lms.model.query.parse_int_query(lms.model.groups.GroupQuery, text, check_email = False)
Attempt to parse a group query from a string. If there is no query, return a None. If the query is malformed, raise an exception.
By default, this method assumes that LMS IDs are ints. Child backends may override this to implement their specific behavior.
1538 def parse_group_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.groups.GroupQuery]: 1539 """ Parse a list of group queries. """ 1540 1541 queries = [] 1542 for text in texts: 1543 query = self.parse_group_query(text) 1544 if (query is not None): 1545 queries.append(query) 1546 1547 return queries
Parse a list of group queries.
1549 def parse_user_query(self, text: typing.Union[str, None]) -> typing.Union[lms.model.users.UserQuery, None]: 1550 """ 1551 Attempt to parse a user query from a string. 1552 If there is no query, return a None. 1553 If the query is malformed, raise an exception. 1554 1555 By default, this method assumes that LMS IDs are ints. 1556 Child backends may override this to implement their specific behavior. 1557 """ 1558 1559 return lms.model.query.parse_int_query(lms.model.users.UserQuery, text, check_email = True)
Attempt to parse a user query from a string. If there is no query, return a None. If the query is malformed, raise an exception.
By default, this method assumes that LMS IDs are ints. Child backends may override this to implement their specific behavior.
1561 def parse_user_queries(self, texts: typing.Collection[typing.Union[str, None]]) -> typing.List[lms.model.users.UserQuery]: 1562 """ Parse a list of user queries. """ 1563 1564 queries = [] 1565 for text in texts: 1566 query = self.parse_user_query(text) 1567 if (query is not None): 1568 queries.append(query) 1569 1570 return queries
Parse a list of user queries.
1572 def resolve_assignment_query(self, 1573 course_id: str, 1574 assignment_query: lms.model.assignments.AssignmentQuery, 1575 **kwargs: typing.Any) -> lms.model.assignments.ResolvedAssignmentQuery: 1576 """ Resolve the assignment query or raise an exception. """ 1577 1578 # Shortcut already resolved queries. 1579 if (isinstance(assignment_query, lms.model.assignments.ResolvedAssignmentQuery)): 1580 return assignment_query 1581 1582 results = self.resolve_assignment_queries(course_id, [assignment_query], **kwargs) 1583 if (len(results) == 0): 1584 raise ValueError(f"Could not resolve assignment query: '{assignment_query}'.") 1585 1586 return results[0]
Resolve the assignment query or raise an exception.
1588 def resolve_assignment_queries(self, 1589 course_id: str, 1590 queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1591 **kwargs: typing.Any) -> typing.List[lms.model.assignments.ResolvedAssignmentQuery]: 1592 """ 1593 Resolve a list of assignment queries into a list of resolved assignment queries. 1594 See _resolve_queries(). 1595 """ 1596 1597 results = self._resolve_queries( 1598 queries, 1599 'assignment', 1600 self.courses_assignments_list(course_id, **kwargs), 1601 lms.model.assignments.ResolvedAssignmentQuery, 1602 **kwargs) 1603 1604 return typing.cast(typing.List[lms.model.assignments.ResolvedAssignmentQuery], results)
Resolve a list of assignment queries into a list of resolved assignment queries. See _resolve_queries().
1606 def resolve_course_query(self, 1607 query: lms.model.courses.CourseQuery, 1608 **kwargs: typing.Any) -> lms.model.courses.ResolvedCourseQuery: 1609 """ Resolve the course query or raise an exception. """ 1610 1611 # Shortcut already resolved queries. 1612 if (isinstance(query, lms.model.courses.ResolvedCourseQuery)): 1613 return query 1614 1615 results = self.resolve_course_queries([query], **kwargs) 1616 if (len(results) == 0): 1617 raise ValueError(f"Could not resolve course query: '{query}'.") 1618 1619 return results[0]
Resolve the course query or raise an exception.
1621 def resolve_course_queries(self, 1622 queries: typing.Collection[lms.model.courses.CourseQuery], 1623 **kwargs: typing.Any) -> typing.List[lms.model.courses.ResolvedCourseQuery]: 1624 """ 1625 Resolve a list of course queries into a list of resolved course queries. 1626 See _resolve_queries(). 1627 """ 1628 1629 results = self._resolve_queries( 1630 queries, 1631 'course', 1632 self.courses_list(**kwargs), 1633 lms.model.courses.ResolvedCourseQuery, 1634 **kwargs) 1635 1636 return typing.cast(typing.List[lms.model.courses.ResolvedCourseQuery], results)
Resolve a list of course queries into a list of resolved course queries. See _resolve_queries().
1638 def resolve_group_queries(self, 1639 course_id: str, 1640 groupset_id: str, 1641 queries: typing.Collection[lms.model.groups.GroupQuery], 1642 **kwargs: typing.Any) -> typing.List[lms.model.groups.ResolvedGroupQuery]: 1643 """ 1644 Resolve a list of group queries into a list of resolved group queries. 1645 See _resolve_queries(). 1646 """ 1647 1648 results = self._resolve_queries( 1649 queries, 1650 'group', 1651 self.courses_groups_list(course_id, groupset_id, **kwargs), 1652 lms.model.groups.ResolvedGroupQuery, 1653 **kwargs) 1654 1655 return typing.cast(typing.List[lms.model.groups.ResolvedGroupQuery], results)
Resolve a list of group queries into a list of resolved group queries. See _resolve_queries().
1657 def resolve_group_query(self, 1658 course_id: str, 1659 groupset_id: str, 1660 query: lms.model.groups.GroupQuery, 1661 **kwargs: typing.Any) -> lms.model.groups.ResolvedGroupQuery: 1662 """ Resolve the group query or raise an exception. """ 1663 1664 # Shortcut already resolved queries. 1665 if (isinstance(query, lms.model.groups.ResolvedGroupQuery)): 1666 return query 1667 1668 results = self.resolve_group_queries(course_id, groupset_id, [query], **kwargs) 1669 if (len(results) == 0): 1670 raise ValueError(f"Could not resolve group query: '{query}'.") 1671 1672 return results[0]
Resolve the group query or raise an exception.
1674 def resolve_groupset_queries(self, 1675 course_id: str, 1676 queries: typing.Collection[lms.model.groupsets.GroupSetQuery], 1677 **kwargs: typing.Any) -> typing.List[lms.model.groupsets.ResolvedGroupSetQuery]: 1678 """ 1679 Resolve a list of group set queries into a list of resolved group set queries. 1680 See _resolve_queries(). 1681 """ 1682 1683 results = self._resolve_queries( 1684 queries, 1685 'group set', 1686 self.courses_groupsets_list(course_id, **kwargs), 1687 lms.model.groupsets.ResolvedGroupSetQuery, 1688 **kwargs) 1689 1690 return typing.cast(typing.List[lms.model.groupsets.ResolvedGroupSetQuery], results)
Resolve a list of group set queries into a list of resolved group set queries. See _resolve_queries().
1692 def resolve_groupset_query(self, 1693 course_id: str, 1694 groupset_query: lms.model.groupsets.GroupSetQuery, 1695 **kwargs: typing.Any) -> lms.model.groupsets.ResolvedGroupSetQuery: 1696 """ Resolve the group set query or raise an exception. """ 1697 1698 # Shortcut already resolved queries. 1699 if (isinstance(groupset_query, lms.model.groupsets.ResolvedGroupSetQuery)): 1700 return groupset_query 1701 1702 results = self.resolve_groupset_queries(course_id, [groupset_query], **kwargs) 1703 if (len(results) == 0): 1704 raise ValueError(f"Could not resolve group set query: '{groupset_query}'.") 1705 1706 return results[0]
Resolve the group set query or raise an exception.
1708 def resolve_quiz_query(self, 1709 course_id: str, 1710 quiz_query: lms.model.assignments.AssignmentQuery, 1711 **kwargs: typing.Any) -> lms.model.assignments.ResolvedAssignmentQuery: 1712 """ Resolve the quiz query or raise an exception. """ 1713 1714 # Shortcut already resolved queries. 1715 if (isinstance(quiz_query, lms.model.assignments.ResolvedAssignmentQuery)): 1716 return quiz_query 1717 1718 results = self.resolve_quiz_queries(course_id, [quiz_query], **kwargs) 1719 if (len(results) == 0): 1720 raise ValueError(f"Could not resolve quiz query: '{quiz_query}'.") 1721 1722 return results[0]
Resolve the quiz query or raise an exception.
1724 def resolve_quiz_queries(self, 1725 course_id: str, 1726 queries: typing.Collection[lms.model.assignments.AssignmentQuery], 1727 **kwargs: typing.Any) -> typing.List[lms.model.assignments.ResolvedAssignmentQuery]: 1728 """ 1729 Resolve a list of quiz queries into a list of resolved quiz queries. 1730 See _resolve_queries(). 1731 """ 1732 1733 results = self._resolve_queries( 1734 queries, 1735 'quiz', 1736 self.courses_quizzes_list(course_id, **kwargs), 1737 lms.model.assignments.ResolvedAssignmentQuery, 1738 **kwargs) 1739 1740 return typing.cast(typing.List[lms.model.assignments.ResolvedAssignmentQuery], results)
Resolve a list of quiz queries into a list of resolved quiz queries. See _resolve_queries().
1742 def resolve_user_queries(self, 1743 course_id: str, 1744 queries: typing.Collection[lms.model.users.UserQuery], 1745 only_students: bool = False, 1746 **kwargs: typing.Any) -> typing.List[lms.model.users.ResolvedUserQuery]: 1747 """ 1748 Resolve a list of user queries into a list of resolved user queries. 1749 See _resolve_queries(). 1750 """ 1751 1752 filter_func = None 1753 if (only_students): 1754 filter_func = lambda user: user.is_student() # pylint: disable=unnecessary-lambda-assignment 1755 1756 results = self._resolve_queries( 1757 queries, 1758 'user', 1759 self.courses_users_list(course_id, **kwargs), 1760 lms.model.users.ResolvedUserQuery, 1761 filter_func = filter_func, 1762 **kwargs) 1763 1764 return typing.cast(typing.List[lms.model.users.ResolvedUserQuery], results)
Resolve a list of user queries into a list of resolved user queries. See _resolve_queries().