Script to change review frequency of projects in specific folders

Hello,

I’ve seen scripts to change review frequency by active/on hold and review date by review frequency.

But I’m trying to write a script that will change the review interval based on the folder containing the project.

For example, I have the following folder structure:

Personal

  • Big 3
  • Active
  • Pending/On Hold

Work

  • Big 3
  • Active
  • Pending/On Hold

I basically want a script that does the following (I’ll say it in English for now)
- For each project that is active or on hold in the database
– If project is contained by a folder called “Big 3”, then change review interval to every 1 day
– If project is contained by a folder called “Active” change review interval to every 3 days
– If project is contained by a folder called “Pending/On Hold” AND project is Active, change review interval to every 1 week
– If project is contained by a folder called “Pending/On Hold” AND project is On Hold, change review interval to every 2 weeks

This doesn’t seem like it would be too difficult. Is there an apple/omniscript function that
(a) checks all projects in OF (would this be a “for” function?)
(b) checks to see if a specific project’s folder (container?) is called “X”?

My goal is to run the script during my shutdown routine (or even better, auto run every day at 3pm). My purpose is to make the Review more useful and cut out the fat on all my project/Areas of focus categories.

The result being that I can change the project status by moving it up and down the folder as it gains or loses priority. The script ensures that the Review intervals are in keeping with the project priority, which is defined by the containing folder.

I’ve tried using flags and tags to do project priority, but I don’t like how OF tasks inherit, or sometimes don’t inherit, tasks and flags from projects.

Many thanks if you can provide some ideas on the functions I might use to figure this out.

One function for you to play with:

// isContainedBy :: OFProject -> OFFolder -> Bool
const isContainedBy = project =>
    folder => project.parentFolder === folder

As a starting point, you can try this example script in Script Editor:

  • setting language tab to JavaScript.
  • pressing Script > Run menu item.

This script operates on selected projects. Could be extended to the entire database.

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = strName => {
        // main :: IO ()
        const main = () => {
            const
                pvs = [
                    [
                        proj => isContainedBy(proj)(folderNamed("Big 3")),
                        [1, "days"]
                    ],
                    [
                        proj => isContainedBy(proj)(folderNamed("Active")),
                        [3, "days"]
                    ],
                    [
                        proj => isContainedBy(proj)(folderNamed("Pending/On Hold")) &&
                        proj.status === Project.Status.Active,
                        [1, "weeks"]
                    ],
                    [
                        proj => isContainedBy(proj)(folderNamed("Pending/On Hold")) &&
                        proj.status === Project.Status.OnHold,
                        [2, "weeks"]
                    ]
                ];
            return map(
                compose(
                    uncurry(reviewInt),
                    fanArrow(
                        identity
                    )(caseOf(pvs)([1, "days"]))
                )
            )(document.windows[0].selection.projects)
        };

        // OMNIFOCUS FUNCTIONS

        // isContainedBy :: OFProject -> OFFolder -> Bool
        const isContainedBy = project =>
            folder => project.parentFolder === folder

        // reviewInt :: OFProject -> (Int, String) -> OF Project
        const reviewInt = project => tpl => Object.assign(
            project, {
                "reviewInterval": Object.assign(
                    project.reviewInterval, {
                        "steps": tpl[0],
                        "unit": tpl[1]
                    }
                )
            }
        )


        // FUNCTIONS --
        // https://github.com/RobTrew/prelude-jxa
        // JS Prelude --------------------------------------------------
        // Just :: a -> Maybe a
        const Just = x => ({
            type: 'Maybe',
            Nothing: false,
            Just: x
        });

        // Nothing :: Maybe a
        const Nothing = () => ({
            type: 'Maybe',
            Nothing: true,
        });

        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            b => ({
                type: 'Tuple',
                '0': a,
                '1': b,
                length: 2
            });

        // caseOf :: [(a -> Bool, b)] -> b -> a ->  b
        const caseOf = pvs =>
            // List of (Predicate, value) tuples -> Default value 
            //         -> Value to test -> Output value
            otherwise => x => {
                const mb = pvs.reduce((a, pv) =>
                    a.Nothing ? (
                        pv[0](x) ? Just(pv[1]) : a
                    ) : a, Nothing());
                return mb.Nothing ? otherwise : mb.Just;
            };

        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );

        // fanArrow (&&&) :: (a -> b) -> (a -> c) -> (a -> (b, c))
        const fanArrow = f =>
            // A function from x to a tuple of (f(x), g(x))
            // ((,) . f <*> g)
            g => x => Tuple(f(x))(
                g(x)
            );

        // identity :: a -> a
        const identity = x =>
            // The identity function.
            x;

        // length :: [a] -> Int
        const length = xs =>
            // Returns Infinity over objects without finite
            // length. This enables zip and zipWith to choose
            // the shorter argument when one is non-finite,
            // like cycle, repeat etc
            'GeneratorFunction' !== xs.constructor
            .constructor.name ? (
                xs.length
            ) : Infinity;

        // map :: (a -> b) -> [a] -> [b]
        const map = f =>
            // The list obtained by applying f
            // to each element of xs.
            // (The image of xs under f).
            xs => [...xs].map(f);

        // uncurry :: (a -> b -> c) -> ((a, b) -> c)
        const uncurry = f =>
            // A function over a pair, derived
            // from a curried function.
            function () {
                const
                    args = arguments,
                    xy = Boolean(args.length % 2) ? (
                        args[0]
                    ) : args;
                return f(xy[0])(xy[1]);
            };

        return main();
    };


    // OmniJS Context Evaluation ------------------------------------------------
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})()`
        )
    ) : 'No documents open in OmniFocus.'
})();
2 Likes

Thanks for this @unlocked2412 ! I will review and report back, although it may take me some time to play around with this.

You are truly the dark knight of scripting questions !

Good luck with the script ! Do report if you have any troubles.

Related to this would be my case - where right now I want the review frequency of the Summer project to go from monthly to weekly. And, conversely, the review frequency of Winter to go from weekly to Monthly.

So the Automated Review Frequency Adjustment case is more general than the OP’s one- entirely valid though that is.

Well, I can document how to customise it.

pvs name contains a list of predicate-value pairs. In this specific function, each predicate (first component of the pair) has the signature:

OFProject -> Bool

The second component is a list with two items:

  • the first one, number of steps.
  • the second one could be: ["days", "weeks", "months", "years"].

In your specific case, pvs could be something like (assuming the definition of isSummer() and ìsWinter() functions):

pvs = [
    [
        proj => "Summer" === proj.name && !isSummer(),
        [1, "months"]
    ],
    [
        proj => "Summer" === proj.name && isSummer(),
        [1, "weeks"]
    ],
    [
        proj => "Winter" === proj.name && !isWinter(),
        [1, "weeks"]
    ],
    [
        proj => "Summer" === proj.name && isSummer(),
        [1, "months"]
    ]
]

Tell me if you need further assistance, @MartinPacker.

3 Likes

Thanks for the included script! What would need to be changed to make it recurse for all folders in the database at a click (from the toolbar, for instance)?

My equivalents of OP’s ‘Big 3’ and ‘Active’ are together inside a top-level folder. This script only recognizes the selected projects if the named folders are top-level. Also, I’d like to not have to select the projects ;)

I’ll send you a PM since this is a very specific use case.