diff --git a/README.md b/README.md index 8d5466737..414d53cd4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Business logic core of the application and a REST API that provides access to ot ### Prerequisites -You need a web server with PHP (8.1+) and MySQL or MariaDB database. +You need a web server with PHP (8.2+, 8.3 currently used in development) and MySQL or MariaDB database. We recommend installing PHP from remi repository: @@ -28,7 +28,7 @@ You may list the PHP modules thusly: ...and select the right module: ``` -# dnf module enable php:remi-8.1 +# dnf module enable php:remi-8.3 ``` If you install core-api as a package, the PHP will be installed as dependencies. @@ -65,7 +65,7 @@ installation to the end. ### Manual Installation -The web API requires a PHP runtime version at least 8.1. Which one depends on +The web API requires a PHP runtime version at least 8.2. Which one depends on actual configuration, there is a choice between _mod_php_ inside Apache, _php-fpm_ with Apache or Nginx proxy or running it as standalone uWSGI script. Also see the required PHP modules in the prerequisites section. diff --git a/app/V1Module/presenters/GroupExternalAttributesPresenter.php b/app/V1Module/presenters/GroupExternalAttributesPresenter.php index 0cdd3df80..717ecac8b 100644 --- a/app/V1Module/presenters/GroupExternalAttributesPresenter.php +++ b/app/V1Module/presenters/GroupExternalAttributesPresenter.php @@ -2,19 +2,22 @@ namespace App\V1Module\Presenters; +use App\Exceptions\BadRequestException; use App\Helpers\MetaFormats\Attributes\Post; use App\Helpers\MetaFormats\Attributes\Query; use App\Helpers\MetaFormats\Attributes\Path; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; -use App\Exceptions\BadRequestException; +use App\Exceptions\NotFoundException; +use App\Exceptions\InternalServerException; use App\Model\Repository\GroupExternalAttributes; +use App\Model\Repository\GroupMemberships; use App\Model\Repository\Groups; use App\Model\Entity\GroupExternalAttribute; use App\Model\View\GroupViewFactory; use App\Security\ACL\IGroupPermissions; -use InvalidArgumentException; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; /** * Additional attributes used by 3rd parties to keep relations between groups and entities in external systems. @@ -28,6 +31,12 @@ class GroupExternalAttributesPresenter extends BasePresenter */ public $groupExternalAttributes; + /** + * @var GroupMemberships + * @inject + */ + public $groupMemberships; + /** * @var Groups * @inject @@ -54,48 +63,41 @@ public function checkDefault() } /** - * Return all attributes that correspond to given filtering parameters. + * Return special brief groups entities with injected external attributes and given user affiliation. * @GET - * - * The filter is encoded as array of objects (logically represented as disjunction of clauses) - * -- i.e., [clause1 OR clause2 ...]. Each clause is an object with the following keys: - * "group", "service", "key", "value" that match properties of GroupExternalAttribute entity. - * The values are expected values matched with == in the search. Any of the keys may be omitted or null - * which indicate it should not be matched in the particular clause. - * A clause must contain at least one of the four keys. - * - * The endpoint will return a list of matching attributes and all related group entities. */ - #[Query("filter", new VString(), "JSON-encoded filter query in DNF as [clause OR clause...]", required: true)] - public function actionDefault(?string $filter) + #[Query("instance", new VUuid(), "ID of the instance, whose groups are returned.", required: true)] + #[Query( + "service", + new VString(), + "ID of the external service, of which the attributes are returned. If missing, all attributes are returned.", + required: false + )] + #[Query( + "user", + new VUuid(), + "Relationship info of this user is included for each returned group.", + required: false + )] + public function actionDefault(string $instance, ?string $service, ?string $user) { - $filterStruct = json_decode($filter ?? '', true); - if (!$filterStruct || !is_array($filterStruct)) { - throw new BadRequestException("Invalid filter format."); - } - - try { - $attributes = $this->groupExternalAttributes->findByFilter($filterStruct); - } catch (InvalidArgumentException $e) { - throw new BadRequestException($e->getMessage(), '', null, $e); - } - - $groupIds = []; - foreach ($attributes as $attribute) { - $groupIds[$attribute->getGroup()->getId()] = true; // id is key to make it unique - } - - $groups = $this->groups->groupsAncestralClosure(array_keys($groupIds)); - $this->sendSuccessResponse([ - "attributes" => $attributes, - "groups" => $this->groupViewFactory->getGroups($groups), - ]); + $filter = $service ? [['service' => $service]] : []; + $attributes = $this->groupExternalAttributes->findByFilter($filter); // all attributes of selected service + $groups = $this->groups->findFiltered(null, $instance, null, false); // all but archived groups + $memberships = $user ? $this->groupMemberships->findByUser($user) : []; + + $this->sendSuccessResponse($this->groupViewFactory->getGroupsForExtension( + $groups, + $attributes, + $memberships, + )); } - public function checkAdd() + public function checkAdd(string $groupId) { - if (!$this->groupAcl->canSetExternalAttributes()) { + $group = $this->groups->findOrThrow($groupId); + if (!$this->groupAcl->canSetExternalAttributes($group)) { throw new ForbiddenRequestException(); } } @@ -107,7 +109,7 @@ public function checkAdd() #[Post("service", new VString(1, 32), "Identifier of the external service creating the attribute", required: true)] #[Post("key", new VString(1, 32), "Key of the attribute (must be valid identifier)", required: true)] #[Post("value", new VString(0, 255), "Value of the attribute (arbitrary string)", required: true)] - #[Path("groupId", new VString(), required: true)] + #[Path("groupId", new VUuid(), required: true)] public function actionAdd(string $groupId) { $group = $this->groups->findOrThrow($groupId); @@ -116,15 +118,21 @@ public function actionAdd(string $groupId) $service = $req->getPost("service"); $key = $req->getPost("key"); $value = $req->getPost("value"); - $attribute = new GroupExternalAttribute($group, $service, $key, $value); - $this->groupExternalAttributes->persist($attribute); + + try { + $attribute = new GroupExternalAttribute($group, $service, $key, $value); + $this->groupExternalAttributes->persist($attribute); + } catch (UniqueConstraintViolationException) { + throw new BadRequestException("Attribute already exists."); + } $this->sendSuccessResponse("OK"); } - public function checkRemove() + public function checkRemove(string $groupId) { - if (!$this->groupAcl->canSetExternalAttributes()) { + $group = $this->groups->findOrThrow($groupId); + if (!$this->groupAcl->canSetExternalAttributes($group)) { throw new ForbiddenRequestException(); } } @@ -133,11 +141,26 @@ public function checkRemove() * Remove selected attribute * @DELETE */ - #[Path("id", new VUuid(), "Identifier of the external attribute.", required: true)] - public function actionRemove(string $id) + #[Query("service", new VString(1, 32), "Identifier of the external service creating the attribute", required: true)] + #[Query("key", new VString(1, 32), "Key of the attribute (must be valid identifier)", required: true)] + #[Query("value", new VString(0, 255), "Value of the attribute (arbitrary string)", required: true)] + #[Path("groupId", new VUuid(), required: true)] + public function actionRemove(string $groupId, string $service, string $key, string $value) { - $attribute = $this->groupExternalAttributes->findOrThrow($id); - $this->groupExternalAttributes->remove($attribute); + $attributes = $this->groupExternalAttributes->findBy( + ['group' => $groupId, 'service' => $service, 'key' => $key, 'value' => $value] + ); + if (!$attributes) { + throw new NotFoundException("Specified attribute not found at selected group"); + } + if (count($attributes) > 1) { + throw new InternalServerException( + "Unique constraint violation " + . "(multiple '$key' => '$value' attributes found at $groupId from service $service)" + ); + } + + $this->groupExternalAttributes->remove($attributes[0]); $this->sendSuccessResponse("OK"); } } diff --git a/app/V1Module/router/RouterFactory.php b/app/V1Module/router/RouterFactory.php index e8da44161..afeafc2f7 100644 --- a/app/V1Module/router/RouterFactory.php +++ b/app/V1Module/router/RouterFactory.php @@ -312,7 +312,7 @@ private static function createGroupAttributesRoutes(string $prefix): RouteList $router[] = new GetRoute($prefix, "GroupExternalAttributes:"); $router[] = new PostRoute("$prefix/", "GroupExternalAttributes:add"); - $router[] = new DeleteRoute("$prefix/", "GroupExternalAttributes:remove"); + $router[] = new DeleteRoute("$prefix/", "GroupExternalAttributes:remove"); return $router; } diff --git a/app/V1Module/security/ACL/IGroupPermissions.php b/app/V1Module/security/ACL/IGroupPermissions.php index 594e0c3ea..ba20ab979 100644 --- a/app/V1Module/security/ACL/IGroupPermissions.php +++ b/app/V1Module/security/ACL/IGroupPermissions.php @@ -78,5 +78,5 @@ public function canUnlockStudent(Group $group, User $student): bool; public function canViewExternalAttributes(): bool; - public function canSetExternalAttributes(): bool; + public function canSetExternalAttributes(Group $group): bool; } diff --git a/app/V1Module/security/TokenScope.php b/app/V1Module/security/TokenScope.php index 766c0848f..4e99f7092 100644 --- a/app/V1Module/security/TokenScope.php +++ b/app/V1Module/security/TokenScope.php @@ -43,9 +43,9 @@ class TokenScope public const EMAIL_VERIFICATION = "email-verification"; /** - * Scope used for 3rd party tools designed to externally manage groups and student memeberships. + * Scope used for 3rd party tools designed to externally manage groups and student memberships. */ - public const GROUP_EXTERNAL_ATTRIBUTES = "group-external-attributes"; + public const GROUP_EXTERNAL = "group-external"; /** * Scope for managing the users. Used in case the user data needs to be updated from an external database. diff --git a/app/config/permissions.neon b/app/config/permissions.neon index 2834f66b1..744f17136 100644 --- a/app/config/permissions.neon +++ b/app/config/permissions.neon @@ -75,19 +75,32 @@ permissions: - viewDetail - allow: true - role: scope-group-external-attributes + role: scope-group-external resource: group actions: - viewExternalAttributes + + - allow: true + role: scope-group-external + resource: group + actions: - setExternalAttributes - - viewStudents - - viewAll - - viewPublicDetail - - viewDetail - addStudent - removeStudent - addMember - removeMember + - addSubgroup + conditions: + - group.isNotArchived + + - allow: true + resource: group + role: scope-group-external + actions: + - addSubgroup + conditions: + - group.isNotArchived + - group.isNotExam - allow: true role: student diff --git a/app/model/entity/GroupExternalAttribute.php b/app/model/entity/GroupExternalAttribute.php index b003e8343..ac2c52725 100644 --- a/app/model/entity/GroupExternalAttribute.php +++ b/app/model/entity/GroupExternalAttribute.php @@ -38,7 +38,7 @@ class GroupExternalAttribute implements JsonSerializable protected $service; /** - * @ORM\Column(type="string", length=32) + * @ORM\Column(name="`key`", type="string", length=32) * Key of the attribute under which it can be searched. */ protected $key; diff --git a/app/model/repository/GroupMemberships.php b/app/model/repository/GroupMemberships.php index ee34959e4..c8f5a7963 100644 --- a/app/model/repository/GroupMemberships.php +++ b/app/model/repository/GroupMemberships.php @@ -14,4 +14,18 @@ public function __construct(EntityManagerInterface $em) { parent::__construct($em, GroupMembership::class); } + + /** + * Find all group memberships for a specific user in non-archived groups. + * @param string $userId + * @return GroupMembership[] + */ + public function findByUser(string $userId): array + { + $qb = $this->createQueryBuilder('gm')->join('gm.group', 'g'); + $qb->where('g.archivedAt IS NULL'); + $qb->andWhere($qb->expr()->eq('gm.user', ':userId')) + ->setParameter('userId', $userId); + return $qb->getQuery()->getResult(); + } } diff --git a/app/model/view/GroupViewFactory.php b/app/model/view/GroupViewFactory.php index 5f01a99df..74c441e42 100644 --- a/app/model/view/GroupViewFactory.php +++ b/app/model/view/GroupViewFactory.php @@ -9,6 +9,8 @@ use App\Model\Entity\AssignmentSolution; use App\Model\Entity\Group; use App\Model\Entity\GroupExamLock; +use App\Model\Entity\GroupExternalAttribute; +use App\Model\Entity\GroupMembership; use App\Model\Entity\ShadowAssignment; use App\Model\Entity\ShadowAssignmentPoints; use App\Model\Entity\User; @@ -326,4 +328,89 @@ public function getGroupExamLocks(Group $group, array $locks): array return $res; }, $locks); } + + /** + * Get a subset of group data relevant (available) for ReCodEx extensions (plugins). + * @param Group $group to be rendered + * @param array $injectAttributes [service][key] => value + * @param string|null $membershipType GroupMembership::TYPE_* value for user who made the search + * @return array + */ + public function getGroupForExtension( + Group $group, + array $injectAttributes = [], + ?string $membershipType = null, + array $adminUsers = [], + ): array { + $admins = []; + foreach ($group->getPrimaryAdminsIds() as $id) { + $admins[$id] = null; + if (array_key_exists($id, $adminUsers)) { + $admins[$id] = $adminUsers[$id]->getNameParts(); // an array with name and title components + $admins[$id]['email'] = $adminUsers[$id]->getEmail(); + } + } + return [ + "id" => $group->getId(), + "parentGroupId" => $group->getParentGroup() ? $group->getParentGroup()->getId() : null, + "admins" => $admins, + "localizedTexts" => $group->getLocalizedTexts()->getValues(), + "organizational" => $group->isOrganizational(), + "exam" => $group->isExam(), + "public" => $group->isPublic(), + "detaining" => $group->isDetaining(), + "attributes" => $injectAttributes, + "membership" => $membershipType, + ]; + } + + /** + * Get a subset of groups data relevant (available) for ReCodEx extensions (plugins). + * @param Group[] $groups + * @param GroupExternalAttribute[] $attributes + * @param GroupMembership[] $memberships GroupMembership[] to filter by + */ + public function getGroupsForExtension(array $groups, array $attributes = [], array $memberships = []): array + { + // create a multi-dimensional map [groupId][attr-service][attr-key] => attr-value + $attributesMap = []; + foreach ($attributes as $attribute) { + $gid = $attribute->getGroup()->getId(); + $attributesMap[$gid] = $attributesMap[$gid] ?? []; + + $service = $attribute->getService(); + $attributesMap[$gid][$service] = $attributesMap[$gid][$service] ?? []; + + $key = $attribute->getKey(); + $attributesMap[$gid][$service][$key] = $attributesMap[$gid][$service][$key] ?? []; + $attributesMap[$gid][$service][$key][] = $attribute->getValue(); + } + + // create membership mapping group-id => membership-type + $membershipsMap = []; + foreach ($memberships as $membership) { + $gid = $membership->getGroup()->getId(); + $membershipsMap[$gid] = $membership->getType(); + } + + $admins = []; + foreach ($groups as $group) { + foreach ($group->getPrimaryAdmins() as $admin) { + $admins[$admin->getId()] = $admin; + } + } + + return array_map( + function (Group $group) use ($attributesMap, $membershipsMap, $admins) { + $id = $group->getId(); + return $this->getGroupForExtension( + $group, + $attributesMap[$id] ?? [], + $membershipsMap[$id] ?? null, + $admins + ); + }, + $groups + ); + } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index aa4086e65..7318a38b6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2228,7 +2228,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object /v1/groups/validate-add-group-data: post: @@ -2292,7 +2292,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object post: summary: 'Update group info' @@ -2365,7 +2365,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object delete: summary: 'Delete a group' @@ -2443,7 +2443,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object '/v1/groups/{id}/archived': post: @@ -2486,7 +2486,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object '/v1/groups/{id}/examPeriod': post: @@ -2539,7 +2539,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object delete: summary: 'Change the group back to regular group (remove information about an exam).' @@ -2568,7 +2568,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object '/v1/groups/{id}/exam/{examId}': get: @@ -2701,7 +2701,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object delete: summary: 'Remove a student from a group' @@ -2738,7 +2738,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object '/v1/groups/{id}/students/{userId}/solutions': get: @@ -2885,7 +2885,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object delete: summary: 'Remove a member (other than student) from a group' @@ -2922,7 +2922,7 @@ paths: properties: success: { description: 'Whether the request was processed successfully.', type: boolean, example: 'true', nullable: false } code: { description: 'HTTP response code.', type: integer, example: '0', nullable: false } - payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'The instance ID of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the group has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: 'Whether the group statistics are public', type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group is detaining', type: boolean, example: 'true', nullable: false }, threshold: { description: 'The group assignment point threshold', type: number, example: '0.1', nullable: false }, pointsLimit: { description: 'The group points limit', type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether there is a strict exam lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All group exams', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } + payload: { description: 'The payload of the response.', properties: { id: { description: 'An identifier of the group', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, externalId: { description: 'An informative, human readable identifier of the group', type: string, example: text, nullable: true }, organizational: { description: 'Whether the group is organizational (no assignments nor students).', type: boolean, example: 'true', nullable: false }, exam: { description: 'Whether the group is an exam group.', type: boolean, example: 'true', nullable: false }, archived: { description: 'Whether the group is archived', type: boolean, example: 'true', nullable: false }, public: { description: 'Should the group be visible to all student?', type: boolean, example: 'true', nullable: false }, directlyArchived: { description: 'Whether the group was explicitly marked as archived', type: boolean, example: 'true', nullable: false }, localizedTexts: { description: 'Localized names and descriptions', type: array, items: { }, nullable: false }, primaryAdminsIds: { description: 'IDs of users which are explicitly listed as direct admins of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, parentGroupId: { description: 'Identifier of the parent group (absent for a top-level group)', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, parentGroupsIds: { description: 'Identifications of groups in descending order.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, childGroups: { description: 'Identifications of child groups.', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, privateData: { description: '', properties: { admins: { description: 'IDs of all users that have admin privileges to this group (including inherited)', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, supervisors: { description: 'IDs of all group supervisors', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, observers: { description: 'IDs of all group observers', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, students: { description: 'IDs of the students of this group', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, instanceId: { description: 'ID of an instance in which the group belongs', type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000, nullable: false }, hasValidLicence: { description: 'Whether the instance where the group belongs has a valid license', type: boolean, example: 'true', nullable: false }, assignments: { description: 'IDs of all group assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, shadowAssignments: { description: 'IDs of all group shadow assignments', type: array, items: { type: string, pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', example: 10000000-2000-4000-8000-160000000000 }, nullable: false }, publicStats: { description: "Whether the student's results are visible to other students", type: boolean, example: 'true', nullable: false }, detaining: { description: 'Whether the group detains the students (so they can be released only by the teacher)', type: boolean, example: 'true', nullable: false }, threshold: { description: 'A relative number of points a student must receive from assignments to fulfill the requirements of the group', type: number, example: '0.1', nullable: false }, pointsLimit: { description: "A minimal number of points that a student must receive to fulfill the group's requirements", type: integer, example: '0', nullable: false }, bindings: { description: 'Entities bound to the group', type: array, items: { }, nullable: false }, examBegin: { description: 'The time when the exam starts if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examEnd: { description: 'The time when the exam ends if there is an exam scheduled', type: integer, example: '1740135333', nullable: false }, examLockStrict: { description: 'Whether the scheduled exam requires a strict access lock', type: boolean, example: 'true', nullable: false }, exams: { description: 'All past exams (with at least one student locked)', type: array, items: { }, nullable: false } }, type: object, nullable: false }, permissionHints: { description: '', type: array, items: { }, nullable: false } }, type: object, nullable: false } type: object '/v1/groups/{id}/assignments': get: @@ -3099,17 +3099,35 @@ paths: description: 'Placeholder response' /v1/group-attributes: get: - summary: 'Return all attributes that correspond to given filtering parameters.' - description: 'Return all attributes that correspond to given filtering parameters.' + summary: 'Return special brief groups entities with injected external attributes and given user affiliation.' + description: 'Return special brief groups entities with injected external attributes and given user affiliation.' operationId: groupExternalAttributesPresenterActionDefault parameters: - - name: filter + name: instance in: query - description: 'JSON-encoded filter query in DNF as [clause OR clause...]' + description: 'ID of the instance, whose groups are returned.' required: true schema: type: string + pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' + nullable: false + - + name: service + in: query + description: 'ID of the external service, of which the attributes are returned. If missing, all attributes are returned.' + required: false + schema: + type: string + nullable: false + - + name: user + in: query + description: 'Relationship info of this user is included for each returned group.' + required: false + schema: + type: string + pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' nullable: false responses: '200': @@ -3127,6 +3145,7 @@ paths: required: true schema: type: string + pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' nullable: false requestBody: content: @@ -3161,21 +3180,49 @@ paths: responses: '200': description: 'Placeholder response' - '/v1/group-attributes/{id}': delete: summary: 'Remove selected attribute' description: 'Remove selected attribute' operationId: groupExternalAttributesPresenterActionRemove parameters: - - name: id + name: groupId in: path - description: 'Identifier of the external attribute.' + description: '' required: true schema: type: string pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' nullable: false + - + name: service + in: query + description: 'Identifier of the external service creating the attribute' + required: true + schema: + type: string + maxLength: 32 + minLength: 1 + nullable: false + - + name: key + in: query + description: 'Key of the attribute (must be valid identifier)' + required: true + schema: + type: string + maxLength: 32 + minLength: 1 + nullable: false + - + name: value + in: query + description: 'Value of the attribute (arbitrary string)' + required: true + schema: + type: string + maxLength: 255 + nullable: false responses: '200': description: 'Placeholder response' @@ -6997,4 +7044,4 @@ paths: responses: '200': description: 'Placeholder response' - + diff --git a/tests/Presenters/GroupExternalAttributesPresenter.phpt b/tests/Presenters/GroupExternalAttributesPresenter.phpt index 5f74012ba..ef57c4ef0 100644 --- a/tests/Presenters/GroupExternalAttributesPresenter.phpt +++ b/tests/Presenters/GroupExternalAttributesPresenter.phpt @@ -2,11 +2,9 @@ $container = require_once __DIR__ . "/../bootstrap.php"; -use App\Model\Entity\Group; -use App\Model\Entity\GroupInvitation; -use App\Model\Repository\Groups; -use App\Model\Repository\GroupInvitations; use App\V1Module\Presenters\GroupExternalAttributesPresenter; +use App\Model\Repository\Users; +use App\Model\Repository\GroupMemberships; use App\Exceptions\BadRequestException; use App\Security\TokenScope; use Doctrine\ORM\EntityManagerInterface; @@ -31,6 +29,12 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase /** @var Nette\Security\User */ private $user; + /** @var Users */ + private $users; + + /** @var GroupMemberships */ + private $groupMemberships; + /** @var App\Security\AccessManager */ private $accessManager; @@ -41,6 +45,8 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase $this->container = $container; $this->em = PresenterTestHelper::getEntityManager($container); $this->user = $container->getByType(\Nette\Security\User::class); + $this->users = $container->getByType(Users::class); + $this->groupMemberships = $container->getByType(GroupMemberships::class); $this->accessManager = $container->getByType(\App\Security\AccessManager::class); } @@ -58,144 +64,129 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase Mockery::close(); } - private function checkFilterResults(array $payload, int $attrCount, int $groupCount) - { - Assert::count(2, $payload); - Assert::true(array_key_exists('attributes', $payload)); - Assert::true(array_key_exists('groups', $payload)); - Assert::count($attrCount, $payload['attributes']); - Assert::count($groupCount, $payload['groups']); - - $indexedGroups = []; - foreach ($payload['groups'] as $group) { - $indexedGroups[$group['id']] = $group['parentGroupId']; - } - - foreach ($payload['attributes'] as $attribute) { - $groupId = $attribute->getGroup()->getId(); - while ($groupId) { - Assert::true(array_key_exists($groupId, $indexedGroups)); - $groupId = $indexedGroups[$groupId]; - } - } - } - - public function testGetAttributesSemester() + public function testGetGroupsNoUser() { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); + PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL]); + $groups = $this->presenter->groups->findBy(['archivedAt' => null]); + Assert::true(count($groups) > 0); + $instanceId = $groups[0]->getInstance()->getId(); - $filter = [[ "key" => "semester", "value" => "summer" ]]; $payload = PresenterTestHelper::performPresenterRequest( $this->presenter, 'V1:GroupExternalAttributes', 'GET', - ['action' => 'default', 'filter' => json_encode($filter)], + ['action' => 'default', 'instance' => $instanceId, 'service' => 'test'], ); - $this->checkFilterResults($payload, 1, 3); - Assert::equal('test', $payload['attributes'][0]->getService()); - Assert::equal('semester', $payload['attributes'][0]->getKey()); - Assert::equal('summer', $payload['attributes'][0]->getValue()); - } + Assert::count(count($groups), $payload); + $indexedPayload = []; + foreach ($payload as $group) { + $indexedPayload[$group['id']] = $group; + } - public function testGetAttributesLecture() - { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); + foreach ($groups as $group) { + Assert::true(array_key_exists($group->getId(), $indexedPayload)); + $groupPayload = $indexedPayload[$group->getId()]; + + // check basic group parameters + Assert::count(count($group->getPrimaryAdmins()), $groupPayload['admins']); + Assert::equal($group->isOrganizational(), $groupPayload['organizational']); + Assert::equal($group->isExam(), $groupPayload['exam']); + Assert::equal($group->isPublic(), $groupPayload['public']); + Assert::equal($group->isDetaining(), $groupPayload['detaining']); + + // attributes + Assert::true(array_key_exists('attributes', $groupPayload)); + $testAttributesCount = 0; + foreach ($group->getExternalAttributes() as $attribute) { + if ($attribute->getService() !== 'test') { + continue; // only 'test' attributes were requested + } + ++$testAttributesCount; + $values = $groupPayload['attributes'][$attribute->getService()][$attribute->getKey()] ?? null; + Assert::true(in_array($attribute->getValue(), $values)); + } - $filter = [[ "key" => "lecture", "value" => "demo" ]]; - $payload = PresenterTestHelper::performPresenterRequest( - $this->presenter, - 'V1:GroupExternalAttributes', - 'GET', - ['action' => 'default', 'filter' => json_encode($filter)], - ); + $actualCount = 0; + foreach ($groupPayload['attributes']['test'] ?? [] as $values) { + $actualCount += count($values); + } + Assert::equal($testAttributesCount, $actualCount); - $this->checkFilterResults($payload, 1, 2); - Assert::equal('test', $payload['attributes'][0]->getService()); - Assert::equal('lecture', $payload['attributes'][0]->getKey()); - Assert::equal('demo', $payload['attributes'][0]->getValue()); + // user membership + Assert::null($groupPayload['membership']); + } } - public function testGetAttributesMulti() + public function testGetGroupsStudent() { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); + PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL]); + $users = $this->users->findBy(['email' => PresenterTestHelper::STUDENT_GROUP_MEMBER_LOGIN]); + Assert::count(1, $users); + $user = current($users); + + $groups = $this->presenter->groups->findBy(['archivedAt' => null]); + Assert::true(count($groups) > 0); + $instanceId = $groups[0]->getInstance()->getId(); - $filter = [ - [ "service" => "test", "key" => "semester", ], - [ "key" => "lecture", "value" => "demo" ], - ]; $payload = PresenterTestHelper::performPresenterRequest( $this->presenter, 'V1:GroupExternalAttributes', 'GET', - ['action' => 'default', 'filter' => json_encode($filter)], + ['action' => 'default', 'instance' => $instanceId, 'service' => 'test', 'user' => $user->getId()], ); - $this->checkFilterResults($payload, 3, 4); - $attrs = []; - foreach ($payload['attributes'] as $attr) { - Assert::equal('test', $attr->getService()); - $attrs[] = $attr->getKey() . '=' . $attr->getValue(); + Assert::count(count($groups), $payload); + $total = 0; + foreach ($payload as $group) { + $memberships = $this->groupMemberships->findBy(['group' => $group['id'], 'user' => $user->getId()]); + if (empty($memberships)) { + Assert::null($group['membership']); + } else { + Assert::count(1, $memberships); + Assert::equal(current($memberships)->getType(), $group['membership']); + ++$total; + } } - sort($attrs); - Assert::equal(['lecture=demo', 'semester=summer', 'semester=winter'], $attrs); + Assert::true($total > 0); } - public function testGetAttributesEmpty() + public function testGetGroupsTeacher() { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); + PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL]); + $users = $this->users->findBy(['email' => PresenterTestHelper::GROUP_SUPERVISOR_LOGIN]); + Assert::count(1, $users); + $user = current($users); - $filter = [[ "key" => "lecture", "value" => "sleeping" ]]; - $payload = PresenterTestHelper::performPresenterRequest( - $this->presenter, - 'V1:GroupExternalAttributes', - 'GET', - ['action' => 'default', 'filter' => json_encode($filter)], - ); + $groups = $this->presenter->groups->findBy(['archivedAt' => null]); + Assert::true(count($groups) > 0); + $instanceId = $groups[0]->getInstance()->getId(); - $this->checkFilterResults($payload, 0, 0); - } - - public function testGetAttributesEmpty2() - { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); - - $filter = [[ "service" => "3rdparty", "key" => "lecture" ]]; $payload = PresenterTestHelper::performPresenterRequest( $this->presenter, 'V1:GroupExternalAttributes', 'GET', - ['action' => 'default', 'filter' => json_encode($filter)], + ['action' => 'default', 'instance' => $instanceId, 'service' => 'test', 'user' => $user->getId()], ); - $this->checkFilterResults($payload, 0, 0); - } - - public function testGetAttributesFails() - { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); - - $filters = [ - [ "key" => "semester", "value" => "summer" ], - "semester: summer", - [[ "semester" => "summer" ]], - [[ "key" => "semester", "value" => 1 ]], - ]; - foreach ($filters as $filter) { - Assert::exception(function () use ($filter) { - PresenterTestHelper::performPresenterRequest( - $this->presenter, - 'V1:GroupExternalAttributes', - 'GET', - ['action' => 'default', 'filter' => json_encode($filter)], - ); - }, BadRequestException::class); + Assert::count(count($groups), $payload); + $total = 0; + foreach ($payload as $group) { + $memberships = $this->groupMemberships->findBy(['group' => $group['id'], 'user' => $user->getId()]); + if (empty($memberships)) { + Assert::null($group['membership']); + } else { + Assert::count(1, $memberships); + Assert::equal(current($memberships)->getType(), $group['membership']); + ++$total; + } } + Assert::true($total > 0); } public function testGetAttributesAdd() { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); + PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL]); $attributes = $this->presenter->groupExternalAttributes->findAll(); Assert::count(5, $attributes); $attribute = current($attributes); @@ -223,7 +214,7 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase public function testGetAttributesRemove() { - PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]); + PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL]); $attributes = $this->presenter->groupExternalAttributes->findAll(); Assert::count(5, $attributes); $attribute = current($attributes); @@ -233,7 +224,13 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase $this->presenter, 'V1:GroupExternalAttributes', 'DELETE', - ['action' => 'remove', 'id' => $id], + [ + 'action' => 'remove', + 'groupId' => $attribute->getGroup()->getId(), + 'service' => $attribute->getService(), + 'key' => $attribute->getKey(), + 'value' => $attribute->getValue() + ], ); Assert::equal("OK", $payload); diff --git a/tests/Presenters/SubmitPresenter.phpt b/tests/Presenters/SubmitPresenter.phpt index 573879c3d..98b43a4f3 100644 --- a/tests/Presenters/SubmitPresenter.phpt +++ b/tests/Presenters/SubmitPresenter.phpt @@ -57,7 +57,7 @@ class TestSubmitPresenter extends Tester\TestCase $this->assignments = $container->getByType(Assignments::class); $this->assignmentSolvers = $container->getByType(AssignmentSolvers::class); - // patch container, since we cannot create actual file storage manarer + // patch container, since we cannot create actual file storage manager $fsName = current($this->container->findByType(FileStorageManager::class)); $this->container->removeService($fsName); $this->container->addService($fsName, new FileStorageManager( @@ -270,7 +270,7 @@ class TestSubmitPresenter extends Tester\TestCase Assert::equal($tasksCount, $webSocketChannel['expectedTasksCount']); $author = $this->presenter->users->findOrThrow($solution['authorId']); - $solvers = $this->assignmentSolvers->findBy([ "assignment" => $assignment, "solver" => $author ]); + $solvers = $this->assignmentSolvers->findBy(["assignment" => $assignment, "solver" => $author]); Assert::count(1, $solvers); Assert::equal($solvers[0]->getLastAttemptIndex(), $solution['attemptIndex']); }