Wednesday, September 18, 2019

#1 2019-09-09 09:26:14 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Scripting Contacts

I have a script that ran perfectly with the Address Book application.  Moving it to run with the Contacts application now throws an error about how it must be a 'person' to add to a cohort. The contact info is sucked in from a spreadsheet satisfactorily.  Here is the complete code:

Applescript:


global Cohort
global Roster
global NA
set WasFound to false
set Roster to {}
set NA to "#N/A"

launch application "Contacts"

tell application "Microsoft Excel" to activate
tell application "Microsoft Excel" to set Cohort to string value of cell "Edit!A37"
tell application "Contacts" to activate
tell application "Contacts"
   set myGroups to (get name of every group)
   if myGroups contains Cohort then set WasFound to true
   if not WasFound then set theGroup to make new group with properties {name:Cohort}
end tell
tell application "Microsoft Excel" to activate
tell application "Microsoft Excel"
   repeat with i from 2 to 33
       if string value of cell ("Edit!B" & i) is NA then
           set j to i - 1
           exit repeat
       end if
   end repeat
end tell

repeat with i from 2 to j
   set TEMP to {key:i - 1, LastName:"", FirstName:"", MI:"", Rank:"", |Hash|:"", |Nickname|:"", Srvc:"", Corps:"", Addressline1:"", |City|:"", |State|:"", |Zip|:"", HomePhone:"", |Email|:"", ParentMilitaryCommand:"", MobilePhone:"", WorkPhone:"", EmailDomain:""}
   tell application "Microsoft Excel"
       set LastName of TEMP to string value of cell ("Edit!B" & i)
       set FirstName of TEMP to string value of cell ("Edit!C" & i)
       set MI of TEMP to string value of cell ("Edit!D" & i)
       set Rank of TEMP to string value of cell ("Edit!F" & i)
       set |Hash| of TEMP to string value of cell ("Edit!G" & i)
       set |Nickname| of TEMP to string value of cell ("Edit!H" & i)
       set Srvc of TEMP to string value of cell ("Edit!J" & i)
       set Corps of TEMP to string value of cell ("Edit!M" & i)
       if Corps of TEMP is NA then set Corps of TEMP to ""
       set Addressline1 of TEMP to string value of cell ("Edit!N" & i)
       try
           if (not cell ("Edit!O" & i) is missing value) and (string value of cell ("Edit!O" & i) ≠ "") then
               set Addressline1 of TEMP to Addressline1 of TEMP & ", " & string value of cell ("Edit!O" & i)
               --AddressLine2
           end if
       end try
       set |City| of TEMP to string value of cell ("Edit!P" & i)
       set |State| of TEMP to string value of cell ("Edit!Q" & i)
       set |Zip| of TEMP to string value of cell ("Edit!R" & i)
       set HomePhone of TEMP to string value of cell ("Edit!S" & i)
       set |Email| of TEMP to string value of cell ("Edit!T" & i)
       set ParentMilitaryCommand of TEMP to string value of cell ("Edit!U" & i)
       set MobilePhone of TEMP to string value of cell ("Edit!V" & i)
       set WorkPhone of TEMP to string value of cell ("Edit!W" & i)
       set EmailDomain of TEMP to my rightstr(|Email| of TEMP, 4)
   end tell
   tell application "Contacts"
       set theStudent to make new person
       set last name of theStudent to LastName of TEMP
       set first name of theStudent to FirstName of TEMP
       set middle name of theStudent to MI of TEMP
       set title of theStudent to Rank of TEMP
       set note of theStudent to "Cohort: " & Cohort & linefeed & "Hash: " & |Hash| of TEMP
       set nickname of theStudent to |Nickname| of TEMP
       set suffix of theStudent to Srvc of TEMP
       set job title of theStudent to Corps of TEMP
       set organization of theStudent to ParentMilitaryCommand of TEMP
       if EmailDomain of TEMP is ".mil" then
           make new email at end of every email of theStudent with properties ¬
               {label:"Work", value:|Email| of TEMP}
       else if EmailDomain of TEMP is ".gov" then
           make new email at end of every email of theStudent with properties ¬
               {label:"Work", value:|Email| of TEMP}
       else
           make new email at end of every email of theStudent with properties ¬
               {label:"Home", value:|Email| of TEMP}
       end if
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Work", value:WorkPhone of TEMP}
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Home", value:HomePhone of TEMP}
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Mobile", value:MobilePhone of TEMP}
       make new address at end of every address of theStudent with properties ¬
           {label:"Home", street:Addressline1 of TEMP, city:|City| of TEMP, state:|State| of TEMP, zip:|Zip| of TEMP}
       set cls to class of theStudent
       add theStudent to group Cohort
       save
   end tell
   set the end of Roster to theStudent
end repeat
beep

on rightstr(txt, N)
   set C to length of txt
   if N > C then set N to C
   set rtntxt to text (C - N + 1) thru C of txt
   return rtntxt
end rightstr

You'll note that I verify the class of thestudent just before the add step. It is a person, yet the error is, "Contacts got an error: You can only add a person to a group." Yeah, OK, but?

I'm clueless about the Contacts error.  All help appreciated. Thanks


Filed under: contacts

Offline

 

#2 2019-09-09 10:28:28 am

KniazidisR
Member
Registered: 2019-03-03
Posts: 538

Re: Scripting Contacts

As first step, replace this code:

Applescript:

tell application "Contacts" to activate
tell application "Contacts"
set myGroups to (get name of every group)
if myGroups contains Cohort then set WasFound to true
if not WasFound then set theGroup to make new group with properties {name:Cohort}
end tell

WITH THIS:

Applescript:

tell application "Contacts"
   activate
   if not (group Cohort exists) then make new group with properties {name:Cohort}
   save
end tell

Then, as boolean variable WasFound no need, remove code line set WasFound to false.

Last edited by KniazidisR (2019-09-09 10:35:11 am)


macOS Mojave -- version 10.14.4
Safari -- version 12.1

Offline

 

#3 2019-09-09 01:45:18 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

I can't see any reason why your Contacts code shouldn't work. Simply making a person and adding it to a group works with Contacts 12.0 in Mojave and there's nothing in your code to change the values of theStudent or Cohort to anything other than what you think they are.  hmm

A couple of things I'd try — just in desperation to see if they made any difference — would be to create the person in situ in the group rather than adding it later …

Applescript:

tell application "Contacts"
   set theStudent to (make new person at group Cohort with properties {last name:(LastName of TEMP), first name:(FirstName of TEMP)})
   -- etc.
end tell

… and moving the 'save' command to after the repeat so that it's only executed once after all the person creation is done.


NG

Offline

 

#4 2019-09-09 02:16:06 pm

KniazidisR
Member
Registered: 2019-03-03
Posts: 538

Re: Scripting Contacts

Hi, Nigel.

I checked - his checking for the existence of the group did not create the group in case of its absence. Now I have no time to go into subtleties, but with my code it was created.

In addition, I see a lot of extra code here. Is it really possible to do without a template (TMP) and set the person’s properties directly from Excel?

I go to work.

Last edited by KniazidisR (2019-09-09 02:16:39 pm)


macOS Mojave -- version 10.14.4
Safari -- version 12.1

Offline

 

#5 2019-09-09 03:22:16 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Hi KniazidisR.

Your code's more concise, but the original does also create the group if it doesn't exist. It's your 'save' command which makes any newly created group visible in Contacts. It may be a good idea to include this anyway after creating a group, although I wouldn't bother after not doing so.

Applescript:

tell application "Contacts"
   activate
   if not (group Cohort exists) then
       make new group with properties {name:Cohort}
       save
   end if
end tell

But if the problem were the group not existing when the person was added, the error message would be "Can't get group \"Blah\"." — taking "Blah" as the value of Cohort.  wink

Is it really possible to do without a template (TMP) and set the person’s properties directly from Excel?


Possibly. I don't know anything about scripting Excel. The immediate problem's to work out why the Contacts code's not working on Mick536's system. I haven't been able to reproduce the error message yet.  hmm


NG

Offline

 

#6 2019-09-09 07:01:53 pm

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hi Nigel -- Thank you for your help.  Working on it tonight.

System is High Sierra, running Contacts 11.0, and Script Debugger 7.0.8.

--Mick536

Offline

 

#7 2019-09-09 07:51:28 pm

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hello All --

I believe I have made all recommended changes, as well as wrapping the email, phone, and address properties in parens per Nigel's suggestion.  Code is now:

Applescript:


global Cohort
global Roster
global NA
set Roster to {}
set NA to "#N/A"

launch application "Contacts"

tell application "Microsoft Excel"
   activate
   set Cohort to string value of cell "Edit!A37"
end tell
tell application "Contacts"
   activate
   if not (group Cohort exists) then make new group with properties {name:Cohort}
   save
end tell

tell application "Microsoft Excel"
   activate
   repeat with i from 2 to 33
       if string value of cell ("Edit!B" & i) is NA then
           set j to i - 1
           exit repeat
       end if
   end repeat
end tell

repeat with i from 2 to j
   set TEMP to {key:i - 1, LastName:"", FirstName:"", MI:"", Rank:"", |Hash|:"", |Nickname|:"", Srvc:"", Corps:"", Addressline1:"", |City|:"", |State|:"", |Zip|:"", HomePhone:"", |Email|:"", ParentMilitaryCommand:"", MobilePhone:"", WorkPhone:"", EmailDomain:""}
   tell application "Microsoft Excel"
       set LastName of TEMP to string value of cell ("Edit!B" & i)
       set FirstName of TEMP to string value of cell ("Edit!C" & i)
       set MI of TEMP to string value of cell ("Edit!D" & i)
       set Rank of TEMP to string value of cell ("Edit!F" & i)
       set |Hash| of TEMP to string value of cell ("Edit!G" & i)
       set |Nickname| of TEMP to string value of cell ("Edit!H" & i)
       set Srvc of TEMP to string value of cell ("Edit!J" & i)
       set Corps of TEMP to string value of cell ("Edit!M" & i)
       if Corps of TEMP is NA then set Corps of TEMP to ""
       set Addressline1 of TEMP to string value of cell ("Edit!N" & i)
       try
           if (not cell ("Edit!O" & i) is missing value) and (string value of cell ("Edit!O" & i) ≠ "") then
               set Addressline1 of TEMP to Addressline1 of TEMP & ", " & string value of cell ("Edit!O" & i)
               --AddressLine2
           end if
       end try
       set |City| of TEMP to string value of cell ("Edit!P" & i)
       set |State| of TEMP to string value of cell ("Edit!Q" & i)
       set |Zip| of TEMP to string value of cell ("Edit!R" & i)
       set HomePhone of TEMP to string value of cell ("Edit!S" & i)
       set |Email| of TEMP to string value of cell ("Edit!T" & i)
       set ParentMilitaryCommand of TEMP to string value of cell ("Edit!U" & i)
       set MobilePhone of TEMP to string value of cell ("Edit!V" & i)
       set WorkPhone of TEMP to string value of cell ("Edit!W" & i)
       set EmailDomain of TEMP to my rightstr(|Email| of TEMP, 4)
   end tell
   tell application "Contacts"
       set theStudent to make new person with properties ¬
           {last name:(LastName of TEMP), first name:(FirstName of TEMP), middle name:(MI of TEMP), title:(Rank of TEMP), note:("Cohort: " & Cohort & linefeed & "Hash: " & |Hash| of TEMP), nickname:(|Nickname| of TEMP), suffix:(Srvc of TEMP), job title:(Corps of TEMP), organization:(ParentMilitaryCommand of TEMP)}
       if EmailDomain of TEMP is ".mil" then
           make new email at end of every email of theStudent with properties ¬
               {label:"Work", value:(|Email| of TEMP)}
       else if EmailDomain of TEMP is ".gov" then
           make new email at end of every email of theStudent with properties ¬
               {label:"Work", value:(|Email| of TEMP)}
       else
           make new email at end of every email of theStudent with properties ¬
               {label:"Home", value:(|Email| of TEMP)}
       end if
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Work", value:(WorkPhone of TEMP)}
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Home", value:(HomePhone of TEMP)}
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Mobile", value:(MobilePhone of TEMP)}
       make new address at end of every address of theStudent with properties ¬
           {label:"Home", street:(Addressline1 of TEMP), city:(|City| of TEMP), state:(|State| of TEMP), zip:(|Zip| of TEMP)}
   
       set cls to class of theStudent
       add theStudent to group Cohort
   end tell
   set the end of Roster to theStudent
end repeat
save
beep

on rightstr(txt, N)
   set C to length of txt
   if N > C then set N to C
   set rtntxt to text (C - N + 1) thru C of txt
   return rtntxt
end rightstr

Unfortunately, we have the same error. TheStudent id looks like this: "7192B836-A9B8-4FE8-AC41-147B0995959A:ABPerson" I find -10002 errors in all the email, phone, and address properties. They look like this for the address: 'country of address "home" of theStudent error getting country code -10002' as per Script Debugger's variable window for example. -10002 is "Invalid key form was detected."

So I feel the problem is there in the 'Make new' syntax. Just don't know what it is.  All values are properly set in TEMP.

Thanks for any help.

...Mick536

Model: MacBook Pro 17" late 2011
Browser: Firefox 69.0
Operating System: macOS 10.13

Last edited by Mick536 (2019-09-09 07:58:36 pm)

Offline

 

#8 2019-09-09 09:11:17 pm

KniazidisR
Member
Registered: 2019-03-03
Posts: 538

Re: Scripting Contacts

Of course, it’s better to put the save command outside the repeat loop, but it should remain inside the tell application "Contacts" block. That is, this piece of code:

Applescript:

set cls to class of theStudent
add theStudent to group Cohort
end tell
set the end of Roster to theStudent
end repeat
save
beep

should be like this:

Applescript:

set cls to class of theStudent
add theStudent to group Cohort
set the end of Roster to theStudent
end repeat
save
end tell
beep

Check your phone numbers in Excel document too. List of valid International Calling Codes you can find HERE

Last edited by KniazidisR (2019-09-09 10:00:45 pm)


macOS Mojave -- version 10.14.4
Safari -- version 12.1

Offline

 

#9 2019-09-10 02:44:12 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

This Excel-free test version of the script works perfectly for me, both when creating the person in the group and when adding it after creation. (Contacts 12.0, macOS 10.14.6 (supplementary update), Script Editor and Script Debugger.) If it works for Mick536 too, it would suggest something wrong with the values coming out of Excel. What does TEMP look like after its values are set?

If this script doesn't work for Mick536, it would suggest some problem with Contacts.  hmm  But first try quitting Contacts to clear any unsaved work from previous script runs.

Applescript:

set Cohort to "Cohort Test" -- Group name. Change if required.

tell application "Contacts"
   activate
   if not (group Cohort exists) then
       make new group with properties {name:Cohort}
       save
   end if
end tell

set j to 10
repeat with i from 2 to j
   set TEMP to {key:i - 1, LastName:"Bloggs", FirstName:"Bill", MI:"Bertie", Rank:"", |Hash|:"Huh?", |Nickname|:"Fishface", Srvc:"BSA & handlebar", Corps:"Sergeant", Addressline1:"3 Railway Cuttings", |City|:"Coventry", |State|:"", |Zip|:"CV1 1AB", HomePhone:"01234 70526", |Email|:"", ParentMilitaryCommand:"Royal Arquebusiers", MobilePhone:"07569 406789", WorkPhone:"", EmailDomain:""}
   set |Email| of TEMP to some item of {"fface@gmail.mil", "bbbloggs@gmail.gov", "billb99@gmail.com"}
   set EmailDomain of TEMP to my rightstr(|Email| of TEMP, 4)
   
   tell application "Contacts"
       -- Create the person in the group.
       set theStudent to make new person at group Cohort with properties ¬
           {last name:(LastName of TEMP), first name:(FirstName of TEMP), middle name:(MI of TEMP), title:(Rank of TEMP), note:("Cohort: " & Cohort & linefeed & "Hash: " & |Hash| of TEMP), nickname:(|Nickname| of TEMP), suffix:(Srvc of TEMP), job title:(Corps of TEMP), organization:(ParentMilitaryCommand of TEMP)}
       (* -- Or create the person to add to the group later.
       set theStudent to make new person with properties ¬
       {last name:(LastName of TEMP), first name:(FirstName of TEMP), middle name:(MI of TEMP), title:(Rank of TEMP), note:("Cohort: " & Cohort & linefeed & "Hash: " & |Hash| of TEMP), nickname:(|Nickname| of TEMP), suffix:(Srvc of TEMP), job title:(Corps of TEMP), organization:(ParentMilitaryCommand of TEMP)} *)

       if EmailDomain of TEMP is ".mil" then
           make new email at end of every email of theStudent with properties ¬
               {label:"Work", value:(|Email| of TEMP)}
       else if EmailDomain of TEMP is ".gov" then
           make new email at end of every email of theStudent with properties ¬
               {label:"Work", value:(|Email| of TEMP)}
       else
           make new email at end of every email of theStudent with properties ¬
               {label:"Home", value:(|Email| of TEMP)}
       end if
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Work", value:(WorkPhone of TEMP)}
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Home", value:(HomePhone of TEMP)}
       make new phone at end of every phone of theStudent with properties ¬
           {label:"Mobile", value:(MobilePhone of TEMP)}
       make new address at end of every address of theStudent with properties ¬
           {label:"Home", street:(Addressline1 of TEMP), city:(|City| of TEMP), state:(|State| of TEMP), zip:(|Zip| of TEMP)}
       
       set cls to class of theStudent
       -- add theStudent to group Cohort
   end tell
end repeat
tell application "Contacts" to save

on rightstr(txt, N)
   set C to length of txt
   if N > C then set N to C
   return text -N thru C of txt
end rightstr


NG

Offline

 

#10 2019-09-10 08:22:35 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hello Nigel, R —

Thank you for your help. (R: I never get that far, but I've made a change. neutral)

Good news and bad news. The good news is that Nigel's test ran (except to iCloud instead of 'on my Mac'. I'd like to know how to specify that.) The bad news is that I booted off a system 10.6 external drive, and my original script ran perfectly to the 'Address Book' on that disk using the exact same spreadsheet (it's on a common 3rd drive.) The good news is that Script Debugger 5 reported the same errors, but the Address Book doesn't care. The bad news is that Contacts does. Oh, and I've got 18 'Fishface' Bloggs in my Contacts. smile

When I get a moment, I'll comment out all the address, phone, and email lines, and report back.

Thanks again.
...Mick

Offline

 

#11 2019-09-10 10:02:28 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hello, All —

Nigel's code created a group "Cohort Test" on iCloud for me. I changed only line 60 above to

Applescript:


set theStudent to make new person at group "Cohort Test" with properties ...

and I populated "Cohort Test" with my 20 students! No changes to Excel of any sort. So I deleted the group associated with the variable Cohort from 'On My Mac' and ran the script with the original line 60. And Ta Da! I now have my cohort on the cloud with my students, more or less how it used to be. All errors were meaningless to Contacts, too.

So how do I specify where to look to see if the group exists 'On My Mac' and not the default of the cloud?

Thanks.
...Mick

Offline

 

#12 2019-09-10 03:40:26 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Mick536 wrote:

So how do I specify where to look to see if the group exists 'On My Mac' and not the default of the cloud?


Contacts's scripting dictionary doesn't mention accounts. If you run this script while your iCloud and On My Mac accounts are both available …

Applescript:

tell application "Contacts"
   name of groups
end tell

… do the names of the groups come from both accounts or just from one of them?

Do you want the groups to be created in a particular account?

It looks as if it may be possible to do a few accounty things using the "Contacts" framework in ASObjC rather than the Contacts application — or perhaps using a bit of both. But I'd need to do some more research and only have an On My Mac account with which to play. What does this script return on your machine while you're on line?

Applescript:

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later.
use framework "Foundation"
use framework "Contacts"

set contactStore to current application's class "CNContactStore"'s new()
set allContainers to contactStore's containersMatchingPredicate:(missing value) |error|:(missing value)

return (allContainers's valueForKey:("identifier")) as list

Hopefully the result will be a two-item list with one of the items being "_local:ABAccount" and the other being the ID of your iCloud account. There's no need to post the actual result if it gives away anything about your iCloud account. I just need to know if its definitely possible to distinguish between the accounts this way.


NG

Offline

 

#13 2019-09-10 08:00:03 pm

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hi Nigel —

Scriplet #1:  From both iCloud and 'On My Mac'

Scriplet #2: Returned 3 items.  I have a Google account, too. As follows:
item 1 of result
"(account ID):ABAccount"
item 2 of result
"(account ID):ABAccount"
item 3 of result
"_local:ABAccount"

The Google account has no groups.  I have the account for internet purposes, but I don't even have a gmail account.  Don't know how it got there.  Don't know which of item 1 or item 2 it is.

This is high level Applescript sorcery.  Thank you.

...best, Mick

Last edited by Mick536 (2019-09-10 08:09:15 pm)

Offline

 

#14 2019-09-11 03:14:18 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Hi Mick.

Thanks. That's helpful. I'm experimenting this morning. Meanwhile, if you change "identifier" to "name" in my ASObjC script above, do the names other than "On My Mac" make it easier to identify the accounts?


NG

Offline

 

#15 2019-09-11 06:08:09 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

G'morning, Nigel —

Well, it's morning my time, anyway.  Thank you.
item 1 of result: "Google"
item 2 of result: "iCloud"
item 3 of result: "On My Mac"

So, yes, yes they do. smile

...best, Mick

Offline

 

#16 2019-09-11 06:54:46 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Great. Here's what I've got so far. I've only been able to test it with my On My Mac account, but in theory it should work with any account which supports groups. At the moment, it asks you to choose the account you want to use, but this can be hard-coded in if you prefer. The setGroup() handler at the bottom creates the group in the required account if there isn't already one there with the given name and returns the ID of the created or existing group. Referring to the group by its ID rather than by its name should be enough for Contacts to identify it correctly.

It's possible to create and place the 'people' too by scripting the "Contacts" framework, but (for the moment at least) I've left it at creating and/or identifying the correct group.

Applescript:

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later.
use framework "Foundation"
use framework "Contacts"
use scripting additions

property preferredAccount : "On My Mac"

on main()
   set roster to {}
   set NA to "#N/A"
   
   launch application "Contacts"
   
   tell application "Microsoft Excel"
       activate
       set Cohort to string value of cell "Edit!A37"
   end tell
   
   set CohortGroupID to setGroup(Cohort)
   
   tell application "Microsoft Excel"
       repeat with i from 2 to 33
           if string value of cell ("Edit!B" & i) is NA then
               set j to i - 1
               exit repeat
           end if
       end repeat
   end tell
   
   repeat with i from 2 to j
       set TEMP to {key:i - 1, LastName:"", FirstName:"", MI:"", Rank:"", |Hash|:"", |Nickname|:"", Srvc:"", Corps:"", Addressline1:"", |City|:"", |State|:"", |Zip|:"", HomePhone:"", |Email|:"", ParentMilitaryCommand:"", MobilePhone:"", WorkPhone:"", EmailDomain:""}
       tell application "Microsoft Excel"
           set LastName of TEMP to string value of cell ("Edit!B" & i)
           set FirstName of TEMP to string value of cell ("Edit!C" & i)
           set MI of TEMP to string value of cell ("Edit!D" & i)
           set Rank of TEMP to string value of cell ("Edit!F" & i)
           set |Hash| of TEMP to string value of cell ("Edit!G" & i)
           set |Nickname| of TEMP to string value of cell ("Edit!H" & i)
           set Srvc of TEMP to string value of cell ("Edit!J" & i)
           set Corps of TEMP to string value of cell ("Edit!M" & i)
           if Corps of TEMP is NA then set Corps of TEMP to ""
           set Addressline1 of TEMP to string value of cell ("Edit!N" & i)
           try
               if (not cell ("Edit!O" & i) is missing value) and (string value of cell ("Edit!O" & i) ≠ "") then
                   set Addressline1 of TEMP to Addressline1 of TEMP & ", " & string value of cell ("Edit!O" & i)
                   --AddressLine2
               end if
           end try
           set |City| of TEMP to string value of cell ("Edit!P" & i)
           set |State| of TEMP to string value of cell ("Edit!Q" & i)
           set |Zip| of TEMP to string value of cell ("Edit!R" & i)
           set HomePhone of TEMP to string value of cell ("Edit!S" & i)
           set |Email| of TEMP to string value of cell ("Edit!T" & i)
           set ParentMilitaryCommand of TEMP to string value of cell ("Edit!U" & i)
           set MobilePhone of TEMP to string value of cell ("Edit!V" & i)
           set WorkPhone of TEMP to string value of cell ("Edit!W" & i)
       end tell
       set EmailDomain of TEMP to rightStr(|Email| of TEMP, 4)
       tell application "Contacts"
           set theStudent to make new person with properties ¬
               {last name:(LastName of TEMP), first name:(FirstName of TEMP), middle name:(MI of TEMP), title:(Rank of TEMP), note:("Cohort: " & Cohort & linefeed & "Hash: " & |Hash| of TEMP), nickname:(|Nickname| of TEMP), suffix:(Srvc of TEMP), job title:(Corps of TEMP), organization:(ParentMilitaryCommand of TEMP)}
           if EmailDomain of TEMP is ".mil" then
               make new email at end of every email of theStudent with properties ¬
                   {label:"Work", value:(|Email| of TEMP)}
           else if EmailDomain of TEMP is ".gov" then
               make new email at end of every email of theStudent with properties ¬
                   {label:"Work", value:(|Email| of TEMP)}
           else
               make new email at end of every email of theStudent with properties ¬
                   {label:"Home", value:(|Email| of TEMP)}
           end if
           make new phone at end of every phone of theStudent with properties ¬
               {label:"Work", value:(WorkPhone of TEMP)}
           make new phone at end of every phone of theStudent with properties ¬
               {label:"Home", value:(HomePhone of TEMP)}
           make new phone at end of every phone of theStudent with properties ¬
               {label:"Mobile", value:(MobilePhone of TEMP)}
           make new address at end of every address of theStudent with properties ¬
               {label:"Home", street:(Addressline1 of TEMP), city:(|City| of TEMP), state:(|State| of TEMP), zip:(|Zip| of TEMP)}
           
           set cls to class of theStudent
           add theStudent to group id CohortGroupID
       end tell
       set the end of roster to theStudent
   end repeat
   tell application "Contacts" to save
   beep
end main

on rightStr(txt, N)
   set C to length of txt
   if N > C then set N to C
   set rtntxt to text -N thru C of txt
   return rtntxt
end rightStr

on setGroup(Cohort)
   set |⌘| to current application
   
   set contactStore to |⌘|'s class "CNContactStore"'s new()
   -- Ask the user to choose a Contacts account. A container doesn't exactly correspond to an account, but they're close enough for the current purposes.
   set allContainers to contactStore's containersMatchingPredicate:(missing value) |error|:(missing value)
   set containerNames to (allContainers's valueForKey:("name")) as list
   set containerChoice to (choose from list containerNames with prompt "Which Contacts account?" default items {preferredAccount})
   if (containerChoice is false) then error number -128
   
   -- Get the ID of the container with the chosen name.
   set containerFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("name == %@") argumentArray:(containerChoice)
   set containerID to (allContainers's filteredArrayUsingPredicate:(containerFilter))'s firstObject()'s identifier()
   
   -- Get the names of the existing groups in this container.
   set groupFilter to |⌘|'s class "CNGroup"'s predicateForGroupsInContainerWithIdentifier:(containerID)
   set groupsInThisContainer to contactStore's groupsMatchingPredicate:(groupFilter) |error|:(missing value)
   set groupNames to groupsInThisContainer's valueForKey:("name")
   
   -- If the name passed to this handler is amongst them, get the corresponding group. Otherwise create a new one.
   -- NB. DUPLICATE NAMES ARE ALLOWED IN CONTACTS. IF THE CONTAINER CONTAINS MORE THAN ONE GROUP WITH THE SAME NAME, THIS MAY NOT RETURN THE CORRECT ONE.
   if (groupNames's containsObject:(Cohort)) then
       -- Get the (first) existing group with the given name.
       set groupFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat_("name == %@", Cohort)
       set targetGroup to (groupsInThisContainer's filteredArrayUsingPredicate:(groupFilter))'s firstObject()
   else
       -- Create a new one.
       set targetGroup to |⌘|'s class "CNMutableGroup"'s new()
       tell targetGroup to setName:(Cohort)
       -- Set up a request to save it in the container chosen above.
       set saveRequest to |⌘|'s class "CNSaveRequest"'s new()
       tell saveRequest to addGroup:(targetGroup) toContainerWithIdentifier:(containerID)
       -- Execute the request. The new group appears in the Contacts application instantly (on my machine) without having to quit and reopen it.
       tell contactStore to executeSaveRequest:(saveRequest) |error|:(missing value)
   end if
   
   -- Return the group's ID. This should be enough for the Contacts application.
   return (targetGroup's identifier()) as text
end setGroup


NG

Offline

 

#17 2019-09-11 09:15:24 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hi Nigel —

Thanks.  Nitnoid typo, line 10.  NS should be NA

The script creates the group at all 3 sites successfully, but only populates the one on iCloud. At Google and 'On My Mac', both fail at the same spot as way before: line 82 'add theStudent to group id CohortGroupID' complaining that the student must be a person, which we verify on line 81.

Thinking that maybe multiple groups on multiple sites with the same name cause confusion, I deleted all of them and tried again. Same as above.

If it might make a difference, I commented out the main() wrappers. I don't know how to see main()'s variables in Script Debugger otherwise when I 'run main()'.

Obviously, you love a challenge.  But I do feel that I may be monopolising < smile your time.

...best, Mick

Offline

 

#18 2019-09-11 12:25:52 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Mick536 wrote:

Nitnoid typo, line 10.  NS should be NA


Ah yes. Sorry. I can get "NS" on the brain when writing ASObjCode.  roll  Now corrected.

The script creates the group at all 3 sites successfully, but only populates the one on iCloud.


Hmm.  hmm  So it looks as if the ASObjC code works, but the original problem remains: Contacts isn't able to do anything about the account — even though it can see the target group (your report: "Scriplet #1:  From both iCloud and 'On My Mac'") and is unequivocably using its ID (the point of my code returning it).

both fail at the same spot as way before: line 82 'add theStudent to group id CohortGroupID' complaining that the student must be a person, which we verify on line 81.


The error message you quoted in post #1 was "Contacts got an error: You can only add a person to a group." You've been taking this to be a complaint that theStudent isn't a person, but it seems ever more likely that the problem's actually to do with adding to the group, ie. "You can't add a person to anything other than a group." Contacts can see the group — otherwise it wouldn't return its name in response to my earlier test script and the error would be "Can't get group …." It's just not recognising it as something that can be added to. The reason may something to do with the way Contacts is set up not to have accounts (or containers) in its AppleScript implementation.

If it might make a difference, I commented out the main() wrappers.


I put the main code in its own handler because I like to keep variables local — and therefore non-persistent — unless there's a good reason for doing otherwise. I couldn't see a reason to have globals or other persistent variables in your original code. The property in my version is a behaviour-influencing variable conveniently placed for editing near the top of the script.  smile

I'll think further about the adding problem. The research is interesting and educational even if the problem doesn't finally get licked.  smile


NG

Offline

 

#19 2019-09-11 01:25:28 pm

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hi Nigel —

I'm evaluating if what I want is what I need.  I'll live with it for a while and see.  What I certainly don't want is to have to manually add 20+ records into Contacts with every class, and we've achieved that.

And there is a work-around that's not tedious: Export the vCards from iCloud and import them 'On My Mac.'

A last, simple question.  Has the requirement to preface rightstr() with my (as in 'my rightstr()') been relieved? I use it and noted that you don't, which might just be my answer. smile

Anyway, much thanks for the abundant shared knowledge.

...best, Mick

Offline

 

#20 2019-09-11 01:38:00 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Mick536 wrote:

Has the requirement to preface rightstr() with my (as in 'my rightstr()') been relieved? I use it and noted that you don't


Hi Mick.

The 'my' was only needed because the call to the handler was inside the Microsoft Excel 'tell' statement. I moved the call to one line later — outside the 'tell' statement, where the handler belongs to 'me' (the script) anyway. I also simplified the handler itself slightly.  smile


NG

Offline

 

#21 2019-09-12 11:53:03 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

OK. This explicitly adds each contact to the specified container (account) and to the specified group there — assuming the Xcode documentation's correct and I haven't introduced any more typos when copying from my test script!

Applescript:

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later.
use framework "Foundation"
use framework "Contacts"
use scripting additions

property preferredAccount : "On My Mac"
property |⌘| : current application

on main()
   set roster to {}
   set NA to "#N/A"
   
   activate application "Contacts"
   
   tell application "Microsoft Excel"
       activate
       set Cohort to string value of cell "Edit!A37"
   end tell
   
   -- Identify the target account (container) and group, creating the latter if necessary.
   set contactStore to |⌘|'s class "CNContactStore"'s new()
   set containerID to setContainer(contactStore)
   set targetGroup to setGroup(contactStore, containerID, Cohort)
   -- Initialise a save request to be added to during the repeat below.
   set saveRequest to |⌘|'s class "CNSaveRequest"'s new()

   tell application "Microsoft Excel"
       repeat with i from 2 to 33
           if string value of cell ("Edit!B" & i) is NA then
               set j to i - 1
               exit repeat
           end if
       end repeat
   end tell
   
   repeat with i from 2 to j
       -- Get data from Excel into a record.
       set TEMP to {key:i - 1, LastName:"", FirstName:"", MI:"", Rank:"", |Hash|:"", |Nickname|:"", Srvc:"", Corps:"", Addressline1:"", |City|:"", |State|:"", |Zip|:"", HomePhone:"", |Email|:"", ParentMilitaryCommand:"", MobilePhone:"", WorkPhone:""}
       tell application "Microsoft Excel"
           set LastName of TEMP to string value of cell ("Edit!B" & i)
           set FirstName of TEMP to string value of cell ("Edit!C" & i)
           set MI of TEMP to string value of cell ("Edit!D" & i)
           set Rank of TEMP to string value of cell ("Edit!F" & i)
           set |Hash| of TEMP to string value of cell ("Edit!G" & i)
           set |Nickname| of TEMP to string value of cell ("Edit!H" & i)
           set Srvc of TEMP to string value of cell ("Edit!J" & i)
           set Corps of TEMP to string value of cell ("Edit!M" & i)
           if Corps of TEMP is NA then set Corps of TEMP to ""
           set Addressline1 of TEMP to string value of cell ("Edit!N" & i)
           try
               if (not cell ("Edit!O" & i) is missing value) and (string value of cell ("Edit!O" & i) ≠ "") then
                   set Addressline1 of TEMP to Addressline1 of TEMP & ", " & string value of cell ("Edit!O" & i)
                   --AddressLine2
               end if
           end try
           set |City| of TEMP to string value of cell ("Edit!P" & i)
           set |State| of TEMP to string value of cell ("Edit!Q" & i)
           set |Zip| of TEMP to string value of cell ("Edit!R" & i)
           set HomePhone of TEMP to string value of cell ("Edit!S" & i)
           set |Email| of TEMP to string value of cell ("Edit!T" & i)
           set ParentMilitaryCommand of TEMP to string value of cell ("Edit!U" & i)
           set MobilePhone of TEMP to string value of cell ("Edit!V" & i)
           set WorkPhone of TEMP to string value of cell ("Edit!W" & i)
           -- (EmailDomain and rightstr() replaced with 'ends with' in makeStudent() below.)
       end tell
       
       -- Use the data to create a new "student" (person).
       set theStudent to makeStudent(TEMP, Cohort)
       -- Add saving the student to the target account and adding it to the target group to the save request.
       tell saveRequest to addContact:(theStudent) toContainerWithIdentifier:(containerID)
       tell saveRequest to addMember:(theStudent) toGroup:(targetGroup)
       -- Add the student's Contacts ID to the end of the roster list.
       set the end of roster to (theStudent's identifier()) as text
   end repeat
   -- When done, execute all the saves stored in the save request. The results appear in Contacts immediately.
   tell contactStore to executeSaveRequest:(saveRequest) |error|:(reference)
   -- Replace the IDs in 'roster' with the equivalent Contacts 'person' specifiers, which are what it contained in the original script.
   tell application "Contacts"
       repeat with i from 1 to (count roster)
           set item i of roster to person id (item i of roster)
       end repeat
   end tell
   beep
   
   return roster
end main

on setContainer(contactStore)
   -- Ask the user to choose a Contacts account. A container doesn't exactly correspond to an account, but they're close enough for the current purposes.
   set allContainers to contactStore's containersMatchingPredicate:(missing value) |error|:(missing value)
   set containerNames to (allContainers's valueForKey:("name")) as list
   set containerChoice to (choose from list containerNames with prompt "Which Contacts account?" default items {preferredAccount})
   if (containerChoice is false) then error number -128
   
   -- Get the ID of the container with the chosen name.
   set containerFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("name == %@") argumentArray:(containerChoice)
   set targetContainer to (allContainers's filteredArrayUsingPredicate:(containerFilter))'s firstObject()
   
   return targetContainer's identifier()
end setContainer

on setGroup(contactStore, containerID, groupName)
   -- Get the names of the existing groups in this container.
   set groupFilter to |⌘|'s class "CNGroup"'s predicateForGroupsInContainerWithIdentifier:(containerID)
   set groupsInThisContainer to contactStore's groupsMatchingPredicate:(groupFilter) |error|:(missing value)
   set groupNames to groupsInThisContainer's valueForKey:("name")
   
   -- If the name passed to this handler is amongst them, get the corresponding group. Otherwise create a new one.
   -- NB. DUPLICATE NAMES ARE ALLOWED IN CONTACTS. IF THE CONTAINER CONTAINS MORE THAN ONE GROUP WITH THE SAME NAME, THIS MAY NOT RETURN THE CORRECT ONE.
   if (groupNames's containsObject:(groupName)) then
       -- Get the (first) existing group with the given name.
       set groupFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat_("name == %@", groupName)
       set targetGroup to (groupsInThisContainer's filteredArrayUsingPredicate:(groupFilter))'s firstObject()
   else
       -- Create a new one.
       set targetGroup to |⌘|'s class "CNMutableGroup"'s new()
       tell targetGroup to setName:(groupName)
       -- Set up a request to save it in the container chosen above.
       set saveRequest to |⌘|'s class "CNSaveRequest"'s new()
       tell saveRequest to addGroup:(targetGroup) toContainerWithIdentifier:(containerID)
       -- Execute the request. The new group appears in the Contacts application instantly (on my machine) without having to quit and reopen it.
       tell contactStore to executeSaveRequest:(saveRequest) |error|:(missing value)
   end if
   
   -- Return the group (a CNMutableGroup).
   return targetGroup
end setGroup

on makeStudent(TEMP, groupName)
   -- Make a new contact and set its simpler properties.
   set thisPerson to |⌘|'s class "CNMutableContact"'s new()
   tell thisPerson to setFamilyName:(TEMP's LastName)
   tell thisPerson to setGivenName:(TEMP's FirstName)
   tell thisPerson to setMiddleName:(TEMP's MI)
   tell thisPerson to setNamePrefix:(TEMP's Rank)
   tell thisPerson to setNote:("Cohort: " & groupName & linefeed & "Hash: " & TEMP's |Hash|)
   tell thisPerson to setNickname:(TEMP's |Nickname|)
   tell thisPerson to setNameSuffix:(TEMP's Srvc)
   tell thisPerson to setJobTitle:(TEMP's Corps)
   tell thisPerson to setOrganizationName:(TEMP's ParentMilitaryCommand)
   
   -- Set an array for the e-mail.
   set emailArray to |⌘|'s class "NSMutableArray"'s new()
   set emailAddress to TEMP's |Email|
   if (emailAddress > "") then
       if ((emailAddress ends with ".mil") or (emailAddress ends with ".gov")) then
           set emailLabel to "Work"
       else
           set emailLabel to "Home"
       end if
       tell emailArray to addObject:(my makeEmail(emailLabel, emailAddress))
   end if
   tell thisPerson to setEmailAddresses:(emailArray)
   -- An array for the phones.
   set phoneArray to |⌘|'s class "NSMutableArray"'s new()
   if (TEMP's WorkPhone > "") then tell phoneArray to addObject:(my makePhone("Work", TEMP's WorkPhone))
   if (TEMP's MobilePhone > "") then tell phoneArray to addObject:(my makePhone("Mobile", TEMP's MobilePhone))
   if (TEMP's HomePhone > "") then tell phoneArray to addObject:(my makePhone("Home", TEMP's HomePhone))
   tell thisPerson to setPhoneNumbers:(phoneArray)
   -- An array for the address.    
   set addressArray to |⌘|'s class "NSMutableArray"'s new()
   set addressRecord to {street:TEMP's Addressline1, city:TEMP's |City|, state:TEMP's |State|, postalCode:TEMP's |Zip|}
   if (addressRecord is not {street:"", city:"", state:"", postalCode:""}) then tell addressArray to addObject:(my makeAddress("Home", addressRecord))
   tell thisPerson to setPostalAddresses:(addressArray)
   
   return thisPerson
end makeStudent

on makeEmail(label, emailAddress)
   if (emailAddress is "") then return false
   
   return |⌘|'s class "CNLabeledValue"'s labeledValueWithLabel:(label) value:(emailAddress)
end makeEmail

on makePhone(label, phoneNumber)
   if (phoneNumber is "") then return false
   
   set thisPhone to |⌘|'s class "CNPhoneNumber"'s phoneNumberWithStringValue:(phoneNumber)
   return |⌘|'s class "CNLabeledValue"'s labeledValueWithLabel:(label) value:(thisPhone)
end makePhone

on makeAddress(label, addressRecord)
   set emptyValues to {street:"", city:"", state:"", postalCode:"", country:"", countryCode:"", subAdministrativeArea:"", subLocality:""}
   set addressRecord to addressRecord & emptyValues
   if (addressRecord is emptyValues) then return false
   
   set thisAddress to |⌘|'s class "CNPostalAddress"'s new()
   tell thisAddress to setStreet:(addressRecord's street)
   tell thisAddress to setCity:(addressRecord's city)
   tell thisAddress to setState:(addressRecord's state)
   tell thisAddress to setPostalCode:(addressRecord's postalCode)
   tell thisAddress to setCountry:(addressRecord's country)
   tell thisAddress to setISOCountryCode:(addressRecord's countryCode)
   -- The following two properties were introduced in 10.12.4 (Sierra) and still have no equivalents in Contacts 12.0 in Mojave (10.14).
   -- Require Mojave or later to set them.
   considering numeric strings
       if (AppleScript's version ≥ "2.7") then
           tell thisAddress to setSubAdministrativeArea:(addressRecord's subAdministrativeArea)
           tell thisAddress to setSubLocality:(addressRecord's subLocality)
       end if
   end considering
   
   return |⌘|'s class "CNLabeledValue"'s labeledValueWithLabel:(label) value:(thisAddress)
end makeAddress

Edit: Modified to make the makeEmail(), makePhone(), and makeAddress() handlers usable more generally. Typos corrected.  roll


NG

Offline

 

#22 2019-09-14 08:34:31 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Hello Nigel —

Normally, when I run the script, I have this version of Excel running

Macintosh SSD:Applications:Office 2011:Microsoft Excel.app

with the applicable spreadsheet open. Now, the script is throwing errors after launching

Macintosh SSD:Applications:Microsoft Excel.app

(Excel 2016) which doesn't have a spreadsheet open, etc, which is a new wrinkle.  I'm in syntactical knots trying to specify the running version, so I thought I'd ask. My workaround has been to ask Microsoft Excel if it is running, and then doing nothing. I do sense a lack of elegance. smile

Need to set variable j, which you may not have in your test suite. I combined at the top.

Applescript:


tell application "Microsoft Excel"
   activate
   set Cohort to string value of cell "Edit!A37"
   repeat with i from 2 to 33
       if string value of cell ("Edit!B" & i) is NA then
           set j to i - 1
           exit repeat
       end if
   end repeat
end tell

Getting an error at my line 75 on "set the end of roster to (theStudent's indentifier()) as text"
-[CNMutableContact indentifier]: unrecognized selector sent to instance 0x7fa242f98db0

I commented it out, and added 20 students to the cohort group ON MY MAC! Apologies for the delay.

...best, Mick

Offline

 

#23 2019-09-14 10:17:12 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5005

Re: Scripting Contacts

Hi Mick.

Sorry about the typos after all that. I've now corrected them above.

Normally if you have two or more versions of an application on the same machine, and they all have the same name, a script will address the one that's open at the time. Otherwise it'll go for the newest version. With Numbers, I force scripts to use the ’09 version by including the path to it in the 'tell' line. Perhaps this would work for you with Excel:

Applescript:

tell application "Macintosh SSD:Applications:Office 2011:Microsoft Excel.app"
   activate
   
   -- etc.
end tell

You wrote:

Need to set variable j, which you may not have in your test suite.


Yeah. Sorry about that. I don't have Excel, so I just set a hard value for j. Obviously I overlooked restoring your j-setting repeat when integrating my new code into your original script.

Getting an error at my line 75 on "set the end of roster to (theStudent's indentifier()) as text"


"indentifier" should of course have been "identifier". Your original script adds each Contacts 'person' created to your 'roster' list. I don't know what that's for, but 'theStudent' in my code is a "Contacts" framework 'CNContact', not a Contacts application 'person', so I've stored the contact IDs instead and tell the Contacts application to replace them with the corresponding 'persons' later. I'm not sure how useful that is, but anyway, it's there.  smile


NG

Offline

 

#24 2019-09-15 08:19:08 am

Mick536
Member
From:: New England
Registered: 2012-03-05
Posts: 40

Re: Scripting Contacts

Nigel —

Studying what you've done is going to take some time, so I'm waiting for a wintery day, maybe a wintery month, to do it.  But much thanks.  I've got exactly what I wanted. And thank you, too, for all that you do around here.

...best regards, Mick

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)