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

View File

@@ -875,8 +875,11 @@ class GamCLArgs():
OB_CONTACT_GROUP_ITEM = 'ContactGroupItem' OB_CONTACT_GROUP_ITEM = 'ContactGroupItem'
OB_COURSE_ALIAS = 'CourseAlias' OB_COURSE_ALIAS = 'CourseAlias'
OB_COURSE_ALIAS_ENTITY = 'CourseAliasEntity' OB_COURSE_ALIAS_ENTITY = 'CourseAliasEntity'
OB_COURSE_ANNOUNCEMENT_ID = "CourseAnnouncementID"
OB_COURSE_ANNOUNCEMENT_ID_ENTITY = "CourseAnnouncementIDEntity" OB_COURSE_ANNOUNCEMENT_ID_ENTITY = "CourseAnnouncementIDEntity"
OB_COURSE_ANNOUNCEMENT_STATE_LIST = "CourseAnnouncementStateList" 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_ENTITY = 'CourseEntity'
OB_COURSE_ID = 'CourseID' OB_COURSE_ID = 'CourseID'
OB_COURSE_MATERIAL_ID_ENTITY = 'CourseMaterialIDEntity' OB_COURSE_MATERIAL_ID_ENTITY = 'CourseMaterialIDEntity'

View File

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

View File

@@ -8,6 +8,7 @@
- [Create and update courses](#create-and-update-courses) - [Create and update courses](#create-and-update-courses)
- [Delete courses](#delete-courses) - [Delete courses](#delete-courses)
- [Manage course aliases](#manage-course-aliases) - [Manage course aliases](#manage-course-aliases)
- [Manage course announcements](#manage-course-announcements)
- [Manage course topics](#manage-course-topics) - [Manage course topics](#manage-course-topics)
- [Display courses](#display-courses) - [Display courses](#display-courses)
- [Display course counts](#display-course-counts) - [Display course counts](#display-course-counts)
@@ -55,6 +56,11 @@ gam user user@domain.com check|update serviceaccount
<CourseAliasEntity> ::= <CourseAliasEntity> ::=
<CourseAliasList>|<FileSelector>|<CSVFileSelector>|<CSVkmdSelector>|<CSVDataSelector> <CourseAliasList>|<FileSelector>|<CSVFileSelector>|<CSVkmdSelector>|<CSVDataSelector>
See: https://github.com/GAM-team/GAM/wiki/Collections-of-Items See: https://github.com/GAM-team/GAM/wiki/Collections-of-Items
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
<CourseAnnouncementID> ::= <Number> <CourseAnnouncementID> ::= <Number>
<CourseAnnouncementIDList> ::= "<CourseAnnouncementID>(,<CourseAnnouncementID>)*" <CourseAnnouncementIDList> ::= "<CourseAnnouncementID>(,<CourseAnnouncementID>)*"
<CourseAnnouncementIDEntity> ::= <CourseAnnouncementIDEntity> ::=
@@ -389,17 +395,40 @@ These commands can process multiple courses.
gam courses <CourseEntity> add alias <CourseAliasEntity> gam courses <CourseEntity> add alias <CourseAliasEntity>
gam courses <CourseEntity> delete 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 ## Manage course topics
These commands can process a single course. These commands can process a single course.
``` ```
gam course <CourseID> add topic <CourseTopic> gam course <CourseID> add topic <CourseTopic>
gam course <CourseID> delete topic <CourseTopicID> gam course <CourseID> delete topic <CourseTopicID>
gam course <CourseID> update topic <CourseTopicID> <CourseTopic>
``` ```
These commands can process multiple courses. These commands can process multiple courses.
``` ```
gam courses <CourseEntity> add topic <CourseTopicEntity> gam courses <CourseEntity> add topic <CourseTopicEntity>
gam courses <CourseEntity> delete topic <CourseTopicIDEntity> gam courses <CourseEntity> delete topic <CourseTopicIDEntity>
gam courses <CourseEntity> update topic <CourseTopicIDEntity> <CourseTopic>
``` ```
## Display courses ## Display courses
``` ```
gam info course <CourseID> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly] 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 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 ### 7.10.09
Added option `showwebviewlink` to `gam [<UserTypeEntity>] print|show shareddrives` that 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$ rm -f /Users/admin/GAMConfig/oauth2.txt
admin@server:/Users/admin$ gam version 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 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> GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64 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:\>del C:\GAMConfig\oauth2.txt
C:\>gam version C:\>gam version
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found 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> GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final Python 3.13.5 64-bit final
Windows-10-10.0.17134 AMD64 Windows-10-10.0.17134 AMD64

View File

@@ -373,7 +373,7 @@ gam [<UserTypeEntity>] show shareddrives
[adminaccess|asadmin] [teamdriveadminquery|query <QueryTeamDrive>] [adminaccess|asadmin] [teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>] [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
[fields <SharedDriveFieldNameList>] [fields <SharedDriveFieldNameList>]
[showwebviewlink] [showwebviewlink text|hyperlink]
[formatjson] [formatjson]
``` ```
By default, all Shared Drives are displayed; use the following options to select a subset of Shared Drives: 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. * `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 * `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. By default, Gam displays the information as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format. * `formatjson` - Display the fields in JSON format.
``` ```
@@ -388,7 +392,7 @@ gam [<UserTypeEntity>] print shareddrives [todrive <ToDriveAttribute>*]
[adminaccess|asadmin] [teamdriveadminquery|query <QueryTeamDrive>] [adminaccess|asadmin] [teamdriveadminquery|query <QueryTeamDrive>]
[matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>] [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
[fields <SharedDriveFieldNameList>] [fields <SharedDriveFieldNameList>]
[showwebviewlink] [showwebviewlink text|hyperlink]
[formatjson [quotechar <Character>]] [formatjson [quotechar <Character>]]
``` ```
By default, all Shared Drives are displayed; use the following options to select a subset of Shared Drives: 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. * `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 * `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, 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. * `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,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 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 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,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,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 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 gam <UserTypeEntity> show shareddrives
[matchname <REMatchPattern>] (role|roles <SharedDriveACLRoleList>)* [matchname <REMatchPattern>] (role|roles <SharedDriveACLRoleList>)*
[fields <SharedDriveFieldNameList>] [fields <SharedDriveFieldNameList>]
[showwebviewlink] [showwebviewlink text|hyperlink]
[guiroles [<Boolean>] [formatjson] [guiroles [<Boolean>] [formatjson]
``` ```
By default, Gam displays all Teams Drives accessible by the user. By default, Gam displays all Teams Drives accessible by the user.
* `matchname <REMatchPattern>` - Display Shared Drives with names that match a pattern. * `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. * `(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. By default, Gam displays the information as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format. * `formatjson` - Display the fields in JSON format.
``` ```
gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*] gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*]
[matchname <REMatchPattern>] (role|roles <SharedDriveACLRoleList>)* [matchname <REMatchPattern>] (role|roles <SharedDriveACLRoleList>)*
[fields <SharedDriveFieldNameList>] [fields <SharedDriveFieldNameList>]
[showwebviewlink] [showwebviewlink text|hyperlink]
[guiroles [<Boolean>]] [formatjson [quotechar <Character>]] [guiroles [<Boolean>]] [formatjson [quotechar <Character>]]
``` ```
By default, Gam displays all Teams Drives accessible by the user. By default, Gam displays all Teams Drives accessible by the user.
* `matchname <REMatchPattern>` - Display Shared Drives with names that match a pattern. * `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. * `(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: The Google Drive API does not list roles for Shared Drives so GAM generates a role from the capabilities:
* `commenter - canComment: True, canEdit: False` * `commenter - canComment: True, canEdit: False`
* `reader - canComment: False, 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 counts by OrgUnit](#print-user-counts-by-orgunit)
- [Print user list](#print-user-list) - [Print user list](#print-user-list)
- [Display user counts](#display-user-counts) - [Display user counts](#display-user-counts)
- [Verify domain membership]($verify-domain-membership) - [Verify domain membership](#verify-domain-membership)
## API documentation ## API documentation
* [Directory API - Users](https://developers.google.com/admin-sdk/directory/reference/rest/v1/users) * [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 Print the current version of Gam with details
``` ```
gam version 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> GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64 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 Print the current version of Gam with details and time offset information
``` ```
gam version timeoffset 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> GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64 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 Print the current version of Gam with extended details and SSL information
``` ```
gam version extended 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> GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64 MacOS Sequoia 15.5 x86_64
@@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/Admin/bin/gam7 Path: /Users/Admin/bin/gam7
Version Check: Version Check:
Current: 5.35.08 Current: 5.35.08
Latest: 7.10.09 Latest: 7.11.00
echo $? echo $?
1 1
``` ```
@@ -72,7 +72,7 @@ echo $?
Print the current version number without details Print the current version number without details
``` ```
gam version simple gam version simple
7.10.09 7.11.00
``` ```
In Linux/MacOS you can do: In Linux/MacOS you can do:
``` ```
@@ -82,7 +82,7 @@ echo $VER
Print the current version of Gam and address of this Wiki Print the current version of Gam and address of this Wiki
``` ```
gam help 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> GAM Team <google-apps-manager@googlegroups.com>
Python 3.13.5 64-bit final Python 3.13.5 64-bit final
MacOS Sequoia 15.5 x86_64 MacOS Sequoia 15.5 x86_64