Интеграция GoogleDocs с Redmine
Введение Если вы завязаны в разработке, то так или иначе сталкивались с баг-трекерными системами. В наши дни обойтись без них в процессе разработки программного обеспечения не просто трудно, а невозможно. Естественно, и нас это не обошло стороной. В компании мы пользуемся системой Redmine. Тут есть все, что нам необходимо: — Отслеживание состояния задач— Группировка задач в трекере— Внутрипроектное обсуждение при необходимости— Ведение документации (хоть и возможности весьма ограничены)— Учет времени сотрудников и видов их деятельностиВсе эти данные собираются не просто так. Каждая из перечисленных составляющих так или иначе включены во внутренние метрики компании, которые позволяют оценивать эффективность производственного процесса и анализировать слабые места проектов, чтобы не повторять ошибок и в следующий раз сделать лучше.
Задача Очевидно, что сбор данных должен производиться постоянно, чтобы можно было оперативно отреагировать на негативные изменения и попытаться направить усилия в правильное русло. Однако, из-за большого объема данных, с которыми приходится работать, ручная обработка становится проблематичной. Помимо этого, необходимо обеспечить доступ сотрудников к такому документу, чтобы каждый мог зайти и оценить ситуацию на проекте самостоятельно.Из этой проблемы и вытекает задача — автоматизировать сбор данных из баг-трекерной системы в более удобные для чтения и визуализации источники.
Подбор решения В качестве удобного источника идеально подходит электронная таблица — данные в ней можно хорошо структурировать, чтобы они были читаемы, а так же есть возможность воспользоваться функциями визуализации массивов данных (графики, диаграммы и т.п.).В наборе GoogleDocs есть электронные таблицы, с которыми можно работать из любой точки мира, если у вас есть интернет, а так же управлять правами доступа к ним. Так же GoogleDocs предоставляют возможность пользоваться сервисом GoogleScript для обработки данных как внутри, так и вне документа. Доступ и управление внешними ресурсами осуществляется через API соответствующего сервиса.
Redmine предоставляет возможность получить данные с помощью своего API.
Такое сочетание идеально подходит, потому что решает поставленные задачи.
С чего начать? Для начала необходимо подключиться из таблицы к API Redmine. Для этого проделываем ряд нехитрых действий:1. Создаем новый скрипт внутри нашей таблицы (Инструменты → Редактор скриптов…)2. В открывшемся окне со сккриптом нужно убрать созданную функцию — мы будем писать свои.
Для начала нам потребуется следующая информация: ссылка, по которой доступен Redmine и API-key. С первым проблем не должно возникнуть — если вы пользуетесь баг-трекером, то точно знаете по какому адресу доступна стартовая страница (у нас, например, это redmine.greensight.ru). С ключом чуть сложнее. Достать его можно, отправив какой-нибудь запрос к API. Открываем FireFox, и пытаемся, например, получить список проектов. Для этого в адресной строке вводим <ссылка на редмайн>/projects.xml:
Вводим логин и пароль пользователя, зарегистрированного в системе. Далее в панели разработчика переходим во вкладку «Сеть» подраздел «Заголовки запроса». То, что написано в поле «Authorization» после слова «Basic» и будет нужным нам ключом.
Теперь попробуем получить то же самое с помощью GoogleScript. Возвращаемся в созданный нами скрипт в таблице и пишем следующий код:
var baseUrl = »<ссылка в Redmine>»;
var key = »
function APIRequest (reqUrl) { var url = encodeURI (baseUrl + reqUrl); var payload = { 'Authorization' : 'Basic ' + key}; var opt = { 'method' : 'GET', 'headers' : payload, 'muteHttpExceptions' : true }; var response = UrlFetchApp.fetch (url, opt); return XmlService.parse (response.getContentText ()).getRootElement (); }
function getRedmineProjects () { var response = APIRequest («projects.xml»); Logger.log (response); } Здесь необходимо заменить и на то, что вы получили выше. Теперь немного поподробнее о том, что мы сделали.Переменная baseURL хранит в себе ссылку на главную страницу Redmine. Она нужна, чтобы в дальнейшем лишний раз не прописывать ее в функциях вызова методов API. Соответственно, переменная key нужна для того, чтобы Redmine предоставлял данные в ответ на запрос (подробнее можно почитать любую статью по Basic Authorization). Функция APIRequest осуществляет запрос к Redmine и возвращает ответ от него. opt — это объект, который хранит в себе параметры обращения к API — метод GET, заголовки, содержащие в себе данные о пользователе, который осуществляет запрос, игнорирование исключений.
UrlFetchApp — это класс GoogleScript, позволяющий работать с запросами к внешним сервисам. В данном случае мы пользуемся методом fetch, в который передаем запрос и его параметры.
Redmine умеет возвращать ответы на запросы в двух различных формах — xml и JSON. в данном варианте я пользовался xml-представлением ответа, поэтому в функции возврата ответа на запрос находится XmlService.parse (). Функция getRedmineProjects получает список проектов и выводит их в лог исполнения скрипта (чтобы выполнить скрипт нужно выбрать в меню Выполнить → getRedmineProjects, а чтобы посмотреть результаты нажать Ctrl+Enter).
Собираем данные
Для сбора данных осталось только обратиться к результатам выполнения функции APIRequest и правильно взять из них данные. Я покажу на примере одного xml-файла как снимать данные. Механизм сбора для остальных аналогичный, а список доступных файлов описан по ссылке выше. Пусть это будет одна из метрик, которая будет полезна всем — списанное время (эта метрика взята для примера, чтобы показать как доставать данные, у нас, конечно же, более сложные метрики на проектах). Для этого будем брать данные из файла time_entries.xml. Их можно отфильтровать средствами Redmine, например, параметр spent_on отвечает за время, списанное в определенную дату или промежуток, а project_id — за время, списанное в определенный проект (более подробное описание есть в документации к API). Посмотрим что из себя представляет этот файл:
function getTimes (id, params) {
var time = new Array ();
var offset = 0;
do {
var response = APIRequest («time_entries.xml? project_id=» + id + »&spent_on=><" + params + "&offset="+offset + "&limit=100");
for (i=0; i
Таким образом мы можем получить любое имя параметра любой вложенности любого из детей корневого элемента xml-файла.
Для получения самих значений используем response.getChildren ('time_entry')[i].getChild ('hours').getText (). По аналогии с командой выше мы получаем списанное время:
Дальнейшие действия функции направлены на подсчет времени управления и времени разработки по проекту.
Теперь нам осталось только организовать вывод посчитанных показателей. Для этого необходимо написать небольшую функцию, которая вставляет полученное значение в нужные ячейки нашей таблицы:
function getAllTimes () {
var value;
SpreadsheetApp.getActive ().setActiveSheet (SpreadsheetApp.getActive ().getSheetByName ('Project'));
var data = SpreadsheetApp.getActive ().getDataRange ().getValues ();
for (k=0; k
Вот такой вот способ автоматизировать снятие метрик для проекта. Пользуясь возможностями API Redmine, можно получать в автоматическом режиме сколь угодно сложные данные и не тратить время на ручной расчет.
Грабли Естественно, в процессе выполнения задачи пришлось столкнуться с некоторыми неочевидными проблемами. Я постараюсь привести основные.1. Ограничение на вывод количества записей по запросу — не более 100 штук. Даже если вы прописываете в запросе limit=500, выведется все равно не более 100.
2. Для того, чтобы вывести задачи во всех статусах, нужно добавлять в запрос «status_id=*», иначе в выборку не будут попадать задачи в статусах «Закрыта», «Отменена» и т.п.
3. Можно попытаться сделать пользовательскую функцию и потом вставлять ее в ячейку таблицы так же, как мы пользуемся любыми другими формулами в таблице (например,»=gettimes (id, params)»). Этот способ будет давать сбой при большом количестве данных и вместо того, чтобы увидеть данные по проектам, выбудете видеть в ячейках ERROR.
4. Не рекомендуется выводить данные через Logger в консоль. Возникает похожая проблема с п.3 — не до конца выполняется скрипт и в итоге мы имеем не полную картину.
5. Очень внимательно следите за данными, которые могут всплыть (в основном, это в пользовательских полях) — если оно будет отсутствовать, то попытка чтения такого поля или свойства в записи через getChild () приведет к выдачи объекта null и дальнейшая работа с ним будет невозможна.