Added use_classroom_owner_access Boolean variable to gam.cfg

This commit is contained in:
Ross Scroggs
2024-02-18 20:59:58 -08:00
parent d949ca2cad
commit d5255615fd
10 changed files with 363 additions and 211 deletions

View File

@@ -3,9 +3,10 @@
- [Notes](#notes)
- [Definitions](#definitions)
- [Create classroom invitations](#create-classroom-invitations)
- [Accept classroom invitations](#accept-classroom-invitations)
- [Delete classroom invitations](#delete-classroom-invitations)
- [Accept classroom invitations by user](#accept-classroom-invitations-by-user)
- [Delete classroom invitations by user](#delete-classroom-invitations-by-user)
- [Display classroom invitations by user](#display-classroom-invitations-by-user)
- [Delete classroom invitations by course](#delete-classroom-invitations-by-course)
- [Display classroom invitations by course](#display-classroom-invitations-by-course)
## API documentation
@@ -24,8 +25,6 @@ Scope: https://www.googleapis.com/auth/classroom.rosters , Checked: FA
```
Follow the directions to authorize the Service Account scopes.
The Classroom API does not support inviting users from outside your domain.
## Definitions
```
<DomainName> ::= <String>(.<String>)+
@@ -49,12 +48,18 @@ The Classroom API does not support inviting users from outside your domain.
Invite users to classes.
```
gam <UserTypeEntity> create classroominvitation courses <CourseEntity> [role owner|student|teacher]
[adminaccess|asadmin] [csvformat] [todrive <ToDriveAttributes>*] [formatjson [quotechar <Character>]]
[adminaccess|asadmin]
[csv|csvformat] [todrive <ToDriveAttributes>*] [formatjson [quotechar <Character>]]
```
If `role` is not specified, `student` will be used.
You can only invite a co-teacher to be an owner of a course.
By default, classroom invitations are issued by the owner of the course, the `adminaccess` option causes the invitations to be issued by the admin named in `oauth2.txt`.
By default, when an invitation is created, GAM outputs details of the invitation as indented keywords and values.
* `csv|csvformat [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]` - Output the details in CSV format.
### Example
Suppose you have a CSV file CourseStudent.csv with two columns: Course,Student.
@@ -66,11 +71,13 @@ This command will invite all students to their courses in parallel
```
gam redirect stdout ./Invites.out multiprocess redirect stderr stdout multiprocess csv CourseStudent.csv gam user ~Student create classroominvitation role student course ~Course
```
## Accept classroom invitations
Accept classroom invitations for users. You can only invite a co-teacher to be an owner of a course.
## Accept classroom invitations by user
Accept classroom invitations for users.
```
gam <UserTypeEntity> accept classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
```
`<UserTypeEntity>` must specify users in your domain.
By default, all invitations for the specified users will be accepted.
Select specific invitations to accept:
@@ -81,11 +88,13 @@ Select courses and accept invitations for those courses.
By default, invitations for all roles will be accepted; you can limit the acceptances to invitations of a specific role.
## Delete classroom invitations
## Delete classroom invitations by user
Delete classroom invitations for users.
```
gam <UserTypeEntity> delete classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
```
`<UserTypeEntity>` must specify users in your domain.
By default, all invitations for the specified users will be deleted.
Select specific invitations to delete:
@@ -104,8 +113,23 @@ gam <UserTypeEntity> show classroominvitations [role all|owner|student|teacher]
gam <UserTypeEntity> print classroominvitations [todrive <ToDriveAttributes>*] [role all|owner|student|teacher]
[formatjson [quotechar <Character>]]
```
`<UserTypeEntity>` must specify users in your domain.
By default, invitations for all roles will be displayed; you can limit the display to invitations of a specific role.
## Delete classroom invitations by course
Delete classroom invitations for courses. This command must be used to delete non-domain member invitations.
```
gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
```
Select courses and delete invitations for those courses.
* `courses <CourseEntity>` - Specify courses
Select specific invitations to delete:
* `ids <ClassroomInvitationIDEntity>` - Specify invitation IDs
Select invitations to delete by role. By default, invitations for all roles will be deleted; you can limit the deletions to invitations of a specific role.
## Display classroom invitations by course
```
gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])

View File

@@ -48,10 +48,7 @@ The `quotechar <Character>` option allows you to choose an alternate quote chara
## Display File Ownership for Old files
If the above commands fail, you can try to loop through all accounts, however this might take a long time if you are on a large Google Workspace Account.
```
gam config auto_batch_min 1 redirect csv - multiprocess redirect stderr null multiprocess all users print filelist select id <DriveFileID> fields id,name,owners.emailaddress norecursion showownedby any
```
Starting with version 6.07.26, this can be made more efficient by terminating processing after the owner is identified.
```
gam config auto_batch_min 1 multiprocessexit rc=0 redirect csv - multiprocess redirect stderr null multiprocess all users print filelist select id <DriveFileID> fields id,name,owners.emailaddress norecursion showownedby any
gam config auto_batch_min 1 multiprocessexit rc=0 redirect csv - multiprocess redirect stderr null multiprocess all users print filelist select name <DriveFileName> fields id,name,owners.emailaddress norecursion showownedby any
```

View File

@@ -10,6 +10,26 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation
### 6.69.00
Added `use_classroom_owner_access` Boolean variable to `gam.cfg` that controls how GAM gets
classroom member information and removes students/teachers. Client access does not provide
complete information about non-domain students/teachers.
* `False` - Use client access; this is the default. Use if you don't have non-domain members in your courses.
* `True` - Use service account access as the classroom owner. An extra API call is required per course to authenticate the owner; this will affect performance
Added the following command which must be used to delete classroom invitations for non-domain students/teachers.
```
gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
```
You can obtain the classroom invitation IDs with these commands:
```
gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[role all|owner|student|teacher] [formatjson]
gam print classroominvitations [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[role all|owner|student|teacher] [formatjson [quotechar <Character>]]
```
### 6.68.08
Updated `gam <UserTypeEntity> print filelist|drivefileacls|shareddriveacls ... oneitemperrow` to print

View File

@@ -334,7 +334,7 @@ writes the credentials into the file oauth2.txt.
admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt
admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version
WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found
GAMADV-XTD3 6.68.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.69.00 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
@@ -1002,7 +1002,7 @@ writes the credentials into the file oauth2.txt.
C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt
C:\GAMADV-XTD3>gam version
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
GAMADV-XTD3 6.68.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.69.00 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
Windows-10-10.0.17134 AMD64

View File

@@ -13,3 +13,4 @@ Thank you.
* Goldy Arora - https://www.goldyarora.com/license-notifier/
* Paul Ogier (Taming.Tech) - GAMADV-XTD3 Tutorials https://www.youtube.com/watch?v=g9LDeyXQNLI&list=PL_dLiK09pJVhKJxZHNk9CHK0q5hkZ856w
* Paul Ogier (Taming.Tech) - GAMADV-XTD3 Course on Udemy https://taming.tech/GAMCourse
* Paul Ogier (Taming.Tech) - https://taming.tech/taming-gam-a-practical-guide-to-gam-and-gamadv-xtd3/

View File

@@ -3,7 +3,7 @@
Print the current version of Gam with details
```
gam version
GAMADV-XTD3 6.68.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.69.00 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
@@ -15,7 +15,7 @@ Time: 2023-06-02T21:10:00-07:00
Print the current version of Gam with details and time offset information
```
gam version timeoffset
GAMADV-XTD3 6.68.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.69.00 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
@@ -27,7 +27,7 @@ Your system time differs from www.googleapis.com by less than 1 second
Print the current version of Gam with extended details and SSL information
```
gam version extended
GAMADV-XTD3 6.68.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.69.00 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
@@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/Admin/bin/gamadv-xtd3
Version Check:
Current: 5.35.08
Latest: 6.68.08
Latest: 6.69.00
echo $?
1
```
@@ -72,7 +72,7 @@ echo $?
Print the current version number without details
```
gam version simple
6.68.08
6.69.00
```
In Linux/MacOS you can do:
```
@@ -82,7 +82,7 @@ echo $VER
Print the current version of Gam and address of this Wiki
```
gam help
GAM 6.68.08 - https://github.com/taers232c/GAMADV-XTD3
GAM 6.69.00 - https://github.com/taers232c/GAMADV-XTD3
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64

View File

@@ -150,7 +150,7 @@ csv_input_column_delimiter
Default: ','
csv_input_no_escape_char
When reading a CSV file, should `\` be ignored as an escape character.
Set this to False if the input file data was written using `\` as an escape character.
Set this to False if the input file data was written using `\` as an escape character.
Default: True
csv_input_quote_char
A one-character string used to quote fields containing special characters,
@@ -220,7 +220,7 @@ csv_output_line_terminator
Default: lf
csv_output_no_escape_char
When writing a CSV file, should `\` be ignored as an escape character.
Set this to True if the output file data is to be read by a non-Python program.
Set this to True if the output file data is to be read by a non-Python program.
Default: False
csv_output_quote_char
A one-character string used to quote fields containing special characters,
@@ -420,23 +420,23 @@ print_agu_domains
gam print groups
gam print|show group-members
gam print users
This allows predefining the list of domains so they don't have to be specified in each command.
This allows predefining the list of domains so they don't have to be specified in each command.
Default: Blank
print_cros_ous
A comma separated list of org unit that are used in these commands:
gam print cros
gam print crosactivity
This allows predefining the list of org units so they don't have to be specified in each command.
This allows predefining the list of org units so they don't have to be specified in each command.
Default: Blank
print_cros_ous_and_children
A comma separated list of org unit names that are used in these commands:
gam print cros
gam print crosactivity
This allows predefining the list of org units so they don't have to be specified in each command.
This allows predefining the list of org units so they don't have to be specified in each command.
Default: Blank
process_wait_limit
When processing batch/CSV files, how long (in seconds) GAM should wait for all batch|csv processes to complete
after all have been started. If the limit is reached, GAM terminates any remaining processes.
When processing batch/CSV files, how long (in seconds) GAM should wait for all batch|csv processes to complete
after all have been started. If the limit is reached, GAM terminates any remaining processes.
Default: 0: no limit
Range: 0 - Unlimited
quick_cros_move
@@ -573,6 +573,13 @@ update_cros_ou_with_id
Set to true if you are getting the following error:
`400: invalidInput - Invalid Input: Inconsistent Orgunit id and path in request`
Default: False
use_classroom_owner_access
How is classroom member information obtained and how are classroom members deleted.
Client access does not provide complete information about non-domain students/teachers.
When False, GAM uses client access to get classroom member information and to delete members
When True, GAM uses service account access as the classroom owner.
An extra API call is required per course to authenticate the owner
Default: False
use_projectid_as_name
When False, new projects have a default project name of "GAM Project"
and a default app name of "GAM".

View File

@@ -2876,7 +2876,7 @@ gam course <CourseID> delete topic <CourseTopicID>
gam course <CourseID> create|add teachers [makefirstteacherowner] <UserItem>
gam course <CourseID> create|add students <UserItem>
gam course <CourseID> delete|remove teachers|students <UserItem>
gam course <CourseID> delete|remove teachers|students [owneraccess] <UserItem>
gam course <CourseID> clear teachers|students
gam course <CourseID> sync teachers [addonly|removeonly] [makefirstteacherowner] <UserTypeEntity>
gam course <CourseID> sync students [addonly|removeonly] <UserTypeEntity>
@@ -2889,15 +2889,17 @@ gam courses <CourseEntity> delete topic <CourseTopicIDEntity>
gam courses <CourseEntity> create|add teachers [makefirstteacherowner] <UserTypeEntity>
gam courses <CourseEntity> create|add students <UserTypeEntity>
gam courses <CourseEntity> delete|remove teachers|students <UserTypeEntity>
gam courses <CourseEntity> delete|remove teachers|students [owneraccess] <UserTypeEntity>
gam courses <CourseEntity> clear teachers|students
gam courses <CourseEntity> sync teachers [addonly|removeonly] [makefirstteacherowner] <UserTypeEntity>
gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
gam info course <CourseID> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
gam info course <CourseID> [owneraccess]
[owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
[formatjson]
gam info courses <CourseEntity> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
gam info courses <CourseEntity> [owneraccess]
[owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
[formatjson]
gam print courses [todrive <ToDriveAttribute>*]
@@ -3041,8 +3043,8 @@ gam print course-works [todrive <ToDriveAttribute>*]
# Classroom - Invitations
gam <UserTypeEntity> create classroominvitation courses <CourseEntity> [role owner|student|teacher]
[adminaccess|asadmin] [csv|csvformat] [todrive <ToDriveAttribute>*]
[formatjson [quotechar <Character>]]
[adminaccess|asadmin]
[csv|csvformat] [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]
gam <UserTypeEntity> accept classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
gam <UserTypeEntity> delete classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
gam <UserTypeEntity> show classroominvitations [role all|owner|student|teacher]
@@ -3050,6 +3052,7 @@ gam <UserTypeEntity> show classroominvitations [role all|owner|student|teacher]
gam <UserTypeEntity> print classroominvitations [todrive <ToDriveAttribute>*] [role all|owner|student|teacher]
[formatjson [quotechar <Character>]]
gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[role all|owner|student|teacher]
[formatjson]

View File

@@ -2,6 +2,26 @@
Merged GAM-Team version
6.69.00
Added `use_classroom_owner_access` Boolean variable to `gam.cfg` that controls how GAM gets
classroom member information and removes students/teachers. Client access does not provide
complete information about non-domain students/teachers.
* `False` - Use client access; this is the default. Use if you don't have non-domain members in your courses.
* `True` - Use service account access as the classroom owner. An extra API call is required per course to authenticate the owner; this will affect performance
Added the following command which must be used to delete classroom invitations for non-domain students/teachers.
```
gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
```
You can obtain the classroom invitation IDs with these commands:
```
gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[role all|owner|student|teacher] [formatjson]
gam print classroominvitations [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[role all|owner|student|teacher] [formatjson [quotechar <Character>]]
```
6.68.08
Updated `gam <UserTypeEntity> print filelist|drivefileacls|shareddriveacls ... oneitemperrow` to print

View File

@@ -3395,7 +3395,7 @@ def SetGlobalVariables():
def _getCfgHeaderFilter(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
headerFilters = []
if not value:
if not value or (len(value) == 2 and _stringInQuotes(value)):
return headerFilters
splitStatus, filters = shlexSplitListStatus(value)
if splitStatus:
@@ -3411,7 +3411,7 @@ def SetGlobalVariables():
def _getCfgHeaderForce(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
headerForce = []
if not value:
if not value or (len(value) == 2 and _stringInQuotes(value)):
return headerForce
splitStatus, headerForce = shlexSplitListStatus(value)
if not splitStatus:
@@ -6338,15 +6338,15 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
elif entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_TEACHERS, Cmd.ENTITY_STUDENTS}:
croom = buildGAPIObject(API.CLASSROOM)
if not noListConversion:
courses = convertEntityToList(entity)
courseIdList = convertEntityToList(entity)
else:
courses = [entity]
for course in courses:
courseId = addCourseIdScope(course)
courseIdList = [entity]
_, _, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
for courseId, courseInfo in coursesInfo.items():
try:
if entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_TEACHERS}:
printGettingAllEntityItemsForWhom(Ent.TEACHER, removeCourseIdScope(courseId), entityType=Ent.COURSE)
result = callGAPIpages(croom.courses().teachers(), 'list', 'teachers',
result = callGAPIpages(courseInfo['croom'].courses().teachers(), 'list', 'teachers',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -6359,7 +6359,7 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
entityList.append(email)
if entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_STUDENTS}:
printGettingAllEntityItemsForWhom(Ent.STUDENT, removeCourseIdScope(courseId), entityType=Ent.COURSE)
result = callGAPIpages(croom.courses().students(), 'list', 'students',
result = callGAPIpages(courseInfo['croom'].courses().students(), 'list', 'students',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -17979,7 +17979,7 @@ def doPrintAliases():
except (GAPI.invalidOrgunit, GAPI.invalidInput):
entityActionFailedWarning([Ent.ALIAS, None], invalidQuery(query))
continue
except GAPI.domainNotFound as e :
except GAPI.domainNotFound as e:
entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e))
continue
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
@@ -18008,13 +18008,16 @@ def doPrintAliases():
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
throwReasons=GAPI.GROUP_LIST_THROW_REASONS,
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
query=query, orderBy='email',
fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs)
for group in entityList:
writeAliases(group, group['email'], 'Group')
except GAPI.domainNotFound as e :
except (GAPI.invalidMember, GAPI.invalidInput) as e:
if not invalidMember(query):
entityActionFailedExit([Ent.GROUP, None], str(e))
except GAPI.domainNotFound as e:
entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e))
continue
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
@@ -41517,7 +41520,7 @@ def doCreateUser():
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE],
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.CONDITION_NOT_MET],
body=body,
fields=fields,
resolveConflictAccount=resolveConflictAccount)
@@ -41536,7 +41539,7 @@ def doCreateUser():
except GAPI.invalidOrgunit:
entityActionFailedExit([Ent.USER, user], Msg.INVALID_ORGUNIT)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter) as e:
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter, GAPI.conditionNotMet) as e:
entityActionFailedExit([Ent.USER, user], str(e))
if PwdOpts.filename and PwdOpts.password:
writeFile(PwdOpts.filename, f'{user},{PwdOpts.password}\n', mode='a', continueOnError=True)
@@ -41698,7 +41701,7 @@ def updateUsers(entityList):
result = callGAPI(cd.users(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE],
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.CONDITION_NOT_MET],
body=body,
fields=fields,
resolveConflictAccount=resolveConflictAccount)
@@ -41730,7 +41733,7 @@ def updateUsers(entityList):
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_ORGUNIT, i, count)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter, GAPI.insufficientArchivedUserLicenses,
GAPI.conflict, GAPI.badRequest, GAPI.backendError, GAPI.systemError) as e:
GAPI.conflict, GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
# gam update users <UserTypeEntity> ...
@@ -44212,6 +44215,8 @@ class CourseAttributes():
def __init__(self, croom, updateMode):
self.croom = croom
self.ocroom = croom
self.tcroom = None
self.updateMode = updateMode
self.body = {}
self.courseId = None
@@ -44371,15 +44376,20 @@ class CourseAttributes():
missingArgumentExit('copyfrom <CourseID>')
else:
return True
# ocroom - copyfrom course owner
if self.announcementStates or self.materialStates or self.workStates or self.copyTopics or self.members != 'none':
_, self.ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{self.ownerId}')
if self.ocroom is None:
return False
if self.members != 'none':
_, self.teachers, self.students = _getCourseAliasesMembers(self.croom, self.croom, self.courseId, {'members': self.members},
_, self.teachers, self.students = _getCourseAliasesMembers(self.croom, self.ocroom, self.courseId, {'members': self.members},
'nextPageToken,teachers(profile(emailAddress,id))',
'nextPageToken,students(profile(emailAddress))')
if self.announcementStates:
printGettingAllEntityItemsForWhom(Ent.COURSE_ANNOUNCEMENT_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
_gettingCourseEntityQuery(Ent.COURSE_ANNOUNCEMENT_STATE, self.announcementStates))
try:
self.courseAnnouncements = callGAPIpages(self.croom.courses().announcements(), 'list', 'announcements',
self.courseAnnouncements = callGAPIpages(self.ocroom.courses().announcements(), 'list', 'announcements',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -44396,7 +44406,7 @@ class CourseAttributes():
printGettingAllEntityItemsForWhom(Ent.COURSE_MATERIAL_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
_gettingCourseEntityQuery(Ent.COURSE_MATERIAL_STATE, self.materialStates))
try:
self.courseMaterials = callGAPIpages(self.croom.courses().courseWorkMaterials(), 'list', 'courseWorkMaterial',
self.courseMaterials = callGAPIpages(self.ocroom.courses().courseWorkMaterials(), 'list', 'courseWorkMaterial',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -44417,7 +44427,7 @@ class CourseAttributes():
printGettingAllEntityItemsForWhom(Ent.COURSE_WORK_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
_gettingCourseEntityQuery(Ent.COURSE_WORK_STATE, self.workStates))
try:
self.courseWorks = callGAPIpages(self.croom.courses().courseWork(), 'list', 'courseWork',
self.courseWorks = callGAPIpages(self.ocroom.courses().courseWork(), 'list', 'courseWork',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -44441,7 +44451,7 @@ class CourseAttributes():
if self.copyTopics:
printGettingAllEntityItemsForWhom(Ent.COURSE_TOPIC, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0)
try:
courseTopics = callGAPIpages(self.croom.courses().topics(), 'list', 'topic',
courseTopics = callGAPIpages(self.ocroom.courses().topics(), 'list', 'topic',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -44517,14 +44527,16 @@ class CourseAttributes():
newCourseId = newCourse['id']
ownerId = newCourse['ownerId']
teacherFolderId = newCourse['teacherFolder']['id']
# tcroom - new/update course owner
if self.announcementStates or self.materialStates or self.workStates or self.copyTopics:
_, tcroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{ownerId}')
if tcroom is None:
_, self.tcroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{ownerId}')
if self.tcroom is None:
return
if (self.announcementStates or self.materialStates or self.workStates) and self.copyMaterialsFiles:
_, tdrive = buildGAPIServiceObject(API.DRIVE3, f'uid:{ownerId}')
if tdrive is None:
return
# Adds are done with domain admin
if self.members in {'all', 'students'}:
addParticipants = [student['profile']['emailAddress'] for student in self.students if 'emailAddress' in student['profile']]
_batchAddItemsToCourse(self.croom, newCourseId, i, count, addParticipants, Ent.STUDENT)
@@ -44533,7 +44545,7 @@ class CourseAttributes():
_batchAddItemsToCourse(self.croom, newCourseId, i, count, addParticipants, Ent.TEACHER)
if self.copyTopics:
try:
newCourseTopics = callGAPIpages(self.croom.courses().topics(), 'list', 'topic',
newCourseTopics = callGAPIpages(self.tcroom.courses().topics(), 'list', 'topic',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.FAILED_PRECONDITION, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, fields='nextPageToken,topic(topicId,name)',
@@ -44556,7 +44568,7 @@ class CourseAttributes():
[Ent.COURSE, self.courseId], Msg.DUPLICATE, j, jcount)
continue
try:
result = callGAPI(tcroom.courses().topics(), 'create',
result = callGAPI(self.tcroom.courses().topics(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, body={'name': topicName}, fields='topicId')
@@ -44583,7 +44595,7 @@ class CourseAttributes():
if self.copyMaterialsFiles:
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncementId, teacherFolderId)
try:
result = callGAPI(tcroom.courses().announcements(), 'create',
result = callGAPI(self.tcroom.courses().announcements(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR, GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -44619,7 +44631,7 @@ class CourseAttributes():
if newTopicId:
body['topicId'] = newTopicId
try:
result = callGAPI(tcroom.courses().courseWorkMaterials(), 'create',
result = callGAPI(self.tcroom.courses().courseWorkMaterials(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR, GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -44658,7 +44670,7 @@ class CourseAttributes():
body.pop('dueDate', None)
body.pop('dueTime', None)
try:
result = callGAPI(tcroom.courses().courseWork(), 'create',
result = callGAPI(self.tcroom.courses().courseWork(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR,
@@ -45003,6 +45015,30 @@ def _convertCourseUserIdToEmail(croom, userId, emails, entityValueList, i, count
emails[userId] = userEmail
return userEmail
def _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess):
coursesInfo = {}
for courseId in courseIds:
courseId = addCourseIdScope(courseId)
if courseId not in coursesInfo:
try:
course = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='name,ownerId')
if useOwnerAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
else:
ocroom = croom
if ocroom is not None:
coursesInfo[courseId] = {'name': course['name'], 'croom': ocroom}
except GAPI.notFound:
entityDoesNotExistWarning(Ent.COURSE, courseId)
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
return 0, len(coursesInfo), coursesInfo
def _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, showGettings=False, i=0, count=0):
aliases = []
teachers = []
@@ -45059,7 +45095,7 @@ def _doInfoCourses(courseIdList):
courseShowProperties = _initCourseShowProperties()
courseShowProperties['ownerEmail'] = True
ownerEmails = {}
useOwnerAccess = False
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
@@ -45069,8 +45105,6 @@ def _doInfoCourses(courseIdList):
useOwnerAccess = True
else:
FJQC.GetFormatJSON(myarg)
coursesInfo = {}
_getCoursesOwnerInfo(croom, courseIdList, coursesInfo, not useOwnerAccess)
fields = _setCourseFields(courseShowProperties, False)
if courseShowProperties['members'] != 'none':
if courseShowProperties['countsOnly']:
@@ -45081,14 +45115,9 @@ def _doInfoCourses(courseIdList):
studentsFields = 'nextPageToken,students(profile)'
else:
teachersFields = studentsFields = None
i = 0
count = len(courseIdList)
for courseId in courseIdList:
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess)
for courseId, courseInfo in coursesInfo.items():
i += 1
courseId = addCourseIdScope(courseId)
courseInfo = coursesInfo[courseId]
if not courseInfo:
continue
try:
course = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
@@ -45159,12 +45188,14 @@ def _doInfoCourses(courseIdList):
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
# gam info courses <CourseEntity> [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
# gam info courses <CourseEntity> [owneraccess]
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
def doInfoCourses():
_doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
# gam info course <CourseID> [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
# gam info course <CourseID> [owneraccess]
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
def doInfoCourse():
_doInfoCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
@@ -45322,6 +45353,7 @@ def doPrintCourses():
ownerEmails = {}
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showItemCountOnly = False
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
@@ -45342,7 +45374,7 @@ def doPrintCourses():
if applyCourseItemFilter:
if courseShowProperties['fields']:
courseShowProperties['fields'].append(courseItemFilter['timefilter'])
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
if coursesInfo is None:
if showItemCountOnly:
writeStdout('0\n')
@@ -45381,6 +45413,12 @@ def doPrintCourses():
for field in courseShowProperties['skips']:
course.pop(field, None)
courseId = course['id']
if useOwnerAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
if not ocroom:
continue
else:
ocroom = croom
if courseShowProperties['ownerEmail']:
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
[Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count)
@@ -45389,7 +45427,7 @@ def doPrintCourses():
if showItemCountOnly:
itemCount += 1
continue
aliases, teachers, students = _getCourseAliasesMembers(croom, croom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
aliases, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
if courseShowProperties['aliases']:
if not courseShowProperties['aliasesInColumns']:
course['Aliases'] = delimiter.join([removeCourseAliasScope(alias['alias']) for alias in aliases])
@@ -45614,7 +45652,6 @@ def doPrintCourseTopics():
courseId = course['id']
if courseTopicIdsLists:
courseTopicIds = courseTopicIdsLists[courseId]
if not courseTopicIds:
fields = getItemFieldsFromFieldsList('topic', fieldsList)
printGettingAllEntityItemsForWhom(Ent.COURSE_TOPIC, Ent.TypeName(Ent.COURSE, courseId), i, count)
@@ -46098,6 +46135,7 @@ def doPrintCourseParticipants():
courseShowProperties = _initCourseShowProperties(['name'])
courseShowProperties['members'] = 'all'
showItemCountOnly = False
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
@@ -46110,7 +46148,7 @@ def doPrintCourseParticipants():
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
if coursesInfo is None:
if showItemCountOnly:
writeStdout('0\n')
@@ -46132,7 +46170,13 @@ def doPrintCourseParticipants():
for course in coursesInfo:
i += 1
courseId = course['id']
_, teachers, students = _getCourseAliasesMembers(croom, croom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
if useOwnerAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
if not ocroom:
continue
else:
ocroom = croom
_, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
if showItemCountOnly:
if courseShowProperties['members'] != 'students':
itemCount += len(teachers)
@@ -46319,29 +46363,6 @@ def _batchRemoveItemsFromCourse(croom, courseId, i, count, removeParticipants, r
dbatch.execute()
Ind.Decrement()
def _getCoursesOwnerInfo(croom, courseIds, coursesInfo, useAdminAccess):
for courseId in courseIds:
courseId = addCourseIdScope(courseId)
if courseId not in coursesInfo:
coursesInfo[courseId] = {}
try:
info = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='name,ownerId')
if not useAdminAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{info["ownerId"]}')
else:
ocroom = croom
if ocroom is not None:
coursesInfo[courseId] = {'name': info['name'], 'croom': ocroom}
except GAPI.notFound:
entityDoesNotExistWarning(Ent.COURSE, courseId)
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
def _updateCourseOwner(croom, courseId, owner, i, count):
action = Act.Get()
Act.Set(Act.UPDATE_OWNER)
@@ -46398,7 +46419,6 @@ def doCourseAddItems(courseIdList, getEntityListArg):
makeFirstTeacherOwner = checkArgumentPresent(['makefirstteacherowner'])
else:
makeFirstTeacherOwner = False
coursesInfo = {}
if not getEntityListArg:
if role in {Ent.STUDENT, Ent.TEACHER}:
addItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
@@ -46422,22 +46442,17 @@ def doCourseAddItems(courseIdList, getEntityListArg):
if makeFirstTeacherOwner and addItems:
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
checkForExtraneousArguments()
_getCoursesOwnerInfo(croom, courseIdList, coursesInfo, role != Ent.COURSE_TOPIC)
i = 0
count = len(courseIdList)
for courseId in courseIdList:
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, role == Ent.COURSE_TOPIC)
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseParticipantLists:
addItems = courseParticipantLists[courseId]
firstTeacher = None
if makeFirstTeacherOwner and addItems:
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
courseId = addCourseIdScope(courseId)
courseInfo = coursesInfo[courseId]
if courseInfo:
_batchAddItemsToCourse(courseInfo['croom'], courseId, i, count, addItems, role)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(courseInfo['croom'], courseId, firstTeacher, i, count)
_batchAddItemsToCourse(courseInfo['croom'], courseId, i, count, addItems, role)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(courseInfo['croom'], courseId, firstTeacher, i, count)
# gam courses <CourseEntity> remove alias <CourseAliasEntity>
# gam course <CourseID> remove alias <CourseAlias>
@@ -46448,10 +46463,11 @@ def doCourseAddItems(courseIdList, getEntityListArg):
def doCourseRemoveItems(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
coursesInfo = {}
if not getEntityListArg:
if role in {Ent.STUDENT, Ent.TEACHER}:
useOwnerAccess = checkArgumentPresent(OWNER_ACCESS_OPTIONS)
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
if checkArgumentPresent(OWNER_ACCESS_OPTIONS):
useOwnerAccess = True
removeItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
elif role == Ent.COURSE_ALIAS:
useOwnerAccess = False
@@ -46473,17 +46489,12 @@ def doCourseRemoveItems(courseIdList, getEntityListArg):
removeItems = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY, shlexSplit=True)
courseParticipantLists = removeItems if isinstance(removeItems, dict) else None
checkForExtraneousArguments()
_getCoursesOwnerInfo(croom, courseIdList, coursesInfo, not useOwnerAccess)
i = 0
count = len(courseIdList)
for courseId in courseIdList:
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess)
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseParticipantLists:
removeItems = courseParticipantLists[courseId]
courseId = addCourseIdScope(courseId)
courseInfo = coursesInfo[courseId]
if courseInfo:
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, role)
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, role)
# gam courses <CourseEntity> clear teachers|students
# gam course <CourseID> clear teacher|student
@@ -46491,14 +46502,13 @@ def doCourseClearParticipants(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(CLEAR_SYNC_PARTICIPANT_TYPES_MAP, mapChoice=True)
checkForExtraneousArguments()
i = 0
count = len(courseIdList)
for courseId in courseIdList:
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
for courseId, courseInfo in coursesInfo.items():
i += 1
removeParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
continue
_batchRemoveItemsFromCourse(croom, courseId, i, count, removeParticipants, role)
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeParticipants, role)
# gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
# gam course <CourseID> sync students [addonly|removeonly] <UserTypeEntity>
@@ -46525,9 +46535,8 @@ def doCourseSyncParticipants(courseIdList, getEntityListArg):
syncParticipantsSet.add(normalizeEmailAddressOrUID(user))
if makeFirstTeacherOwner:
firstTeacher = normalizeEmailAddressOrUID(syncParticipants[0])
i = 0
count = len(courseIdList)
for courseId in courseIdList:
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseParticipantLists:
syncParticipantsSet = set()
@@ -46537,21 +46546,18 @@ def doCourseSyncParticipants(courseIdList, getEntityListArg):
syncParticipantsSet.add(normalizeEmailAddressOrUID(user))
if makeFirstTeacherOwner:
firstTeacher = normalizeEmailAddressOrUID(courseParticipantLists[courseId][0])
courseInfo = checkCourseExists(croom, courseId, i, count)
if courseInfo:
courseId = courseInfo['id']
currentParticipantsSet = set()
currentParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
continue
for user in currentParticipants:
currentParticipantsSet.add(normalizeEmailAddressOrUID(user))
if syncOperation != 'removeonly':
_batchAddItemsToCourse(croom, courseId, i, count, list(syncParticipantsSet-currentParticipantsSet), role)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(croom, courseId, firstTeacher, i, count)
if syncOperation != 'addonly':
_batchRemoveItemsFromCourse(croom, courseId, i, count, list(currentParticipantsSet-syncParticipantsSet), role)
currentParticipantsSet = set()
currentParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
continue
for user in currentParticipants:
currentParticipantsSet.add(normalizeEmailAddressOrUID(user))
if syncOperation != 'removeonly':
_batchAddItemsToCourse(croom, courseId, i, count, list(syncParticipantsSet-currentParticipantsSet), role)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(croom, courseId, firstTeacher, i, count)
if syncOperation != 'addonly':
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, list(currentParticipantsSet-syncParticipantsSet), role)
def studentUnknownWarning(studentId, errMsg, i, count):
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
@@ -47208,9 +47214,8 @@ def createClassroomInvitations(users):
croom = buildGAPIObject(API.CLASSROOM)
classroomEmails = {}
courseIds = None
coursesInfo = {}
role = CLASSROOM_ROLE_STUDENT
useAdminAccess = False
useOwnerAccess = True
csvPF = None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
@@ -47225,20 +47230,20 @@ def createClassroomInvitations(users):
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ADMIN_ACCESS_OPTIONS:
useAdminAccess = True
useOwnerAccess = False
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if courseIds is None:
missingArgumentExit('courses <CourseEntity>')
if csvPF:
if FJQC.formatJSON:
csvPF.SetTitles(['userEmail', 'JSON'])
csvPF.SetJSONTitles(['userEmail', 'JSON'])
else:
csvPF.SetTitles(['userId', 'userEmail', 'courseId', 'courseName', 'id', 'role'])
csvPF.SetSortAllTitles()
csvPF.SetTitles(['userEmail', 'courseId', 'courseName', 'id', 'role'])
csvPF.SetSortAllTitles()
courseIdsLists = courseIds if isinstance(courseIds, dict) else None
if courseIdsLists is None:
_getCoursesOwnerInfo(croom, courseIds, coursesInfo, useAdminAccess)
j, jcount, coursesInfo = _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess)
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
i, count, users = getEntityArgument(users)
for user in users:
@@ -47246,25 +47251,20 @@ def createClassroomInvitations(users):
userId = normalizeEmailAddressOrUID(user)
userEmail = _getClassroomEmail(croom, classroomEmails, userId, userId)
if courseIdsLists:
courseIds = courseIdsLists[user]
_getCoursesOwnerInfo(croom, courseIds, coursesInfo, useAdminAccess)
jcount = len(courseIds)
j, jcount, coursesInfo = _getCoursesOwnerInfo(croom, courseIdsLists[user], useOwnerAccess)
if csvPF or not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
if jcount == 0:
continue
j = 0
for courseId in courseIds:
for courseId, courseInfo in coursesInfo.items():
j += 1
courseId = addCourseIdScope(courseId)
courseInfo = coursesInfo[courseId]
if not courseInfo:
continue
courseNameId = f'{courseInfo["name"]} ({courseId})'
try:
invitation = callGAPI(courseInfo['croom'].invitations(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.ALREADY_EXISTS, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
body={'userId': userId, 'courseId': courseId, 'role': role})
invitation['courseName'] = courseInfo['name']
invitation['userEmail'] = userEmail
if not csvPF:
if not FJQC.formatJSON:
Ind.Increment()
@@ -47274,14 +47274,12 @@ def createClassroomInvitations(users):
printLine(json.dumps(cleanJSON(invitation), ensure_ascii=False, sort_keys=True))
else:
if not FJQC.formatJSON:
invitation['courseName'] = courseInfo['name']
invitation['userEmail'] = userEmail
csvPF.WriteRow(invitation)
else:
csvPF.WriteRowNoFilter({'userEmail': userEmail,
'JSON': json.dumps(cleanJSON(invitation), ensure_ascii=False, sort_keys=True)})
except GAPI.permissionDenied:
entityUnknownWarning(Ent.USER, userId, i, count)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, userId, Ent.COURSE, courseNameId, entityType, None], str(e), j, jcount)
break
except GAPI.notFound:
entityUnknownWarning(Ent.COURSE, courseNameId, j, jcount)
@@ -47422,6 +47420,49 @@ def printShowClassroomInvitations(users):
if csvPF:
csvPF.writeCSVfile('ClassroomInvitations')
# gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
def doDeleteClassroomInvitations():
croom = buildGAPIObject(API.CLASSROOM)
courseIdList = invitationIds = None
role = CLASSROOM_ROLE_ALL
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'course', 'courses', 'class', 'classes'}:
courseIdList = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
elif myarg in {'id', 'ids'}:
invitationIds = getEntityList(Cmd.OB_CLASSROOM_INVITATION_ID_ENTITY)
elif myarg == 'role':
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
else:
unknownArgumentExit()
if courseIdList is None:
missingArgumentExit('courses <CourseEntity>')
entityType = Ent.CLASSROOM_INVITATION
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, True)
for courseId, courseInfo in coursesInfo.items():
i += 1
courseNameId = f'{courseInfo["name"]} ({courseId})'
if invitationIds is not None:
userInvitationIds = invitationIds
else:
status, userInvitationIds = _getClassroomInvitationIds(courseInfo['croom'], None, [courseId], role, i, count)
if status < 0:
continue
jcount = len(userInvitationIds)
entityPerformActionNumItems([Ent.COURSE, courseNameId], jcount, entityType, i, count)
Ind.Increment()
j = 0
for invitationId in userInvitationIds:
j += 1
try:
callGAPI(courseInfo['croom'].invitations(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
id=invitationId)
entityActionPerformed([Ent.COURSE, courseNameId, entityType, invitationId], j, jcount)
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.COURSE, courseNameId, entityType, invitationId], str(e), j, jcount)
Ind.Decrement()
# gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
# [role all|owner|student|teacher] [formatjson]
# gam print classroominvitations [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
@@ -51304,6 +51345,11 @@ def _mapDriveRevisionNames(revision):
revision['lastModifyingUserName'] = revision['lastModifyingUser']['displayName']
_mapDriveUser(revision['lastModifyingUser'])
DRIVEFILE_BASIC_PERMISSION_FIELDS = [
'displayName', 'id', 'emailAddress', 'domain', 'role', 'type',
'allowFileDiscovery', 'expirationTime', 'deleted', 'permissionDetails' #permissionDetails must be last
]
DRIVE_FIELDS_CHOICE_MAP = {
'alternatelink': 'webViewLink',
'appdatacontents': 'spaces',
@@ -51362,8 +51408,9 @@ DRIVE_FIELDS_CHOICE_MAP = {
'ownernames': 'owners.displayName',
'owners': 'owners',
'parents': 'parents',
'permissions': 'permissions',
'permissiondetails': 'permissions.permissionDetails',
'permissionids': 'permissionIds',
'permissions': 'permissions',
'properties': 'properties',
'quotabytesused': 'quotaBytesUsed',
'quotaused': 'quotaBytesUsed',
@@ -51719,7 +51766,7 @@ def showFileInfo(users):
showLabels = getChoice(SHOWLABELS_CHOICES)
elif myarg == 'showshareddrivepermissions':
getPermissionsForSharedDrives = True
permissionsFields = 'nextPageToken,permissions'
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
elif myarg == 'returnidonly':
returnIdOnly = True
elif DFF.ProcessArgument(myarg):
@@ -51727,7 +51774,8 @@ def showFileInfo(users):
else:
FJQC.GetFormatJSON(myarg)
if DFF.fieldsList:
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
if not getPermissionsForSharedDrives:
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
_setSelectionFields()
fields = getFieldsFromFieldsList(DFF.fieldsList)
showNoParents = 'parents' in DFF.fieldsList
@@ -52581,6 +52629,7 @@ DRIVEFILE_ACL_ROLES_MAP = {
}
DRIVEFILE_ACL_PERMISSION_TYPES = ['anyone', 'domain', 'group', 'user'] # anyone must be first element
DRIVEFILE_ACL_PERMISSION_DETAILS_TYPES = ['file', 'member']
class PermissionMatch():
_PERMISSION_MATCH_ACTION_MAP = {'process': True, 'skip': False}
@@ -52667,10 +52716,13 @@ class PermissionMatch():
self.permissionFields.add('expirationTime')
elif myarg == 'deleted':
deletedLocation = Cmd.Location()
body['deleted'] = getBoolean()
body[myarg] = getBoolean()
self.permissionFields.add('deleted')
elif myarg == 'inherited':
body['inherited'] = getBoolean()
body[myarg] = getBoolean()
self.permissionFields.add('permissionDetails')
elif myarg == 'permtype':
body['permissionType'] = getChoice(DRIVEFILE_ACL_PERMISSION_DETAILS_TYPES)
self.permissionFields.add('permissionDetails')
elif myarg in {'em', 'endmatch'}:
break
@@ -52710,21 +52762,27 @@ class PermissionMatch():
if field in {'type', 'role'}:
if permission.get(field, '') not in value:
break
elif field in {'nottype'}:
elif field == 'nottype':
if permission.get('type', '') in value:
break
elif field in {'notrole'}:
elif field == 'notrole':
if permission.get('role', '') in value:
break
elif field in {'allowFileDiscovery', 'deleted'}:
if value != permission.get(field, False):
break
elif field in {'inherited'}:
elif field == 'inherited':
if 'permissionDetails' in permission:
if value != permission['permissionDetails'][0].get(field, False):
break
else:
break
elif field == 'permissionType':
if 'permissionDetails' in permission:
if value != permission['permissionDetails'][0].get(field, ''):
break
else:
break
elif field in {'expirationstart', 'expirationend'}:
if 'expirationTime' in permission:
expirationDateTime, _ = iso8601.parse_date(permission['expirationTime'])
@@ -52736,14 +52794,14 @@ class PermissionMatch():
break
else:
break
elif field in {'emailaddresslist'}:
elif field == 'emailaddresslist':
emailAddress = permission.get('emailAddress')
if emailAddress:
if emailAddress not in value:
break
else:
break
elif field in {'permissionidlist'}:
elif field == 'permissionidlist':
permissionId = permission.get('id')
if permissionId:
if permissionId not in value:
@@ -53141,6 +53199,16 @@ def printFileList(users):
if DLP.onlySharedDrives or getPermissionsForSharedDrives or DFF.showSharedDriveNames:
_setSkipObjects(skipObjects, ['driveId'], DFF.fieldsList)
def _printFileInfoRow(baserow, fileInfo):
row = baserow.copy()
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
simpleLists=simpleLists, delimiter=delimiter))
else:
row['JSON'] = json.dumps(cleanJSON(fileInfo, skipObjects=skipObjects, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowTitlesJSONNoFilter(row)
def _printFileInfo(drive, user, f_file, cleanFileName):
driveId = f_file.get('driveId')
checkSharedDrivePermissions = getPermissionsForSharedDrives and driveId and 'permissions' not in f_file
@@ -53218,18 +53286,15 @@ def printFileList(users):
baserow[fileNameTitle] = fileInfo[fileNameTitle]
if 'owners' in fileInfo:
flattenJSON({'owners': fileInfo['owners']}, flattened=baserow, skipObjects=skipObjects)
permissions = fileInfo.pop('permissions')
for permission in permissions:
row = baserow.copy()
for permission in fileInfo.pop('permissions'):
fileInfo['permission'] = permission
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
simpleLists=simpleLists, delimiter=delimiter))
pdetails = fileInfo['permission'].pop('permissionDetails', [])
if not pdetails:
_printFileInfoRow(baserow, fileInfo)
else:
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON(fileInfo, skipObjects=skipObjects, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowTitlesJSONNoFilter(row)
for pdetail in pdetails:
fileInfo['permission']['permissionDetails'] = pdetail
_printFileInfoRow(baserow, fileInfo)
else:
if not countsRowFilter:
csvPF.UpdateMimeTypeCounts(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
@@ -53417,7 +53482,7 @@ def printFileList(users):
showLabels = getChoice(SHOWLABELS_CHOICES)
elif myarg == 'showshareddrivepermissions':
getPermissionsForSharedDrives = True
permissionsFields = 'nextPageToken,permissions'
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
elif myarg == 'pmfilter':
pmselect = False
elif myarg == 'oneitemperrow':
@@ -53459,9 +53524,10 @@ def printFileList(users):
DLP.Finalize(fileIdEntity)
if DLP.PM.permissionMatches:
getPermissionsForSharedDrives = True
permissionsFields = 'nextPageToken,permissions'
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
elif DFF.fieldsList:
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
if not getPermissionsForSharedDrives:
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
if DFF.fieldsList:
_setSelectionFields()
fields = getFieldsFromFieldsList(DFF.fieldsList)
@@ -60875,24 +60941,19 @@ def infoDriveFileACLs(users, useDomainAdminAccess=False):
def doInfoDriveFileACLs():
infoDriveFileACLs([_getAdminEmail()], True)
DRIVEFILE_BASIC_PERMISSION_FIELDS = [
'displayName', 'id', 'emailAddress', 'domain', 'role', 'type',
'allowFileDiscovery', 'expirationTime', 'deleted'
]
DRIVEFILE_PERMISSIONS_FOR_VIEW_CHOICES = ['published']
def getDriveFilePermissionsFields(myarg, fieldsList):
if myarg in DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP:
fieldsList.append(DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP[myarg])
elif myarg == 'basicpermissions':
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS)
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS[:-1])
elif myarg == 'fields':
for field in _getFieldsList():
if field in DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP:
fieldsList.append(DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP[field])
elif field == 'basicpermissions':
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS)
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS[:-1])
else:
invalidChoiceExit(field, DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP, True)
else:
@@ -60916,6 +60977,17 @@ def getDriveFilePermissionsFields(myarg, fieldsList):
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [formatjson] [adminaccess|asadmin]
def printShowDriveFileACLs(users, useDomainAdminAccess=False):
def _printPermissionRow(baserow, permission):
row = baserow.copy()
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fileIdEntity = getDriveFileEntity()
@@ -61036,16 +61108,14 @@ def printShowDriveFileACLs(users, useDomainAdminAccess=False):
baserow[fileNameTitle] = fileName
if oneItemPerRow:
for permission in permissions:
row = baserow.copy()
_mapDrivePermissionNames(permission)
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
pdetails = permission.pop('permissionDetails', [])
if not pdetails:
_printPermissionRow(baserow, permission)
else:
for pdetail in pdetails:
permission['permissionDetails'] = pdetail
_printPermissionRow(baserow, permission)
else:
row = baserow.copy()
for permission in permissions:
@@ -62287,6 +62357,17 @@ SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP = {
# [shownopermissionsdrives false|true|only]
# [formatjsn]
def printShowSharedDriveACLs(users, useDomainAdminAccess=False):
def _printPermissionRow(baserow, permission):
row = baserow.copy()
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
csvPF = CSVPrintFile(['User', 'id', 'name', 'createdTime'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
roles = set()
@@ -62469,16 +62550,14 @@ def printShowSharedDriveACLs(users, useDomainAdminAccess=False):
baserow = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name'], 'createdTime': shareddrive['createdTime']}
if shareddrive['permissions']:
for permission in shareddrive['permissions']:
row = baserow.copy()
_mapDrivePermissionNames(permission)
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
pdetails = permission.pop('permissionDetails', [])
if not pdetails:
_printPermissionRow(baserow, permission)
else:
for pdetail in pdetails:
permission['permissionDetails'] = pdetail
_printPermissionRow(baserow, permission)
else:
if not FJQC.formatJSON:
csvPF.WriteRowTitles(baserow)
@@ -71448,6 +71527,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
Cmd.ARG_CHROMENETWORK: doDeleteChromeNetwork,
Cmd.ARG_CHROMEPOLICY: doDeleteChromePolicy,
Cmd.ARG_CIGROUP: doDeleteCIGroups,
Cmd.ARG_CLASSROOMINVITATION: doDeleteClassroomInvitations,
Cmd.ARG_CONTACT: doDeleteDomainContacts,
Cmd.ARG_CONTACTPHOTO: doDeleteDomainContactPhoto,
Cmd.ARG_COURSE: doDeleteCourse,