Compare commits

...

9 Commits

Author SHA1 Message Date
Ross Scroggs
b218abaae7 Updated gam course <CourseID> create|update announcement 2025-07-02 21:35:43 -07:00
Ross Scroggs
967898fa86 Updates
Some checks failed
Push wiki / pushwiki (push) Has been cancelled
2025-07-02 14:23:05 -07:00
Ross Scroggs
80570f2fda Updates
Some checks failed
Push wiki / pushwiki (push) Has been cancelled
Build and test GAM / build (build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (build, 10, Build Intel Windows, windows-2022) (push) Has been cancelled
Build and test GAM / build (build, 11, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (build, 7, Build Intel MacOS, macos-13) (push) Has been cancelled
Build and test GAM / build (build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (test, 12, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (test, 13, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (test, 14, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (test, 15, Test Python 3.14-dev, ubuntu-24.04, 3.14-dev) (push) Has been cancelled
Build and test GAM / merge (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-apis (push) Has been cancelled
2025-07-01 23:29:22 -07:00
Ross Scroggs
6437547e33 Added commands to manage classroom/course announcements. 2025-07-01 22:51:03 -07:00
Ross Scroggs
f87f000be2 Added commands to manage classroom/course announcements. 2025-07-01 22:45:22 -07:00
Jay Lee
08f6f86d10 actions: rebuild for OpenSSL 3.5.1
Some checks failed
Build and test GAM / build (build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (build, 10, Build Intel Windows, windows-2022) (push) Has been cancelled
Build and test GAM / build (build, 11, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (build, 7, Build Intel MacOS, macos-13) (push) Has been cancelled
Build and test GAM / build (build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (test, 12, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (test, 13, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (test, 14, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (test, 15, Test Python 3.14-dev, ubuntu-24.04, 3.14-dev) (push) Has been cancelled
Build and test GAM / merge (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-apis (push) Has been cancelled
2025-07-01 08:27:04 -04:00
Ross Scroggs
e70bfca92a Added choices text and hyperlink to option `showwebviewlink
Some checks failed
Build and test GAM / build (build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (build, 10, Build Intel Windows, windows-2022) (push) Has been cancelled
Build and test GAM / build (build, 11, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (build, 7, Build Intel MacOS, macos-13) (push) Has been cancelled
Build and test GAM / build (build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (test, 12, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (test, 13, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (test, 14, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (test, 15, Test Python 3.14-dev, ubuntu-24.04, 3.14-dev) (push) Has been cancelled
Build and test GAM / merge (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-apis (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
2025-06-30 13:06:05 -07:00
Ross Scroggs
2be5d40f44 Added choices text and hyperlink to option showwebviewlink 2025-06-30 12:11:32 -07:00
Ross Scroggs
170e188f1f Added choices text and hyperlink to option showwebviewlink 2025-06-30 12:11:05 -07:00
14 changed files with 453 additions and 145 deletions

View File

@@ -126,7 +126,7 @@ jobs:
with:
path: |
cache.tar.xz
key: gam-${{ matrix.jid }}-20250611
key: gam-${{ matrix.jid }}-20250701
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'

View File

@@ -1,4 +1,4 @@
his document describes the GAM command line syntax in modified BNF, see https://en.wikipedia.org/wiki/Backus-Naur_Form
This document describes the GAM command line syntax in modified BNF, see https://en.wikipedia.org/wiki/Backus-Naur_Form
Skip the History section and start reading at Introduction.
Items on the command line are space separated, when an actual space character is required, it will be indicated by <Space>.
@@ -405,6 +405,11 @@ If an item contains spaces, it should be surrounded by ".
<ContactGroupItem> ::= <ContactGroupID>|<ContactGroupName>
<CorporaAttribute> ::= alldrives|allteamdrives|domain|onlyteamdrives|user
<CourseAlias> ::= <String>
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
<CourseAnnouncementID> ::= <Number>
<CourseAnnouncementState> ::= draft|published|deleted
<CourseID> ::= <Number>|d:<CourseAlias>
@@ -3120,8 +3125,21 @@ gam delete courses <CourseEntity> [archive|archived]
gam course <CourseID> create|add alias <CourseAlias>
gam course <CourseID> delete alias <CourseAlias>
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
gam course <CourseID> create announcement
<CourseAnnouncementContent> [scheduledtime <Time>] [state draft|published]
gam course <CourseID> remove announcement <CourseAnnouncementID>
gam course <CourseID> update announcement <CourseAnnouncementID>
[<CourseAnnouncementContent>] [scheduledtime <Time>] [state published]
gam course <CourseID> create|add topic <CourseTopic>
gam course <CourseID> delete topic <CourseTopicID>
gam course <CourseID> update topic <CourseTopicID> <CourseTopic>
gam course <CourseID> create|add teachers [makefirstteacherowner] <UserItem>
gam course <CourseID> create|add students <UserItem>
@@ -3133,8 +3151,15 @@ gam course <CourseID> sync students [addonly|removeonly] <UserTypeEntity>
gam courses <CourseEntity> create|add alias <CourseAliasEntity>
gam courses <CourseEntity> delete alias <CourseAliasEntity>
gam courses <CourseEntity> create announcement
<CourseAnnouncementContent>> [scheduledtime <Time>] [state draft|published]
gam courses <CourseEntity> remove announcement <CourseAnnouncementIDEntity>
gam courses <CourseEntity> update announcement <CourseAnnouncementIDEntity>
[<CourseAnnouncementContent>] [scheduledtime <Time>] [state published]
gam courses <CourseEntity> create|add topic <CourseTopicEntity>
gam courses <CourseEntity> delete topic <CourseTopicIDEntity>
gam courses <CourseEntity> update topic <CourseTopicIDEntity> <CourseTopic>
gam courses <CourseEntity> create|add teachers [makefirstteacherowner] <UserTypeEntity>
gam courses <CourseEntity> create|add students <UserTypeEntity>
@@ -4817,13 +4842,13 @@ gam print shareddrives [todrive <ToDriveAttribute>*]
[teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
[fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[formatjson [quotechar <Character>]]
gam show shareddrives
[teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
[fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[formatjson] [noorgunits [<Boolean>]]
gam print shareddriveorganizers [todrive <ToDriveAttribute>*]
@@ -4904,14 +4929,14 @@ gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
(role|roles <SharedDriveACLRoleList>)*
[fields <SharedDriveFieldNameList>]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[guiroles [<Boolean>]] [formatjson [quotechar <Character>]]
gam <UserTypeEntity> show shareddrives
[teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
(role|roles <SharedDriveACLRoleList>)*
[fields <SharedDriveFieldNameList>]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[guiroles [<Boolean>]] [formatjson]
<PermissionMatch> ::=

View File

@@ -1,3 +1,29 @@
7.11.01
Updated `gam course <CourseID> create|update announcement` to accept input from
a literal string, a file or a Google Doc.
```
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
```
7.11.00
Added commands to manage classroom/course announcements.
* See: https://github.com/GAM-team/GAM/wiki/Classroom-Courses#manage-course-announcements
Upgraded to OpenSSL 3.5.1.
7.10.10
Added choices `text` and `hyperlink` to option `showwebviewlink` in `gam [<UserTypeEntity>] print|show shareddrives`.
* `showwebviewlink text` - Displays `https://drive.google.com/drive/folders/<SharedDriveID>`
* `showwebviewlink hyperlink` - Displays `=HYPERLINK("https://drive.google.com/drive/folders/<SharedDriveID>", "<SharedDriveName>")`
7.10.09
Added option `showwebviewlink` to `gam [<UserTypeEntity>] print|show shareddrives` that

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
"""
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.10.09'
__version__ = '7.11.01'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
#pylint: disable=wrong-import-position
@@ -634,7 +634,8 @@ def accessErrorMessage(cd, errMsg=None):
cd = buildGAPIObject(API.DIRECTORY)
try:
callGAPI(cd.customers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customerKey=GC.Values[GC.CUSTOMER_ID], fields='id')
except (GAPI.badRequest, GAPI.invalidInput):
return formatKeyValueList('',
@@ -671,14 +672,16 @@ def accessErrorExitNonDirectory(api, errMsg):
''))
def ClientAPIAccessDeniedExit(errMsg=None):
stderrErrorMsg(Msg.API_ACCESS_DENIED)
if errMsg:
if errMsg is None:
stderrErrorMsg(Msg.API_ACCESS_DENIED)
missingScopes = API.getClientScopesSet(GM.Globals[GM.CURRENT_CLIENT_API])-GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
if missingScopes:
writeStderr(Msg.API_CHECK_CLIENT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2_CLIENT_ID],
','.join(sorted(missingScopes))))
systemErrorExit(API_ACCESS_DENIED_RC, None)
else:
stderrErrorMsg(errMsg)
missingScopes = API.getClientScopesSet(GM.Globals[GM.CURRENT_CLIENT_API])-GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
if missingScopes:
writeStderr(Msg.API_CHECK_CLIENT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2_CLIENT_ID],
','.join(sorted(missingScopes))))
systemErrorExit(API_ACCESS_DENIED_RC, None)
systemErrorExit(API_ACCESS_DENIED_RC, Msg.REAUTHENTICATION_IS_NEEDED)
def SvcAcctAPIAccessDenied():
_getSvcAcctData()
@@ -4458,6 +4461,8 @@ def handleOAuthTokenError(e, softErrors, displayError=False, i=0, count=0):
errMsg.startswith('invalid_request: Invalid impersonation &quot;sub&quot; field')):
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
ClientAPIAccessDeniedExit()
# 403 Forbidden, API disabled, user not enabled
# 400 Bad Request, user not defined
if softErrors:
entityActionFailedWarning([Ent.USER, GM.Globals[GM.CURRENT_SVCACCT_USER], Ent.USER, None], errMsg, i, count)
return None
@@ -4465,6 +4470,7 @@ def handleOAuthTokenError(e, softErrors, displayError=False, i=0, count=0):
if errMsg in API.OAUTH2_UNAUTHORIZED_ERRORS:
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
ClientAPIAccessDeniedExit()
# 401 Unauthorized, API disabled, user enabled
if softErrors:
if displayError:
apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]))
@@ -6515,7 +6521,8 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
printGettingAllEntityItemsForWhom(Ent.TEACHER, removeCourseIdScope(courseId), entityType=Ent.COURSE)
result = callGAPIpages(courseInfo['croom'].courses().teachers(), 'list', 'teachers',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, fields='nextPageToken,teachers/profile/emailAddress',
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
@@ -6528,7 +6535,8 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
printGettingAllEntityItemsForWhom(Ent.STUDENT, removeCourseIdScope(courseId), entityType=Ent.COURSE)
result = callGAPIpages(courseInfo['croom'].courses().students(), 'list', 'students',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, fields='nextPageToken,students/profile/emailAddress',
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
@@ -6544,8 +6552,8 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
entityActionNotPerformedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e))
GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE] = True
break
except (GAPI.forbidden, GAPI.badRequest):
ClientAPIAccessDeniedExit()
except (GAPI.forbidden, GAPI.permissionDenied, GAPI.badRequest) as e:
ClientAPIAccessDeniedExit(str(e))
elif entityType == Cmd.ENTITY_CROS:
buildGAPIObject(API.DIRECTORY)
result = convertEntityToList(entity)
@@ -9023,7 +9031,7 @@ class CSVPrintFile():
normalizeSortHeaders()
if self.outputTranspose:
newRows = []
newTitlesList = [i for i in range(len(self.rows)+1)]
newTitlesList = list(range(len(self.rows) + 1))
for title in titlesList:
i = 0
newRow = {i: title}
@@ -16010,8 +16018,9 @@ def doCreateDomainAlias():
checkForExtraneousArguments()
try:
callGAPI(cd.domainAliases(), 'insert',
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.DUPLICATE, GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.CONFLICT],
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.DUPLICATE, GAPI.INVALID, GAPI.CONFLICT,
GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformed([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']])
except GAPI.domainNotFound:
@@ -16020,8 +16029,10 @@ def doCreateDomainAlias():
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']], Msg.DUPLICATE)
except (GAPI.invalid, GAPI.conflict) as e:
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
except (GAPI.badRequest, GAPI.notFound) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
# gam delete domainalias|aliasdomain <DomainAlias>
def doDeleteDomainAlias():
@@ -16030,13 +16041,16 @@ def doDeleteDomainAlias():
checkForExtraneousArguments()
try:
callGAPI(cd.domainAliases(), 'delete',
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customer=GC.Values[GC.CUSTOMER_ID], domainAliasName=domainAliasName)
entityActionPerformed([Ent.DOMAIN_ALIAS, domainAliasName])
except GAPI.domainAliasNotFound:
entityActionFailedWarning([Ent.DOMAIN_ALIAS, domainAliasName], Msg.DOES_NOT_EXIST)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
except (GAPI.badRequest, GAPI.notFound) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
DOMAIN_TIME_OBJECTS = {'creationTime'}
DOMAIN_ALIAS_PRINT_ORDER = ['parentDomainName', 'creationTime', 'verified']
@@ -16064,14 +16078,17 @@ def doInfoDomainAlias():
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
try:
result = callGAPI(cd.domainAliases(), 'get',
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customer=GC.Values[GC.CUSTOMER_ID], domainAliasName=domainAliasName)
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
_showDomainAlias(result, FJQC, aliasSkipObjects)
except GAPI.domainAliasNotFound:
entityActionFailedWarning([Ent.DOMAIN_ALIAS, domainAliasName], Msg.DOES_NOT_EXIST)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
except (GAPI.badRequest, GAPI.notFound) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
def _printDomain(domain, csvPF):
row = {}
@@ -16107,7 +16124,8 @@ def doPrintShowDomainAliases():
FJQC.GetFormatJSONQuoteChar(myarg, True)
try:
domainAliases = callGAPIitems(cd.domainAliases(), 'list', 'domainAliases',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customer=GC.Values[GC.CUSTOMER_ID])
count = len(domainAliases)
if showItemCountOnly:
@@ -16125,8 +16143,10 @@ def doPrintShowDomainAliases():
csvPF.WriteRowNoFilter({'domainAliasName': domainAlias['domainAliasName'],
'JSON': json.dumps(cleanJSON(domainAlias, timeObjects=DOMAIN_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
except (GAPI.badRequest, GAPI.notFound) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
if csvPF:
csvPF.writeCSVfile('Domain Aliases')
@@ -16137,15 +16157,19 @@ def doCreateDomain():
checkForExtraneousArguments()
try:
callGAPI(cd.domains(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.CONFLICT],
throwReasons=[GAPI.DUPLICATE, GAPI.CONFLICT,
GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformed([Ent.DOMAIN, body['domainName']])
except GAPI.duplicate:
entityDuplicateWarning([Ent.DOMAIN, body['domainName']])
except GAPI.conflict as e:
entityActionFailedWarning([Ent.DOMAIN, body['domainName']], str(e))
except (GAPI.domainNotFound, GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
except (GAPI.domainNotFound, GAPI.badRequest, GAPI.notFound) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
# gam update domain <DomainName> primary
def doUpdateDomain():
@@ -16162,13 +16186,17 @@ def doUpdateDomain():
missingArgumentExit('primary')
try:
callGAPI(cd.customers(), 'update',
throwReasons=[GAPI.DOMAIN_NOT_VERIFIED_SECONDARY, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
throwReasons=[GAPI.DOMAIN_NOT_VERIFIED_SECONDARY, GAPI.BAD_REQUEST,
GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_INPUT,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customerKey=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformedMessage([Ent.DOMAIN, domainName], Msg.NOW_THE_PRIMARY_DOMAIN)
except GAPI.domainNotVerifiedSecondary:
entityActionFailedWarning([Ent.DOMAIN, domainName], Msg.DOMAIN_NOT_VERIFIED_SECONDARY)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalidInput) as e:
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.invalidInput) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
# gam delete domain <DomainName>
def doDeleteDomain():
@@ -16177,11 +16205,14 @@ def doDeleteDomain():
checkForExtraneousArguments()
try:
callGAPI(cd.domains(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
customer=GC.Values[GC.CUSTOMER_ID], domainName=domainName)
entityActionPerformed([Ent.DOMAIN, domainName])
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
except (GAPI.badRequest, GAPI.notFound) as e:
accessErrorExit(cd, str(e))
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
CUSTOMER_LICENSE_MAP = {
'accounts:num_users': 'Total Users',
@@ -16210,7 +16241,7 @@ def _showCustomerLicenseInfo(customerInfo, FJQC):
while True:
try:
result = callGAPI(rep.customerUsageReports(), 'get',
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN],
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
date=tryDate, customerId=customerInfo['id'],
fields='warnings,usageReports', parameters=parameters)
usageReports = numUsersAvailable(result)
@@ -16228,8 +16259,8 @@ def _showCustomerLicenseInfo(customerInfo, FJQC):
if not tryDate:
return
continue
except GAPI.forbidden:
return
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit(str(e))
if not FJQC.formatJSON:
printKeyValueList([f'User counts as of {tryDate}:'])
Ind.Increment()
@@ -46776,6 +46807,13 @@ COURSE_STATE_MAPS = {
'published': 'PUBLISHED',
'deleted': 'DELETED',
},
Cmd.OB_COURSE_ANNOUNCEMENT_ADD_STATE_LIST: {
'draft': 'DRAFT',
'published': 'PUBLISHED',
},
Cmd.OB_COURSE_ANNOUNCEMENT_UPDATE_STATE_LIST: {
'published': 'PUBLISHED',
},
Cmd.OB_COURSE_WORK_STATE_LIST: {
'draft': 'DRAFT',
'published': 'PUBLISHED',
@@ -47681,7 +47719,8 @@ def _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess, addCIIdScope=True):
if courseId not in coursesInfo:
try:
course = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
throwReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='name,ownerId')
if useOwnerAccess:
@@ -47692,9 +47731,9 @@ def _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess, addCIIdScope=True):
coursesInfo[ciCourseId] = {'name': course['name'], 'croom': ocroom}
except GAPI.notFound:
entityDoesNotExistWarning(Ent.COURSE, courseId)
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
except GAPI.forbidden:
except (GAPI.forbidden, GAPI.permissionDenied) as e:
ClientAPIAccessDeniedExit()
return 0, len(coursesInfo), coursesInfo
@@ -48923,77 +48962,114 @@ def doPrintCourseParticipants():
csvPF.SetSortTitles(COURSE_PARTICIPANTS_SORT_TITLES)
csvPF.writeCSVfile('Course Participants')
def _batchAddItemsToCourse(croom, courseId, i, count, addParticipants, role):
def _batchAddItemsToCourse(croom, courseId, i, count, addItems, addType):
def _addIdToResponse(response, riItem):
if addType == Ent.COURSE_ANNOUNCEMENT:
respId = response.get('id', '')
elif addType == Ent.COURSE_TOPIC:
respId = response.get('topicId', '')
else:
respId = ''
if respId:
return riItem + f'({respId})'
return riItem
_ADD_PART_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.ALREADY_EXISTS: Msg.DUPLICATE,
GAPI.FAILED_PRECONDITION: Msg.NOT_ALLOWED}
def _callbackAddItemsToCourse(request_id, _, exception):
def _callbackAddItemsToCourse(request_id, response, exception):
ri = request_id.splitlines()
if addType == Ent.COURSE_ANNOUNCEMENT:
mg = re.match(r"^{'text': '(.+)'}$", ri[RI_ITEM])
if mg:
riText = mg.group(1)
else:
riText = ''
if len(riText) > 100:
riItem = riText[0:100]+'...'
else:
riItem = riText
else:
riItem = ri[RI_ITEM]
if exception is None:
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
riItem = _addIdToResponse(response, riItem)
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], addType, riItem], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if (reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}) and ((reason != GAPI.NOT_FOUND) or (ri[RI_ROLE] == Ent.COURSE_ALIAS)):
if (reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}) and ((reason != GAPI.NOT_FOUND) or (addType == Ent.COURSE_ALIAS)):
if reason in [GAPI.FORBIDDEN, GAPI.BACKEND_ERROR]:
errMsg = getPhraseDNEorSNA(ri[RI_ITEM])
errMsg = getPhraseDNEorSNA(riItem)
else:
errMsg = getHTTPError(_ADD_PART_REASON_TO_MESSAGE_MAP, http_status, reason, message)
if (reason == GAPI.PERMISSION_DENIED) and (ri[RI_ROLE] in {Ent.STUDENT, Ent.TEACHER}) and ('CannotDirectAddUser' in errMsg):
errMsg += f' Add external user with: gam user {ri[RI_ITEM]} create classroominvitation courses {ri[RI_ENTITY]} role {Ent.Singular(ri[RI_ROLE])}'
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
if (reason == GAPI.PERMISSION_DENIED) and (addType in {Ent.STUDENT, Ent.TEACHER}) and ('CannotDirectAddUser' in errMsg):
errMsg += f' Add external user with: gam user {riItem} create classroominvitation courses {ri[RI_ENTITY]} addType {Ent.Singular(addType)}'
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], addType, riItem], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
return
waitOnFailure(1, 10, reason, message)
if addType in {Ent.STUDENT, Ent.TEACHER, Ent.COURSE_TOPIC}:
rbody = {attribute: riItem}
elif addType == Ent.COURSE_ALIAS:
rbody = {attribute: addCourseAliasScope(riItem)}
else: # addType == Ent.COURSE_ANNOUNCEMENT:
rbody = ri[RI_ITEM]
try:
callGAPI(service, 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]),
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
fields='')
result = callGAPI(service, 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]), body=rbody, fields=returnFields)
riItem = _addIdToResponse(result, riItem)
except (GAPI.notFound, GAPI.backendError, GAPI.forbidden):
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], getPhraseDNEorSNA(ri[RI_ITEM]), int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], addType, riItem], getPhraseDNEorSNA(riItem), int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.alreadyExists:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.DUPLICATE, int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], addType, riItem], Msg.DUPLICATE, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.failedPrecondition:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.NOT_ALLOWED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], addType, riItem], Msg.NOT_ALLOWED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], addType, riItem], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
if role == Ent.STUDENT:
returnFields = ''
if addType == Ent.STUDENT:
service = croom.courses().students()
attribute = 'userId'
elif role == Ent.TEACHER:
elif addType == Ent.TEACHER:
service = croom.courses().teachers()
attribute = 'userId'
elif role == Ent.COURSE_ALIAS:
elif addType == Ent.COURSE_ALIAS:
service = croom.courses().aliases()
attribute = 'alias'
else: # role == Ent.COURSE_TOPIC:
elif addType == Ent.COURSE_TOPIC:
service = croom.courses().topics()
attribute = 'name'
returnFields = 'topicId'
else: # addType == Ent.COURSE_ANNOUNCEMENT:
service = croom.courses().announcements()
attribute = 'text'
returnFields = 'id'
method = getattr(service, 'create')
Act.Set(Act.ADD)
jcount = len(addParticipants)
jcount = len(addItems)
noScopeCourseId = removeCourseIdScope(courseId)
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, role, i, count)
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, addType, i, count)
Ind.Increment()
svcargs = dict([('courseId', courseId), ('body', {attribute: None}), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
svcargs = dict([('courseId', courseId), ('body', {attribute: None}), ('fields', returnFields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
dbatch = croom.new_batch_http_request(callback=_callbackAddItemsToCourse)
bcount = 0
j = 0
for participant in addParticipants:
for addItem in addItems:
j += 1
svcparms = svcargs.copy()
if role in {Ent.STUDENT, Ent.TEACHER}:
svcparms['body'][attribute] = cleanItem = normalizeEmailAddressOrUID(participant)
elif role == Ent.COURSE_ALIAS:
svcparms['body'][attribute] = addCourseAliasScope(participant)
if addType in {Ent.STUDENT, Ent.TEACHER}:
svcparms['body'][attribute] = cleanItem = normalizeEmailAddressOrUID(addItem)
elif addType == Ent.COURSE_ALIAS:
svcparms['body'][attribute] = addCourseAliasScope(addItem)
cleanItem = removeCourseAliasScope(svcparms['body'][attribute])
else: # role == Ent.COURSE_TOPIC:
svcparms['body'][attribute] = cleanItem = participant
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, role))
elif addType == Ent.COURSE_TOPIC:
svcparms['body'][attribute] = cleanItem = addItem
else: # addType == Ent.COURSE_ANNOUNCEMENT:
svcparms['body'] = cleanItem = addItem
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, addType))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
@@ -49003,74 +49079,82 @@ def _batchAddItemsToCourse(croom, courseId, i, count, addParticipants, role):
dbatch.execute()
Ind.Decrement()
def _batchRemoveItemsFromCourse(croom, courseId, i, count, removeParticipants, role):
def _batchRemoveItemsFromCourse(croom, courseId, i, count, removeItems, removeType):
_REMOVE_PART_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.FORBIDDEN: Msg.FORBIDDEN,
GAPI.PERMISSION_DENIED: Msg.PERMISSION_DENIED}
def _callbackRemoveItemsFromCourse(request_id, _, exception):
ri = request_id.splitlines()
riItem = ri[RI_ITEM]
if exception is None:
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], removeType, riItem], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}:
if reason == GAPI.NOT_FOUND and ri[RI_ROLE] != Ent.COURSE_ALIAS:
errMsg = f'{Msg.NOT_A} {Ent.Singular(ri[RI_ROLE])}'
if reason == GAPI.NOT_FOUND and removeType != Ent.COURSE_ALIAS:
errMsg = f'{Msg.NOT_A} {Ent.Singular(removeType)}'
else:
errMsg = getHTTPError(_REMOVE_PART_REASON_TO_MESSAGE_MAP, http_status, reason, message)
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], removeType, riItem], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
return
waitOnFailure(1, 10, reason, message)
if removeType in {Ent.STUDENT, Ent.TEACHER, Ent.COURSE_TOPIC, Ent.COURSE_ANNOUNCEMENT}:
rbody = {attribute: riItem}
else: # removeType == Ent.COURSE_ALIAS:
rbody = {attribute: addCourseAliasScope(riItem)}
try:
callGAPI(service, 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE, GAPI.FAILED_PRECONDITION],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]),
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
fields='')
courseId=addCourseIdScope(ri[RI_ENTITY]), body=rbody, fields='')
except GAPI.notFound:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.DOES_NOT_EXIST, int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], removeType, riItem], Msg.DOES_NOT_EXIST, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.forbidden:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.FORBIDDEN, int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], removeType, riItem], Msg.FORBIDDEN, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.PERMISSION_DENIED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], removeType, riItem], Msg.PERMISSION_DENIED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], removeType, riItem], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
if role == Ent.STUDENT:
if removeType == Ent.STUDENT:
service = croom.courses().students()
attribute = 'userId'
elif role == Ent.TEACHER:
elif removeType == Ent.TEACHER:
service = croom.courses().teachers()
attribute = 'userId'
elif role == Ent.COURSE_ALIAS:
elif removeType == Ent.COURSE_ALIAS:
service = croom.courses().aliases()
attribute = 'alias'
else: # role == Ent.COURSE_TOPIC:
elif removeType == Ent.COURSE_TOPIC:
service = croom.courses().topics()
attribute = 'id'
else: # removeType == Ent.COURSE_ANNOUNCEMENT:
service = croom.courses().announcements()
attribute = 'id'
method = getattr(service, 'delete')
Act.Set(Act.REMOVE)
jcount = len(removeParticipants)
jcount = len(removeItems)
noScopeCourseId = removeCourseIdScope(courseId)
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, role, i, count)
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, removeType, i, count)
Ind.Increment()
svcargs = dict([('courseId', courseId), ('fields', ''), (attribute, None)]+GM.Globals[GM.EXTRA_ARGS_LIST])
dbatch = croom.new_batch_http_request(callback=_callbackRemoveItemsFromCourse)
bcount = 0
j = 0
for participant in removeParticipants:
for removeItem in removeItems:
j += 1
svcparms = svcargs.copy()
if role in {Ent.STUDENT, Ent.TEACHER}:
svcparms[attribute] = cleanItem = normalizeEmailAddressOrUID(participant)
elif role == Ent.COURSE_ALIAS:
svcparms[attribute] = addCourseAliasScope(participant)
if removeType in {Ent.STUDENT, Ent.TEACHER}:
svcparms[attribute] = cleanItem = normalizeEmailAddressOrUID(removeItem)
elif removeType == Ent.COURSE_ALIAS:
svcparms[attribute] = addCourseAliasScope(removeItem)
cleanItem = removeCourseAliasScope(svcparms[attribute])
else: # role == Ent.COURSE_TOPIC:
svcparms[attribute] = cleanItem = participant
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, role))
elif removeType == Ent.COURSE_TOPIC:
svcparms[attribute] = cleanItem = removeItem
else: # removeType == Ent.COURSE_ANNOUNCEMENT:
svcparms[attribute] = cleanItem = removeItem
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, removeType))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
@@ -49100,9 +49184,28 @@ def _updateCourseOwner(croom, courseId, owner, i, count):
entityActionPerformedMessage([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], Msg.ALREADY_WAS_OWNER, i, count)
Act.Set(action)
ADD_REMOVE_PARTICIPANT_TYPES_MAP = {
def getCourseAnnouncement(createCmd):
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in SORF_TEXT_ARGUMENTS:
body['text'] = getStringOrFile(myarg, minLen=1, unescapeCRLF=True)[0]
elif myarg == 'scheduledtime':
body['scheduledTime'] = getTimeOrDeltaFromNow()
elif myarg == 'state':
body['state'] = getChoice(COURSE_STATE_MAPS[Cmd.OB_COURSE_ANNOUNCEMENT_ADD_STATE_LIST if createCmd else Cmd.OB_COURSE_ANNOUNCEMENT_UPDATE_STATE_LIST],
mapChoice=True)
else:
unknownArgumentExit()
if createCmd and 'text' not in body:
missingArgumentExit('text <String>')
return body
ADD_REMOVE_UPDATE_ITEM_TYPES_MAP = {
'alias': Ent.COURSE_ALIAS,
'aliases': Ent.COURSE_ALIAS,
'announcement': Ent.COURSE_ANNOUNCEMENT,
'announcements': Ent.COURSE_ANNOUNCEMENT,
'student': Ent.STUDENT,
'students': Ent.STUDENT,
'teacher': Ent.TEACHER,
@@ -49123,6 +49226,10 @@ PARTICIPANT_EN_MAP = {
# gam courses <CourseEntity> create alias <CourseAliasEntity>
# gam course <CourseID> create alias <CourseAlias>
# gam courses <CourseEntity> create announcement
# <CourseAnnouncementContent> [scheduledtime <Time>] [state draft|published]
# gam course <CourseID> create announcement
# <CourseAnnouncementContent> [scheduledtime <Time>] [state draft|published]
# gam courses <CourseEntity> create topic <CourseTopicEntity>
# gam course <CourseID> create topic <CourseTopic>
# gam courses <CourseEntity> create students <UserTypeEntity>
@@ -49131,35 +49238,39 @@ PARTICIPANT_EN_MAP = {
# gam course <CourseID> create teacher [makefirstteacherowner] <EmailAddress>
def doCourseAddItems(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
if role == Ent.TEACHER:
addType = getChoice(ADD_REMOVE_UPDATE_ITEM_TYPES_MAP, mapChoice=True)
if addType == Ent.TEACHER:
makeFirstTeacherOwner = checkArgumentPresent(['makefirstteacherowner'])
else:
makeFirstTeacherOwner = False
if not getEntityListArg:
if role in {Ent.STUDENT, Ent.TEACHER}:
if addType in {Ent.STUDENT, Ent.TEACHER}:
addItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
elif role == Ent.COURSE_ALIAS:
elif addType == Ent.COURSE_ALIAS:
addItems = getStringReturnInList(Cmd.OB_COURSE_ALIAS)
else: # role == Ent.COURSE_TOPIC:
elif addType == Ent.COURSE_TOPIC:
addItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC)
else: # addType == Ent.COURSE_ANNOUNCEMENT:
addItems = [getCourseAnnouncement(True)]
courseParticipantLists = None
else:
if role in {Ent.STUDENT, Ent.TEACHER}:
if addType in {Ent.STUDENT, Ent.TEACHER}:
_, addItems = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]},
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[addType]},
isSuspended=False, isArchived=False)
elif role == Ent.COURSE_ALIAS:
elif addType == Ent.COURSE_ALIAS:
addItems = getEntityList(Cmd.OB_COURSE_ALIAS_ENTITY, shlexSplit=True)
else: # role == Ent.COURSE_TOPIC:
elif addType == Ent.COURSE_TOPIC:
addItems = getEntityList(Cmd.OB_COURSE_TOPIC_ENTITY, shlexSplit=True)
else: # addType == Ent.COURSE_ANNOUNCEMENT:
addItems = getCourseAnnouncement(True)
courseParticipantLists = addItems if isinstance(addItems, dict) else None
if courseParticipantLists is None:
firstTeacher = None
if makeFirstTeacherOwner and addItems:
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
checkForExtraneousArguments()
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, role == Ent.COURSE_TOPIC,
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, addType in {Ent.COURSE_TOPIC, Ent.COURSE_ANNOUNCEMENT},
addCIIdScope=courseParticipantLists is None)
for courseId, courseInfo in coursesInfo.items():
i += 1
@@ -49169,43 +49280,51 @@ def doCourseAddItems(courseIdList, getEntityListArg):
if makeFirstTeacherOwner and addItems:
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
courseId = addCourseIdScope(courseId)
_batchAddItemsToCourse(courseInfo['croom'], courseId, i, count, addItems, role)
_batchAddItemsToCourse(courseInfo['croom'], courseId, i, count, addItems, addType)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(courseInfo['croom'], courseId, firstTeacher, i, count)
# gam courses <CourseEntity> remove alias <CourseAliasEntity>
# gam course <CourseID> remove alias <CourseAlias>
# gam courses <CourseEntity> remove announcement <CourseAnnouncementIDEntity>
# gam course <CourseID> remove announcement <CourseAnnouncementID>
# gam courses <CourseEntity> remove topic <CourseTopicIDEntity>
# gam course <CourseID> remove topic <CourseTopicID>
# gam courses <CourseEntity> remove teachers|students [owneracccess] <UserTypeEntity>
# gam course <CourseID> remove teacher|student [owneracccess] <EmailAddress>
def doCourseRemoveItems(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
removeType = getChoice(ADD_REMOVE_UPDATE_ITEM_TYPES_MAP, mapChoice=True)
if not getEntityListArg:
if role in {Ent.STUDENT, Ent.TEACHER}:
if removeType in {Ent.STUDENT, Ent.TEACHER}:
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:
elif removeType == Ent.COURSE_ALIAS:
useOwnerAccess = False
removeItems = getStringReturnInList(Cmd.OB_COURSE_ALIAS)
else: # role == Ent.COURSE_TOPIC:
elif removeType == Ent.COURSE_TOPIC:
useOwnerAccess = True
removeItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC_ID)
else: # removeType == Ent.COURSE_ANNOUNCEMENT:
useOwnerAccess = True
removeItems = getStringReturnInList(Cmd.OB_COURSE_ANNOUNCEMENT_ID)
courseParticipantLists = None
else:
if role in {Ent.STUDENT, Ent.TEACHER}:
if removeType in {Ent.STUDENT, Ent.TEACHER}:
useOwnerAccess = checkArgumentPresent(OWNER_ACCESS_OPTIONS)
_, removeItems = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]})
elif role == Ent.COURSE_ALIAS:
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[removeType]})
elif removeType == Ent.COURSE_ALIAS:
useOwnerAccess = False
removeItems = getEntityList(Cmd.OB_COURSE_ALIAS_ENTITY, shlexSplit=True)
else: # role == Ent.COURSE_TOPIC:
elif removeType == Ent.COURSE_TOPIC:
useOwnerAccess = True
removeItems = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY, shlexSplit=True)
else: # removeType == Ent.COURSE_ANNOUNCEMENT:
useOwnerAccess = True
removeItems = getEntityList(Cmd.OB_COURSE_ANNOUNCEMENT_ID_ENTITY, shlexSplit=True)
courseParticipantLists = removeItems if isinstance(removeItems, dict) else None
checkForExtraneousArguments()
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess,
@@ -49215,7 +49334,72 @@ def doCourseRemoveItems(courseIdList, getEntityListArg):
if courseParticipantLists:
removeItems = courseParticipantLists[courseId]
courseId = addCourseIdScope(courseId)
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, role)
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, removeType)
# gam courses <CourseEntity> update announcement <CourseAnnouncemntIDEntity>
# [<CourseAnnouncementContent>] [scheduledtime <Time>] [state published]
# gam course <CourseID> update announcement <CourseAnnouncementID>
# [<CourseAnnouncementContent>] [scheduledtime <Time>] [state published]
# gam courses <CourseEntity> update topic <CourseTopicIDEntity> <CourseTopic>
# gam course <CourseID> update topic <CourseTopicID> <CourseTopic>
def doCourseUpdateItems(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
updateType = getChoice(ADD_REMOVE_UPDATE_ITEM_TYPES_MAP, mapChoice=True)
if not getEntityListArg:
if updateType == Ent.COURSE_TOPIC:
useOwnerAccess = True
updateItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC_ID)
body = {'name': getString(Cmd.OB_COURSE_TOPIC)}
else: # updateType == Ent.COURSE_ANNOUNCEMENT:
useOwnerAccess = True
updateItems = getStringReturnInList(Cmd.OB_COURSE_ANNOUNCEMENT_ID)
body = getCourseAnnouncement(False)
courseItemLists = None
else:
if updateType == Ent.COURSE_TOPIC:
useOwnerAccess = True
updateItems = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY, shlexSplit=True)
body = {'name': getString(Cmd.OB_COURSE_TOPIC)}
else: # updateType == Ent.COURSE_ANNOUNCEMENT:
useOwnerAccess = True
updateItems = getEntityList(Cmd.OB_COURSE_ANNOUNCEMENT_ID_ENTITY, shlexSplit=True)
body = getCourseAnnouncement(False)
courseItemLists = updateItems if isinstance(updateItems, dict) else None
checkForExtraneousArguments()
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess,
addCIIdScope=courseItemLists is None)
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseItemLists:
updateItems = courseItemLists[courseId]
courseId = addCourseIdScope(courseId)
jcount = len(updateItems)
noScopeCourseId = removeCourseIdScope(courseId)
if updateType == Ent.COURSE_TOPIC:
service = courseInfo['croom'].courses().topics()
else: # updateType == Ent.COURSE_ANNOUNCEMENT:
service = courseInfo['croom'].courses().announcements()
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, updateType, i, count)
Ind.Increment()
j = 0
for updateItem in updateItems:
j += 1
try:
callGAPI(service, 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE],
courseId=addCourseIdScope(courseId), id=updateItem, updateMask=','.join(body.keys()), body=body, fields='')
entityActionPerformed([Ent.COURSE, courseId, updateType, updateItem], j, jcount)
except GAPI.notFound:
entityActionFailedWarning([Ent.COURSE, courseId, updateType, updateItem], Msg.DOES_NOT_EXIST, j, jcount)
except GAPI.forbidden:
entityActionFailedWarning([Ent.COURSE, courseId, updateType, updateItem], Msg.FORBIDDEN, j, jcount)
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.COURSE, courseId, updateType, updateItem], Msg.PERMISSION_DENIED, j, jcount)
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, courseId, updateType, updateItem], str(e), j, jcount)
Ind.Decrement()
# gam courses <CourseEntity> clear teachers|students
# gam course <CourseID> clear teacher|student
@@ -65870,12 +66054,14 @@ SHAREDDRIVE_ACL_ROLES_MAP = {
'writer': 'writer',
}
SHOWWEBVIEWLINK_CHOICES = {'text', 'hyperlink'}
# gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*]
# [asadmin [shareddriveadminquery|query <QuerySharedDrive>]]
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
# (role|roles <SharedDriveACLRoleList>)*
# [fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
# [showwebviewlink]
# [showwebviewlink [text|hyperlink]]
# [guiroles [<Boolean>]] [formatjson [quotechar <Character>]]
# [showitemcountonly]
# gam <UserTypeEntity> show shareddrives
@@ -65883,7 +66069,7 @@ SHAREDDRIVE_ACL_ROLES_MAP = {
# [matchname <REMatchPattrn>] [orgunit|org|ou <OrgUnitPath>]
# (role|roles <SharedDriveACLRoleLIst>)*
# [fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
# [showwebviewlink]
# [showwebviewlink [text|hyperlink]]
# [guiroles [<Boolean>]] [formatjson]
# [showitemcountonly]
def printShowSharedDrives(users, useDomainAdminAccess=False):
@@ -65893,7 +66079,10 @@ def printShowSharedDrives(users, useDomainAdminAccess=False):
if td_ouid:
shareddrive['orgUnit'] = orgUnitIdToPathMap.get(f'id:{td_ouid}', UNKNOWN)
if showWebViewLink:
shareddrive['webViewLink'] = 'https://drive.google.com/drive/folders/'+shareddrive['id']
if showWebViewLink == 'text':
shareddrive['webViewLink'] = 'https://drive.google.com/drive/folders/'+shareddrive['id']
else:
shareddrive['webViewLink'] = '=HYPERLINK("https://drive.google.com/drive/folders/'+shareddrive['id']+'", "'+shareddrive['name']+'")'
if not showFields:
return shareddrive
sshareddrive = {}
@@ -65912,7 +66101,7 @@ def printShowSharedDrives(users, useDomainAdminAccess=False):
showOrgUnitPaths = True
orgUnitIdToPathMap = None
guiRoles = showItemCountOnly = False
showWebViewLink = False
showWebViewLink = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
@@ -65943,7 +66132,7 @@ def printShowSharedDrives(users, useDomainAdminAccess=False):
elif myarg == 'guiroles':
guiRoles = getBoolean()
elif myarg == 'showwebviewlink':
showWebViewLink = True
showWebViewLink = getChoice(SHOWWEBVIEWLINK_CHOICES)
elif myarg == 'showitemcountonly':
showItemCountOnly = True
showOrgUnitPaths = False
@@ -77160,6 +77349,7 @@ COURSE_SUBCOMMANDS = {
'add': (Act.ADD, doCourseAddItems),
'clear': (Act.REMOVE, doCourseClearParticipants),
'remove': (Act.REMOVE, doCourseRemoveItems),
'update': (Act.UPDATE, doCourseUpdateItems),
'sync': (Act.SYNC, doCourseSyncParticipants),
}

View File

@@ -875,8 +875,11 @@ class GamCLArgs():
OB_CONTACT_GROUP_ITEM = 'ContactGroupItem'
OB_COURSE_ALIAS = 'CourseAlias'
OB_COURSE_ALIAS_ENTITY = 'CourseAliasEntity'
OB_COURSE_ANNOUNCEMENT_ID = "CourseAnnouncementID"
OB_COURSE_ANNOUNCEMENT_ID_ENTITY = "CourseAnnouncementIDEntity"
OB_COURSE_ANNOUNCEMENT_STATE_LIST = "CourseAnnouncementStateList"
OB_COURSE_ANNOUNCEMENT_ADD_STATE_LIST = "CourseAnnouncementAddStateList"
OB_COURSE_ANNOUNCEMENT_UPDATE_STATE_LIST = "CourseAnnouncementUpdateStateList"
OB_COURSE_ENTITY = 'CourseEntity'
OB_COURSE_ID = 'CourseID'
OB_COURSE_MATERIAL_ID_ENTITY = 'CourseMaterialIDEntity'

View File

@@ -302,6 +302,11 @@
<ContactGroupItem> ::= <ContactGroupID>|<ContactGroupName>
<CorporaAttribute> ::= alldrives|allteamdrives|domain|onlyteamdrives|user
<CourseAlias> ::= <String>
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
<CourseAnnouncementID> ::= <Number>
<CourseAnnouncementState> ::= draft|published|deleted
<CourseID> ::= <Number>|d:<CourseAlias>

View File

@@ -8,6 +8,7 @@
- [Create and update courses](#create-and-update-courses)
- [Delete courses](#delete-courses)
- [Manage course aliases](#manage-course-aliases)
- [Manage course announcements](#manage-course-announcements)
- [Manage course topics](#manage-course-topics)
- [Display courses](#display-courses)
- [Display course counts](#display-course-counts)
@@ -55,6 +56,11 @@ gam user user@domain.com check|update serviceaccount
<CourseAliasEntity> ::=
<CourseAliasList>|<FileSelector>|<CSVFileSelector>|<CSVkmdSelector>|<CSVDataSelector>
See: https://github.com/GAM-team/GAM/wiki/Collections-of-Items
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
<CourseAnnouncementID> ::= <Number>
<CourseAnnouncementIDList> ::= "<CourseAnnouncementID>(,<CourseAnnouncementID>)*"
<CourseAnnouncementIDEntity> ::=
@@ -389,17 +395,40 @@ These commands can process multiple courses.
gam courses <CourseEntity> add alias <CourseAliasEntity>
gam courses <CourseEntity> delete alias <CourseAliasEntity>
```
## Manage course announcements
These commands can process a single course.
```
gam course <CourseID> add announcement
<CourseAnnouncementContent> [scheduledtime <Time>] [state draft|published]
gam course <CourseID> delete announcement <CourseAnnouncementID>
gam course <CourseID> update announcement <CourseAnnouncementID>
[<CourseAnnouncementContent>] [scheduledtime <Time>] [state published]
```
These commands can process multiple courses.
```
gam courses <CourseEntity> add announcement
<CourseAnnouncementContent> [scheduledtime <Time>] [state draft|published]
gam courses <CourseEntity> delete announcement <CourseAnnouncementIDEntity>
gam courses <CourseEntity> update announcement <CourseAnnouncementIDEntity>
[<CourseAnnouncementContent>] [scheduledtime <Time>] [state published]
```
## Manage course topics
These commands can process a single course.
```
gam course <CourseID> add topic <CourseTopic>
gam course <CourseID> delete topic <CourseTopicID>
gam course <CourseID> update topic <CourseTopicID> <CourseTopic>
```
These commands can process multiple courses.
```
gam courses <CourseEntity> add topic <CourseTopicEntity>
gam courses <CourseEntity> delete topic <CourseTopicIDEntity>
gam courses <CourseEntity> update topic <CourseTopicIDEntity> <CourseTopic>
```
## Display courses
```
gam info course <CourseID> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly]

View File

@@ -10,6 +10,20 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
See [Downloads-Installs-GAM7](https://github.com/GAM-team/GAM/wiki/Downloads-Installs) for Windows or other options, including manual installation
### 7.11.00
Added commands to manage classroom/course announcements.
* See: https://github.com/GAM-team/GAM/wiki/Classroom-Courses#manage-course-announcements
Upgraded to OpenSSL 3.5.1.
### 7.10.10
Added choices `text` and `hyperlink` to option `showwebviewlink` in `gam [<UserTypeEntity>] print|show shareddrives`.
* `showwebviewlink text` - Displays `https://drive.google.com/drive/folders/<SharedDriveID>`
* `showwebviewlink hyperlink` - Displays `=HYPERLINK("https://drive.google.com/drive/folders/<SharedDriveID>", "<SharedDriveName>")`
### 7.10.09
Added option `showwebviewlink` to `gam [<UserTypeEntity>] print|show shareddrives` that

View File

@@ -251,7 +251,7 @@ writes the credentials into the file oauth2.txt.
admin@server:/Users/admin$ rm -f /Users/admin/GAMConfig/oauth2.txt
admin@server:/Users/admin$ gam version
WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found
GAM 7.10.09 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.11.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64
@@ -989,7 +989,7 @@ writes the credentials into the file oauth2.txt.
C:\>del C:\GAMConfig\oauth2.txt
C:\>gam version
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
GAM 7.10.09 - https://github.com/GAM-team/GAM - pythonsource
GAM 7.11.00 - https://github.com/GAM-team/GAM - pythonsource
GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final
Windows-10-10.0.17134 AMD64

View File

@@ -373,7 +373,7 @@ gam [<UserTypeEntity>] show shareddrives
[adminaccess|asadmin] [teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
[fields <SharedDriveFieldNameList>]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[formatjson]
```
By default, all Shared Drives are displayed; use the following options to select a subset of Shared Drives:
@@ -381,6 +381,10 @@ By default, all Shared Drives are displayed; use the following options to select
* `matchname <REMatchPattern>` - Retrieve Shared Drives with names that match a pattern.
* `orgunit|org|ou <OrgUnitPath>` - Only Shared Drives in the specified Org Unit are selected
Use option `showwebviewlink` to display the web view link for the Shared Drive.
* `showwebviewlink text` - Displays `https://drive.google.com/drive/folders/<SharedDriveID>`
* `showwebviewlink hyperlink` - Dsiplays `=HYPERLINK("https://drive.google.com/drive/folders/<SharedDriveID>", "<SharedDriveName>")`
By default, Gam displays the information as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format.
```
@@ -388,7 +392,7 @@ gam [<UserTypeEntity>] print shareddrives [todrive <ToDriveAttribute>*]
[adminaccess|asadmin] [teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
[fields <SharedDriveFieldNameList>]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[formatjson [quotechar <Character>]]
```
By default, all Shared Drives are displayed; use the following options to select a subset of Shared Drives:
@@ -396,6 +400,10 @@ By default, all Shared Drives are displayed; use the following options to select
* `matchname <REMatchPattern>` - Retrieve Shared Drives with names that match a pattern.
* `orgunit|org|ou <OrgUnitPath>` - Only Shared Drives in the specified Org Unit are selected
Use option `showwebviewlink` to display the web view link for the Shared Drive.
* `showwebviewlink text` - Displays `https://drive.google.com/drive/folders/<SharedDriveID>`
* `showwebviewlink hyperlink` - Dsiplays `=HYPERLINK("https://drive.google.com/drive/folders/<SharedDriveID>", "<SharedDriveName>")`
By default, Gam displays the information as columns of fields; the following option causes the output to be in JSON format,
* `formatjson` - Display the fields in JSON format.

View File

@@ -1723,9 +1723,9 @@ User,Owner,id,name,ownedByMe,trashed,explicitlyTrashed,directFileCount,directFil
user@domain.com,user@domain.com,012YenC8f12ALUk9PVA,My Drive,,False,False,100,138212,24,167,189598,79,-1,My Drive
user@domain.com,user@domain.com,Trash,Trash,,True,True,0,0,1,3,3072,9,-1,Trash
$ gam redirect csv ./MyDriveUsage.csv user user@domain.com print diskusage shareddriveid 0AL5LiIe4dqxZUk9PVA show summaryandtrash
$ gam redirect csv ./SharedDriveUsage.csv user user@domain.com print diskusage shareddriveid 0AL5LiIe4dqxZUk9PVA show summaryandtrash
User: user@domain.com, Print 1 Drive Disk Usage
$ more MyDriveUsage.csv
$ more SharedDriveUsage.csv
User,id,name,trashed,explicitlyTrashed,directFileCount,directFileSize,directFolderCount,totalFileCount,totalFileSize,totalFolderCount,depth,path
user@domain.com,0125LiIe4dqxZUk9PVA,TS Shared Drive 1,False,False,16,6144,7,42,73799,25,-1,SharedDrives/TS Shared Drive 1
user@domain.com,Trash,Trash,True,True,1,1024,0,1,1024,0,-1,Trash

View File

@@ -318,26 +318,34 @@ gam <UserTypeEntity> show shareddriveinfo <SharedDriveEntity>
gam <UserTypeEntity> show shareddrives
[matchname <REMatchPattern>] (role|roles <SharedDriveACLRoleList>)*
[fields <SharedDriveFieldNameList>]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[guiroles [<Boolean>] [formatjson]
```
By default, Gam displays all Teams Drives accessible by the user.
* `matchname <REMatchPattern>` - Display Shared Drives with names that match a pattern.
* `(role|roles <SharedDriveACLRoleList>)*` - Display Shared Drives where the user has one of the specified roles.
Use option `showwebviewlink` to display the web view link for the Shared Drive.
* `showwebviewlink text` - Displays `https://drive.google.com/drive/folders/<SharedDriveID>`
* `showwebviewlink hyperlink` - Dsiplays `=HYPERLINK("https://drive.google.com/drive/folders/<SharedDriveID>", "<SharedDriveName>")`
By default, Gam displays the information as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format.
```
gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*]
[matchname <REMatchPattern>] (role|roles <SharedDriveACLRoleList>)*
[fields <SharedDriveFieldNameList>]
[showwebviewlink]
[showwebviewlink text|hyperlink]
[guiroles [<Boolean>]] [formatjson [quotechar <Character>]]
```
By default, Gam displays all Teams Drives accessible by the user.
* `matchname <REMatchPattern>` - Display Shared Drives with names that match a pattern.
* `(role|roles <SharedDriveACLRoleList>)*` - Display Shared Drives where the user has one of the specified roles.
Use option `showwebviewlink` to display the web view link for the Shared Drive.
* `showwebviewlink text` - Displays `https://drive.google.com/drive/folders/<SharedDriveID>`
* `showwebviewlink hyperlink` - Dsiplays `=HYPERLINK("https://drive.google.com/drive/folders/<SharedDriveID>", "<SharedDriveName>")`
The Google Drive API does not list roles for Shared Drives so GAM generates a role from the capabilities:
* `commenter - canComment: True, canEdit: False`
* `reader - canComment: False, canEdit: False`

View File

@@ -38,7 +38,7 @@
- [Print user counts by OrgUnit](#print-user-counts-by-orgunit)
- [Print user list](#print-user-list)
- [Display user counts](#display-user-counts)
- [Verify domain membership]($verify-domain-membership)
- [Verify domain membership](#verify-domain-membership)
## API documentation
* [Directory API - Users](https://developers.google.com/admin-sdk/directory/reference/rest/v1/users)

View File

@@ -3,7 +3,7 @@
Print the current version of Gam with details
```
gam version
GAM 7.10.09 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.11.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final
MacOS Sequoia 15.5 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
GAM 7.10.09 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.11.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final
MacOS Sequoia 15.5 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
GAM 7.10.09 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.11.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64
@@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/Admin/bin/gam7
Version Check:
Current: 5.35.08
Latest: 7.10.09
Latest: 7.11.00
echo $?
1
```
@@ -72,7 +72,7 @@ echo $?
Print the current version number without details
```
gam version simple
7.10.09
7.11.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 7.10.09 - https://github.com/GAM-team/GAM
GAM 7.11.00 - https://github.com/GAM-team/GAM
GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64