diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 1338655f..b3ed8007 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -3661,7 +3661,7 @@ gam info user [] [nobuildingnames|buildingnames] [nogroups|groups] [nolicenses|nolicences|licenses|licences] - [noschemas|allschemas|(schemas|custom )|(customschemas )] + [noschemas|allschemas|(schemas|custom|customschemas )] [userview] * [fields ] [(products|product )|(skus|sku )] [formatjson] @@ -3686,7 +3686,7 @@ gam info users [nobuildingnames|buildingnames] [nogroups|groups] [nolicenses|nolicences|licenses|licences] - [noschemas|allschemas|(schemas|custom )|(customschemas )] + [noschemas|allschemas|(schemas|custom|customschemas )] [userview] * [fields ] [(products|product )|(skus|sku )] [formatjson] @@ -3711,7 +3711,7 @@ gam info users [nobuildingnames|buildingnames] [nogroups|groups] [nolicenses|nolicences|licenses|licences] - [noschemas|allschemas|(schemas|custom )|(customschemas )] + [noschemas|allschemas|(schemas|custom|customschemas )] [userview] * [fields ] [(products|product )|(skus|sku )] [formatjson] @@ -3724,7 +3724,9 @@ gam print users [todrive *] ([domain ] [(query )|(queries )] [limittoou ] [deleted_only|only_deleted]) [orderby [ascending|descending]] - [groups|groupsincolumns] [license|licenses|licence|licences] [emailpart|emailparts|username] [schemas|custom all|] + [groups|groupsincolumns] [license|licenses|licence|licences] + [schemas|custom|customschemas all|] + [emailpart|emailparts|username] [userview] [basic|full|allfields | * | fields ] [delimiter ] [sortheaders []] [scalarsfirst []] [formatjson [quotechar ]] [quoteplusphonenumbers] @@ -3734,7 +3736,9 @@ Print fields for specified users. gam print users [todrive *] select [orderby [ascending|descending]] - [groups|groupsincolumns] [license|licenses|licence|licences] [emailpart|emailparts|username] [schemas|custom all|] + [groups|groupsincolumns] [license|licenses|licence|licences] + [schemas|custom|customschemas all|] + [emailpart|emailparts|username] [userview] [basic|full|allfields | * | fields ] [delimiter ] [sortheaders []] [scalarsfirst []] [formatjson [quotechar ]] [quoteplusphonenumbers] @@ -3742,7 +3746,9 @@ gam print users [todrive *] select gam print users [todrive *] [orderby [ascending|descending]] - [groups|groupsincolumns] [license|licenses|licence|licences] [emailpart|emailparts|username] [schemas|custom all|] + [groups|groupsincolumns] [license|licenses|licence|licences] + [schemas|custom|customschemas all|] + [emailpart|emailparts|username] [userview] [basic|full|allfields | * | fields ] [delimiter ] [sortheaders []] [scalarsfirst []] [formatjson [quotechar ]] [quoteplusphonenumbers] @@ -4027,7 +4033,7 @@ gam print events [] ]] [todrive *] gam update calattendees [anyorganizer] - [] [doit] + [] [splitupdate] [doit] (csv |(gsheet ))* (delete )* (deleteentity )* diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index dd0cdad8..0279249d 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,12 @@ +6.00.07 + +Added option `splitupdate` to `gam update calattendees` to handle replacing +an email alias with its primary email in the event attendee list. +By default, when you try to replace an email alias with its primary email, the Google Calendar API +detects that the underlying user ID is the same and doesn't change the address. The `splitupdate` +option causes GAM to make two updates to the attendee list; the first removes the alias and +the second adds the primary email. + 6.00.06 Added command `gam print addresses [todrive *]` that produces a @@ -9,7 +18,7 @@ SuspendedUser, User, UserAlias, UserNEAlias. 'NE' is and abbreviation for NonEdi Fixed bug in `gam print vaultcounts ... everyone` which caused the following error: ``` -ERROR: getUsersToModify coding error +ERROR: getUsersToModify coding error ``` 6.00.04 @@ -20,7 +29,7 @@ Previously, you had to specify each email notification event type individually ( ``` ::= eventcreation|eventchange|eventcancellation|eventresponse|agenda - ::= + ::= (,)* ::= diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 3e3b7770..384490ad 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -23,7 +23,7 @@ """ __author__ = 'Ross Scroggs ' -__version__ = '6.00.06' +__version__ = '6.00.07' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' import base64 @@ -26221,14 +26221,13 @@ def _validateCalendarGetEvents(origUser, user, origCal, calId, j, jcount, calend except (GAPI.notFound, GAPI.deleted) as e: if not checkCalendarExists(cal, calId): entityUnknownWarning(Ent.CALENDAR, calId, j, jcount) - return (calId, cal, [], 0) - entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), k, kcount) + else: + entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount) except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e: entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount) - return (calId, cal, [], 0) except (GAPI.serviceNotAvailable, GAPI.authError): entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount) - return (calId, cal, [], 0) + return (calId, cal, [], 0) def _getCalendarCreateImportUpdateEventOptions(function, calendarEventEntity=None): body = {} @@ -30919,9 +30918,8 @@ def _callbackGetLicense(request_id, response, exception): elif myarg in {'custom', 'schemas', 'customschemas'}: getSchemas = True projection = 'custom' - customFieldMask = getString(Cmd.OB_SCHEMA_NAME_LIST) - if myarg == 'customschemas': - fieldsList.append('customSchemas') + customFieldMask = getString(Cmd.OB_SCHEMA_NAME_LIST).replace(' ', ',') + fieldsList.append('customSchemas') elif myarg in {'products', 'product'}: skus = SKU.convertProductListToSKUList(getGoogleProductList()) elif myarg in {'sku', 'skus'}: @@ -30938,6 +30936,8 @@ def _callbackGetLicense(request_id, response, exception): FJQC.GetFormatJSON(myarg) if fieldsList: fieldsList.append('primaryEmail') + if getAliases: + fieldsList.append('aliases') fields = getFieldsFromFieldsList(fieldsList) if getLicenses: lic = buildGAPIObject(API.LICENSING) @@ -31230,7 +31230,7 @@ def _callbackGetLicense(request_id, response, exception): # [nobuildingnames|buildingnames] # [nogroups|groups] # [nolicenses|nolicences|licenses|licences] -# [noschemas|allschemas|(schemas|custom )|(customschemas )] +# [noschemas|allschemas|(schemas|custom|customschemas )] # [userview] * [fields ] # [(products|product )|(skus|sku )] [formatjson] def doInfoUsers(): @@ -31242,7 +31242,7 @@ def doInfoUsers(): # [nobuildingnames|buildingnames] # [nogroups|groups] # [nolicenses|nolicences|licenses|licences] -# [noschemas|allschemas|(schemas|custom )|(customschemas )] +# [noschemas|allschemas|(schemas|custom|customschemas )] # [userview] * [fields ] # [(products|product )|(skus|sku )] [formatjson] # gam info user @@ -36528,7 +36528,7 @@ def emptyCalendarTrash(users): Ind.Decrement() # gam update calattendees [anyorganizer] -# [] [doit] +# [] [splitupdate] [doit] # (csv |(gsheet ))* # (delete )* # (deleteentity )* @@ -36549,7 +36549,7 @@ def getStatus(option): calendarEntity = getUserCalendarEntity() calendarEventEntity = getCalendarEventEntity() - anyOrganizer = doIt = False + anyOrganizer = doIt = splitUpdate = False parameters = {'sendUpdates': 'none'} attendeeMap = {} errors = 0 @@ -36621,6 +36621,8 @@ def getStatus(option): pass elif myarg == 'doit': doIt = True + elif myarg == 'splitupdate': + splitUpdate = True else: unknownArgumentExit() if not attendeeMap: @@ -36628,6 +36630,8 @@ def getStatus(option): ucount = len(attendeeMap) if errors: systemErrorExit(USAGE_ERROR_RC, '') + removeMessage = Msg.ATTENDEES_REMOVE + addMessage = Msg.ATTENDEES_ADD_REMOVE if not splitUpdate else Msg.ATTENDEES_ADD fieldsList = ['attendees', 'id', 'organizer', 'status', 'summary'] i, count, users = getEntityArgument(users) for user in users: @@ -36659,18 +36663,23 @@ def getStatus(option): needsUpdate = False for _, v in sorted(iter(attendeeMap.items())): v['done'] = False - updatedAttendees = [] + updatedAttendeesAdd = [] + updatedAttendeesRemove = [] entityPerformActionNumItems([Ent.EVENT, eventSummary], ucount, Ent.ATTENDEE, k, kcount) Ind.Increment() u = 0 for attendee in event.get('attendees', []): oldAddr = attendee.get('email', '').lower() if not oldAddr: - updatedAttendees.append(attendee) + updatedAttendeesAdd.append(attendee) + if splitUpdate: + updatedAttendeesRemove.append(attendee) continue update = attendeeMap.get(oldAddr) if not update: - updatedAttendees.append(attendee) + updatedAttendeesAdd.append(attendee) + if splitUpdate: + updatedAttendeesRemove.append(attendee) continue updOp = update['op'] if updOp == 'delete': @@ -36697,7 +36706,7 @@ def getStatus(option): else: Act.Set(Act.SKIP) entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount) - updatedAttendees.append(attendee) + updatedAttendeesAdd.append(attendee) else: #replace u += 1 update['done'] = True @@ -36706,7 +36715,7 @@ def getStatus(option): attendee['optional'] = updOptional if updOptional is not None else oldOptional Act.Set(Act.REPLACE) entityPerformActionModifierNewValue([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], Act.MODIFIER_WITH, update['email'], u, ucount) - updatedAttendees.append(attendee) + updatedAttendeesAdd.append(attendee) needsUpdate = True for newAddr, v in sorted(iter(attendeeMap.items())): if v['op'] == 'add' and not v['done']: @@ -36719,7 +36728,7 @@ def getStatus(option): attendee['optional'] = v['optional'] Act.Set(Act.ADD) entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, newAddr], u, ucount) - updatedAttendees.append(attendee) + updatedAttendeesAdd.append(attendee) needsUpdate = True for newAddr, v in sorted(iter(attendeeMap.items())): if not v['done']: @@ -36730,23 +36739,44 @@ def getStatus(option): if needsUpdate: Act.Set(Act.UPDATE) if doIt: - try: - callGAPI(cal.events(), 'patch', - throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID], - calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendees}, - sendUpdates=parameters['sendUpdates'], fields='') - entityActionPerformed([Ent.EVENT, eventSummary], j, jcount) - except GAPI.notFound as e: - if not checkCalendarExists(cal, calId): - entityUnknownWarning(Ent.CALENDAR, calId, j, jcount) + status = True + if splitUpdate: + try: + callGAPI(cal.events(), 'patch', + throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID], + calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendeesRemove}, + sendUpdates=parameters['sendUpdates'], fields='') + entityActionPerformedMessage([Ent.EVENT, eventSummary], removeMessage, j, jcount) + except GAPI.notFound as e: + if not checkCalendarExists(cal, calId): + entityUnknownWarning(Ent.CALENDAR, calId, j, jcount) + break + entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount) + status = False + except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e: + entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount) + break + except (GAPI.serviceNotAvailable, GAPI.authError): + entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount) + break + if status: + try: + callGAPI(cal.events(), 'patch', + throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID], + calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendeesAdd}, + sendUpdates=parameters['sendUpdates'], fields='') + entityActionPerformedMessage([Ent.EVENT, eventSummary], addMessage, jcount) + except GAPI.notFound as e: + if not checkCalendarExists(cal, calId): + entityUnknownWarning(Ent.CALENDAR, calId, j, jcount) + break + entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount) + except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e: + entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount) + break + except (GAPI.serviceNotAvailable, GAPI.authError): + entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount) break - entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount) - except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e: - entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount) - break - except (GAPI.serviceNotAvailable, GAPI.authError): - entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount) - break else: entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount) Ind.Decrement() diff --git a/src/gam/gamlib/glmsgs.py b/src/gam/gamlib/glmsgs.py index 58abb287..cabd69ee 100644 --- a/src/gam/gamlib/glmsgs.py +++ b/src/gam/gamlib/glmsgs.py @@ -129,6 +129,9 @@ API_ERROR_SETTINGS = 'API error, some settings not set' ARE_MUTUALLY_EXCLUSIVE = 'Arguments {0} and {1} are mutually exclusive' AS = 'as' +ATTENDEES_ADD = 'Add Attendees' +ATTENDEES_ADD_REMOVE = 'Add/Remove Attendees' +ATTENDEES_REMOVE = 'Remove Attendees' AUTHORIZATION_FILE_ALREADY_EXISTS = '{0} already exists. Please delete or rename it before attempting to {1} project.' AUTHENTICATION_FLOW_COMPLETE = 'The authentication flow has completed. You may close this browser window and return to GAM.' AUTHENTICATION_FLOW_FAILED = 'The authentication flow failed (invalid verification code entered?): {0}'