Шаблон иерархии для крупных управляющих аккаунтов

Через управляющий аккаунт AdWords можно работать с сотнями или даже тысячами клиентских аккаунтов. Выполнение скрипта сразу во всех них может быть непростой задачей, поскольку параллельная работа с использованием метода executeInParallel возможна максимум с 50 аккаунтами, а последовательная при большом количестве аккаунтов может привести к превышению времени ожидания.

Скрипт "Шаблон иерархии для крупных управляющих аккаунтов" решает эту проблему: при каждом выполнении он обрабатывает один набор аккаунтов. Промежуточные результаты сохраняются во временный файл на Google Диске, так что при последующих запусках скрипта будет известно, какие аккаунты остались необработанными. За несколько раз скрипт обработает их все, после чего начнется новый цикл (в зависимости от указанной вами частоты).

Шаблон состоит из двух разделов: STANDARD TEMPLATE позволяет обрабатывать аккаунты за несколько циклов, поэтому редактировать этот раздел не требуется, а YOUR IMPLEMENTATION дает возможность использовать несколько функций заполнения или реализовать собственную логику обработки аккаунтов. Пример исходного кода приведен ниже.

На этой странице термин выполнение относится к однократному выполнению скрипта, а цикл означает, что скрипт обрабатывает все аккаунты за несколько раз.

Настройка скрипта

Чтобы использовать шаблон:

  1. Создайте скрипт на основе шаблона.
  2. Задайте параметры на уровне шаблона.
  3. Укажите основную логику.

Создание скрипта на основе шаблона

Создайте скрипт, используя приведенный далее исходный код. В объекте TEMPLATE_CONFIG укажите имя файла (FILENAME), в который будут сохраняться промежуточные результаты на Google Диске. Скрипт создаст этот файл при первом выполнении и будет использовать его при всех последующих. Значение FILENAME может быть любым, однако мы рекомендуем использовать название скрипта – так вам будет проще опознать файл в будущем.

Значение FILENAME должно быть уникальным для всех скриптов, использующих этот шаблон в вашем аккаунте. В противном случае промежуточные данные из разных скриптов будут перезаписывать друг друга. Значение FILENAME также не должно совпадать с названием какого-либо другого файла на Google Диске.

Установка параметров на уровне шаблона

Вы можете задать несколько параметров на уровне шаблона.

  • MIN_FREQUENCY. Определяет минимальную частоту циклов. Следующий цикл не запустится до тех пор, пока с начала предыдущего не пройдет как минимум столько дней, сколько вы указали. Обратите внимание, что действительная частота может быть длиннее, если цикл занимает больше времени, чем MIN_FREQUENCY. Это может происходить из-за большого числа аккаунтов или потому, что сам скрипт выполняется слишком редко, чтобы обработать все аккаунты за указанное в MIN_FREQUENCY количество дней.
  • USE_PARALLEL_MODE. Определяет, обрабатывает ли скрипт аккаунты параллельно за один раз или последовательно за несколько раз.
  • MAX_ACCOUNTS. Указывает максимальное число аккаунтов, которые скрипт попытается обработать за одно выполнение. Параллельно может быть обработано не более 50 аккаунтов.
  • ACCOUNT_CONDITIONS (необязательно). Массив условий ManagedAccountSelector, определяющих, какие аккаунты обрабатываются скриптом. Например, вы можете добавить условие LabelNames CONTAINS "ACCOUNT_LABEL", чтобы выбрать только аккаунты с определенным ярлыком.

В разделе Планирование ниже вы найдете несколько рекомендаций, как задавать эти параметры в разных ситуациях.

Использование собственной основной логики

Этот шаблон был бы бесполезен, если бы у вас не было возможности использовать собственную логику обработки аккаунтов. Для этого предусмотрено пять функций, которые вызываются в определенные моменты во время каждого цикла, как описано ниже. Вы можете предоставить реализацию каждой из этих функций, чтобы скрипт работал именно так, как вам нужно.

Для каждой функции в шаблоне приведен пример реализации. Вам нужно будет заменить его собственной логикой. Вы также можете добавить в скрипт собственные функции и переменные.

initializeCycle(customerIds)
Вызывается однократно в начале каждого цикла со списком аккаунтов, которые будут обработаны (т. е. соответствующих ACCOUNT_CONDITIONS). Используйте эту функцию, чтобы инициализировать скрипт для всего цикла. Вот несколько примеров ее применения:
  • Отправка электронного сообщения, информирующего о начале цикла.
  • Загрузка или создание статических данных, которые должны оставаться одинаковыми на протяжении всего цикла. Вы должны сохранить эти данные, например на Диске или в таблице, чтобы их можно было загружать при каждом последующем выполнении.
Если инициализация не требуется, оставьте реализацию этого метода пустой.
initializeExecution(customerIds)
Вызывается однократно в начале каждого выполнения с набором аккаунтов, которые будут обработаны на этот раз. Используйте эту функцию, чтобы инициализировать скрипт для каждого выполнения. Вот несколько примеров ее применения:
  • Отправка электронного сообщения, информирующего о начале выполнения.
  • Загрузка статических данных, которые должны оставаться одинаковыми при каждом выполнении (см. выше).
Если инициализация не требуется, оставьте реализацию этого метода пустой.
processAccount()
Вызывается однократно за весь цикл для каждого аккаунта AdWords. Используйте эту функцию, чтобы выполнить главную логику обработки скрипта (извлечение и анализ отчетов, приостановка и возобновление кампаний, модификация ставок, создание объявлений, отправка электронного оповещения, сохранение данных в таблицу или другие задачи, для которых применяются скрипты AdWords). Эта функция также может возвращать произвольные результаты обработки аккаунта. Их можно объединить перед выводом. Например, вместо нескольких оповещений по каждому аккаунту можно отправить одно с результатами по всем аккаунтам.
processIntermediateResults(results)
Вызывается однократно в конце каждого выполнения с результатами из аккаунтов, обработанных за этот раз. Используйте эту функцию для анализа, объединения и выдачи результатов выполнения. Вот несколько примеров ее применения:
  • Добавление результатов этого выполнения в таблицу.
  • Создание и отправка электронного сообщения со сводкой всех результатов этого выполнения.
Если у вас нет результатов для вывода или если вы хотите вывести их только после завершения всего цикла, оставьте реализацию этого метода пустой.
processFinalResults(results)
Вызывается однократно в конце каждого цикла с результатами из всех аккаунтов, обработанных во время этого цикла. Используйте эту функцию для анализа, объединения и вывода результатов по всем аккаунтам из цикла. Вот несколько примеров ее применения:
  • Выполнение анализа, для которого необходимы результаты по всем аккаунтам.
  • Добавление в таблицу всех результатов одновременно.
  • Создание и отправка электронного сообщения со сводкой результатов по всем аккаунтам.
Если у вас нет результатов для вывода, оставьте реализацию этого метода пустой.

Планирование

Запланировать выполнение этого скрипта немного сложнее, чем других, так как приходится помнить о циклах. В большинстве случаев требуется, чтобы циклы завершались как можно скорее. Вот как этого добиться:

  • Настройте в AdWords ежечасное выполнение скрипта (это самая высокая частота).
  • Оцените, сколько времени занимает реализация processAccount() для одного аккаунта. Для этого можно выполнить скрипт в последовательном режиме, задав ACCOUNT_CONDITIONS таким образом, чтобы было обработано лишь несколько аккаунтов, а затем разделить полученное время выполнения на их число.
  • Задайте USE_PARALLEL_MODE и MAX_ACCOUNTS, как описано ниже.
    • В последовательном режиме скрипт может работать 30 минут (1800 секунд), прежде чем истечет время ожидания. В параллельном режиме может быть обработано до 50 аккаунтов. В каком режиме будет обработано больше аккаунтов за выполнение, зависит от того, сколько времени занимает обработка каждого аккаунта.
    • Если расчетное время обработки одного аккаунта составляет 36 секунд (1800 / 50) или меньше, предпочтительней последовательный режим, а если больше – параллельный. Чтобы учесть вариабельность времени обработки аккаунтов и надбавку на постобработку, имеет смысл ориентироваться на 30 секунд вместо 36.
    • Если используется параллельный режим, установите для MAX_ACCOUNTS значение 50. Указывать более высокое число не имеет смысла: 50 аккаунтов – допустимый предел.
    • При использовании последовательного режима в качестве значения MAX_ACCOUNTS задайте максимальное количество аккаунтов, которые можно обработать за 30 минут. Например, если, по вашей оценке, на обработку каждого аккаунта требуется 10 секунд, укажите для MAX_ACCOUNTS значение 180. В действительности лучше выбрать более низкое число, чтобы оставить время на выполнение processIntermediateResults() и processFinalResults().

Хотя вы запланировали ежечасное выполнение в AdWords, можно задать значение MIN_FREQUENCY, чтобы циклы происходили через определенный промежуток. Например, если вы хотите получать отчет по всем аккаунтам раз в неделю, создайте таблицу в функции processFinalResults() и укажите для MIN_FREQUENCY значение 7.

Принцип работы

Главная функция скрипта, main(), находится в разделе STANDARD TEMPLATE. Объект stateManager предоставляет методы для загрузки и сохранения состояния цикла при предыдущих выполнениях.

В начале каждого выполнения функция main() загружает предыдущее состояние и запускает новый цикл, если с последнего завершенного цикла прошло достаточно времени. Затем она определяет набор обрабатываемых аккаунтов и вызывает initializeCycle(), если это требуется, а также initializeExecution(). Затем с помощью функции executeByMode() обрабатывается набор аккаунтов в последовательном или параллельном режиме. В обоих случаях для каждого аккаунта из набора вызывается функция processAccount(), а результаты передаются функции completeExecution(), которая сохраняет их на Google Диск. И наконец, шаблон вызывает функцию processIntermediateResults() и, если это завершение цикла, processFinalResults.

Хотя шаблон предназначен для управляющих аккаунтов, его можно использовать и в отдельных. Это может быть полезно при разработке и отладке основной логики. Вы также можете применить к отдельному аккаунту скрипт, который раньше использовали в управляющем аккаунте, изменив только FILENAME.

Настройка

  • Создайте скрипт с приведенным ниже кодом.
  • Выполните шаги, перечисленные в разделе Настройка скрипта.

Исходный код

// Copyright 2016, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name Large Manager Hierarchy Template
 *
 * @overview The Large Manager Hierarchy Template script provides a general way
 *     to run script logic on all client accounts within a manager account
 *     hierarchy, splitting the work across multiple executions if necessary.
 *     Each execution of the script processes a subset of the hierarchy's client
 *     accounts that it hadn't previously processed, saving the results to a
 *     temporary file on Drive. Once the script processes the final subset of
 *     accounts, the consolidated results can be output and the cycle can begin
 *     again.
 *     See
 *     https://developers.google.com/adwords/scripts/docs/solutions/mccapp-manager-template
 *     for more details.
 *
 * @author AdWords Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.0
 *
 * @changelog
 * - version 1.0
 *   - Released initial version.
 */

/*************** START OF YOUR IMPLEMENTATION ***************/

var TEMPLATE_CONFIG = {
  // The name of the file that will be created on Drive to store data
  // between executions of the script. You must use a different
  // filename for each each script running in the account, or data
  // from different scripts may overwrite one another.
  FILENAME: 'UNIQUE_FILENAME_HERE',

  // The minimum number of days between the start of each cycle.
  MIN_FREQUENCY: 1,

  // Controls whether child accounts will be processed in parallel (true)
  // or sequentially (false).
  USE_PARALLEL_MODE: true,

  // Controls the maximum number of accounts that will be processed in a
  // single script execution.
  MAX_ACCOUNTS: 50,

  // A list of ManagedAccountSelector conditions to restrict the population
  // of child accounts that will be processed. Leave blank or comment out
  // to include all child accounts.
  ACCOUNT_CONDITIONS: []
};

// The possible statuses for the script as a whole or an individual account.
var Statuses = {
  NOT_STARTED: 'Not Started',
  STARTED: 'Started',
  FAILED: 'Failed',
  COMPLETE: 'Complete'
};

/**
 * Your main logic for initializing a cycle for your script.
 *
 * @param {Array.<string>} customerIds The customerIds that this cycle
 *     will process.
 */
function initializeCycle(customerIds) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply prints the accounts that will be process in
  // this cycle.
  Logger.log('Accounts to be processed this cycle:');
  for (var i = 0; i < customerIds.length; i++) {
    Logger.log(customerIds[i]);
  }
}

/**
 * Your main logic for initializing a single execution of the script.
 *
 * @param {Array.<string>} customerIds The customerIds that this
 *     execution will process.
 */
function initializeExecution(customerIds) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply prints the accounts that will be process in
  // this execution.
  Logger.log('Accounts to be processed this execution:');
  for (var i = 0; i < customerIds.length; i++) {
    Logger.log(customerIds[i]);
  }
}

/**
 * Your main logic for processing a single AdWords account. This function
 * can perform any sort of processing on the account, followed by
 * outputting results immediately (e.g., sending an email, saving to a
 * spreadsheet, etc.) and/or returning results to be output later, e.g.,
 * to be combined with the output from other accounts.
 *
 * @return {Object} An object containing any results of your processing
 *    that you want to output later.
 */
function processAccount() {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply returns the number of campaigns and ad groups
  // in the account.
  return {
    numCampaigns: AdWordsApp.campaigns().get().totalNumEntities(),
    numAdGroups: AdWordsApp.adGroups().get().totalNumEntities()
  };
}

/**
 * Your main logic for consolidating or outputting results after
 * a single execution of the script. These single execution results may
 * reflect the processing on only a subset of your accounts.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object,
 *       error: string
 *     }>} results The results for the accounts processed in this
 *    execution of the script, keyed by customerId. The status will be
 *    Statuses.COMPLETE if the account was processed successfully,
 *    Statuses.FAILED if there was an error, and Statuses.STARTED if it
 *    timed out. The returnValue field is present when the status is
 *    Statuses.COMPLETE and corresponds to the object you returned in
 *    processAccount(). The error field is present when the status is
 *    Statuses.FAILED.
 */
function processIntermediateResults(results) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply logs the number of campaigns and ad groups
  // in each of the accounts successfully processed in this execution.
  Logger.log('Results of this execution:');
  for (var customerId in results) {
    var result = results[customerId];
    if (result.status == Statuses.COMPLETE) {
      Logger.log(customerId + ': ' + result.returnValue.numCampaigns +
                 ' campaigns, ' + result.returnValue.numAdGroups +
                 ' ad groups');
    } else if (result.status == Statuses.STARTED) {
      Logger.log(customerId + ': timed out');
    } else {
      Logger.log(customerId + ': failed due to "' + result.error + '"');
    }
  }
}

/**
 * Your main logic for consolidating or outputting results after
 * the script has executed a complete cycle across all of your accounts.
 * This function will only be called once per complete cycle.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object,
 *       error: string
 *     }>} results The results for the accounts processed in this
 *    execution of the script, keyed by customerId. The status will be
 *    Statuses.COMPLETE if the account was processed successfully,
 *    Statuses.FAILED if there was an error, and Statuses.STARTED if it
 *    timed out. The returnValue field is present when the status is
 *    Statuses.COMPLETE and corresponds to the object you returned in
 *    processAccount(). The error field is present when the status is
 *    Statuses.FAILED.
 */
function processFinalResults(results) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This template simply logs the total number of campaigns and ad
  // groups across all accounts successfully processed in the cycle.
  var numCampaigns = 0;
  var numAdGroups = 0;

  Logger.log('Results of this cycle:');
  for (var customerId in results) {
    var result = results[customerId];
    if (result.status == Statuses.COMPLETE) {
      Logger.log(customerId + ': successful');
      numCampaigns += result.returnValue.numCampaigns;
      numAdGroups += result.returnValue.numAdGroups;
    } else if (result.status == Statuses.STARTED) {
      Logger.log(customerId + ': timed out');
    } else {
      Logger.log(customerId + ': failed due to "' + result.error + '"');
    }
  }

  Logger.log('Total number of campaigns: ' + numCampaigns);
  Logger.log('Total number of ad groups: ' + numAdGroups);
}

/**************** END OF YOUR IMPLEMENTATION ****************/

/**************** START OF STANDARD TEMPLATE ****************/

// Whether or not the script is running in a manager account.
var IS_MANAGER = typeof MccApp !== 'undefined';

// The maximum number of accounts that can be processed when using
// executeInParallel().
var MAX_PARALLEL = 50;

// The possible modes in which the script can execute.
var Modes = {
  SINGLE: 'Single',
  MANAGER_SEQUENTIAL: 'Manager Sequential',
  MANAGER_PARALLEL: 'Manager Parallel'
};

function main() {
  var mode = getMode();
  stateManager.loadState();

  // The last execution may have attempted the final set of accounts but
  // failed to actually complete the cycle because of a timeout in
  // processIntermediateResults(). In that case, complete the cycle now.
  if (stateManager.getAccountsWithStatus().length > 0) {
    completeCycleIfNecessary();
  }

  // If the cycle is complete and enough time has passed since the start of
  // the last cycle, reset it to begin a new cycle.
  if (stateManager.getStatus() == Statuses.COMPLETE) {
    if (dayDifference(stateManager.getLastStartTime(), new Date()) >
        TEMPLATE_CONFIG.MIN_FREQUENCY) {
      stateManager.resetState();
    } else {
      Logger.log('Waiting until ' + TEMPLATE_CONFIG.MIN_FREQUENCY +
                 ' days have elapsed since the start of the last cycle.');
      return;
    }
  }

  // Find accounts that have not yet been processed. If this is the
  // beginning of a new cycle, this will be all accounts.
  var customerIds =
      stateManager.getAccountsWithStatus(Statuses.NOT_STARTED);

  // The status will be Statuses.NOT_STARTED if this is the very first
  // execution or if the cycle was just reset. In either case, it is the
  // beginning of a new cycle.
  if (stateManager.getStatus() == Statuses.NOT_STARTED) {
    stateManager.setStatus(Statuses.STARTED);
    stateManager.saveState();

    initializeCycle(customerIds);
  }

  // Don't attempt to process more accounts than specified, and
  // enforce the limit on parallel execution if necessary.
  var accountLimit = TEMPLATE_CONFIG.MAX_ACCOUNTS;

  if (mode == Modes.MANAGER_PARALLEL) {
    accountLimit = Math.min(MAX_PARALLEL, accountLimit);
  }

  var customerIdsToProcess = customerIds.slice(0, accountLimit);

  // Save state so that we can detect when an account timed out by it still
  // being in the STARTED state.
  stateManager.setAccountsWithStatus(customerIdsToProcess, Statuses.STARTED);
  stateManager.saveState();

  initializeExecution(customerIdsToProcess);
  executeByMode(mode, customerIdsToProcess);
}

/**
 * Runs the script on a list of accounts in a given mode.
 *
 * @param {string} mode The mode the script should run in.
 * @param {Array.<string>} customerIds The customerIds that this execution
 *     should process. If mode is Modes.SINGLE, customerIds must contain
 *     a single element which is the customerId of the AdWords account.
 */
function executeByMode(mode, customerIds) {
  switch (mode) {
    case Modes.SINGLE:
      var results = {};
      results[customerIds[0]] = tryProcessAccount();
      completeExecution(results);
      break;

    case Modes.MANAGER_SEQUENTIAL:
      var accounts = MccApp.accounts().withIds(customerIds).get();
      var results = {};

      var managerAccount = AdWordsApp.currentAccount();
      while (accounts.hasNext()) {
        var account = accounts.next();
        MccApp.select(account);
        results[account.getCustomerId()] = tryProcessAccount();
      }
      MccApp.select(managerAccount);

      completeExecution(results);
      break;

    case Modes.MANAGER_PARALLEL:
      if (customerIds.length == 0) {
        completeExecution({});
      } else {
        var accountSelector = MccApp.accounts().withIds(customerIds);
        accountSelector.executeInParallel('parallelFunction',
                                          'parallelCallback');
      }
      break;
  }
}

/**
 * Attempts to process the current AdWords account.
 *
 * @return {Object} The result of the processing if successful, or
 *     an object with status Statuses.FAILED and the error message
 *     if unsuccessful.
 */
function tryProcessAccount() {
  try {
    return {
      status: Statuses.COMPLETE,
      returnValue: processAccount()
    };
  } catch (e) {
    return {
      status: Statuses.FAILED,
      error: e.message
    };
  }
}

/**
 * The function given to executeInParallel() when running in parallel mode.
 * This helper function is necessary so that the return value of
 * processAccount() is transformed into a string as required by
 * executeInParallel().
 *
 * @return {string} JSON string representing the return value of
 *     processAccount().
 */
function parallelFunction() {
  var returnValue = processAccount();
  return JSON.stringify(returnValue);
}

/**
 * The callback given to executeInParallel() when running in parallel mode.
 * Processes the execution results into the format used by all execution
 * modes.
 *
 * @param {Array.<Object>} executionResults An array of execution results
 *     from a parallel execution.
 */
function parallelCallback(executionResults) {
  var results = {};

  for (var i = 0; i < executionResults.length; i++) {
    var executionResult = executionResults[i];
    var status;

    if (executionResult.getStatus() == 'OK') {
      status = Statuses.COMPLETE;
    } else if (executionResult.getStatus() == 'TIMEOUT') {
      status = Statuses.STARTED;
    } else {
      status = Statuses.FAILED;
    }

    results[executionResult.getCustomerId()] = {
      status: status,
      returnValue: JSON.parse(executionResult.getReturnValue()),
      error: executionResult.getError()
    };
  }

  // After executeInParallel(), variables in global scope are reevaluated,
  // so reload the state.
  stateManager.loadState();

  completeExecution(results);
}

/**
 * Completes a single execution of the script by saving the results and
 * calling the intermediate and final result handlers as necessary.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object,
 *       error: string
 *     }>} results The results of the current execution of the script.
 */
function completeExecution(results) {
  for (var customerId in results) {
    var result = results[customerId];
    stateManager.setAccountWithResult(customerId, result);
  }
  stateManager.saveState();

  processIntermediateResults(results);
  completeCycleIfNecessary();
}

/**
 * Completes a full cycle of the script if all accounts have been attempted
 * but the cycle has not been marked as complete yet.
 */
function completeCycleIfNecessary() {
  if (stateManager.getAccountsWithStatus(Statuses.NOT_STARTED).length == 0 &&
      stateManager.getStatus() != Statuses.COMPLETE) {
    stateManager.setStatus(Statuses.COMPLETE);
    stateManager.saveState();
    processFinalResults(stateManager.getResults());
  }
}

/**
 * Determines what mode the script should run in.
 *
 * @return {string} The mode to run in.
 */
function getMode() {
  if (IS_MANAGER) {
    if (TEMPLATE_CONFIG.USE_PARALLEL_MODE) {
      return Modes.MANAGER_PARALLEL;
    } else {
      return Modes.MANAGER_SEQUENTIAL;
    }
  } else {
    return Modes.SINGLE;
  }
}

/**
 * Finds all customer IDs that the script could process. For a single account,
 * this is simply the account itself.
 *
 * @return {Array.<string>} A list of customer IDs.
 */
function getCustomerIdsPopulation() {
  if (IS_MANAGER) {
    var customerIds = [];

    var selector = MccApp.accounts();
    var conditions = TEMPLATE_CONFIG.ACCOUNT_CONDITIONS || [];
    for (var i = 0; i < conditions.length; i++) {
      selector = selector.withCondition(conditions[i]);
    }

    var accounts = selector.get();
    while (accounts.hasNext()) {
      customerIds.push(accounts.next().getCustomerId());
    }

    return customerIds;
  } else {
    return [AdWordsApp.currentAccount().getCustomerId()];
  }
}

/**
 * Returns the number of days between two dates.
 *
 * @param {Object} from The older Date object.
 * @param {Object} to The newer (more recent) Date object.
 * @return {number} The number of days between the given dates (possibly
 *     fractional).
 */
function dayDifference(from, to) {
  return (to.getTime() - from.getTime()) / (24 * 3600 * 1000);
}

/**
 * Loads a JavaScript object previously saved as JSON to a file on Drive.
 *
 * @param {string} filename The name of the file in the account's root Drive
 *     folder where the object was previously saved.
 * @return {Object} The JavaScript object, or null if the file was not found.
 */
function loadObject(filename) {
  var files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    return null;
  } else {
    var file = files.next();

    if (files.hasNext()) {
      throwDuplicateFileException(filename);
    }

    return JSON.parse(file.getBlob().getDataAsString());
  }
}

/**
 * Saves a JavaScript object as JSON to a file on Drive. An existing file with
 * the same name is overwritten.
 *
 * @param {string} filename The name of the file in the account's root Drive
 *     folder where the object should be saved.
 * @param {obj} obj The object to save.
 */
function saveObject(filename, obj) {
  var files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    DriveApp.createFile(filename, JSON.stringify(obj));
  } else {
    var file = files.next();

    if (files.hasNext()) {
      throwDuplicateFileException(filename);
    }

    file.setContent(JSON.stringify(obj));
  }
}

/**
 * Throws an exception if there are multiple files with the same name.
 *
 * @param {string} filename The filename that caused the error.
 */
function throwDuplicateFileException(filename) {
  throw 'Multiple files named ' + filename + ' detected. Please ensure ' +
      'there is only one file named ' + filename + ' and try again.';
}

var stateManager = (function() {
  /**
   * @type {{
   *   cycle: {
   *     status: string,
   *     lastUpdate: string,
   *     startTime: string
   *   },
   *   accounts: Object.<string, {
   *     status: string,
   *     lastUpdate: string,
   *     returnValue: Object
   *   }>
   * }}
   */
  var state;

  /**
   * Loads the saved state of the script. If there is no previously
   * saved state, sets the state to an initial default.
   */
  var loadState = function() {
    state = loadObject(TEMPLATE_CONFIG.FILENAME);
    if (!state) {
      resetState();
    }
  };

  /**
   * Saves the state of the script to Drive.
   */
  var saveState = function() {
    saveObject(TEMPLATE_CONFIG.FILENAME, state);
  };

  /**
   * Resets the state to an initial default.
   */
  var resetState = function() {
    state = {};
    var date = Date();

    state.cycle = {
      status: Statuses.NOT_STARTED,
      lastUpdate: date,
      startTime: date
    };

    state.accounts = {};
    var customerIds = getCustomerIdsPopulation();

    for (var i = 0; i < customerIds.length; i++) {
      state.accounts[customerIds[i]] = {
        status: Statuses.NOT_STARTED,
        lastUpdate: date
      };
    }
  };

  /**
   * Gets the status of the current cycle.
   *
   * @return {string} The status of the current cycle.
   */
  var getStatus = function() {
    return state.cycle.status;
  };

  /**
   * Sets the status of the current cycle.
   *
   * @param {string} status The status of the current cycle.
   */
  var setStatus = function(status) {
    var date = Date();

    if (status == Statuses.IN_PROGRESS &&
        state.cycle.status == Statuses.NOT_STARTED) {
      state.cycle.startTime = date;
    }

    state.cycle.status = status;
    state.cycle.lastUpdate = date;
  };

  /**
   * Gets the start time of the current cycle.
   *
   * @return {Object} Date object for the start of the last cycle.
   */
  var getLastStartTime = function() {
    return new Date(state.cycle.startTime);
  };

  /**
   * Gets accounts in the current cycle with a particular status.
   *
   * @param {string} status The status of the accounts to get.
   *     If null, all accounts are retrieved.
   * @return {Array.<string>} A list of matching customerIds.
   */
  var getAccountsWithStatus = function(status) {
    var customerIds = [];

    for (var customerId in state.accounts) {
      if (!status || state.accounts[customerId].status == status) {
        customerIds.push(customerId);
      }
    }

    return customerIds;
  };

  /**
   * Sets accounts in the current cycle with a particular status.
   *
   * @param {Array.<string>} customerIds A list of customerIds.
   * @param {string} status A status to apply to those customerIds.
   */
  var setAccountsWithStatus = function(customerIds, status) {
    var date = Date();

    for (var i = 0; i < customerIds.length; i++) {
      var customerId = customerIds[i];

      if (state.accounts[customerId]) {
        state.accounts[customerId].status = status;
        state.accounts[customerId].lastUpdate = date;
      }
    }
  };

  /**
   * Registers the processing of a particular account with a result.
   *
   * @param {string} customerId The account that was processed.
   * @param {{
   *       status: string,
   *       returnValue: Object
   *       error: string
   *     }} result The object to save for that account.
   */
  var setAccountWithResult = function(customerId, result) {
    if (state.accounts[customerId]) {
      state.accounts[customerId].status = result.status;
      state.accounts[customerId].returnValue = result.returnValue;
      state.accounts[customerId].error = result.error;
      state.accounts[customerId].lastUpdate = Date();
    }
  };

  /**
   * Gets the current results of the cycle for all accounts.
   *
   * @return {Object.<string, {
   *       status: string,
   *       lastUpdate: string,
   *       returnValue: Object,
   *       error: string
   *     }>} The results processed by the script during the cycle,
   *    keyed by account.
   */
  var getResults = function() {
    return state.accounts;
  };

  return {
    loadState: loadState,
    saveState: saveState,
    resetState: resetState,
    getStatus: getStatus,
    setStatus: setStatus,
    getLastStartTime: getLastStartTime,
    getAccountsWithStatus: getAccountsWithStatus,
    setAccountsWithStatus: setAccountsWithStatus,
    setAccountWithResult: setAccountWithResult,
    getResults: getResults
  };
})();

/***************** END OF STANDARD TEMPLATE *****************/

Оставить отзыв о...

Текущей странице
Скрипты AdWords
Скрипты AdWords