Comment trier ses projets dans OmniFocus ? Professionnel vs. personnel ? Par priorité ? Par date d’échéance ? Personnellement je les trie par statut. Si vous regardez l’image ci-dessous vous verrez que j’ai des dossiers nommés « active » (actifs), « stalled » (bloqué), « waiting for » (en attente), etc.

Projets OmniFocus

Pourquoi ai-je choisi de trier mes projets de cette manière ? Parce que cela me permet d’identifier en un coup d’œil les projets sur lesquels je dois travailler, ceux que je dois débloquer, ceux qui sont en attente de quelque chose, etc.

Évidemment cette organisation est un peu pénible s’il faut trier manuellement les projets dans les dossiers en fonction de l’évolution de leur statut. C’est pourquoi j’ai écrit un script AppleScript appelé « housekeeping » qui va classer chaque projet automatiquement dans le bon dossier selon son statut. Vous trouverez le code du script ci-dessous. Je l’ai fait un peu à l’arrache, c’est loin d’être un exemple de code propre, et il faudra sûrement l’adapter un peu pour qu’il fonctionne chez vous, néanmoins j’espère qu’il peut vous aider si vous voulez faire quelque chose de similaire.

Ce script fait également d’autres choses : il définit la prochaine date de revue de manière plus « intelligente » : par exemple si la prochaine date de revue tombe un week-end il va déplacer la date de revue au lundi suivant, il va définir la fréquence de revue selon que le projet soit actif ou en attente, etc.

J’ai codé « en dur » certaines chaînes de caractères comme un gros goret, donc pour que le script fonctionne vous devez absolument avoir des dossiers nommés “active”, “stalled”, “waiting for”, “deferred”, “on hold”, “completed”, “dropped”, “templates” et un contexte nommé “Waiting for”. L’alternative étant d’éditer les chaînes de caractères dans le script utiliser d’autres noms. Il y a peut-être d’autres chose qu’il faudra vérifier pour qu’il fonctionne correctement. Pour rappel, ce genre de script est quasi impossible à débugger dans l’éditeur de scripts fourni en standard par Apple, à la place j’utilise Script Debugger de Late Night Software, un outil indispensable pour tout AppleScripter qui se respecte.

Et concernant le débat tabulations contre espaces, l’exemple ici utilise des espaces parce que les tabulations prenaient trop de place, mais mon script original utilise bien des tabulations !

Enfin il y a quelques bouts de code que j’ai piqué à gauche et à droite sur internet, mais je n’arrive plus à retrouver où, donc je tiens à m’en excuser auprès de leurs auteurs originaux. Les développeurs un peu expérimentés les reconnaîtront sûrement, parce que le style du code est clairement différent.

Attention : je ne suis pas responsable des problèmes que peut occasionner ce script, utilisez-le à vos risques et périls !!!

(*
  This script does the following:
  1. Sort projects in folders based on status
    - flagged
    - active
    - stalled
    - waiting for
    - defered
    - completed
    - dropped
  2. Set review date to now for:
    - Projects modified since last run
    - Projects moved

  3. Set review interval to weekly for:
    - Projects that are active, stalled or waiting for

  4. A few other things
*)

property fiveMinutes : 300
property oneDay : 86400
property sevenDays : 604800
property fourteenDays : 1209600
property twentyoneDays : 1814400
property sixtyDays : 51840000
property twelveWeeks : 7257600
property changeGracePeriod : 0 -- 240
property contextWaitingFor : {"waiting for"}
property contextStalled : {"object", "reference"}
-- if script was never run set last run to 5 minutes ago by default
property lastRun : (current date) - fiveMinutes
property putOnHoldIfWaitingForTooLong : sixtyDays

global theFlaggedFolder
global theActiveFolder
global theStalledFolder
global theWaitingForFolder
global theDeferredFolder
global theOnHoldFolder
global theCompletedFolder
global theDroppedFolder
global theTemplateFolder

global theWaitingForContext

property scriptSuiteName : "Projects Housekeeping"

tell application "OmniFocus"
  tell front document
    
    compact
    try
      -- set theFlaggedFolder to (first folder whose name is "flagged")
      set theActiveFolder to (first folder whose name is "active")
      set theStalledFolder to (first folder whose name is "stalled")
      set theWaitingForFolder to (first folder whose name is "waiting for")
      set theDeferredFolder to (first folder whose name is "deferred")
      set theOnHoldFolder to (first folder whose name is "on hold")
      set theCompletedFolder to (first folder whose name is "completed")
      set theDroppedFolder to (first folder whose name is "dropped")
      set theTemplateFolder to (first folder whose name is "templates")
      
      set theWaitingForContext to (first flattened context whose name is "Waiting For")
      
      set will autosave to false
      -- set selected sidebar tab to
      set projectsMoved to my moveActiveProjects(it, {})
      set projectsMoved to projectsMoved or my moveNonActiveProjects(it)
      set will autosave to true
    on error errText number errNum
      set will autosave to true
      display dialog "Error: " & errNum & return & errText
      return
    end try
    if projectsMoved is true then
      set msg to "Please review projects."
      my notify("Did some housekeeping!", msg)
    else
      set msg to "I had nothing to do."
    end if
  end tell
end tell
set lastRun to (current date)



on moveNonActiveProjects(theContainer)
  set projectsMoved to false
  using terms from application "OmniFocus"
    set theProjects to every flattened project of theContainer whose status is on hold ¬
      and singleton action holder is false
    repeat with aProject in theProjects
      if completed by children of aProject is true then
        -- do not complete projects when all tasks are done to avoid accidental completion
        set completed by children of aProject to false
      end if
      set theResult to my reviewIfTaskModified(aProject)
      if theResult is equal to 0 then
        my reviewMonthly(aProject)
      end if
      set isProjectMoved to my moveProject(aProject, theOnHoldFolder)
      set projectsMoved to projectsMoved or isProjectMoved
    end repeat
    
    set theProjects to every flattened project of theContainer whose status is done ¬
      and singleton action holder is false
    repeat with aProject in theProjects
      if completed by children of aProject is true then
        -- do not complete projects whene all tasks are done to avoid accidental completion
        set completed by children of aProject to false
      end if
      -- No need to change review interval
      set isProjectMoved to my moveProject(aProject, theCompletedFolder)
      set projectsMoved to projectsMoved or isProjectMoved
    end repeat
    
    set theProjects to every flattened project of theContainer whose status is dropped ¬
      and singleton action holder is false
    repeat with aProject in theProjects
      if completed by children of aProject is true then
        -- do not complete projects when all tasks are done to avoid accidental completion
        set completed by children of aProject to false
      end if
      -- No need to change review interval
      set isProjectMoved to my moveProject(aProject, theDroppedFolder)
      set projectsMoved to projectsMoved or isProjectMoved
    end repeat
  end using terms from
  return projectsMoved
end moveNonActiveProjects



on moveActiveProjects(theContainer)
  set projectsMoved to false
  --  log "Executing Housekeeping Script"
  using terms from application "OmniFocus"
    set theProjects to every flattened project of theContainer whose status is active ¬
      and singleton action holder is false
    set projectsMoved to my moveActiveProjectsProjects(theProjects)
  end using terms from
  return projectsMoved
end moveActiveProjects



on moveActiveProjectsProjects(theProjects)
  
  using terms from application "OmniFocus"
    set projectsMoved to false
    repeat with aProject in theProjects
      set projectContainer to container of aProject
      if (class of projectContainer is not folder or (class of projectContainer is folder ¬
        and projectContainer is not hidden)) then
        
        if folder of aProject is not theTemplateFolder then
          
          if completed by children of aProject is true then
            set completed by children of aProject to false
          end if
          
          my reviewIfTaskModified(aProject)
          
          -- Checks if there is a next task
          -- If yes, then get context of next task
          set theNextTask to next task of aProject
          set isNoNextTask to false
          set theNextTaskContextName to "****"
          set isAlreadyProcessed to false
          set isDeferredNextTask to false
          
          if theNextTask is missing value then
            set theActiveTasks to ¬
              (every flattened task of aProject whose completed is false ¬
                and number of tasks is equal to 0)
            if (count of theActiveTasks) is greater than or equal to 1 then
              set theActiveTask to item 1 of theActiveTasks
              if theActiveTask is not missing value then
                if defer date of theActiveTask is not missing value then
                  if defer date of theActiveTask is greater than (current date) then
                    set isDeferredNextTask to true
                  else
                    set theNextTask to theActiveTask
                    set theNextTaskContextName to name of context of theNextTask
                  end if
                else
                  set theNextTask to theActiveTask
                  set theNextTaskContextName to name of context of theNextTask
                end if
              else
                set isNoNextTask to true
              end if
            else -- project has no next tasks
              set isNoNextTask to true
              set theWaitingForTasks to (every flattened task of aProject whose completed is false)
              set isNoNextTask to true
              repeat with theWaitingForTask in theWaitingForTasks
                if context of theWaitingForTask is equal to theWaitingForContext then
                  set theNextTask to theWaitingForTask
                  set isNoNextTask to false
                  if context of theNextTask is not missing value then
                    set theNextTaskContextName to name of context of theNextTask
                  end if
                  exit repeat
                end if
              end repeat
              
            end if
          else
            -- If the next task is the project itself, consider there is no task left.
            if id of theNextTask is equal to id of root task of aProject then
              set isNoNextTask to true
            else
              set isNoNextTask to false
              if context of theNextTask is not missing value then
                set theNextTaskContextName to name of context of theNextTask
              end if
            end if
          end if
          
          -- 
          -- Process DEFERRED projects
          -- 
          if isAlreadyProcessed is false then
            if defer date of aProject is not missing value then
              set theCurrentDate to current date
              set theCurrentDate to theCurrentDate - (theCurrentDate's time)
              -- Second condition is to avoid the project being marked for review again and again 
              -- if review date is today
              if defer date of aProject is greater than or equal to my tomorrow() then
                my setNextReviewDate(aProject, defer date of aProject)
                set isProjectMoved to my moveProject(aProject, theDeferredFolder)
                set projectsMoved to projectsMoved or isProjectMoved
                set isAlreadyProcessed to true
              end if
            end if
          end if
          if isAlreadyProcessed is false then
            if isDeferredNextTask then
              if defer date of theActiveTask is greater than or equal to my tomorrow() then
                my setNextReviewDate(aProject, defer date of theActiveTask)
                set isProjectMoved to my moveProject(aProject, theDeferredFolder)
                set projectsMoved to projectsMoved or isProjectMoved
                set isAlreadyProcessed to true
              end if
            end if
          end if
          
          --
          -- Process WAITING FOR projects
          --
          if isAlreadyProcessed is false then
            if theNextTaskContextName is in contextWaitingFor then
              --
              -- ...then it is waiting for but if waiting for more than twenty one days...
              --
              set theTimeDifference to (current date) - (modification date of theNextTask)
              if theTimeDifference is greater than putOnHoldIfWaitingForTooLong ¬
                and putOnHoldIfWaitingForTooLong is not equal to 0 then
                --
                -- ...then we were patient enough, change status and move it to on hold
                --
                my reviewDaily(aProject)
                my setStatus(aProject, on hold)
                set isProjectMoved to my moveProject(aProject, theOnHoldFolder)
                set projectsMoved to projectsMoved or isProjectMoved
                set isAlreadyProcessed to true
              else
                --
                -- ...else it's really waiting for
                --
                if theTimeDifference is greater than sevenDays then
                  -- waiting for more than seven days? Review weekly
                  my reviewWeekly(aProject)
                else
                  -- waiting for less than one week? Review daily
                  my reviewDaily(aProject)
                end if
                set isProjectMoved to my moveProject(aProject, theWaitingForFolder)
                set projectsMoved to projectsMoved or isProjectMoved
                set isAlreadyProcessed to true
              end if
            end if
          end if
          
          --
          -- Process STALLED projects
          -- 
          if isAlreadyProcessed is false then
            if isNoNextTask is true or theNextTaskContextName is in contextStalled then
              my reviewDaily(aProject)
              set isProjectMoved to my moveProject(aProject, theStalledFolder)
              set projectsMoved to projectsMoved or isProjectMoved
              set isAlreadyProcessed to true
            end if
          end if
          
          --
          -- Process ACTIVE projects
          --
          if isAlreadyProcessed is false then
            my reviewDaily(aProject)
            if folder of aProject is not theActiveFolder then
              set isProjectMoved to my moveProject(aProject, theActiveFolder)
              set projectsMoved to projectsMoved or isProjectMoved
              set isAlreadyProcessed to true
            end if
          end if
          
        end if
      end if
      
    end repeat
  end using terms from
  return projectsMoved
end moveActiveProjectsProjects

(*
Changes the status of a project and logs it.
*)
on setStatus(aProject, aStatus)
  tell application "OmniFocus"
    tell front document
      if status of aProject is not aStatus then
        set status of aProject to aStatus
        log "Changed status of project: " & (name of aProject)
      end if
    end tell
  end tell
end setStatus

(*
  Moves a Project to a new folder
*)
on moveProject(aProject, aFolder)
  set projectMoved to false
  tell application "OmniFocus"
    tell front document
      if folder of aProject is not aFolder then
        my reviewNow(aProject)
        move aProject to (beginning of sections of aFolder)
        set projectMoved to true
        log "Moved project: " & (name of aProject)
      end if
    end tell
  end tell
  return projectMoved
end moveProject

(*
  Mark a project for review if a task has been modified
*)
on reviewIfTaskModified(aProject)
  tell application "OmniFocus"
    tell front document
      set theTasks to (every flattened task of aProject ¬
        whose modification date is greater than lastRun and completed is false)
      if (count of theTasks) is greater than 0 then
        my reviewNow(aProject)
        return 1
      else
        return 0
      end if
    end tell
  end tell
end reviewIfTaskModified

(*
  Gets tomorrow, midnight.
*)
on tomorrow()
  copy (current date) to theDate
  return (theDate + oneDay - (theDate's time))
end tomorrow

on reviewNow(aProject)
  set theNextReview to current date
  -- note: do not change review interval in this case
  my setNextReviewDate(aProject, theNextReview)
end reviewNow

on reviewDaily(aProject)
  set theNextReview to my tomorrow()
  tell application "OmniFocus"
    tell front document
      set _ri to {unit:(day), steps:(1), fixed:(false)}
      my setReviewInterval(aProject, _ri)
      my setNextReviewDate(aProject, theNextReview)
    end tell
  end tell
end reviewDaily

on reviewWeekly(aProject)
  -- Set the next weekly review to next Monday
set theNextReview to ¬
	DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate(current date, Monday, 1)
	  
  using terms from application "OmniFocus"
    set _ri to {unit:(week), steps:(1), fixed:(false)}
  end using terms from
  tell application "OmniFocus"
    tell front document
      my setReviewInterval(aProject, _ri)
      my setNextReviewDate(aProject, theNextReview)
    end tell
  end tell
end reviewWeekly

(*
  Sets the next review date to the next first Monday of a Month
  (this month if first Monday not already passed)
  (next month if first Monday already passed)
*)
on reviewMonthly(aProject)
  copy (current date) to theNextReview
	if (weekday of theNextReview is Monday) ¬
	and (day of theNextReview ≤ 7) and (theNextReview > (current date)) then
    -- do nothing because next review is already first Monday and in the future
  else
    set theNextReview's day to 32
    set theNextReview's day to 1
    -- That stuff below is to always set the monthly review on the first Monday of the Month
    set dateOffset to {1, 0, 6, 5, 4, 3, 2}
    set theOffset to item ((weekday of theNextReview) as integer) of dateOffset
    set theNextReview's day to 1 + theOffset
    set theNextReview to theNextReview - (theNextReview's time)
    tell application "OmniFocus"
      tell front document
        set _ri to {unit:(month), steps:(1), fixed:(false)}
        my setReviewInterval(aProject, _ri)
        my setNextReviewDate(aProject, theNextReview)
      end tell
    end tell
  end if
end reviewMonthly

on setReviewInterval(aProject, _ri)
  tell application "OmniFocus"
    tell front document
      if review interval of aProject is not equal to _ri then
        set review interval of aProject to _ri
        log "Changed review interval: " & (name of aProject)
      end if
    end tell
  end tell
end setReviewInterval

(*
  Sets the review date for the project to the date specified, but only if:
  - the project's review date is greater than today (is in the future)
  - the project's review date is not already equal to the new review date
  If the review date is a Saturday or Sunday, sets it to the next Monday
*)
on setNextReviewDate(aProject, aDate)
  tell application "OmniFocus"
    tell front document
      -- Skip Saturdays and Sundays, use Monday instead, and remove Date's time.
      set aDate to (my skipWeekends(aDate)) - (aDate's time)
      -- Only change review date if it is greater than now and is different from the new date
      if next review date of aProject is greater than (current date) ¬
        and next review date of aProject is not equal to aDate then
        set next review date of aProject to aDate
        log "Changed review date: " & (name of aProject)
      end if
    end tell
  end tell
end setNextReviewDate

on skipWeekends(aDate)
  if weekday of aDate is Saturday then
    set aDate to aDate + 2 * oneDay
  else if weekday of aDate is Sunday then
    set aDate to aDate + oneDay
  end if
  return aDate
end skipWeekends

on DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate(d, w, i) -- returns a date
  -- Keep an note of whether the instance value *starts* as zero
  set instanceIsZero to (i is 0)
  -- Increment negative instances to compensate for the following subtraction loop
  if i < 0 and d's weekday is not w then set i to i + 1
  -- Subtract a day at a time until the required weekday is reached
  repeat until d's weekday is w
    set d to d - days
    -- Increment an original zero instance to 1 if subtracting from Sunday into Saturday 
    if instanceIsZero and d's weekday is Saturday then set i to 1
  end repeat
  -- Add (adjusted instance) * weeks to the date just obtained and zero the time
  d + i * weeks - (d's time)
end DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate

(*
  Uses Notification Center to display a notification message.
  theTitle – a string giving the notification title
  theDescription – a string describing the notification event
*)
on notify(theTitle, theDescription)
  display notification theDescription with title scriptSuiteName subtitle theTitle
end notify