Acting on all projects using Applescript
I've been having an awful time trying to obtain all the projects in my omnifocus document in Applescript.
I have a script working now, which I've mentioned in another thread. Basically it iterates over all projects and folders that are selected in the sidebar, and changes their "next review date" to the "last changed date", if the project has been changed since the last "Mark Review" event occured. I run this script once at the beginning of my daily "get clean" review, and it's working great for me. As a sidebar, I've submitted an enhancement request for Omni to make this functionality available out-of-the-box. The problem is that I have to select all the folders in my sidebar in order to get the script to act on all of my projects. Not a great hardship obviously, but I use this script every day and I'd like to make it more convenient. I want the script to iterate over every single project, without having to select everything in the sidebar. But I haven't been able to figure out how to obtain the set of all projects. I'm an experienced programmer, but I'll confess sometimes Applescript, and the structures under the hood of OF, really mystify me. Any hints anyone has, to help me get unstuck? Any snippets that'll spell it out? I had hope that some of the tips and hints in this thread were going to help - didn't get me there though: [URL]http://forums.omnigroup.com/showthread.php?t=3926&highlight=flattened[/URL] Thanks! Here's my script, which works now, abridged: [CODE]on run tell application "OmniFocus" tell front document tell document window 1 set selectedTrees to selected trees of sidebar repeat with aTree in selectedTrees if class of value of aTree is project then my processProject(aTree) else if class of value of aTree is folder then my processFolder(aTree) end if end repeat end tell end tell end tell end run on processFolder(theFolderAsTree) using terms from application "OmniFocus" repeat with aTree in trees of theFolderAsTree if class of value of aTree is project then my processProject(aTree) else if class of value of aTree is folder then my processFolder(aTree) end if end repeat end using terms from end processFolder on processProject(theProjectAsTree) using terms from application "OmniFocus" set aProject to value of theProjectAsTree set currentDateAtMidnight to date (short date string of (current date)) set oldReviewDate to last review date of aProject set lastChangedDate to modification date of aProject set nextReviewDateAtMidnight to next review date of aProject if nextReviewDateAtMidnight > currentDateAtMidnight then if oldReviewDate is not equal to lastChangedDate then set next review date of aProject to lastChangedDate end if end if end using terms from end processProject [/CODE] |
There is currently no direct way to get all the projects. The only way to access all the projects is to recurse over all the [I]sections[/I] of the front document. [URL="http://www.rose-hulman.edu/~clifton/software.html#VerifyNA"]My Verify Next Actions Exist[/URL] script does the necessary recursion. Feel free to mine it for ideas.
I really should post a script that just builds a list of all the projects. That wouldn't be too difficult. I'll try to throw something together, but no promises on the timing. |
I couldn't resist the productive procrastination. Here's a script template for processing every project in OF. I hope people will find it useful as a base for building their own scripts. There are three points of customization.
[LIST][*] A property lets you control whether projects inside dropped folders are included. [*] One handler lets you decide whether or not a given project should be included in the list of projects to be processed based on the project's properties and elements. [*] Another handler is called with a list of all the matching projects. You can customize this one to loop over the projects and update their properties.[/LIST] [CODE] (* This script is a template for use in scripts that need to process every project in an OmniFocus database. The template provides stubs for two handlers that script developers can complete to create custom solutions. Script developers, see the handlers shouldProjectBeIncluded and handleProjects. At this writing (Feb 10, 2010) there is no command in OmniFocus's dictionary to get all the projects, so this template uses a brute force recursion over the Library tree. version 0.1, by Curt Clifton Copyright © 2010, Curtis Clifton Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. version 0.1: Original release *) (* TODO: Change this property to control whether the script looks at projects inside folders that are hidden. The hidden property of a folder seems to correspond to the Dropped setting for a folder in the OmniFocus user interface. *) property shouldLookInHiddenFolders : false (* Tests whether the given OmniFocus project should be included in the list of projects to be processed. Developers should customize this handler if they wish to filter out some projects. *) on shouldProjectBeIncluded(theProject) using terms from application "OmniFocus" -- TODO: test theProject and return true to include it or false to exclude it -- For example: return theProject is flagged return true -- ----------------------------------------------------------------------------- end using terms from end shouldProjectBeIncluded (* Processes the list of all matched projects. Developers should customize this handler to process the projects. *) on handleProjects(theMatchedProjects) using terms from application "OmniFocus" -- TODO: replace the following with code to process the projects (perhaps looping over them to process them individually) return count of theMatchedProjects -- ----------------------------------------------------------------------------- end using terms from end handleProjects (* Main entry point. *) tell application "OmniFocus" tell front document set theSections to every section set matchedProjects to my accumulateProjects(theSections, {}) my handleProjects(matchedProjects) end tell end tell (* Recurses over the given sections of the Library. Accumulates a list of projects that satisfy the 'shouldProjectBeIncluded' handler. theSections: a list of folders, projects, and tasks accum: the matching projects that have been found so far *) on accumulateProjects(theSections, accum) if (theSections is {}) then return accum return accumulateProjects(rest of theSections, accumulateProjectsHelper(item 1 of theSections, accum)) end accumulateProjects (* Recurses over the tree rooted at the given item. Accumulates a list of projects that satisfy the 'shouldProjectBeIncluded' handler. theItem: a folder, project, or task accum: the matching projects that have been found so far *) on accumulateProjectsHelper(theItem, accum) using terms from application "OmniFocus" if (class of theItem is project) then -- screens the item before accumulating it if my shouldProjectBeIncluded(theItem) then set end of accum to theItem end if return accum else if (class of theItem is folder) then return my accumulateProjectsInFolder(theItem, accum) else (* The item must be a task. Shouldn't really reach here since we don't recurse into projects or SALs. *) error "Script attempted to process inside a project. This script is supposedly designed to just process projects. So, it looks like there's a bug. Please contact the developer. We apologize for the inconvenience." end if end using terms from end accumulateProjectsHelper (* Recurses over the tree rooted at the given folder. Accumulates a list of projects that satisfy the 'shouldProjectBeIncluded' handler. theFolder: a folder accum: the items lacking next actions that have been found so far *) on accumulateProjectsInFolder(theFolder, accum) using terms from application "OmniFocus" if ((not shouldLookInHiddenFolders) and hidden of theFolder) then return accum set theChildren to every section of theFolder return my accumulateProjects(theChildren, accum) end using terms from end accumulateProjectsInFolder [/CODE] All the customization points are near the top of the script. Search for "TODO". Feel free to use as you see fit, but please respect the attribution requirement in the license agreement. Share and enjoy. |
Sweet. Absolutely freakin' sweet.
Can't thank you enough Curt. This is exactly what I needed. Noticed your comment/suggestion about summing a count - this was on my maybe/someday too, i.e. to have an easy visibility of how many active projects I'm managing, per DA's guideline of 30-100. Think I'll knock this one off tonight! I think I'm going to have several uses for this template. ;-) Thanks again man, cheers. |
I completed my Reset Review Dates script tonight using Curt's template and it works well.
Interestingly enough, the script won't run if I launch it from the scripts menu icon - it only runs when launching it from a toolbar button, or from the Play button in Applescript Editor. I also create a script that counts my active and onhold projects. Here's the code I put in the handleProjects snippet: [CODE] set activeProjectCount to 0 set onholdProjectCount to 0 repeat with theProject in theMatchedProjects if status of theProject is active then set activeProjectCount to activeProjectCount + 1 end if if status of theProject is on hold then set onholdProjectCount to onholdProjectCount + 1 end if end repeat display dialog ((activeProjectCount as string) & " active projects " & (onholdProjectCount as string) & " onhold projects") [/CODE] One more thing I'd like to do here - I'd also like the counts of my Stalled and Pending projects (using the terms from the sidebar filter). But of course, these are not actual statuses for the project, they are derived by OmniFocus. I'm out of time for tonight, but I think Curts's NextActions script would have the logic for the Stalled count... I'll see if I can shoehorn it in later on. (Obviously a simple look at the start date will do it for the Pending.) |
Strange about the script menu. It works from the FastScripts menu for me. I don't have the regular AppleScript menu.
|
Yeah, it is strange. I can't run it from the AppleScript menu or the FastScripts menu. More precisely, I suspect that it is running, but not finishing in a timely manner, so it appeared like it wasn't running it all. This morning after waking my mbp17, when I clicked on the FastScripts menu, my dialog finally showed, displaying the text from an older (i.e. mid-evening) version of the script. FastScripts also showed one sole menu item: "Terminate (or stop) running script". I think it might have been running 2 or 3 instances of the script.
|
Ah, yes. With a complex database (lots of folders and projects), the necessary recursion can be slow. Looping over the matched projects is also slow.
If you're willing to split your script into multiple separate ones, it would be much faster to use the [I]shouldProjectBeIncluded[/I] method to just select a single project kind (e.g., on hold), then just display the [I]count of theMatchedProjects[/I] in [I]handleProjects[/I]. |
Ah, got it - makes sense. Sure, I don't mind splitting it. Thanks again Curt, cheers.
|
[QUOTE=curt.clifton;73133]With a complex database (lots of folders and projects), the necessary recursion can be slow.[/QUOTE]
You can get a flattened project list a little faster (and perhaps a bit more simply) by using [B]where[/B] queries. e.g. [CODE]property pfSkipHiddenFolders : false on run tell application id "com.omnigroup.OmniFocus" set oDoc to front document set lstAllProjects to my ProjectList(oDoc) end tell end run on ProjectList(oParent) using terms from application "OmniFocus" tell oParent set lstProject to (sections where class is project) -- OR e.g. (sections where class is project and flagged is true) if pfSkipHiddenFolders then set lstFolder to (sections where class is folder and hidden is false) else set lstFolder to (sections where class is folder) end if if length of lstFolder > 0 then return lstProject & my FolderProjects(lstFolder) else return lstProject end if end tell end using terms from end ProjectList on FolderProjects(lstFolder) set lstProject to {} repeat with oFolder in lstFolder set lstProject to lstProject & ProjectList(oFolder) end repeat return lstProject end FolderProjects[/CODE] [COLOR="White"]--[/COLOR] |
All times are GMT -8. The time now is 11:14 PM. |
Powered by vBulletin® Version 3.8.7
Copyright ©2000 - 2024, vBulletin Solutions, Inc.