Інтеграція GoogleDocs з Redmine

    

Введення

 Якщо ви зав'язані в розробці, то так чи інакше стикалися з баг-трекерна системами. У наші дні обійтися без них в процесі розробки програмного забезпечення не просто важко, а неможливо. Природно, і нас це не обійшло стороною. У компанії ми користуємося системою Redmine. Тут є все, що нам необхідно:
 - Відстеження стану завдань
 - Групування завдань у трекері
 - Внутріпроектное обговорення при необхідності
 - Ведення документації (хоч і можливості досить обмежені)
 - Облік часу співробітників і видів їх діяльності
 
Всі ці дані збираються не просто так. Кожна з перелічених складових так чи інакше включені у внутрішні метрики компанії, які дозволяють оцінювати ефективність виробничого процесу і аналізувати слабкі місця проектів, щоб не повторювати помилок і наступного разу зробити краще.
 
 

Завдання

 Очевидно, що збір даних повинен проводитися постійно, щоб можна було оперативно відреагувати на негативні зміни і спробувати спрямувати зусилля в правильне русло. Однак, за великого обсягу даних, з якими доводиться працювати, ручна обробка стає проблематичною. Крім цього, необхідно забезпечити доступ співробітників до такого документа, щоб кожен міг зайти і оцінити ситуацію на проекті самостійно.
 
З цієї проблеми і випливає завдання — автоматизувати збір даних з баг-трекерної системи в більш зручні для читання і візуалізації джерела.
 
 

Підбір рішення

 Як зручного джерела ідеально підходить електронна таблиця — дані в ній можна добре структурувати, щоб вони були читані, а так само є можливість скористатися функціями візуалізації масивів даних (графіки, діаграми і т.п.).
 
У наборі GoogleDocs є електронні таблиці, з якими можна працювати з будь-якої точки світу, якщо у вас є інтернет, а так само управляти правами доступу до них. Так само GoogleDocs надають можливість користуватися сервісом GoogleScript для обробки даних як усередині, так і поза документа. Доступ та управління зовнішніми ресурсами здійснюється через API відповідного сервісу.
 
Redmine надає можливість отримати дані за допомогою свого API .
 
Таке поєднання ідеально підходить, тому що вирішує поставлені завдання.
 
 

З чого почати?

 Для початку необхідно підключитися з таблиці до API Redmine. Для цього проробляємо ряд нехитрих дій:
 1. Створюємо новий скрипт всередині нашої таблиці (Інструменти -> Редактор скриптів ...)
 image
  
2. У вікні зі сккріптом потрібно прибрати створену функцію — ми будемо писати свої.
 
Для початку нам потрібно наступна інформація: посилання, по якому доступний Redmine і API-key. З першим проблем не повинно виникнути — якщо ви користуєтеся баг-трекером, то точно знаєте за якою адресою доступна стартова сторінка (у нас, наприклад, це redmine.greensight.ru ). З ключем трохи складніше. Дістати його можна, відправивши небудь запит до API. Відкриваємо FireFox, і намагаємося, наприклад, отримати список проектів. Для цього в адресному рядку вводимо <посилання на редмайн> / projects.xml:
 image
 
Вводимо логін і пароль користувача, зареєстрованого в системі. Далі в панелі розробника переходимо у вкладку "Мережа" підрозділ "Заголовки запиту". Те, що написано в полі "Authorization" після слова "Basic" і буде потрібним нам ключем.
 image
 
Тепер спробуємо отримати те ж саме за допомогою GoogleScript. Повертаємося в створений нами скрипт в таблиці і пишемо наступний код:
 
 
var baseUrl = "<ссылка в Redmine>";
var key = "<API-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);
}

 Тут необхідно замінити <посилання в Redmine> і <API-key> на те, що ви отримали вище. Тепер трохи детальніше про те, що ми зробили.
 
Мінлива 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). Подивимося що з себе представляє цей файл:
 
<time_entries type="array" total_count="11" limit="25" offset="0">
	<time_entry>
		<id>11510</id>
		<project name="Тестовый проект" id="150"/>
		<issue id="7666"/>
		<user name="Чудесный Разработчик" id="62"/>
		<activity name="Разработка" id="9"/>
		<hours>1.5</hours>
		<comments/>
		<spent_on>2014-06-10</spent_on>
		<created_on>2014-06-09T20:23:34Z</created_on>
		<updated_on>2014-06-09T20:23:34Z</updated_on>
	</time_entry>
	<time_entry>
		<id>11520</id>
		<project name="Боевой проект" id="87"/>
		<issue id="7484"/>
		<user name="Отличный Верстальщик" id="23"/>
		<activity name="Верстка" id="9"/>
		<hours>7.5</hours>
		<comments/>
		<spent_on>2014-06-10</spent_on>
		<created_on>2014-06-10T07:57:09Z</created_on>
		<updated_on>2014-06-10T07:57:09Z</updated_on>
	</time_entry>

 Батьківський вузол показує інформацію, яка нам так само потрібно при обробці інформації: total_count — скільки знайдено записів по даному запиту, limit — кількість записів на сторінці, offset — відступ від початку списку. Параметри limit і offset можна так само регулювати в самому запиті. Наприклад, варто зробити limit побільше, щоб було менше запитів при зборі даних.
 
Як ми бачимо, файл має дуже чіткою структурою. Приступимо до його обробці. Припустимо, нам потрібно написати функцію, яка збирає весь час, списане за сьогодні на певному проекті і порахувати скільки було витрачено на управління та розробку:
 
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<response.getChildren('time_entry').length; i++) {
      var obj = new Array();
    obj.push(response.getChildren('time_entry')[i].getChild('activity').getAttribute('name').getValue());
      obj.push(response.getChildren('time_entry')[i].getChild('hours').getText());
      time.push(obj);
    }
    offset += 100;
  } while (response.getChildren('time_entry').length != 0);
  
  var manage = 0;
  var dev = 0;
    
  for (i=0; i<time.length; i++) {
    if (time[i][0] == 'Управление') {
      manage += +time[i][1];
    }
    else {
      dev += +time[i][1];
    }
  }
  return (manage/(manage+dev));
}

 Всередині циклу ми щоразу дістаємо інформацію про час, списаному у проекті з id = id (подивитися id проекту можна так само через API запитом до projects.xml), витраченим під час params. Кожен запит до Redmine отримує 100 записів і, якщо цього недостатньо, рухаємося на offset.
 
Далі, в циклі, ми виконуємо response.getChildren ('time_entry') [i]. GetChild ('activity'). GetAttribute ('name'). GetValue (). Ця команда отримує назву активності з кожного запису списаного часу:
 
 
Таким чином ми можемо отримати будь-яке ім'я параметра будь-якої вкладеності будь-якого з дітей кореневого елемента 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<data.length; k++) {
    value = 0;
    
    if ((data[k][2].toLowerCase()=='активный')&&(data[k][1].toString()!='')) {
      value = getTimes(data[k][1], data[3][9]);
      SpreadsheetApp.getActive().setActiveSelection("K" + (k+1)).setValue(value);
    }
  }
}

 Щоб зрозуміти це необхідно трохи пояснень. Список проектів у нас зберігається на аркуші з назвою "Project". Там же в різних колонках записуються метрики. Часовий проміжок, за яким фільтрується час записаний так само в окремій комірці. У змінну data зчитується вся інформація з таблиці. Цикл далі знаходить всі проекти в статусі "Активний" з присутнім id проекту з Redmine, викликає функцію підрахунку певної метрики для цього проета і записує результат в стовпець K (в даному випадку).
 
Єдине, що тепер залишається — це виконати наш скрипт. Для цього необхідно вибрати пункт меню Виконати-> <Ім'я функції>. Так само можна налаштувати виконання необхідних функцій з меню документа або по динамічному триггеру на періодичний запуск.
 
Ось такий от спосіб автоматизувати зняття метрик для проекту. Користуючись можливостями API Redmine, можна отримувати в автоматичному режимі як завгодно складні дані і не витрачати час на ручний розрахунок.
 
 

Граблі

 Природно, в процесі виконання завдання довелося зіткнутися з деякими неочевидними проблемами. Я постараюся привести основні.
 
1. Обмеження на виведення кількості записів за запитом — не більше 100 штук. Навіть якщо ви прописуєте у запиті limit = 500, виведеться все одно не більше 100.
 
2. Для того, щоб вивести завдання в усіх статусах, потрібно додавати в запит "status_id = *", інакше у вибірку не потраплятимуть завдання в статусах "Закрита", "Скасована" і т.п.
 
3. Можна спробувати зробити користувача функцію і потім вставляти її в комірку таблиці так само, як ми користуємося будь-якими іншими формулами в таблиці (наприклад, "= gettimes (id, params)"). Цей спосіб буде давати збій при великій кількості даних і замість того, щоб побачити дані по проектах, ви
будете бачити в осередках ERROR.
 
4. Не рекомендовано виводити дані через Logger в консоль. Виникає схожа проблема з п.3 — не до кінця виконується скрипт і в результаті ми маємо не повну картину.
 
5. Дуже уважно стежте за даними, які можуть спливти (в основному, це в призначених для користувача полях) — якщо воно буде відсутня, то спроба читання такого поля або властивості в записі через getChild () призведе до видачі об'єкта null і подальша робота з ним буде неможлива.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.