mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-28 18:01:36 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e56b56612f | ||
|
|
550dd6b947 | ||
|
|
38f63188e3 | ||
|
|
8d0865f692 | ||
|
|
2ba6ad10c2 | ||
|
|
9e44511872 | ||
|
|
1ee31b15b2 | ||
|
|
dfb1dd860f | ||
|
|
7aaf8cdaa6 | ||
|
|
66a65a380e | ||
|
|
471bd4f924 | ||
|
|
5525c79f5b | ||
|
|
03d79ec62e | ||
|
|
6f72c9844b | ||
|
|
5b1d876101 | ||
|
|
1cb3223637 | ||
|
|
0330e315d2 | ||
|
|
b8f894fddb | ||
|
|
0b1fee6bc5 | ||
|
|
cc61691f28 | ||
|
|
0ece44575d | ||
|
|
e476ad93ba | ||
|
|
21ba5e3ac0 | ||
|
|
453f3faf62 | ||
|
|
296e629b69 | ||
|
|
a319eb665d | ||
|
|
c3bf865497 | ||
|
|
c8914ddb03 | ||
|
|
e24e127055 | ||
|
|
ed2801d612 | ||
|
|
245c9ca9d7 | ||
|
|
e4352129db | ||
|
|
726f061b16 | ||
|
|
75598e5eb8 | ||
|
|
6e57ae33e8 | ||
|
|
22bc5457a7 | ||
|
|
1ba183e8a5 |
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -643,6 +643,7 @@ jobs:
|
|||||||
# arm64 needs to build a wheel and needs scons to build
|
# arm64 needs to build a wheel and needs scons to build
|
||||||
sudo apt-get -qq --yes install scons
|
sudo apt-get -qq --yes install scons
|
||||||
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
|
"${PYTHON}" -m pip install --upgrade patchelf-wrapper
|
||||||
|
"${PYTHON}" -m pip install --upgrade typing_extensions
|
||||||
# "${PYTHON}" -m pip install --upgrade staticx
|
# "${PYTHON}" -m pip install --upgrade staticx
|
||||||
# install latest github src for staticx
|
# install latest github src for staticx
|
||||||
"${PYTHON}" -m pip install --upgrade "git+https://github.com/JonathonReinhart/staticx"
|
"${PYTHON}" -m pip install --upgrade "git+https://github.com/JonathonReinhart/staticx"
|
||||||
@@ -689,20 +690,6 @@ jobs:
|
|||||||
echo "GAM Version ${GAMVERSION}"
|
echo "GAM Version ${GAMVERSION}"
|
||||||
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
|
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Initialize Windows Desktop Shell
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
Write-Host "Checking for Windows Explorer shell..."
|
|
||||||
if (-not (Get-Process -Name explorer -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Host "Explorer not found. Booting the desktop shell..."
|
|
||||||
Start-Process explorer.exe
|
|
||||||
# Give the desktop a few seconds to fully render the taskbar
|
|
||||||
Start-Sleep -Seconds 10
|
|
||||||
} else {
|
|
||||||
Write-Host "Explorer is already running."
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Install NPM deps
|
- name: Install NPM deps
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
@@ -715,8 +702,8 @@ jobs:
|
|||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
#$url = "https://files.certum.eu/software/SimplySignDesktop/Windows/9.3.4.72/SimplySignDesktop-9.3.4.72-64-bit-en.msi"
|
#$url = "https://files.certum.eu/software/SimplySignDesktop/Windows/9.4.2.86/SimplySignDesktop-9.4.2.86-64-bit-en.msi"
|
||||||
$url = "https://www.files.certum.eu/software/SimplySignDesktop/Windows/9.4.0.84/SimplySignDesktop-9.4.0.84-64-bit-en.msi"
|
$url = "https://files.certum.eu/software/SimplySignDesktop/Windows/9.4.3.90/SimplySignDesktop-9.4.3.90-64-bit-en.msi"
|
||||||
$file = "SimplySignDesktop.msi"
|
$file = "SimplySignDesktop.msi"
|
||||||
Invoke-WebRequest $url -OutFile $file
|
Invoke-WebRequest $url -OutFile $file
|
||||||
$log = "install.log"
|
$log = "install.log"
|
||||||
@@ -725,6 +712,15 @@ jobs:
|
|||||||
$procMain.WaitForExit()
|
$procMain.WaitForExit()
|
||||||
$procLog.Kill()
|
$procLog.Kill()
|
||||||
|
|
||||||
|
- name: Run SSD directly
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Write-Host "Running SSD..."
|
||||||
|
& "C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe"
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
& "C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe"
|
||||||
|
|
||||||
- name: Login to Certum
|
- name: Login to Certum
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -733,10 +729,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
write-host "running SimplySignDesktop login..."
|
write-host "running SimplySignDesktop login..."
|
||||||
node tools/ssd.mjs --log-level warn
|
node tools/ssd.mjs --log-level warn
|
||||||
|
Get-ChildItem -Path $env:GITHUB_WORKSPACE
|
||||||
write-host "sleeping during login..."
|
write-host "sleeping during login..."
|
||||||
Start-Sleep 10
|
Start-Sleep 10
|
||||||
|
|
||||||
- name: Archive png artifacts
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
with:
|
with:
|
||||||
@@ -744,7 +741,8 @@ jobs:
|
|||||||
name: images-${{ matrix.os }}
|
name: images-${{ matrix.os }}
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
path: |
|
path: |
|
||||||
*.png
|
${{ github.workspace }}/*.png
|
||||||
|
${{ github.workspace }}/*.log
|
||||||
|
|
||||||
- name: Sign gam.exe
|
- name: Sign gam.exe
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
|||||||
@@ -3162,6 +3162,7 @@ gam <UserTypeEntity> show contactdelegates [shownames] [csv]
|
|||||||
name|
|
name|
|
||||||
owneremail|
|
owneremail|
|
||||||
ownerid|
|
ownerid|
|
||||||
|
ownername|
|
||||||
room|
|
room|
|
||||||
section|
|
section|
|
||||||
students|
|
students|
|
||||||
@@ -3275,16 +3276,16 @@ gam courses <CourseEntity> sync teachers [addonly|removeonly] [makefirstteachero
|
|||||||
gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
|
gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
|
||||||
|
|
||||||
gam info course <CourseID> [owneraccess]
|
gam info course <CourseID> [owneraccess]
|
||||||
[owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
|
[owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
|
||||||
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
||||||
[formatjson]
|
[formatjson]
|
||||||
gam info courses <CourseEntity> [owneraccess]
|
gam info courses <CourseEntity> [owneraccess]
|
||||||
[owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
|
[owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
|
||||||
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
||||||
[formatjson]
|
[formatjson]
|
||||||
gam print courses [todrive <ToDriveAttribute>*]
|
gam print courses [todrive <ToDriveAttribute>*]
|
||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
||||||
[owneremail] [owneremailmatchpattern <REMatchPattern>]
|
[owneremail] [owneremailmatchpattern <REMatchPattern>] [ownername]
|
||||||
[alias|aliases|aliasesincolumns [delimiter <Character>]]
|
[alias|aliases|aliasesincolumns [delimiter <Character>]]
|
||||||
[show all|students|teachers] [countsonly]
|
[show all|students|teachers] [countsonly]
|
||||||
[timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
@@ -3398,14 +3399,16 @@ gam print course-announcements [todrive <ToDriveAttribute>*]
|
|||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
(announcementids <CourseAnnouncementIDEntity>)|((announcementstates <CourseAnnouncementStateList>)*
|
(announcementids <CourseAnnouncementIDEntity>)|((announcementstates <CourseAnnouncementStateList>)*
|
||||||
(orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
|
(orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
|
||||||
[showcreatoremails|creatoremail] [fields <CourseAnnouncementFieldNameList>]
|
[showcreatoremails|creatoremail] [showcreatornames|creatorname]
|
||||||
|
[fields <CourseAnnouncementFieldNameList>]
|
||||||
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
[countsonly] [formatjson [quotechar <Character>]]
|
[countsonly] [formatjson [quotechar <Character>]]
|
||||||
gam print course-materials [todrive <ToDriveAttribute>*]
|
gam print course-materials [todrive <ToDriveAttribute>*]
|
||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
(materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
|
(materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
|
||||||
(orderby <CourseMaterialOrderByFieldName> [ascending|descending])*)
|
(orderby <CourseMaterialOrderByFieldName> [ascending|descending])*)
|
||||||
[showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>]
|
[showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
|
||||||
|
[fields <CourseMaterialFieldNameList>]
|
||||||
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
[oneitemperrow]
|
[oneitemperrow]
|
||||||
[countsonly] [formatjson [quotechar <Character>]]
|
[countsonly] [formatjson [quotechar <Character>]]
|
||||||
@@ -3426,7 +3429,8 @@ gam print course-works [todrive <ToDriveAttribute>*]
|
|||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
(workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
|
(workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
|
||||||
(orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
(orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
||||||
[showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>]
|
[showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
|
||||||
|
[fields <CourseWorkFieldNameList>]
|
||||||
[showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
[showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
||||||
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
[oneitemperrow]
|
[oneitemperrow]
|
||||||
@@ -3884,6 +3888,8 @@ gam audit monitor list <EmailAddress>
|
|||||||
gam create|add group <EmailAddress>
|
gam create|add group <EmailAddress>
|
||||||
[copyfrom <GroupItem>] <GroupAttribute>*
|
[copyfrom <GroupItem>] <GroupAttribute>*
|
||||||
[verifynotinvitable]
|
[verifynotinvitable]
|
||||||
|
[recentdeleteretries <Integer>] [recentdeleteretrydelay <Integer>]
|
||||||
|
[verifycreationretries <Integer>] [verifycreationinitialdelay <Integer>] [verifycreationretrydelay <Integer>]
|
||||||
gam update group|groups <GroupEntity> [email <EmailAddress>]
|
gam update group|groups <GroupEntity> [email <EmailAddress>]
|
||||||
[updateprimaryemail <RESearchPattern> <RESubstitution>]
|
[updateprimaryemail <RESearchPattern> <RESubstitution>]
|
||||||
[copyfrom <GroupItem>] <GroupAttribute>*
|
[copyfrom <GroupItem>] <GroupAttribute>*
|
||||||
|
|||||||
@@ -1,3 +1,50 @@
|
|||||||
|
7.41.03
|
||||||
|
|
||||||
|
Fixed bug in the following:
|
||||||
|
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
|
||||||
|
synonyms for `count` and `countrange`.
|
||||||
|
```
|
||||||
|
[(any|all):]number<Operator><Number>|
|
||||||
|
[(any|all):]numberrange!=<Number>/<Number>|
|
||||||
|
[(any|all):]numberrange=<Number>/<Number>|
|
||||||
|
```
|
||||||
|
|
||||||
|
7.41.02
|
||||||
|
|
||||||
|
Added option `ownername` to `gam info|print courses` to have GAM display the course owners full name;
|
||||||
|
there is an extra API call per course to get the name.
|
||||||
|
|
||||||
|
Added option `creatorname` to `gam print course-announcements|course-materials|course-works` to have
|
||||||
|
GAM display the item creators full name; there is an extra API call per course to get the name.
|
||||||
|
|
||||||
|
After creating a group, it may be sometime, e.g. 30-45 seconds, before members can
|
||||||
|
successfully be added to the group even though the API reported that the group was created.
|
||||||
|
The following options can be used with `gam create group` to verify that the group is actually ready to be updated.
|
||||||
|
This will be most useful in scripts that are used to create and then populate groups.
|
||||||
|
```
|
||||||
|
verifycreationretries <Integer> - Verify group creation, defaults to 0, no verification performed, range 0-20
|
||||||
|
verifycreationinitialdelay <Integer> - Number of seconds to delay before first verification performed, defaults to 5, range 0-60
|
||||||
|
verifycreationretrydelay <Integer> - Number of seconds to delay between verificaton retries, defaults to 5, range 1-60
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have a script that deletes a group and then immediately tries to create a new group with the same email address,
|
||||||
|
you may run into issues. There seems to be a 30-45 second window after the deletion in which a couple
|
||||||
|
of strange errors can occur on the creation: `Resource not found` and `Duplicate`.
|
||||||
|
The following options can be used with `gam create group` to handle these errors. This will be most useful
|
||||||
|
in scripts that are used to delete and then immediately recreate groups.
|
||||||
|
```
|
||||||
|
recentdeleteretries <Integer> - Handle group delete/create errors, defaults to 0, no errors handled, range 0-20
|
||||||
|
recentdeleteretrydelay <Integer> - Number of seconds to delay between retries, defaults to 5, range 1-60
|
||||||
|
```
|
||||||
|
|
||||||
|
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
|
||||||
|
synonyms for `count` and `countrange`.
|
||||||
|
```
|
||||||
|
[(any|all):]number<Operator><Number>|
|
||||||
|
[(any|all):]numberrange!=<Number>/<Number>|
|
||||||
|
[(any|all):]numberrange=<Number>/<Number>|
|
||||||
|
```
|
||||||
|
|
||||||
7.41.01
|
7.41.01
|
||||||
|
|
||||||
Fixed bug in `gam print cigroups members managers owners countsonly totalcount internal external` that caused a trap.
|
Fixed bug in `gam print cigroups members managers owners countsonly totalcount internal external` that caused a trap.
|
||||||
|
|||||||
@@ -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.41.01'
|
__version__ = '7.41.03'
|
||||||
__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
|
||||||
@@ -114,6 +114,8 @@ from pathvalidate import sanitize_filename, sanitize_filepath
|
|||||||
import google.oauth2.credentials
|
import google.oauth2.credentials
|
||||||
import google.oauth2.id_token
|
import google.oauth2.id_token
|
||||||
import google.auth
|
import google.auth
|
||||||
|
import google.auth.transport.requests
|
||||||
|
from google.auth.compute_engine import _metadata as gce_metadata
|
||||||
from google.auth.jwt import Credentials as JWTCredentials
|
from google.auth.jwt import Credentials as JWTCredentials
|
||||||
import google.oauth2.service_account
|
import google.oauth2.service_account
|
||||||
import google_auth_oauthlib.flow
|
import google_auth_oauthlib.flow
|
||||||
@@ -3579,8 +3581,8 @@ def SetGlobalVariables():
|
|||||||
return headerFilters
|
return headerFilters
|
||||||
|
|
||||||
ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE)
|
ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE)
|
||||||
ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE)
|
ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length|number)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE)
|
||||||
ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
|
ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange|numberrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
|
||||||
ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE)
|
ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE)
|
||||||
ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE)
|
ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE)
|
||||||
ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE)
|
ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE)
|
||||||
@@ -3646,7 +3648,7 @@ def SetGlobalVariables():
|
|||||||
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue))
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue))
|
||||||
else:
|
else:
|
||||||
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue}')
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue}')
|
||||||
else: # filterType in {'count', 'length'}:
|
else: # filterType in {'count', 'length', 'number'}:
|
||||||
if mg.group(3).isdigit():
|
if mg.group(3).isdigit():
|
||||||
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3))))
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3))))
|
||||||
else:
|
else:
|
||||||
@@ -3676,7 +3678,7 @@ def SetGlobalVariables():
|
|||||||
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue1, filterValue2))
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue1, filterValue2))
|
||||||
else:
|
else:
|
||||||
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue1}/{filterValue2}')
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue1}/{filterValue2}')
|
||||||
else: #countrange|lengthrange
|
else: #countrange|lengthrange|numberrange
|
||||||
if mg.group(3).isdigit() and mg.group(4).isdigit():
|
if mg.group(3).isdigit() and mg.group(4).isdigit():
|
||||||
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)), int(mg.group(4))))
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)), int(mg.group(4))))
|
||||||
else:
|
else:
|
||||||
@@ -4221,6 +4223,9 @@ def SetGlobalVariables():
|
|||||||
GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
|
GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
|
||||||
# Override httplib2 settings
|
# Override httplib2 settings
|
||||||
httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL]
|
httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL]
|
||||||
|
# Override requests debuglevel also. Requests is used with
|
||||||
|
# SignJWT/WIF/GCE and a few other places.
|
||||||
|
http.client.HTTPConnection.debuglevel = GC.Values[GC.DEBUG_LEVEL]
|
||||||
# Use our own print function for http.client so we can redact and cleanup
|
# Use our own print function for http.client so we can redact and cleanup
|
||||||
http.client.print = redactable_debug_print
|
http.client.print = redactable_debug_print
|
||||||
# Reset global variables if required
|
# Reset global variables if required
|
||||||
@@ -4516,13 +4521,6 @@ class signjwtJWTCredentials(google.auth.jwt.Credentials):
|
|||||||
jwt = self._signer.sign(payload)
|
jwt = self._signer.sign(payload)
|
||||||
return jwt, expiry.naive
|
return jwt, expiry.naive
|
||||||
|
|
||||||
# Some Workforce Identity Federation endpoints such as GitHub Actions
|
|
||||||
# only allow TLS 1.2 as of April 2023.
|
|
||||||
|
|
||||||
def getTLSv1_2Request():
|
|
||||||
httpc = getHttpObj(override_min_tls='TLSv1_2')
|
|
||||||
return transportCreateRequest(httpc)
|
|
||||||
|
|
||||||
class signjwtCredentials(google.oauth2.service_account.Credentials):
|
class signjwtCredentials(google.oauth2.service_account.Credentials):
|
||||||
''' Class used for DwD '''
|
''' Class used for DwD '''
|
||||||
|
|
||||||
@@ -4543,6 +4541,15 @@ class signjwtCredentials(google.oauth2.service_account.Credentials):
|
|||||||
token = self._signer(payload)
|
token = self._signer(payload)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
def get_adc_request():
|
||||||
|
request = google.auth.transport.requests.Request()
|
||||||
|
if GM.Globals[GM.IS_ON_GCE]:
|
||||||
|
return request
|
||||||
|
if gce_metadata.is_on_gce(request):
|
||||||
|
GM.Globals[GM.IS_ON_GCE] = True
|
||||||
|
return request
|
||||||
|
return transportCreateRequest()
|
||||||
|
|
||||||
class signjwtSignJwt(google.auth.crypt.Signer):
|
class signjwtSignJwt(google.auth.crypt.Signer):
|
||||||
''' Signer class for SignJWT '''
|
''' Signer class for SignJWT '''
|
||||||
def __init__(self, service_account_info):
|
def __init__(self, service_account_info):
|
||||||
@@ -4556,12 +4563,15 @@ class signjwtSignJwt(google.auth.crypt.Signer):
|
|||||||
|
|
||||||
def sign(self, message):
|
def sign(self, message):
|
||||||
''' Call IAM Credentials SignJWT API to get our signed JWT '''
|
''' Call IAM Credentials SignJWT API to get our signed JWT '''
|
||||||
|
request = get_adc_request()
|
||||||
try:
|
try:
|
||||||
credentials, _ = google.auth.default(scopes=[API.IAM_SCOPE],
|
credentials, _ = google.auth.default(scopes=[API.IAM_SCOPE],
|
||||||
request=getTLSv1_2Request())
|
request=request)
|
||||||
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
|
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
|
||||||
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
|
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
|
||||||
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(override_min_tls='TLSv1_2'))
|
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj())
|
||||||
|
# refresh here so we can use the proper request from above
|
||||||
|
httpObj.credentials.refresh(request)
|
||||||
iamc = getService(API.IAM_CREDENTIALS, httpObj)
|
iamc = getService(API.IAM_CREDENTIALS, httpObj)
|
||||||
response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt',
|
response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt',
|
||||||
name=self.name, body={'payload': json.dumps(message)})
|
name=self.name, body={'payload': json.dumps(message)})
|
||||||
@@ -7827,10 +7837,10 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter,
|
|||||||
elif filterVal[2] == 'timeofdayrange':
|
elif filterVal[2] == 'timeofdayrange':
|
||||||
if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
||||||
return True
|
return True
|
||||||
elif filterVal[2] == 'count':
|
elif filterVal[2] in {'count', 'number'}:
|
||||||
if rowCountFilterMatch(filterVal[3], filterVal[4]):
|
if rowCountFilterMatch(filterVal[3], filterVal[4]):
|
||||||
return True
|
return True
|
||||||
elif filterVal[2] == 'countrange':
|
elif filterVal[2] in {'countrange', 'numberrange'}:
|
||||||
if rowCountRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
if rowCountRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
||||||
return True
|
return True
|
||||||
elif filterVal[2] == 'length':
|
elif filterVal[2] == 'length':
|
||||||
@@ -11615,7 +11625,7 @@ def doEnableAPIs():
|
|||||||
automatic = False
|
automatic = False
|
||||||
else:
|
else:
|
||||||
unknownArgumentExit()
|
unknownArgumentExit()
|
||||||
request = getTLSv1_2Request()
|
request = get_adc_request()
|
||||||
try:
|
try:
|
||||||
_, projectId = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
|
_, projectId = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
|
||||||
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError):
|
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError):
|
||||||
@@ -13200,7 +13210,7 @@ def doCreateGCPServiceAccount():
|
|||||||
checkForExtraneousArguments()
|
checkForExtraneousArguments()
|
||||||
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
|
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
|
||||||
sa_info = {'key_type': 'signjwt', 'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'type': 'service_account'}
|
sa_info = {'key_type': 'signjwt', 'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'type': 'service_account'}
|
||||||
request = getTLSv1_2Request()
|
request = get_adc_request()
|
||||||
try:
|
try:
|
||||||
credentials, sa_info['project_id'] = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
|
credentials, sa_info['project_id'] = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
|
||||||
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
|
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
|
||||||
@@ -34008,9 +34018,20 @@ GROUP_ACCESS_TYPE_CHOICE_MAP = {
|
|||||||
|
|
||||||
# gam create group <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>*
|
# gam create group <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>*
|
||||||
# [verifynotinvitable]
|
# [verifynotinvitable]
|
||||||
|
# [verifyduplicateretries <Integer>] [verifyduplicateretrydelay <Integer>]
|
||||||
|
# [verifycreationretries <Integer>] [verifycreationinitialdelay <Integer>] [verifycreationretrydelay <Integer>]
|
||||||
def doCreateGroup(ciGroupsAPI=False):
|
def doCreateGroup(ciGroupsAPI=False):
|
||||||
|
def waitingForCreationToComplete(sleep_time):
|
||||||
|
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.GROUP), sleep_time))
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
cd = buildGAPIObject(API.DIRECTORY)
|
cd = buildGAPIObject(API.DIRECTORY)
|
||||||
verifyNotInvitable = getBeforeUpdate = False
|
verifyNotInvitable = getBeforeUpdate = False
|
||||||
|
recentDeleteRetries = 0
|
||||||
|
recentDeleteRetryDelay = 5
|
||||||
|
verifyCreationRetries = 0
|
||||||
|
verifyCreationInitialDelay = 5
|
||||||
|
verifyCreationRetryDelay = 5
|
||||||
groupEmail = getEmailAddress(noUid=True)
|
groupEmail = getEmailAddress(noUid=True)
|
||||||
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
|
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
|
||||||
if not ciGroupsAPI:
|
if not ciGroupsAPI:
|
||||||
@@ -34050,6 +34071,16 @@ def doCreateGroup(ciGroupsAPI=False):
|
|||||||
body['labels'][CIGROUP_LOCKED_LABEL] = ''
|
body['labels'][CIGROUP_LOCKED_LABEL] = ''
|
||||||
elif myarg == 'verifynotinvitable':
|
elif myarg == 'verifynotinvitable':
|
||||||
verifyNotInvitable = True
|
verifyNotInvitable = True
|
||||||
|
elif myarg == 'recentdeleteretries':
|
||||||
|
recentDeleteRetries = getInteger(minVal=0, maxVal=20)
|
||||||
|
elif myarg == 'recentdeleteretrydelay':
|
||||||
|
recentDeleteRetryDelay = getInteger(minVal=1, maxVal=60)
|
||||||
|
elif myarg == 'verifycreationretries':
|
||||||
|
verifyCreationRetries = getInteger(minVal=0, maxVal=20)
|
||||||
|
elif myarg == 'verifycreationinitialdelay':
|
||||||
|
verifyCreationInitialDelay = getInteger(minVal=0, maxVal=60)
|
||||||
|
elif myarg == 'verifycreationretrydelay':
|
||||||
|
verifyCreationRetryDelay = getInteger(minVal=1, maxVal=60)
|
||||||
else:
|
else:
|
||||||
getGroupAttrValue(myarg, gs_body)
|
getGroupAttrValue(myarg, gs_body)
|
||||||
if verifyNotInvitable:
|
if verifyNotInvitable:
|
||||||
@@ -34070,38 +34101,89 @@ def doCreateGroup(ciGroupsAPI=False):
|
|||||||
return
|
return
|
||||||
if not getBeforeUpdate:
|
if not getBeforeUpdate:
|
||||||
settings = gs_body
|
settings = gs_body
|
||||||
try:
|
duplicateRetries = 0
|
||||||
if not ciGroupsAPI:
|
while True:
|
||||||
callGAPI(cd.groups(), 'insert',
|
try:
|
||||||
throwReasons=GAPI.GROUP_CREATE_THROW_REASONS,
|
if not ciGroupsAPI:
|
||||||
body=body, fields='')
|
callGAPI(cd.groups(), 'insert',
|
||||||
else:
|
throwReasons=GAPI.GROUP_CREATE_THROW_REASONS,
|
||||||
callGAPI(ci.groups(), 'create',
|
body=body, fields='')
|
||||||
throwReasons=GAPI.CIGROUP_CREATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
else:
|
||||||
initialGroupConfig=initialGroupConfig, body=body, fields='')
|
callGAPI(ci.groups(), 'create',
|
||||||
if gs_body and not GroupIsAbuseOrPostmaster(groupEmail):
|
throwReasons=GAPI.CIGROUP_CREATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
||||||
if getBeforeUpdate:
|
initialGroupConfig=initialGroupConfig, body=body, fields='')
|
||||||
settings = callGAPI(gs.groups(), 'get',
|
if gs_body and not GroupIsAbuseOrPostmaster(groupEmail):
|
||||||
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
|
groupUniqueId = mapGroupEmailForSettings(groupEmail)
|
||||||
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND],
|
if getBeforeUpdate:
|
||||||
groupUniqueId=mapGroupEmailForSettings(groupEmail), fields='*')
|
settings = callGAPI(gs.groups(), 'get',
|
||||||
settings.update(gs_body)
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
|
||||||
callGAPI(gs.groups(), 'update',
|
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND],
|
||||||
bailOnInvalidError='messageModerationLevel' in settings,
|
groupUniqueId=groupUniqueId, fields='*')
|
||||||
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
|
settings.update(gs_body)
|
||||||
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
callGAPI(gs.groups(), 'update',
|
||||||
groupUniqueId=mapGroupEmailForSettings(groupEmail), body=settings, fields='')
|
bailOnInvalidError='messageModerationLevel' in settings,
|
||||||
entityActionPerformed([entityType, groupEmail])
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
|
||||||
except (GAPI.alreadyExists, GAPI.duplicate):
|
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
||||||
duplicateAliasGroupUserWarning(cd, [entityType, groupEmail])
|
groupUniqueId=groupUniqueId, body=settings, fields='')
|
||||||
except GAPI.notFound:
|
entityActionPerformed([entityType, groupEmail])
|
||||||
entityActionFailedWarning([entityType, groupEmail], Msg.DOES_NOT_EXIST)
|
break
|
||||||
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
|
except GAPI.resourceNotFound as e:
|
||||||
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.invalidArgument, GAPI.failedPrecondition,
|
# If group we're trying to create was just deleted, Google gets confused; sleep and retry
|
||||||
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
duplicateRetries += 1
|
||||||
entityActionFailedWarning([entityType, groupEmail], str(e))
|
if ciGroupsAPI or duplicateRetries > recentDeleteRetries:
|
||||||
except GAPI.required:
|
entityActionFailedWarning([entityType, groupEmail], str(e))
|
||||||
entityActionFailedWarning([entityType, groupEmail], Msg.INVALID_JSON_SETTING)
|
break
|
||||||
|
time.sleep(recentDeleteRetryDelay)
|
||||||
|
continue
|
||||||
|
except (GAPI.alreadyExists, GAPI.duplicate):
|
||||||
|
duplicateRetries += 1
|
||||||
|
if ciGroupsAPI or duplicateRetries > recentDeleteRetries:
|
||||||
|
duplicateAliasGroupUserWarning(cd, [entityType, groupEmail])
|
||||||
|
break
|
||||||
|
time.sleep(recentDeleteRetryDelay)
|
||||||
|
continue
|
||||||
|
# except GAPI.notFound:
|
||||||
|
# entityActionFailedWarning([entityType, groupEmail], Msg.DOES_NOT_EXIST)
|
||||||
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
|
||||||
|
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.invalidArgument, GAPI.failedPrecondition,
|
||||||
|
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
||||||
|
entityActionFailedWarning([entityType, groupEmail], str(e))
|
||||||
|
break
|
||||||
|
except GAPI.required:
|
||||||
|
entityActionFailedWarning([entityType, groupEmail], Msg.INVALID_JSON_SETTING)
|
||||||
|
break
|
||||||
|
if ciGroupsAPI or not verifyCreationRetries:
|
||||||
|
return
|
||||||
|
Act.Set(Act.VERIFYITEMEXISTS)
|
||||||
|
action = Act.Get()
|
||||||
|
performAction(Ent.GROUP, groupEmail)
|
||||||
|
Ind.Increment()
|
||||||
|
waitingForCreationToComplete(verifyCreationInitialDelay)
|
||||||
|
retries = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
callGAPI(cd.groups(), 'get',
|
||||||
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
||||||
|
groupKey=groupEmail, fields='name')
|
||||||
|
entityActionPerformed([Ent.GROUP, groupEmail])
|
||||||
|
break
|
||||||
|
except GAPI.groupNotFound:
|
||||||
|
retries += 1
|
||||||
|
kvList = [Act.PerformedName(action), False, 'Retry', f'{retries}/{verifyCreationRetries}']
|
||||||
|
printEntityKVList([Ent.GROUP, groupEmail], kvList)
|
||||||
|
if retries >= verifyCreationRetries:
|
||||||
|
entityActionFailedWarning([Ent.GROUP, groupEmail], Msg.RETRIES_EXHAUSTED.format(verifyCreationRetries))
|
||||||
|
break
|
||||||
|
waitingForCreationToComplete(verifyCreationRetryDelay)
|
||||||
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
|
||||||
|
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden,
|
||||||
|
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
||||||
|
entityActionFailedWarning([Ent.GROUP, groupEmail], str(e))
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
entityActionFailedWarning([Ent.GROUP, groupEmail], Msg.CHECK_INTERRUPTED)
|
||||||
|
break
|
||||||
|
Ind.Decrement()
|
||||||
|
|
||||||
# [addonly|removeonly]
|
# [addonly|removeonly]
|
||||||
def getSyncOperation():
|
def getSyncOperation():
|
||||||
@@ -50277,6 +50359,7 @@ COURSE_FIELDS_CHOICE_MAP = {
|
|||||||
'name': 'name',
|
'name': 'name',
|
||||||
'owneremail': 'ownerId',
|
'owneremail': 'ownerId',
|
||||||
'ownerid': 'ownerId',
|
'ownerid': 'ownerId',
|
||||||
|
'ownername': 'ownerId',
|
||||||
'room': 'room',
|
'room': 'room',
|
||||||
'section': 'section',
|
'section': 'section',
|
||||||
'teacherfolder': 'teacherFolder',
|
'teacherfolder': 'teacherFolder',
|
||||||
@@ -50299,6 +50382,7 @@ COURSE_PROPERTY_PRINT_ORDER = [
|
|||||||
'alternateLink',
|
'alternateLink',
|
||||||
'ownerEmail',
|
'ownerEmail',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
|
'ownerName',
|
||||||
'creationTime',
|
'creationTime',
|
||||||
'updateTime',
|
'updateTime',
|
||||||
'calendarId',
|
'calendarId',
|
||||||
@@ -50310,7 +50394,8 @@ COURSE_PROPERTY_PRINT_ORDER = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _initCourseShowProperties(fields=None):
|
def _initCourseShowProperties(fields=None):
|
||||||
return {'aliases': False, 'aliasesInColumns': False, 'ownerEmail': False, 'ownerEmailMatchPattern': None, 'members': 'none', 'countsOnly': False,
|
return {'aliases': False, 'aliasesInColumns': False, 'ownerEmail': False, 'ownerEmailMatchPattern': None,
|
||||||
|
'ownerName': False, 'members': 'none', 'countsOnly': False,
|
||||||
'fields': fields if fields is not None else [], 'skips': []}
|
'fields': fields if fields is not None else [], 'skips': []}
|
||||||
|
|
||||||
def _getCourseShowProperties(myarg, courseShowProperties):
|
def _getCourseShowProperties(myarg, courseShowProperties):
|
||||||
@@ -50325,6 +50410,8 @@ def _getCourseShowProperties(myarg, courseShowProperties):
|
|||||||
elif myarg == 'owneremailmatchpattern':
|
elif myarg == 'owneremailmatchpattern':
|
||||||
courseShowProperties['ownerEmail'] = True
|
courseShowProperties['ownerEmail'] = True
|
||||||
courseShowProperties['ownerEmailMatchPattern'] = getREPattern(re.IGNORECASE)
|
courseShowProperties['ownerEmailMatchPattern'] = getREPattern(re.IGNORECASE)
|
||||||
|
elif myarg == 'ownername':
|
||||||
|
courseShowProperties['ownerName'] = True
|
||||||
elif myarg == 'show':
|
elif myarg == 'show':
|
||||||
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
|
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
|
||||||
elif myarg == 'countsonly':
|
elif myarg == 'countsonly':
|
||||||
@@ -50340,6 +50427,9 @@ def _getCourseShowProperties(myarg, courseShowProperties):
|
|||||||
elif field == 'owneremail':
|
elif field == 'owneremail':
|
||||||
courseShowProperties['ownerEmail'] = True
|
courseShowProperties['ownerEmail'] = True
|
||||||
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
|
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
|
||||||
|
elif field == 'ownername':
|
||||||
|
courseShowProperties['ownerName'] = True
|
||||||
|
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
|
||||||
elif field == 'teachers':
|
elif field == 'teachers':
|
||||||
if courseShowProperties['members'] == 'none':
|
if courseShowProperties['members'] == 'none':
|
||||||
courseShowProperties['members'] = field
|
courseShowProperties['members'] = field
|
||||||
@@ -50381,27 +50471,26 @@ def _setCourseFields(courseShowProperties, pagesMode, getOwnerId=False):
|
|||||||
if not courseShowProperties['fields']:
|
if not courseShowProperties['fields']:
|
||||||
return None
|
return None
|
||||||
courseShowProperties['fields'].append('id')
|
courseShowProperties['fields'].append('id')
|
||||||
if courseShowProperties['ownerEmail'] or getOwnerId:
|
if courseShowProperties['ownerEmail'] or courseShowProperties['ownerName'] or getOwnerId:
|
||||||
courseShowProperties['fields'].append('ownerId')
|
courseShowProperties['fields'].append('ownerId')
|
||||||
if not pagesMode:
|
if not pagesMode:
|
||||||
return ','.join(set(courseShowProperties['fields']))
|
return ','.join(set(courseShowProperties['fields']))
|
||||||
return f'nextPageToken,courses({",".join(set(courseShowProperties["fields"]))})'
|
return f'nextPageToken,courses({",".join(set(courseShowProperties["fields"]))})'
|
||||||
|
|
||||||
def _convertCourseUserIdToEmail(croom, userId, emails, entityValueList, i, count):
|
def _convertCourseUserIdToEmailName(croom, userId, emails, entityValueList, i, count):
|
||||||
userEmail = emails.get(userId)
|
if userId not in emails:
|
||||||
if userEmail is None:
|
|
||||||
try:
|
try:
|
||||||
userEmail = callGAPI(croom.userProfiles(), 'get',
|
result = callGAPI(croom.userProfiles(), 'get',
|
||||||
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
||||||
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
||||||
userId=userId, fields='emailAddress').get('emailAddress')
|
userId=userId, fields='emailAddress,name(fullName)')
|
||||||
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable):
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable):
|
||||||
pass
|
result = {}
|
||||||
if userEmail is None:
|
if not result:
|
||||||
entityDoesNotHaveItemWarning(entityValueList, i, count)
|
entityDoesNotHaveItemWarning(entityValueList, i, count)
|
||||||
userEmail = 'Unknown user'
|
emails[userId] = (result.get('emailAddress', 'Unknown user'),
|
||||||
emails[userId] = userEmail
|
result.get('name', {}).get('fullName', 'Unknown user'))
|
||||||
return userEmail
|
return emails[userId]
|
||||||
|
|
||||||
def _getCourseOwnerSA(croom, course, useOwnerAccess):
|
def _getCourseOwnerSA(croom, course, useOwnerAccess):
|
||||||
if not useOwnerAccess:
|
if not useOwnerAccess:
|
||||||
@@ -50523,9 +50612,13 @@ def _doInfoCourses(courseIdList):
|
|||||||
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
||||||
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
||||||
id=courseId, fields=fields)
|
id=courseId, fields=fields)
|
||||||
if courseShowProperties['ownerEmail']:
|
if courseShowProperties['ownerEmail'] or courseShowProperties['ownerName']:
|
||||||
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
|
ownerEmail, ownerName = _convertCourseUserIdToEmailName(croom, course['ownerId'], ownerEmails,
|
||||||
[Ent.COURSE, course['id'], Ent.OWNER_ID, course['ownerId']], i, count)
|
[Ent.COURSE, course['id'], Ent.OWNER_ID, course['ownerId']], i, count)
|
||||||
|
if courseShowProperties['ownerEmail']:
|
||||||
|
course['ownerEmail'] = ownerEmail
|
||||||
|
if courseShowProperties['ownerName']:
|
||||||
|
course['ownerName'] = ownerName
|
||||||
aliases, teachers, students = _getCourseAliasesMembers(croom, courseInfo['croom'], courseId, courseShowProperties, teachersFields, studentsFields)
|
aliases, teachers, students = _getCourseAliasesMembers(croom, courseInfo['croom'], courseId, courseShowProperties, teachersFields, studentsFields)
|
||||||
if FJQC.formatJSON:
|
if FJQC.formatJSON:
|
||||||
if courseShowProperties['aliases']:
|
if courseShowProperties['aliases']:
|
||||||
@@ -50589,14 +50682,14 @@ def _doInfoCourses(courseIdList):
|
|||||||
ClientAPIAccessDeniedExit(str(e))
|
ClientAPIAccessDeniedExit(str(e))
|
||||||
|
|
||||||
# gam info courses <CourseEntity> [owneraccess]
|
# gam info courses <CourseEntity> [owneraccess]
|
||||||
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
|
# [owneremail] [ownername] [alias|aliases] [show none|all|students|teachers] [countsonly]
|
||||||
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
||||||
# [formatjson]
|
# [formatjson]
|
||||||
def doInfoCourses():
|
def doInfoCourses():
|
||||||
_doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
_doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
||||||
|
|
||||||
# gam info course <CourseID> [owneraccess]
|
# gam info course <CourseID> [owneraccess]
|
||||||
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
|
# [owneremail] [ownername] [alias|aliases] [show none|all|students|teachers] [countsonly]
|
||||||
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
||||||
# [formatjson]
|
# [formatjson]
|
||||||
def doInfoCourse():
|
def doInfoCourse():
|
||||||
@@ -50709,7 +50802,7 @@ def _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getO
|
|||||||
return coursesInfo
|
return coursesInfo
|
||||||
|
|
||||||
# gam print courses [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
# gam print courses [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
||||||
# [owneremail] [owneremailmatchpattern <REMatchPattern>]
|
# [owneremail] [owneremailmatchpattern <REMatchPattern>] [ownername]
|
||||||
# [alias|aliases|aliasesincolumns [delimiter <Character>]]
|
# [alias|aliases|aliasesincolumns [delimiter <Character>]]
|
||||||
# [show none|all|students|teachers] [countsonly]
|
# [show none|all|students|teachers] [countsonly]
|
||||||
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
||||||
@@ -50828,11 +50921,15 @@ def doPrintCourses():
|
|||||||
ocroom = _getCourseOwnerSA(croom, course, useOwnerAccess)
|
ocroom = _getCourseOwnerSA(croom, course, useOwnerAccess)
|
||||||
if not ocroom:
|
if not ocroom:
|
||||||
continue
|
continue
|
||||||
if courseShowProperties['ownerEmail']:
|
if courseShowProperties['ownerEmail'] or courseShowProperties['ownerName']:
|
||||||
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
|
ownerEmail, ownerName = _convertCourseUserIdToEmailName(croom, course['ownerId'], ownerEmails,
|
||||||
[Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count)
|
[Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count)
|
||||||
if courseShowProperties['ownerEmailMatchPattern'] and not courseShowProperties['ownerEmailMatchPattern'].match(course['ownerEmail']):
|
if courseShowProperties['ownerEmail']:
|
||||||
continue
|
course['ownerEmail'] = ownerEmail
|
||||||
|
if courseShowProperties['ownerEmailMatchPattern'] and not courseShowProperties['ownerEmailMatchPattern'].match(ownerEmail):
|
||||||
|
continue
|
||||||
|
if courseShowProperties['ownerName']:
|
||||||
|
course['ownerName'] = ownerName
|
||||||
if showItemCountOnly:
|
if showItemCountOnly:
|
||||||
itemCount += 1
|
itemCount += 1
|
||||||
continue
|
continue
|
||||||
@@ -50934,10 +51031,14 @@ def doPrintCourseAnnouncements():
|
|||||||
def _printCourseAnnouncement(course, courseAnnouncement, i, count):
|
def _printCourseAnnouncement(course, courseAnnouncement, i, count):
|
||||||
if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter):
|
if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter):
|
||||||
return
|
return
|
||||||
if showCreatorEmail:
|
if showCreatorEmail or showCreatorName:
|
||||||
courseAnnouncement['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseAnnouncement['creatorUserId'], creatorEmails,
|
creatorUserEmail, creatorUserName = _convertCourseUserIdToEmailName(croom, courseAnnouncement['creatorUserId'], creatorEmails,
|
||||||
[Ent.COURSE, course['id'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'],
|
[Ent.COURSE, course['id'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'],
|
||||||
Ent.CREATOR_ID, courseAnnouncement['creatorUserId']], i, count)
|
Ent.CREATOR_ID, courseAnnouncement['creatorUserId']], i, count)
|
||||||
|
if showCreatorEmail:
|
||||||
|
courseAnnouncement['creatorUserEmail'] = creatorUserEmail
|
||||||
|
if showCreatorName:
|
||||||
|
courseAnnouncement['creatorUserName'] = creatorUserName
|
||||||
row = flattenJSON(courseAnnouncement, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS)
|
row = flattenJSON(courseAnnouncement, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS)
|
||||||
if not FJQC.formatJSON:
|
if not FJQC.formatJSON:
|
||||||
csvPF.WriteRowTitles(row)
|
csvPF.WriteRowTitles(row)
|
||||||
@@ -50957,7 +51058,7 @@ def doPrintCourseAnnouncements():
|
|||||||
courseAnnouncementStates = []
|
courseAnnouncementStates = []
|
||||||
OBY = OrderBy(COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP)
|
OBY = OrderBy(COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP)
|
||||||
creatorEmails = {}
|
creatorEmails = {}
|
||||||
countsOnly = showCreatorEmail = False
|
countsOnly = showCreatorEmail = showCreatorName = False
|
||||||
items = 'courseAnnouncements'
|
items = 'courseAnnouncements'
|
||||||
while Cmd.ArgumentsRemaining():
|
while Cmd.ArgumentsRemaining():
|
||||||
myarg = getArgument()
|
myarg = getArgument()
|
||||||
@@ -50975,6 +51076,8 @@ def doPrintCourseAnnouncements():
|
|||||||
OBY.GetChoice()
|
OBY.GetChoice()
|
||||||
elif myarg in {'showcreatoremails', 'creatoremail'}:
|
elif myarg in {'showcreatoremails', 'creatoremail'}:
|
||||||
showCreatorEmail = True
|
showCreatorEmail = True
|
||||||
|
elif myarg in {'showcreatornames', 'creatorname'}:
|
||||||
|
showCreatorName = True
|
||||||
elif getFieldsList(myarg, COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
elif getFieldsList(myarg, COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
||||||
pass
|
pass
|
||||||
elif myarg == 'countsonly':
|
elif myarg == 'countsonly':
|
||||||
@@ -51232,10 +51335,14 @@ def doPrintCourseWM(entityIDType, entityStateType):
|
|||||||
def _printCourseWM(course, courseWM, i, count):
|
def _printCourseWM(course, courseWM, i, count):
|
||||||
if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter):
|
if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter):
|
||||||
return
|
return
|
||||||
if showCreatorEmail:
|
if showCreatorEmail or showCreatorName:
|
||||||
courseWM['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseWM['creatorUserId'], creatorEmails,
|
creatorUserEmail, creatorUserName = _convertCourseUserIdToEmailName(croom, courseWM['creatorUserId'], creatorEmails,
|
||||||
[Ent.COURSE, course['id'], entityIDType, courseWM['id'],
|
[Ent.COURSE, course['id'], entityIDType, courseWM['id'],
|
||||||
Ent.CREATOR_ID, courseWM['creatorUserId']], i, count)
|
Ent.CREATOR_ID, courseWM['creatorUserId']], i, count)
|
||||||
|
if showCreatorEmail:
|
||||||
|
courseWM['creatorUserEmail'] = creatorUserEmail
|
||||||
|
if showCreatorName:
|
||||||
|
courseWM['creatorUserName'] = creatorUserName
|
||||||
if showTopicNames:
|
if showTopicNames:
|
||||||
topicId = courseWM.get('topicId')
|
topicId = courseWM.get('topicId')
|
||||||
if topicId:
|
if topicId:
|
||||||
@@ -51284,7 +51391,7 @@ def doPrintCourseWM(entityIDType, entityStateType):
|
|||||||
courseShowProperties = _initCourseShowProperties(['name'])
|
courseShowProperties = _initCourseShowProperties(['name'])
|
||||||
OBY = OrderBy(OrderbyChoiceMap)
|
OBY = OrderBy(OrderbyChoiceMap)
|
||||||
creatorEmails = {}
|
creatorEmails = {}
|
||||||
oneItemPerRow = showCreatorEmail = showTopicNames = False
|
oneItemPerRow = showCreatorEmail = showCreatorName = showTopicNames = False
|
||||||
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
||||||
countsOnly = showStudentsAsList = False
|
countsOnly = showStudentsAsList = False
|
||||||
while Cmd.ArgumentsRemaining():
|
while Cmd.ArgumentsRemaining():
|
||||||
@@ -51306,6 +51413,8 @@ def doPrintCourseWM(entityIDType, entityStateType):
|
|||||||
csvPF.RemoveIndexedTitles('materials')
|
csvPF.RemoveIndexedTitles('materials')
|
||||||
elif myarg in {'showcreatoremails', 'creatoremail'}:
|
elif myarg in {'showcreatoremails', 'creatoremail'}:
|
||||||
showCreatorEmail = True
|
showCreatorEmail = True
|
||||||
|
elif myarg in {'showcreatornames', 'creatorname'}:
|
||||||
|
showCreatorName = True
|
||||||
elif myarg == 'showtopicnames':
|
elif myarg == 'showtopicnames':
|
||||||
showTopicNames = True
|
showTopicNames = True
|
||||||
elif getFieldsList(myarg, FieldsChoiceMap, fieldsList, initialField='id'):
|
elif getFieldsList(myarg, FieldsChoiceMap, fieldsList, initialField='id'):
|
||||||
@@ -51319,7 +51428,7 @@ def doPrintCourseWM(entityIDType, entityStateType):
|
|||||||
csvPF.AddTitles(items)
|
csvPF.AddTitles(items)
|
||||||
else:
|
else:
|
||||||
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
||||||
if showCreatorEmail and fieldsList:
|
if (showCreatorEmail or showCreatorName) and fieldsList:
|
||||||
fieldsList.append('creatorUserId')
|
fieldsList.append('creatorUserId')
|
||||||
if showTopicNames and fieldsList:
|
if showTopicNames and fieldsList:
|
||||||
fieldsList.append('topicId')
|
fieldsList.append('topicId')
|
||||||
@@ -51384,7 +51493,8 @@ def doPrintCourseWM(entityIDType, entityStateType):
|
|||||||
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
# (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
|
# (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
|
||||||
# (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*)
|
# (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*)
|
||||||
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>]
|
# [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
|
||||||
|
# [fields <CourseMaterialFieldNameList>]
|
||||||
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
# [oneitemperrow]
|
# [oneitemperrow]
|
||||||
# [countsonly|(formatjson [quotechar <Character>])]
|
# [countsonly|(formatjson [quotechar <Character>])]
|
||||||
@@ -51395,7 +51505,8 @@ def doPrintCourseMaterials():
|
|||||||
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
|
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
|
||||||
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
||||||
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>]
|
# [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
|
||||||
|
# [fields <CourseWorkFieldNameList>]
|
||||||
# [showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
# [showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
||||||
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
# [oneitemperrow]
|
# [oneitemperrow]
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class GamAction():
|
|||||||
UNZIP = 'unzi'
|
UNZIP = 'unzi'
|
||||||
USE = 'use '
|
USE = 'use '
|
||||||
VERIFY = 'vrfy'
|
VERIFY = 'vrfy'
|
||||||
|
VERIFYITEMEXISTS = 'vexi'
|
||||||
WAITFORMAILBOX = 'wamb'
|
WAITFORMAILBOX = 'wamb'
|
||||||
WATCH = 'watc'
|
WATCH = 'watc'
|
||||||
WIPE = 'wipe'
|
WIPE = 'wipe'
|
||||||
@@ -253,6 +254,7 @@ class GamAction():
|
|||||||
UPLOAD: ['Uploaded', 'Upload'],
|
UPLOAD: ['Uploaded', 'Upload'],
|
||||||
USE: ['Used', 'Use'],
|
USE: ['Used', 'Use'],
|
||||||
VERIFY: ['Verified', 'Verify'],
|
VERIFY: ['Verified', 'Verify'],
|
||||||
|
VERIFYITEMEXISTS: ['Verified Item Exists', 'Verify Item Exists'],
|
||||||
WAITFORMAILBOX: ['Mailbox is Setup', 'Check Mailbox is Setup'],
|
WAITFORMAILBOX: ['Mailbox is Setup', 'Check Mailbox is Setup'],
|
||||||
WATCH: ['Watched', 'Watch'],
|
WATCH: ['Watched', 'Watch'],
|
||||||
WIPE: ['Wiped', 'Wipe'],
|
WIPE: ['Wiped', 'Wipe'],
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ GMAIL_LIST_THROW_REASONS = [FAILED_PRECONDITION, PERMISSION_DENIED, INVALID, INV
|
|||||||
GMAIL_SMIME_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, INVALID_ARGUMENT, FORBIDDEN, NOT_FOUND, PERMISSION_DENIED]
|
GMAIL_SMIME_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, INVALID_ARGUMENT, FORBIDDEN, NOT_FOUND, PERMISSION_DENIED]
|
||||||
GROUP_GET_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR]
|
GROUP_GET_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR]
|
||||||
GROUP_GET_RETRY_REASONS = [INVALID, SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
|
GROUP_GET_RETRY_REASONS = [INVALID, SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
|
||||||
GROUP_CREATE_THROW_REASONS = [DUPLICATE, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT]
|
GROUP_CREATE_THROW_REASONS = [DUPLICATE, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT, RESOURCE_NOT_FOUND]
|
||||||
GROUP_UPDATE_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT]
|
GROUP_UPDATE_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT]
|
||||||
GROUP_SETTINGS_THROW_REASONS = [NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, SYSTEM_ERROR, PERMISSION_DENIED,
|
GROUP_SETTINGS_THROW_REASONS = [NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, SYSTEM_ERROR, PERMISSION_DENIED,
|
||||||
INVALID, INVALID_ARGUMENT, INVALID_PARAMETER, INVALID_ATTRIBUTE_VALUE, INVALID_INPUT,
|
INVALID, INVALID_ARGUMENT, INVALID_PARAMETER, INVALID_ATTRIBUTE_VALUE, INVALID_INPUT,
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ GAM_PATH = 'gpth'
|
|||||||
GAM_TYPE = 'gtyp'
|
GAM_TYPE = 'gtyp'
|
||||||
# Shared Service Account HTTP Object
|
# Shared Service Account HTTP Object
|
||||||
HTTP_OBJECT = 'http'
|
HTTP_OBJECT = 'http'
|
||||||
|
# Are we on Global Compute Engine
|
||||||
|
IS_ON_GCE = 'ogce'
|
||||||
# Length of last Got message
|
# Length of last Got message
|
||||||
LAST_GOT_MSG_LEN = 'lgml'
|
LAST_GOT_MSG_LEN = 'lgml'
|
||||||
# License SKUs
|
# License SKUs
|
||||||
@@ -285,6 +287,7 @@ Globals = {
|
|||||||
GAM_PATH: '.',
|
GAM_PATH: '.',
|
||||||
GAM_TYPE: '',
|
GAM_TYPE: '',
|
||||||
HTTP_OBJECT: None,
|
HTTP_OBJECT: None,
|
||||||
|
IS_ON_GCE: False,
|
||||||
LAST_GOT_MSG_LEN: 0,
|
LAST_GOT_MSG_LEN: 0,
|
||||||
LICENSE_SKUS: [],
|
LICENSE_SKUS: [],
|
||||||
MAKE_BUILDING_ID_NAME_MAP: True,
|
MAKE_BUILDING_ID_NAME_MAP: True,
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ function minimizeAllWindows() {
|
|||||||
async function takeScreenshot(filename) {
|
async function takeScreenshot(filename) {
|
||||||
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||||
const fullPath = path.join(workspace, filename);
|
const fullPath = path.join(workspace, filename);
|
||||||
|
|
||||||
|
// Create a temporary script file path
|
||||||
|
const scriptPath = path.join(workspace, `screenshot_${Date.now()}.ps1`);
|
||||||
|
|
||||||
const psScript = `
|
const psScript = `
|
||||||
Add-Type -AssemblyName System.Windows.Forms;
|
Add-Type -AssemblyName System.Windows.Forms;
|
||||||
@@ -44,47 +47,101 @@ async function takeScreenshot(filename) {
|
|||||||
$bitmap = New-Object System.Drawing.Bitmap $Screen.Width, $Screen.Height;
|
$bitmap = New-Object System.Drawing.Bitmap $Screen.Width, $Screen.Height;
|
||||||
$graphic = [System.Drawing.Graphics]::FromImage($bitmap);
|
$graphic = [System.Drawing.Graphics]::FromImage($bitmap);
|
||||||
$graphic.CopyFromScreen($Screen.Left, $Screen.Top, 0, 0, $bitmap.Size);
|
$graphic.CopyFromScreen($Screen.Left, $Screen.Top, 0, 0, $bitmap.Size);
|
||||||
|
|
||||||
|
# Save the file (using single quotes safely now)
|
||||||
$bitmap.Save('${fullPath}');
|
$bitmap.Save('${fullPath}');
|
||||||
|
Write-Output "Wrote ${fullPath}";
|
||||||
|
|
||||||
|
# Specify ItemType to prevent older PS versions from prompting interactively
|
||||||
|
New-Item -Path "${fullPath}.written" -ItemType File | Out-Null;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`powershell -Command "${psScript}"`);
|
// 1. Write the script to disk
|
||||||
|
fs.writeFileSync(scriptPath, psScript);
|
||||||
|
|
||||||
|
// 2. Execute the file directly, piping stdout/stderr to the Node console
|
||||||
|
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`, {
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`Saved screenshot: ${fullPath}`);
|
console.log(`Saved screenshot: ${fullPath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to save screenshot ${fullPath}:`, err.message);
|
console.error(`Failed to save screenshot ${fullPath}:`, err.message);
|
||||||
|
} finally {
|
||||||
|
// 3. Clean up the temp file so it doesn't clutter your CI artifacts
|
||||||
|
if (fs.existsSync(scriptPath)) {
|
||||||
|
fs.unlinkSync(scriptPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire and forget application launcher
|
// Fire and forget application launcher with logging
|
||||||
function launchSSD() {
|
function launchSSD() {
|
||||||
|
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||||
|
const outLogPath = path.join(workspace, 'ssd_out.log');
|
||||||
|
const errLogPath = path.join(workspace, 'ssd_err.log');
|
||||||
|
|
||||||
|
// Open file descriptors for logging
|
||||||
|
const out = fs.openSync(outLogPath, 'a');
|
||||||
|
const err = fs.openSync(errLogPath, 'a');
|
||||||
|
|
||||||
|
console.log(`Launching SSD... Logging stdout to ${outLogPath} and stderr to ${errLogPath}`);
|
||||||
|
|
||||||
const child = spawn('C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe', [], {
|
const child = spawn('C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe', [], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore'
|
// stdio array: [stdin, stdout, stderr]
|
||||||
|
// We ignore stdin, and pipe stdout/stderr to our files
|
||||||
|
stdio: ['ignore', out, err]
|
||||||
});
|
});
|
||||||
child.unref();
|
|
||||||
|
// Catch immediate errors (e.g., file not found, permission denied)
|
||||||
|
child.on('error', (error) => {
|
||||||
|
console.error('Failed to spawn SimplySign Desktop:', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unreference the child so the parent script can exit
|
||||||
|
child.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSSD() {
|
async function runSSD() {
|
||||||
await takeScreenshot('001.png');
|
const runner_arch = process.env.RUNNER_ARCH;
|
||||||
minimizeAllWindows();
|
if ( runner_arch === "ARM64" ) {
|
||||||
await sleep(2000);
|
console.log('Running on ARM64. Tabbing through OOBE...');
|
||||||
await takeScreenshot('002.png');
|
await sleep(3000);
|
||||||
sendKeys('{ESC}');
|
await takeScreenshot('oob1.png');
|
||||||
await sleep(2000);
|
|
||||||
await takeScreenshot('003.png');
|
// Page 1: Tab through the toggles to reach the "Next" button
|
||||||
//sendKeys('{ESC}');
|
for (let i = 0; i < 7; i++) {
|
||||||
//await sleep(2000);
|
sendKeys('{TAB}');
|
||||||
//await takeScreenshot('004.png');
|
await sleep(200);
|
||||||
//sendKeys('{ESC}');
|
}
|
||||||
//await sleep(2000);
|
sendKeys('{ENTER}');
|
||||||
//await takeScreenshot('005.png');
|
console.log('Clicked Next');
|
||||||
//sendKeys('%{F4}');
|
|
||||||
//await sleep(2000);
|
await sleep(3000);
|
||||||
//await takeScreenshot('006.png');
|
await takeScreenshot('ooob2.png');
|
||||||
//sendKeys('%{F4}');
|
|
||||||
//await sleep(2000);
|
|
||||||
//await takeScreenshot('007.png');
|
|
||||||
|
|
||||||
|
// Page 2: Tab through the remaining toggles to reach the "Accept" button
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
sendKeys('{TAB}');
|
||||||
|
await sleep(200);
|
||||||
|
}
|
||||||
|
sendKeys('{ENTER}');
|
||||||
|
console.log('Clicked Accept');
|
||||||
|
|
||||||
|
await sleep(3000);
|
||||||
|
await takeScreenshot('oob3.png');
|
||||||
|
|
||||||
|
sendKeys('{ESC}');
|
||||||
|
console.log('Escaped start menu');
|
||||||
|
|
||||||
|
await sleep(3000);
|
||||||
|
await takeScreenshot('oob4.png');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log('NOT running on ARM64');
|
||||||
|
}
|
||||||
// Re-execute SSD to open login dialog
|
// Re-execute SSD to open login dialog
|
||||||
launchSSD();
|
launchSSD();
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ These filters can be used alone or in conjunction with the `matchfield|skipfield
|
|||||||
[(any|all):]notdata:<DataSelector>|
|
[(any|all):]notdata:<DataSelector>|
|
||||||
[(any|all):]notregex:<RESearchPattern>|
|
[(any|all):]notregex:<RESearchPattern>|
|
||||||
[(any|all):]notregexcs:<RESearchPattern>|
|
[(any|all):]notregexcs:<RESearchPattern>|
|
||||||
|
[(any|all):]number<Operator><Number>|
|
||||||
|
[(any|all):]numberrange!=<Number>/<Number>|
|
||||||
|
[(any|all):]numberrange=<Number>/<Number>|
|
||||||
[(any|all):]regex:<RESearchPattern>|
|
[(any|all):]regex:<RESearchPattern>|
|
||||||
[(any|all):]regexcs:<RESearchPattern>|
|
[(any|all):]regexcs:<RESearchPattern>|
|
||||||
[(any|all):]text<Operator><String>|
|
[(any|all):]text<Operator><String>|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ on all platforms.
|
|||||||
[(any|all):]notdata:<DataSelector>
|
[(any|all):]notdata:<DataSelector>
|
||||||
[(any|all):]notregex:<RESearchPattern>|
|
[(any|all):]notregex:<RESearchPattern>|
|
||||||
[(any|all):]notregexcs:<RESearchPattern>|
|
[(any|all):]notregexcs:<RESearchPattern>|
|
||||||
|
[(any|all):]number<Operator><Number>|
|
||||||
|
[(any|all):]numberrange!=<Number>/<Number>|
|
||||||
|
[(any|all):]numberrange=<Number>/<Number>|
|
||||||
[(any|all):]regex:<RESearchPattern>|
|
[(any|all):]regex:<RESearchPattern>|
|
||||||
[(any|all):]regexcs:<RESearchPattern>|
|
[(any|all):]regexcs:<RESearchPattern>|
|
||||||
[(any|all):]text<Operator><String>|
|
[(any|all):]text<Operator><String>|
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ gam user user@domain.com check|update serviceaccount
|
|||||||
name|
|
name|
|
||||||
owneremail|
|
owneremail|
|
||||||
ownerid|
|
ownerid|
|
||||||
|
ownername|
|
||||||
room|
|
room|
|
||||||
section|
|
section|
|
||||||
teacherfolder|
|
teacherfolder|
|
||||||
@@ -431,14 +432,16 @@ 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] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
|
||||||
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
|
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
|
||||||
gam info courses <CourseEntity> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly]
|
gam info courses <CourseEntity>
|
||||||
|
[owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
|
||||||
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
|
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
|
||||||
|
|
||||||
gam print courses [todrive <ToDriveAttribute>*]
|
gam print courses [todrive <ToDriveAttribute>*]
|
||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
||||||
[owneremail] [owneremailmatchpattern <REMatchPattern>]
|
[owneremail] [owneremailmatchpattern <REMatchPattern>] [ownername]
|
||||||
[alias|aliases|aliasesincolumns [delimiter <Character>]]
|
[alias|aliases|aliasesincolumns [delimiter <Character>]]
|
||||||
[show all|students|teachers] [countsonly]
|
[show all|students|teachers] [countsonly]
|
||||||
[timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
@@ -471,7 +474,9 @@ To get information about courses created/updated within a particular time frame,
|
|||||||
For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified.
|
For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified.
|
||||||
|
|
||||||
By default, all basic course fields are displayed; use the following options to modify the output.
|
By default, all basic course fields are displayed; use the following options to modify the output.
|
||||||
* `owneremail` - Display course owner email; requires an additional API call per course.
|
* `owneremail` - Display course owner email.
|
||||||
|
* `ownername` - Display course owner name.
|
||||||
|
* These options require an additional API call per course.
|
||||||
* `alias|aliases` - Display course aliases; all aliases are in the single column `Aliases` separated by a delimiter; requires an additional API call per course.
|
* `alias|aliases` - Display course aliases; all aliases are in the single column `Aliases` separated by a delimiter; requires an additional API call per course.
|
||||||
* `delimiter <Character>` - Delimiter between aliases with `print` command.
|
* `delimiter <Character>` - Delimiter between aliases with `print` command.
|
||||||
* `aliasesincolumn` - Display course aliases; the `Aliases` column contains the number of aliases and `Aliases.0`, `Aliases.1`, ... contain the individual aliases; requires an additional API call per course.
|
* `aliasesincolumn` - Display course aliases; the `Aliases` column contains the number of aliases and `Aliases.0`, `Aliases.1`, ... contain the individual aliases; requires an additional API call per course.
|
||||||
@@ -525,7 +530,8 @@ gam print course-announcements [todrive <ToDriveAttribute>*]
|
|||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
(courseannouncementids <CourseAnnouncementIDEntity>)|(announcementstates <CourseAnnouncementStateList>)*
|
(courseannouncementids <CourseAnnouncementIDEntity>)|(announcementstates <CourseAnnouncementStateList>)*
|
||||||
(orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
|
(orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
|
||||||
[creatoremail] [fields <CourseAnnouncementFieldNameList>]
|
[showcreatoremails|creatoremail] [showcreatornames|creatorname]
|
||||||
|
[fields <CourseAnnouncementFieldNameList>]
|
||||||
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
[countsonly] [formatjson [quotechar <Character>]]
|
[countsonly] [formatjson [quotechar <Character>]]
|
||||||
```
|
```
|
||||||
@@ -553,7 +559,10 @@ To get information about course announcements created/updated/scheduled within a
|
|||||||
For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified.
|
For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified.
|
||||||
|
|
||||||
By default, all course announcement fields are displayed; use the following options to modify the output.
|
By default, all course announcement fields are displayed; use the following options to modify the output.
|
||||||
* `creatoremail` - Display course announcement creator email; requires an additional API call per course announcement.
|
* `creatoremail` - Display course announcement creator email.
|
||||||
|
* `creatorname` - Display course announcement creator name.
|
||||||
|
* These options require an additional API call per course.
|
||||||
|
* `alias|aliases` - Display course aliases; all aliases are in the single column `Aliases` separated by a delimiter; requires an additional API call per course.
|
||||||
* `fields <CourseAnnouncementFieldNameList>` - Select specific fields to display.
|
* `fields <CourseAnnouncementFieldNameList>` - Select specific fields to display.
|
||||||
|
|
||||||
Use the `countsonly` option to display the number of announcements in a course but not their details.
|
Use the `countsonly` option to display the number of announcements in a course but not their details.
|
||||||
@@ -573,7 +582,8 @@ gam print course-materials [todrive <ToDriveAttribute>*]
|
|||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
(materialids <CourseMaterialIDEntity>)|(materialstates <CourseMaterialStateList>)*
|
(materialids <CourseMaterialIDEntity>)|(materialstates <CourseMaterialStateList>)*
|
||||||
(orderby <CourseMaterialOrderByFieldName> [ascending|descending])*)
|
(orderby <CourseMaterialOrderByFieldName> [ascending|descending])*)
|
||||||
[showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>]
|
[showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
|
||||||
|
[fields <CourseMaterialFieldNameList>]
|
||||||
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
[oneitemperrow]
|
[oneitemperrow]
|
||||||
[countsonly] [formatjson [quotechar <Character>]]
|
[countsonly] [formatjson [quotechar <Character>]]
|
||||||
@@ -602,8 +612,10 @@ By default, all published course materials for a course are displayed; use the f
|
|||||||
* `materialsstates <CourseMaterialsStateList>` - Display course materials with any of the specified states.
|
* `materialsstates <CourseMaterialsStateList>` - Display course materials with any of the specified states.
|
||||||
|
|
||||||
By default, all course materials fields are displayed; use the following options to modify the output.
|
By default, all course materials fields are displayed; use the following options to modify the output.
|
||||||
* `showcreatoremails` - Display course materials creator email; requires an additional API call per course materials.
|
* `showcreatoremails|creatoremail` - Display course materials creator email.
|
||||||
* `showtopicnames` - Display topic names; requires and additional API call per course.
|
* `showcreatornames|creatorname` - Display course materials creator name.
|
||||||
|
* These options require an additional API call per course.
|
||||||
|
* `showtopicnames` - Display topic names; requires an additional API call per course.
|
||||||
* `fields <CourseMaterialsFieldNameList>` - Select specific fields to display.
|
* `fields <CourseMaterialsFieldNameList>` - Select specific fields to display.
|
||||||
|
|
||||||
With `print course-materials`, the materials selected for display are all output on one row/line as a repeating item with the other course fields.
|
With `print course-materials`, the materials selected for display are all output on one row/line as a repeating item with the other course fields.
|
||||||
@@ -669,7 +681,8 @@ gam print course-work [todrive <ToDriveAttribute>*]
|
|||||||
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
||||||
(workids <CourseWorkIDEntity>)|(workstates <CourseWorkStateList>)*
|
(workids <CourseWorkIDEntity>)|(workstates <CourseWorkStateList>)*
|
||||||
(orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
(orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
||||||
[showcreatoremails] [showtopicnames] [fields <CourseWorkFieldNameList>]
|
[showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
|
||||||
|
[fields <CourseWorkFieldNameList>]
|
||||||
[showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
[showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
||||||
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
||||||
[oneitemperrow]
|
[oneitemperrow]
|
||||||
@@ -699,8 +712,10 @@ By default, all published course work for a course is displayed; use the followi
|
|||||||
* `workstates <CourseWorkStateList>` - Display course work with any of the specified states.
|
* `workstates <CourseWorkStateList>` - Display course work with any of the specified states.
|
||||||
|
|
||||||
By default, all course work fields are displayed; use the following options to modify the output.
|
By default, all course work fields are displayed; use the following options to modify the output.
|
||||||
* `showcreatoremails` - Display course work creator email; requires an additional API call per course work.
|
* `showcreatoremails|creatoremail` - Display course materials creator email.
|
||||||
* `showtopicnames` - Display topic names; requires and additional API call per course.
|
* `showcreatornames|creatorname` - Display course materials creator name.
|
||||||
|
* These options require an additional API call per course.
|
||||||
|
* `showtopicnames` - Display topic names; requires an additional API call per course.
|
||||||
* `fields <CourseWorkFieldNameList>` - Select specific fields to display.
|
* `fields <CourseWorkFieldNameList>` - Select specific fields to display.
|
||||||
|
|
||||||
By default, when course work is assigned to individual students, the student IDs are displayed in multiple indexed columns.
|
By default, when course work is assigned to individual students, the student IDs are displayed in multiple indexed columns.
|
||||||
|
|||||||
@@ -10,6 +10,42 @@ 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.41.02
|
||||||
|
|
||||||
|
Added option `ownername` to `gam info|print courses` to have GAM display the course owners full name;
|
||||||
|
there is an extra API call per course to get the name.
|
||||||
|
|
||||||
|
Added option `creatorname` to `gam print course-announcements|course-materials|course-works` to have
|
||||||
|
GAM display the item creators full name; there is an extra API call per course to get the name.
|
||||||
|
|
||||||
|
After creating a group, it may be sometime, e.g. 30-45 seconds, before members can
|
||||||
|
successfully be added to the group even though the API reported that the group was created.
|
||||||
|
The following options can be used with `gam create group` to verify that the group is actually ready to be updated.
|
||||||
|
This will be most useful in scripts that are used to create and then populate groups.
|
||||||
|
```
|
||||||
|
verifycreationretries <Integer> - Verify group creation, defaults to 0, no verification performed, range 0-20
|
||||||
|
verifycreationinitialdelay <Integer> - Number of seconds to delay before first verification performed, defaults to 5, range 0-60
|
||||||
|
verifycreationretrydelay <Integer> - Number of seconds to delay between verificaton retries, defaults to 5, range 1-60
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have a script that deletes a group and then immediately tries to create a new group with the same email address,
|
||||||
|
you may run into issues. There seems to be a 30-45 second window after the deletion in which a couple
|
||||||
|
of strange errors can occur on the creation: `Resource not found` and `Duplicate`.
|
||||||
|
The following options can be used with `gam create group` to handle these errors. This will be most useful
|
||||||
|
in scripts that are used to delete and then immediately recreate groups.
|
||||||
|
```
|
||||||
|
recentdeleteretries <Integer> - Handle group delete/create errors, defaults to 0, no errors handled, range 0-20
|
||||||
|
recentdeleteretrydelay <Integer> - Number of seconds to delay between retries, defaults to 5, range 1-60
|
||||||
|
```
|
||||||
|
|
||||||
|
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
|
||||||
|
synonyms for `count` and `countrange`.
|
||||||
|
```
|
||||||
|
[(any|all):]number<Operator><Number>|
|
||||||
|
[(any|all):]numberrange!=<Number>/<Number>|
|
||||||
|
[(any|all):]numberrange=<Number>/<Number>|
|
||||||
|
```
|
||||||
|
|
||||||
### 7.41.01
|
### 7.41.01
|
||||||
|
|
||||||
Fixed bug in `gam print cigroups members managers owners countsonly totalcount internal external` that caused a trap.
|
Fixed bug in `gam print cigroups members managers owners countsonly totalcount internal external` that caused a trap.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
- [GUI API Group access type settings mapping](#gui-api-group-access-type-settings-mapping)
|
- [GUI API Group access type settings mapping](#gui-api-group-access-type-settings-mapping)
|
||||||
- [whoCanViewMembership and whoCanDiscoverGroup interactions](#whocanviewmembership-and-whocandiscovergroup-interactions)
|
- [whoCanViewMembership and whoCanDiscoverGroup interactions](#whocanviewmembership-and-whocandiscovergroup-interactions)
|
||||||
- [Manage groups](#manage-groups)
|
- [Manage groups](#manage-groups)
|
||||||
|
- [Handle group deletion and immediate recreation](#handle-group-deletion-and-immediate-recreation)
|
||||||
|
- [Verify group creation](#verify-group-creation)
|
||||||
- [Update a group's primary email address](#update-a-groups-primary-email-address)
|
- [Update a group's primary email address](#update-a-groups-primary-email-address)
|
||||||
- [Update a group's settings with JSON data](#update-a-groups-settings-with-json-data)
|
- [Update a group's settings with JSON data](#update-a-groups-settings-with-json-data)
|
||||||
- [Display information about specific groups](#display-information-about-specific-groups)
|
- [Display information about specific groups](#display-information-about-specific-groups)
|
||||||
@@ -350,6 +352,8 @@ These commands allow you to create, update and delete groups.
|
|||||||
gam create group <EmailAddress>
|
gam create group <EmailAddress>
|
||||||
[copyfrom <GroupItem>] <GroupAttribute>*
|
[copyfrom <GroupItem>] <GroupAttribute>*
|
||||||
[verifynotinvitable]
|
[verifynotinvitable]
|
||||||
|
[recentdeleteretries <Integer>] [recentdeleteretrydelay <Integer>]
|
||||||
|
[verifycreationretries <Integer>] [verifycreationinitialdelay <Integer>] [verifycreationretrydelay <Integer>]
|
||||||
gam update group|groups <GroupEntity> [email <EmailAddress>]
|
gam update group|groups <GroupEntity> [email <EmailAddress>]
|
||||||
[updateprimaryemail <RESearchPattern> <RESubstitution> [preview]]
|
[updateprimaryemail <RESearchPattern> <RESubstitution> [preview]]
|
||||||
[copyfrom <GroupItem>] <GroupAttribute>*
|
[copyfrom <GroupItem>] <GroupAttribute>*
|
||||||
@@ -367,6 +371,28 @@ You can update a group to a security group with the `makesecuritygroup` option.
|
|||||||
|
|
||||||
When deleting and `noactionifalias` is specified, no action is performed if `<GroupEntity>` specifies an alias rather than a primary email address.
|
When deleting and `noactionifalias` is specified, no action is performed if `<GroupEntity>` specifies an alias rather than a primary email address.
|
||||||
|
|
||||||
|
## Handle group deletion and immediate recreation
|
||||||
|
If you have a script that deletes a group and then immediately tries to create a new group with the same email address,
|
||||||
|
you may run into issues. There seems to be a 30-45 second window after the deletion in which a couple
|
||||||
|
of strange errors can occur on the creation: `Resource not found` and `Duplicate`.
|
||||||
|
The following options can be used with `gam create group` to handle these errors. This will be most useful
|
||||||
|
in scripts that are used to delete and then immediately recreate groups.
|
||||||
|
```
|
||||||
|
recentdeleteretries <Integer> - Handle group delete/create errors, defaults to 0, no errors handled, range 0-20
|
||||||
|
recentdeleteretrydelay <Integer> - Number of seconds to delay between retries, defaults to 5, range 1-60
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify group creation
|
||||||
|
After creating a group, it may be sometime, e.g. 30-45 seconds, before members can
|
||||||
|
successfully be added to the group even though the API reported that the group was created.
|
||||||
|
The following options can be used with `gam create group` to verify that the group is actually ready to be updated.
|
||||||
|
This will be most useful in scripts that are used to create and then populate groups.
|
||||||
|
```
|
||||||
|
verifycreationretries <Integer> - Verify group creation, defaults to 0, no verification performed, range 0-20
|
||||||
|
verifycreationinitialdelay <Integer> - Number of seconds to delay before first verification performed, defaults to 5, range 0-60
|
||||||
|
verifycreationretrydelay <Integer> - Number of seconds to delay between verificaton retries, defaults to 5, range 1-60
|
||||||
|
```
|
||||||
|
|
||||||
## Update a group's primary email address
|
## Update a group's primary email address
|
||||||
You can simply update a group's primary email address with the `email` option.
|
You can simply update a group's primary email address with the `email` option.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ writes the credentials into the file oauth2.txt.
|
|||||||
```
|
```
|
||||||
gamteam@server:/Users/gamteam$ rm -f /Users/gamteam/GAMConfig/oauth2.txt
|
gamteam@server:/Users/gamteam$ rm -f /Users/gamteam/GAMConfig/oauth2.txt
|
||||||
gamteam@server:/Users/gamteam$ gam version
|
gamteam@server:/Users/gamteam$ gam version
|
||||||
GAM 7.41.01 - https://github.com/GAM-team/GAM - pyinstaller
|
GAM 7.41.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||||
GAM Team <google-apps-manager@googlegroups.com>
|
GAM Team <google-apps-manager@googlegroups.com>
|
||||||
Python 3.14.4 64-bit final
|
Python 3.14.4 64-bit final
|
||||||
macOS Tahoe 26.4.1 arm64
|
macOS Tahoe 26.4.1 arm64
|
||||||
@@ -1034,7 +1034,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
|
||||||
GAM 7.41.01 - https://github.com/GAM-team/GAM - pythonsource
|
GAM 7.41.02 - https://github.com/GAM-team/GAM - pythonsource
|
||||||
GAM Team <google-apps-manager@googlegroups.com>
|
GAM Team <google-apps-manager@googlegroups.com>
|
||||||
Python 3.14.4 64-bit final
|
Python 3.14.4 64-bit final
|
||||||
Windows 11 10.0.26200 AMD64
|
Windows 11 10.0.26200 AMD64
|
||||||
|
|||||||
@@ -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.41.01 - https://github.com/GAM-team/GAM - pyinstaller
|
GAM 7.41.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||||
GAM Team <google-apps-manager@googlegroups.com>
|
GAM Team <google-apps-manager@googlegroups.com>
|
||||||
Python 3.14.4 64-bit final
|
Python 3.14.4 64-bit final
|
||||||
macOS Tahoe 26.4.1 arm64
|
macOS Tahoe 26.4.1 arm64
|
||||||
@@ -15,7 +15,7 @@ Time: 2026-02-15T07:51:00-08: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.41.01 - https://github.com/GAM-team/GAM - pyinstaller
|
GAM 7.41.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||||
GAM Team <google-apps-manager@googlegroups.com>
|
GAM Team <google-apps-manager@googlegroups.com>
|
||||||
Python 3.14.4 64-bit final
|
Python 3.14.4 64-bit final
|
||||||
macOS Tahoe 26.4.1 arm64
|
macOS Tahoe 26.4.1 arm64
|
||||||
@@ -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.41.01 - https://github.com/GAM-team/GAM - pyinstaller
|
GAM 7.41.02 - https://github.com/GAM-team/GAM - pyinstaller
|
||||||
GAM Team <google-apps-manager@googlegroups.com>
|
GAM Team <google-apps-manager@googlegroups.com>
|
||||||
Python 3.14.4 64-bit final
|
Python 3.14.4 64-bit final
|
||||||
macOS Tahoe 26.4.1 arm64
|
macOS Tahoe 26.4.1 arm64
|
||||||
@@ -68,7 +68,7 @@ MacOS High Sierra 10.13.6 x86_64
|
|||||||
Path: /Users/gamteam/bin/gam7
|
Path: /Users/gamteam/bin/gam7
|
||||||
Version Check:
|
Version Check:
|
||||||
Current: 5.35.08
|
Current: 5.35.08
|
||||||
Latest: 7.41.01
|
Latest: 7.41.02
|
||||||
echo $?
|
echo $?
|
||||||
1
|
1
|
||||||
```
|
```
|
||||||
@@ -76,7 +76,7 @@ echo $?
|
|||||||
Print the current version number without details
|
Print the current version number without details
|
||||||
```
|
```
|
||||||
gam version simple
|
gam version simple
|
||||||
7.41.01
|
7.41.02
|
||||||
```
|
```
|
||||||
In Linux/MacOS you can do:
|
In Linux/MacOS you can do:
|
||||||
```
|
```
|
||||||
@@ -86,7 +86,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.41.01 - https://github.com/GAM-team/GAM
|
GAM 7.41.02 - https://github.com/GAM-team/GAM
|
||||||
GAM Team <google-apps-manager@googlegroups.com>
|
GAM Team <google-apps-manager@googlegroups.com>
|
||||||
Python 3.14.4 64-bit final
|
Python 3.14.4 64-bit final
|
||||||
macOS Tahoe 26.4.1 arm64
|
macOS Tahoe 26.4.1 arm64
|
||||||
|
|||||||
Reference in New Issue
Block a user