Применимость |
Это руководство относится к Jira 7.1.0 и более поздним. |
Уровень опыта |
Это расширенный учебник. Вы должны были пройти хотя бы один промежуточный учебник, прежде чем работать с этим учебником. См. список учебников в DAC. |
Оценка времени |
Для завершения этого урока вам потребуется около 1 часа. |
Обзор учебника
В этом учебном пособии показано, как создавать пользовательские отчеты Jira. В этом уроке вы добавите два отчета:
- Расширенная группа с одним уровнем по отчету .
- Отчет о создании.
Расширенная группа с одним уровнем по отчету основывается на существующем отчете в Jira. Существующий отчет выглядит следующим образом.
РИСУНОК
Когда вы закончите, у вас появится новый отчет, похожий на этот.
РИСУНОК
Обратите внимание, что поля Assignee и Update появятся на выходе.
Отчет о создании отображает гистограмму задач, созданных в течение определенного времени и разбитых на определенные промежутки времени.
Ваше завершенное приложение будет состоять из следующих компонентов:
- Java-классы, инкапсулирующие логику приложения.
- Ресурсы для отображения пользовательского интерфейса приложения (UI).
- Дескриптор приложения для представления пользовательского интерфейса приложения в Jira.
Когда вы закончите, все эти компоненты будут упакованы в один JAR-файл.
Об этих инструкциях
Вы можете использовать любую поддерживаемую комбинацию операционной системы и IDE для создания этого приложения. Эти инструкции были написаны с использованием IntelliJ IDEA 2017.3 на macOS Sierra. Если вы используете другую операционную систему или комбинацию IDE, вы должны использовать эквивалентные операции для своей конкретной среды.
Этот учебник был последний раз проверен с помощью Jira 7.7.1 с использованием Atlassian SDK 6.3.10.
Прежде чем вы начнете
Чтобы завершить этот учебник, вам необходимо знать следующее:
- Основы разработки Java: классы, интерфейсы, методы, использование компилятора и т. д.
- Как создать проект плагина Atlassian с помощью Atlassian Plugin SDK.
- Основы использования и управления Jira.
- Этот учебник также включает в себя создание шаблонов Apache Velocity. Чтобы расширить код учебника, вы должны хорошо разбираться в том, как работают шаблоны Velocity
Источник приложения
Мы рекомендуем вам проработать этот учебник. Если вы хотите пропустить или проверить свою работу, когда закончите, вы можете найти исходный код приложения на Atlassian Bitbucket.
Чтобы клонировать репозиторий, выполните следующую команду:
git clone https://atlassian_tutorial@bitbucket.org/atlassian_tutorial/jira-report-plugin.git
Кроме того, вы можете скачать исходный код, используя страницу «Загрузки» здесь: bitbucket.org/atlassian_tutorial/jira-report-plugin
Шаг 1. Создайте проект приложения
На этом этапе вы будете использовать Atlassian Plugin SDK для создания строительных лесов для вашего проекта приложения. Atlassian Plugin SDK автоматизирует большую часть разработки приложений для вас. Он включает команды для создания приложения и добавления модулей в приложение.
- Настройте SDK Atlassian Plugin и создайте проект, если вы еще этого не сделали.
- Перейдите в каталог, в котором вы хотите сохранить проект приложения, и выполните следующую команду SDK:
atlas-create-jira-plugin
- Чтобы определить ваше приложение, введите следующую информацию.
group-id |
com.atlassian.plugins.tutorial.jira |
artifact-id |
jira-report-plugin |
version |
1.0-SNAPSHOT |
package |
com.atlassian.plugins.tutorial.jira |
- Подтвердите свои записи при появлении запроса.
SDK завершает работу и создает для вас каталог с исходными файлами проекта, включая POM (файл определения объектной модели проекта), исходный код заглушки и ресурсы.
- Перейдите в каталог, созданный с помощью SDK.
cd jira-report-plugin
- Удалите тестовые каталоги.
Настройка тестирования для вашего приложения не входит в этот учебник. Для удаления сгенерированного тестового скелета выполните следующие команды:
rm -rf ./src/test/java
rm -rf ./src/test/resources/
- Удалите ненужные файлы классов Java.
rm -rf ./src/main/java/com/atlassian/plugins/tutorial/jira/*
Шаг 2. Просмотрите и настройте сгенерированный код заглушки
Это хорошая идея, чтобы ознакомиться с файлом конфигурации проекта (то есть pom.xml) и файлами ресурсов. В этом разделе вы просмотрите и настроите файл pom.xml и файл дескриптора приложения.
Откройте проект приложения в своей избранной среде IDE и следуйте инструкциям в следующих разделах.
Добавить метаданные приложения к POM
POM находится в корне вашего проекта и объявляет зависимости проекта и другую информацию. На этом этапе вы добавляете в файл метаданные о своем приложении и вашей компании или организации.
- В корневой папке приложения откройте файл pom.xml.
- Добавьте название организации или организации и URL-адрес веб-сайта в элемент организации.
<organization>
<name>Example Company</name>
<url>http://www.example.com/</url>
</organization>
- Обновите элемент description проекта следующим образом:
<description> Расширяет отчеты о задачах Jira. </ description>
- Удалите комментирование вокруг элемента dependency для артефакта jira-core, это будет включать зависимость в вашем проекте. Этот учебник расширяет существующий отчет Jira, который опирается на API в основном ядре пакета Jira. Таким образом, в то время как вам обычно не нужно это делать, вы делаете это упражнение.
- Сохраните и закройте файл.
Просмотрите созданный дескриптор приложения
Ваш код заглушки содержит файл дескриптора приложения atlassian-plugin.xml. Это XML-файл, который идентифицирует приложение для хост-приложения (Jira) и определяет требуемые функциональные возможности приложения.
В директории src / main / resources вашего дома проекта откройте файл дескриптора.
Вы должны увидеть что-то вроде этого (комментарии удалены):
<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2">
<plugin-info>
<description>${project.description}</description>
<version>${project.version}</version>
<vendor name="${project.organization.name}" url="${project.organization.url}" />
<param name="plugin-icon">images/pluginIcon.png</param>
<param name="plugin-logo">images/pluginLogo.png</param>
</plugin-info>
<resource type="i18n" name="i18n" location="jira-report-plugin"/>
<web-resource key="jira-report-plugin-resources" name="jira-report-plugin Web Resources">
<dependency>com.atlassian.auiplugin:ajs</dependency>
<resource type="download" name="jira-report-plugin.css" location="/css/jira-report-plugin.css"/>
<resource type="download" name="jira-report-plugin.js" location="/js/jira-report-plugin.js"/>
<resource type="download" name="images/" location="/images"/>
<context>jira-report-plugin</context>
</web-resource>
</atlassian-plugin>
Шаг 3. Добавьте модули плагина
Теперь вы будете использовать генератор модуля плагинов (другая команда atlas) для создания кода-заглушки для модулей приложения. Для ваших модулей добавьте два модуля отчетов следующим образом:
- Откройте терминал и перейдите в корневую папку приложения, где находится pom.xml.
- Запустите команду atlas-create-jira-plugin-module.
- Введите номер для модуля «Отчет».
- При появлении запроса введите следующее.
Enter New Classname Введите новое имя класса |
SingleLevelGroupByReportExtended |
Package Name Имя пакета |
com.atlassian.plugins.tutorial.jira.reports |
- Выберите «N» для «Показать расширенную настройку».
- Выберите «Y» для добавить другой модуль плагина.
- Введите номер модуля Report еще раз.
- Введите следующее.
Enter New Classname Введите новое имя класса |
CreationReport |
Package Name Имя пакета |
com.atlassian.plugins.tutorial.jira.reports |
- Выберите «N» для «Показать расширенную настройку».
- Выберите «N» для «Добавить другой модуль плагина».
- Подтвердите выбор.
SDK генерирует файлы кода для модулей и добавляет их в дескриптор приложения. Он также добавляет другие ресурсы, такие как файлы Velocity и файлы ресурсов i18n.
Шаг 4. Добавление свойств модуля
Свойства модуля отчета - это настраиваемые поля, которые приложение предоставляет в интерфейсе Jira. Мы добавим несколько свойств в определение модуля.
- Перейдите в src / main / resources и откройте файл atlassian-plugin.xml.
- В модуле Single Level Group By Report Extended раскомментируйте элемент properties и замените его элементы свойств по умолчанию, которые он содержит:
<report name="Single Level Group By Report Extended"...
...
<properties>
<property>
<key>filterid</key>
<name>report.singlelevelgroupby.filterId</name>
<description>report.singlelevelgroupby.filterId.description</description>
<type>filterpicker</type>
<i18n>false</i18n>
</property>
<property>
<key>mapper</key>
<name>report.singlelevelgroupby.mapper</name>
<description>report.singlelevelgroupby.mapper.description</description>
<type>select</type>
<values class="com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator" />
</property>
</properties>
Мы добавляем два новых свойства: сборщик фильтров и селектор для типа статистики, с помощью которого можно группировать результат.
- В модуле Report Creation раскомментируйте и замените элемент properties следующим:
<report name="Creation Report"...
...
<properties>
<property>
<key>projectid</key>
<name>report.issuecreation.projectid.name</name>
<description>report.issuecreation.projectid.description</description>
<type>filterprojectpicker</type>
</property>
<property>
<key>startDate</key>
<name>report.issuecreation.startdate</name>
<description>report.issuecreation.startdate.description</description>
<type>date</type>
</property>
<property>
<key>endDate</key>
<name>report.issuecreation.enddate</name>
<description>report.issuecreation.enddate.description</description>
<type>date</type>
</property>
<property>
<key>interval</key>
<name>report.issuecreation.interval</name>
<description>report.issuecreation.interval.description</description>
<type>long</type>
<default>3</default>
</property>
</properties>
Здесь мы добавили следующее:
- projectid - позволяет пользователям выбирать проект или фильтр, используемый для генерации отчета. Проекты, доступные в данном экземпляре Jira, извлекаются через средство фильтрации Jira filterprojectpicker.
- startDate - устанавливает начало периода времени, включенного в отчет.
- endDate - устанавливает окончание периода времени в отчете.
- interval - указывает временной интервал, используемый для разделения общего периода времени. Другими словами, это интервал гистограммы.
До сих пор мы работали над двумя модулями сразу, каждый из них соответствует отдельным отчетам в Jira. Теперь давайте возьмем их по одному, начиная с Single Level Group By Report Extended(Расширенная Группа с одним уровнем по отчету).
Шаг 5. Запишите код Расширенной группы с одним уровнем по отчету.
Когда вы использовали SDK для создания модулей, он предоставил вам файлы кода заглушки для отчетов. Код заглушки очень прост: просто конструктор и несколько импортов. Сейчас мы построем его.
В нашем первом отчете мы распространим отчет, предоставленный с Jira, классом SingleLevelGroupByReport. Если у вас есть доступ к исходному коду Jira, вы можете найти исходный код для оригинала в этом месте:
jira-components/jira-core/src/main/java/com/atlassian/jira/plugin/report/impl/SingleLevelGroupByReport.java
Наша цель - включить время последнего обновления для каждой задачи в вывод отчета. Он должен отображаться в соответствующем формате даты и времени, настроенном в Jira.
Шаблон представления получает значения, отображаемые в Jira, используя карту параметров, которая передается кодом модуля. Итак, чтобы добавить объект DateTimeFormatter к шаблону Velocity, мы изменим карту параметров, сгенерированную исходным отчетом Jira.
- Под проектом home at src / main / java / com / atlassian / plugins / tutorial / jira / reports откройте файлjava.
- Замените его содержимое следующим:
package com.atlassian.plugins.tutorial.jira.reports;
import com.atlassian.jira.bc.JiraServiceContext;
import com.atlassian.jira.bc.JiraServiceContextImpl;
import com.atlassian.jira.bc.filter.SearchRequestService;
import com.atlassian.jira.bc.issue.search.SearchService;
import com.atlassian.jira.datetime.DateTimeFormatter;
import com.atlassian.jira.datetime.DateTimeFormatterFactory;
import com.atlassian.jira.datetime.DateTimeStyle;
import com.atlassian.jira.exception.PermissionException;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.IssueFactory;
import com.atlassian.jira.issue.fields.FieldManager;
import com.atlassian.jira.issue.index.IssueIndexManager;
import com.atlassian.jira.issue.search.ReaderCache;
import com.atlassian.jira.issue.search.SearchException;
import com.atlassian.jira.issue.search.SearchProvider;
import com.atlassian.jira.issue.search.SearchRequest;
import com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator;
import com.atlassian.jira.issue.statistics.StatisticsMapper;
import com.atlassian.jira.issue.statistics.StatsGroup;
import com.atlassian.jira.issue.statistics.util.OneDimensionalDocIssueHitCollector;
import com.atlassian.jira.plugin.report.impl.AbstractReport;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.SimpleErrorCollection;
import com.atlassian.jira.web.FieldVisibilityManager;
import com.atlassian.jira.web.action.ProjectActionSupport;
import com.atlassian.jira.web.bean.PagerFilter;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport;
import com.atlassian.util.profiling.UtilTimerStack;
import com.google.common.collect.ImmutableMap;
import com.opensymphony.util.TextUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.lucene.search.Collector;
import java.util.Arrays;
import java.util.Map;
@Scanned
public class SingleLevelGroupByReportExtended extends AbstractReport {
private static final Logger log = Logger.getLogger(SingleLevelGroupByReportExtended.class);
@JiraImport
private final SearchProvider searchProvider;
@JiraImport
private final SearchRequestService searchRequestService;
@JiraImport
private final IssueFactory issueFactory;
@JiraImport
private final CustomFieldManager customFieldManager;
@JiraImport
private final IssueIndexManager issueIndexManager;
@JiraImport
private final SearchService searchService;
@JiraImport
private final FieldVisibilityManager fieldVisibilityManager;
@JiraImport
private final FieldManager fieldManager;
@JiraImport
private final ProjectManager projectManager;
@JiraImport
private final ReaderCache readerCache;
public SingleLevelGroupByReportExtended(final SearchProvider searchProvider,
final SearchRequestService searchRequestService, final IssueFactory issueFactory,
final CustomFieldManager customFieldManager, final IssueIndexManager issueIndexManager,
final SearchService searchService, final FieldVisibilityManager fieldVisibilityManager,
final ReaderCache readerCache,
final FieldManager fieldManager,
final ProjectManager projectManager) {
this.searchProvider = searchProvider;
this.searchRequestService = searchRequestService;
this.issueFactory = issueFactory;
this.customFieldManager = customFieldManager;
this.issueIndexManager = issueIndexManager;
this.searchService = searchService;
this.fieldVisibilityManager = fieldVisibilityManager;
this.readerCache = readerCache;
this.fieldManager = fieldManager;
this.projectManager = projectManager;
}
public StatsGroup getOptions(SearchRequest sr, ApplicationUser user, StatisticsMapper mapper) throws PermissionException {
try {
return searchMapIssueKeys(sr, user, mapper);
} catch (SearchException e) {
log.error("Exception rendering " + this.getClass().getName() + ". Exception \n" + Arrays.toString(e.getStackTrace()));
return null;
}
}
public StatsGroup searchMapIssueKeys(SearchRequest request, ApplicationUser searcher, StatisticsMapper mapper) throws SearchException {
try {
UtilTimerStack.push("Search Count Map");
StatsGroup statsGroup = new StatsGroup(mapper);
Collector hitCollector = new OneDimensionalDocIssueHitCollector(mapper.getDocumentConstant(), statsGroup,
issueIndexManager.getIssueSearcher().getIndexReader(), issueFactory,
fieldVisibilityManager, readerCache, fieldManager, projectManager);
searchProvider.searchAndSort((request != null) ? request.getQuery() : null, searcher, hitCollector, PagerFilter.getUnlimitedFilter());
return statsGroup;
} finally {
UtilTimerStack.pop("Search Count Map");
}
}
public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception {
String filterId = (String) params.get("filterid");
if (filterId == null) {
log.info("Single Level Group By Report run without a project selected (JRA-5042): params=" + params);
return "<span class='errMsg'>No search filter has been selected. Please "
+ "<a href=\"IssueNavigator.jspa?reset=Update&pid="
+ TextUtils.htmlEncode((String) params.get("selectedProjectId"))
+ "\">create one</a>, and re-run this report.";
}
String mapperName = (String) params.get("mapper");
final StatisticsMapper mapper = new FilterStatisticsValuesGenerator().getStatsMapper(mapperName);
final JiraServiceContext ctx = new JiraServiceContextImpl(action.getLoggedInUser());
final SearchRequest request = searchRequestService.getFilter(ctx, new Long(filterId));
try {
final Map startingParams = ImmutableMap.builder()
.put("action", action)
.put("statsGroup", getOptions(request, action.getLoggedInUser(), mapper))
.put("searchRequest", request)
.put("mapperType", mapperName)
.put("customFieldManager", customFieldManager)
.put("fieldVisibility", fieldVisibilityManager)
.put("searchService", searchService)
.put("portlet", this).build();
return descriptor.getHtml("view", startingParams);
} catch (PermissionException e) {
log.error(e.getStackTrace());
return null;
}
}
public void validate(ProjectActionSupport action, Map params) {
super.validate(action, params);
String filterId = (String) params.get("filterid");
if (StringUtils.isEmpty(filterId)) {
action.addError("filterid", action.getText("report.singlelevelgroupby.filter.is.required"));
} else {
validateFilterId(action, filterId);
}
}
private void validateFilterId(ProjectActionSupport action, String filterId) {
try {
JiraServiceContextImpl serviceContext = new JiraServiceContextImpl(
action.getLoggedInUser(), new SimpleErrorCollection());
SearchRequest searchRequest = searchRequestService.getFilter(serviceContext, new Long(filterId));
if (searchRequest == null) {
action.addErrorMessage(action.getText("report.error.no.filter"));
}
} catch (NumberFormatException nfe) {
action.addError("filterId", action.getText("report.error.filter.id.not.a.number", filterId));
}
}
}
Это просто оригинальный отчет с аннотациями Atlassian Spring Scanner. Затем мы добавим код, который представляет время последнего обновления для отчета.
- В рамках существующих объявлений полей для класса добавьте новое поле.
private final DateTimeFormatter formatter;
- Добавьте новое поле в качестве параметра, переданного конструктору класса.
public SingleLevelGroupByReportExtended( ...
@JiraImport DateTimeFormatterFactory dateTimeFormatterFactory )
...
this.formatter = dateTimeFormatterFactory.formatter().withStyle(DateTimeStyle.DATE).forLoggedInUser();
...
- В методе generateReportHtml () добавьте следующую строку:
startingParams
...
.put("formatter", formatter).build();
return descriptor.getHtml("view", startingParams);
Он должен появиться в блоке try, в котором код присваивает значения карте параметров.
Этот код добавляет дополнительный параметр к карте параметров, созданной этим методом.
- Сохраните изменения.
Измененный класс должен выглядеть примерно так:
package com.atlassian.plugins.tutorial.jira.reports;
import com.atlassian.jira.bc.JiraServiceContext;
import com.atlassian.jira.bc.JiraServiceContextImpl;
import com.atlassian.jira.bc.filter.SearchRequestService;
import com.atlassian.jira.bc.issue.search.SearchService;
import com.atlassian.jira.datetime.DateTimeFormatter;
import com.atlassian.jira.datetime.DateTimeFormatterFactory;
import com.atlassian.jira.datetime.DateTimeStyle;
import com.atlassian.jira.exception.PermissionException;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.IssueFactory;
import com.atlassian.jira.issue.fields.FieldManager;
import com.atlassian.jira.issue.index.IssueIndexManager;
import com.atlassian.jira.issue.search.ReaderCache;
import com.atlassian.jira.issue.search.SearchException;
import com.atlassian.jira.issue.search.SearchProvider;
import com.atlassian.jira.issue.search.SearchRequest;
import com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator;
import com.atlassian.jira.issue.statistics.StatisticsMapper;
import com.atlassian.jira.issue.statistics.StatsGroup;
import com.atlassian.jira.issue.statistics.util.OneDimensionalDocIssueHitCollector;
import com.atlassian.jira.plugin.report.impl.AbstractReport;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.SimpleErrorCollection;
import com.atlassian.jira.web.FieldVisibilityManager;
import com.atlassian.jira.web.action.ProjectActionSupport;
import com.atlassian.jira.web.bean.PagerFilter;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport;
import com.atlassian.util.profiling.UtilTimerStack;
import com.google.common.collect.ImmutableMap;
import com.opensymphony.util.TextUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.lucene.search.Collector;
import java.util.Arrays;
import java.util.Map;
@Scanned
public class SingleLevelGroupByReportExtended extends AbstractReport {
private static final Logger log = Logger.getLogger(SingleLevelGroupByReportExtended.class);
@JiraImport
private final SearchProvider searchProvider;
@JiraImport
private final SearchRequestService searchRequestService;
@JiraImport
private final IssueFactory issueFactory;
@JiraImport
private final CustomFieldManager customFieldManager;
@JiraImport
private final IssueIndexManager issueIndexManager;
@JiraImport
private final SearchService searchService;
@JiraImport
private final FieldVisibilityManager fieldVisibilityManager;
@JiraImport
private final FieldManager fieldManager;
@JiraImport
private final ProjectManager projectManager;
@JiraImport
private final ReaderCache readerCache;
private final DateTimeFormatter formatter;
public SingleLevelGroupByReportExtended(final SearchProvider searchProvider,
final SearchRequestService searchRequestService, final IssueFactory issueFactory,
final CustomFieldManager customFieldManager, final IssueIndexManager issueIndexManager,
final SearchService searchService, final FieldVisibilityManager fieldVisibilityManager,
final ReaderCache readerCache,
final FieldManager fieldManager,
final ProjectManager projectManager,
@JiraImport DateTimeFormatterFactory dateTimeFormatterFactory) {
this.searchProvider = searchProvider;
this.searchRequestService = searchRequestService;
this.issueFactory = issueFactory;
this.customFieldManager = customFieldManager;
this.issueIndexManager = issueIndexManager;
this.searchService = searchService;
this.fieldVisibilityManager = fieldVisibilityManager;
this.readerCache = readerCache;
this.fieldManager = fieldManager;
this.projectManager = projectManager;
this.formatter = dateTimeFormatterFactory.formatter().withStyle(DateTimeStyle.DATE).forLoggedInUser();
}
public StatsGroup getOptions(SearchRequest sr, ApplicationUser user, StatisticsMapper mapper) throws PermissionException {
try {
return searchMapIssueKeys(sr, user, mapper);
} catch (SearchException e) {
log.error("Exception rendering " + this.getClass().getName() + ". Exception \n" + Arrays.toString(e.getStackTrace()));
return null;
}
}
public StatsGroup searchMapIssueKeys(SearchRequest request, ApplicationUser searcher, StatisticsMapper mapper) throws SearchException {
try {
UtilTimerStack.push("Search Count Map");
StatsGroup statsGroup = new StatsGroup(mapper);
Collector hitCollector = new OneDimensionalDocIssueHitCollector(mapper.getDocumentConstant(), statsGroup,
issueIndexManager.getIssueSearcher().getIndexReader(), issueFactory,
fieldVisibilityManager, readerCache, fieldManager, projectManager);
searchProvider.searchAndSort((request != null) ? request.getQuery() : null, searcher, hitCollector, PagerFilter.getUnlimitedFilter());
return statsGroup;
} finally {
UtilTimerStack.pop("Search Count Map");
}
}
public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception {
String filterId = (String) params.get("filterid");
if (filterId == null) {
log.info("Single Level Group By Report run without a project selected (JRA-5042): params=" + params);
return "<span class='errMsg'>No search filter has been selected. Please "
+ "<a href=\"IssueNavigator.jspa?reset=Update&pid="
+ TextUtils.htmlEncode((String) params.get("selectedProjectId"))
+ "\">create one</a>, and re-run this report.";
}
String mapperName = (String) params.get("mapper");
final StatisticsMapper mapper = new FilterStatisticsValuesGenerator().getStatsMapper(mapperName);
final JiraServiceContext ctx = new JiraServiceContextImpl(action.getLoggedInUser());
final SearchRequest request = searchRequestService.getFilter(ctx, new Long(filterId));
try {
final Map startingParams = ImmutableMap.builder()
.put("action", action)
.put("statsGroup", getOptions(request, action.getLoggedInUser(), mapper))
.put("searchRequest", request)
.put("mapperType", mapperName)
.put("customFieldManager", customFieldManager)
.put("fieldVisibility", fieldVisibilityManager)
.put("searchService", searchService)
.put("portlet", this)
.put("formatter", formatter).build();
return descriptor.getHtml("view", startingParams);
} catch (PermissionException e) {
log.error(e.getStackTrace());
return null;
}
}
public void validate(ProjectActionSupport action, Map params) {
super.validate(action, params);
String filterId = (String) params.get("filterid");
if (StringUtils.isEmpty(filterId)) {
action.addError("filterid", action.getText("report.singlelevelgroupby.filter.is.required"));
} else {
validateFilterId(action, filterId);
}
}
private void validateFilterId(ProjectActionSupport action, String filterId) {
try {
JiraServiceContextImpl serviceContext = new JiraServiceContextImpl(
action.getLoggedInUser(), new SimpleErrorCollection());
SearchRequest searchRequest = searchRequestService.getFilter(serviceContext, new Long(filterId));
if (searchRequest == null) {
action.addErrorMessage(action.getText("report.error.no.filter"));
}
} catch (NumberFormatException nfe) {
action.addError("filterId", action.getText("report.error.filter.id.not.a.number", filterId));
}
}
}
Шаг 6. Создайте шаблон представления для отчета.
На этом шаге мы редактируем шаблон представления отчета, чтобы отобразить представителя и последнее время обновления. Эта версия отличается от исходного отчета Jira в двух местах, где мы перебираем результат, чтобы отображать время.
Давайте обновим файлы ресурсов, которые нам дал SDK.
- Перейдите к src / main / resources / templates / reports / single-level-group-by-report-extended и откройте файлvm.
- Замените содержимое заполнителя на следующее:
<tbody>
#foreach ($option in $statsGroup.entrySet())
#set ($issues = $option.value)
#set ($graphModel = $statsGroup.getResolvedIssues($option.key))
<tr>
<th colspan="6" class="stat-heading">
<div class="stat-progress">
<span class="graphLabel">$i18n.getText("common.words.progress"):</span>
#percentageGraphDiv ($graphModel)
#if ($issues.size() > 0)
<span class="graphDescription">$i18n.getText("roadmap.issuesresolved", "$statsGroup.getResolvedIssueCount($issues)", "$issues.size()")</span>
#end
</div>
<h3>#statHeading ($mapperType $option.key $customFieldManager "${urlPrefix}$!searchService.getQueryString($user, $statsGroup.getMapper().getSearchUrlSuffix($option.key, $searchRequest).getQuery())")</h3>
</th>
</tr>
#if ($issues.size() > 0)
#foreach ($issue in $issues)
<tr>
<td width="5%"> </td>
#issueLineItem ($issue)
<td nowrap class="assignee">
#if($issue.getAssignee())
$issue.getAssignee().getDisplayName()
#else
$i18n.getText('common.concepts.unassigned')
#end</td>
<td nowrap class="last-updated"> $formatter.format($issue.getUpdated())</td>
</tr>
#end
#else
<tr>
<td colspan="6">
<span class="subText">$action.getText("common.concepts.noissues").</span>
</td>
</tr>
#end
#end
## Render the Irrelevant issues if there are any
#if($statsGroup.getIrrelevantIssues().size() > 0)
#set ($issues = $statsGroup.getIrrelevantIssues())
#set ($graphModel = $statsGroup.getIrrelevantResolvedIssues())
<tr>
<th colspan="6">
<div class="stat-progress">
<span class="graphLabel">$i18n.getText("common.words.progress"):</span>
#percentageGraphDiv ($graphModel)
#if ($issues.size() > 0)
<span class="graphDescription">$i18n.getText("roadmap.issuesresolved", "$statsGroup.getResolvedIssueCount($issues)", "$issues.size()")</span>
#end
</div>
<h3><span title="$i18n.getText('common.concepts.irrelevant.desc')">$i18n.getText('common.concepts.irrelevant')</span></h3>
</th>
</tr>
#if ($issues.size() > 0)
#foreach ($issue in $issues)
<tr>
<td width="5%"> </td>
#issueLineItem ($issue)
<td nowrap class="assignee">
#if($issue.getAssignee())
$issue.getAssignee().getDisplayName()
#else
$i18n.getText('common.concepts.unassigned')
#end</td>
<td nowrap class="last-updated">$formatter.format($issue.getUpdated())</td>
</tr>
#end
#else
<tr>
<td colspan="6">
<span class="subText">$action.getText("common.concepts.noissues").</span>
</td>
</tr>
#end
#end
</tbody>
</table>
Этот код шаблона идентичен шаблону для исходного отчета, но с добавленной ячейкой таблицы на выходе, содержащей нашу дату.
- Перейдите в src / main / resources и откройте файлproperties.
- Замените его содержимое следующим текстом:
report.singlelevelgroupby.label.extended = Single Level Group By Report Extended
report.singlelevelgroupby.filterId = Filter
report.singlelevelgroupby.filterId.description = Select a filter to display
report.singlelevelgroupby.mapper = Statistic Type
report.singlelevelgroupby.mapper.description = Select a field to group by
report.singlelevelgroupby.mapper.filterid.name = Filter
report.singlelevelgroupby.description = "Этот отчет позволяет отобразить задачи сгруппированные по определенному полю
Это текстовые строки, которые будут отображаться в пользовательском интерфейсе, создаваемом в отчете.
- Сохраните и закройте файл.
Это дает нам достаточно, чтобы попробовать его в Jira. Вы сделаете это на следующем шаге.
Шаг 7. Запустите Jira и попробуйте отчет
- Откройте терминал и перейдите в корневой каталог проекта (где находится POM), а затем выполните следующую команду:
atlas-run
Дайте SDK несколько минут, чтобы загрузить файлы Jira и запустить экземпляр. Если вы столкнулись с ошибками сборки, убедитесь, что вы включили зависимость для артефакта jira-core в проекте POM.
- Откройте локальный экземпляр Jira и войдите в систему с учетными данными администратора / администратора по умолчанию.
-
Прежде чем опробовать свой отчет, создайте несколько артефактов Jira:
- Создайте проект. При первом запуске Jira мастер предложит вам создать его.
- Создайте несколько тестовых задач в проекте. Чтобы предоставить вашему отчету собственность для группировки, выберите различных представителей или типы задач для ваших задач.
- Создайте хотя бы один фильтр. Вы можете сделать это со страницы поиска. Для получения дополнительной информации см. Документация по фильтрам Jira.
- Перейдите на страницу «Обзор» для своего проекта.
- В представлении «Резюме» найдите два новых отчета в нижней части списка.
- Нажмите кнопку Single Level Group By Report Extended.
- Выберите «Фильтр» и тип статистики, который вы хотите сгруппировать, и нажмите «Далее». Ваш отчет должен выглядеть примерно так.
РИСУНОК
Посмотрите, как выглядит отчет о создании в Jira. Из-за свойств, которые вы добавили в дескриптор приложения, у нас есть поля ввода пользователя. Но для ярлыков есть только заполнители, и если вы нажмете «Далее», вы получите пустую страницу. Вы будете работать над этим на следующем шаге.
Тем временем вы можете оставить Jira запущенной и загрузить изменения приложения с помощью QuickReload.
Шаг 8. Напишите код отчета о создании
На этом шаге мы делаем Report Creation более полезным.
- Перейдите в src / main / java / com / atlassian / plugins / tutorial / jira / reports и откройте файлjava.
- Замените его содержимое следующим текстом:
package com.atlassian.plugins.tutorial.jira.reports;
import com.atlassian.core.util.DateUtils;
import com.atlassian.jira.datetime.DateTimeFormatter;
import com.atlassian.jira.datetime.DateTimeFormatterFactory;
import com.atlassian.jira.datetime.DateTimeStyle;
import com.atlassian.jira.issue.search.SearchException;
import com.atlassian.jira.issue.search.SearchProvider;
import com.atlassian.jira.jql.builder.JqlQueryBuilder;
import com.atlassian.jira.plugin.report.impl.AbstractReport;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.ParameterUtils;
import com.atlassian.jira.web.action.ProjectActionSupport;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport;
import com.atlassian.query.Query;
import org.apache.log4j.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Scanned
public class CreationReport extends AbstractReport {
private static final Logger log = Logger.getLogger(CreationReport.class);
private static final int MAX_HEIGHT = 360;
private long maxCount = 0;
private Collection<Long> openIssuesCounts = new ArrayList<>();
private Collection<String> formattedDates = new ArrayList<>();
@JiraImport
private final SearchProvider searchProvider;
@JiraImport
private final ProjectManager projectManager;
private final DateTimeFormatter formatter;
private Date startDate;
private Date endDate;
private Long interval;
private Long projectId;
public CreationReport(SearchProvider searchProvider, ProjectManager projectManager,
@JiraImport DateTimeFormatterFactory dateTimeFormatterFactory) {
this.searchProvider = searchProvider;
this.projectManager = projectManager;
this.formatter = dateTimeFormatterFactory.formatter().withStyle(DateTimeStyle.DATE).forLoggedInUser();
}
public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception {
//action.getLoggedInUser() since Jira 7.0.
//getLoggedInApplicationUser() since Jira 5.2
fillIssuesCounts(startDate, endDate, interval, action.getLoggedInUser(), projectId);
List<Number> issueBarHeights = new ArrayList<>();
if (maxCount > 0) {
openIssuesCounts.forEach(issueCount ->
issueBarHeights.add((issueCount.floatValue() / maxCount) * MAX_HEIGHT)
);
}
Map<String, Object> velocityParams = new HashMap<>();
velocityParams.put("startDate", formatter.format(startDate));
velocityParams.put("endDate", formatter.format(endDate));
velocityParams.put("openCount", openIssuesCounts);
velocityParams.put("issueBarHeights", issueBarHeights);
velocityParams.put("dates", formattedDates);
velocityParams.put("maxHeight", MAX_HEIGHT);
velocityParams.put("projectName", projectManager.getProjectObj(projectId).getName());
velocityParams.put("interval", interval);
return descriptor.getHtml("view", velocityParams);
}
private long getOpenIssueCount(ApplicationUser user, Date startDate, Date endDate, Long projectId) throws SearchException {
JqlQueryBuilder queryBuilder = JqlQueryBuilder.newBuilder();
Query query = queryBuilder.where().createdBetween(startDate, endDate).and().project(projectId).buildQuery();
return searchProvider.searchCount(query, user);
}
private void fillIssuesCounts(Date startDate, Date endDate, Long interval, ApplicationUser user, Long projectId) throws SearchException {
long intervalValue = interval * DateUtils.DAY_MILLIS;
Date newStartDate;
long count;
while (startDate.before(endDate)) {
newStartDate = new Date(startDate.getTime() + intervalValue);
if (newStartDate.after(endDate))
count = getOpenIssueCount(user, startDate, endDate, projectId);
else
count = getOpenIssueCount(user, startDate, newStartDate, projectId);
if (maxCount < count)
maxCount = count;
openIssuesCounts.add(count);
formattedDates.add(formatter.format(startDate));
startDate = newStartDate;
}
}
public void validate(ProjectActionSupport action, Map params) {
try {
startDate = formatter.parse(ParameterUtils.getStringParam(params, "startDate"));
} catch (IllegalArgumentException e) {
action.addError("startDate", action.getText("report.issuecreation.startdate.required"));
log.error("Exception while parsing startDate");
}
try {
endDate = formatter.parse(ParameterUtils.getStringParam(params, "endDate"));
} catch (IllegalArgumentException e) {
action.addError("endDate", action.getText("report.issuecreation.enddate.required"));
log.error("Exception while parsing endDate");
}
interval = ParameterUtils.getLongParam(params, "interval");
projectId = ParameterUtils.getLongParam(params, "selectedProjectId");
if (interval == null || interval <= 0) {
action.addError("interval", action.getText("report.issuecreation.interval.invalid"));
log.error("Invalid interval");
}
if (projectId == null || projectManager.getProjectObj(projectId) == null){
action.addError("selectedProjectId", action.getText("report.issuecreation.projectid.invalid"));
log.error("Invalid projectId");
}
if (startDate != null && endDate != null && endDate.before(startDate)) {
action.addError("endDate", action.getText("report.issuecreation.before.startdate"));
log.error("Invalid dates: start date should be before end date");
}
}
}
Код получает параметры, заданные пользователем. Затем он получает соответствующие подсчеты задач из настроенного диапазона времени; они делятся на временные интервалы, заданные пользователем. Количество подсчетов нормируется, что дает пользователю сбалансированную гистограмму. Наконец, соответствующие детали передаются в шаблон Velocity.
- Сохраните и закройте файл.
Шаг 9. Разработка пользовательского интерфейса отчета о создании
Если вы сейчас попробуете отчет в Jira, вы все равно получите пустую страницу. Чтобы увидеть отчет, вам нужно закодировать шаблон презентации вместе с текстовыми строками, которые будут отображаться в пользовательском интерфейсе.
- Перейдите в src / main / resources / templates / reports / creation-report и откройте файлvm.
- Замените его содержимое следующим текстом:
<div style="padding: 5px">
<!-- Display the report configuration -->
<h4 id="creation-report-parameters">
$i18n.getText('report.issuecreation.project'): $projectName |
$i18n.getText('report.issuecreation.duration'):$startDate - $endDate |
$i18n.getText('report.issuecreation.interval'): $interval $i18n.getText('report.issuecreation.interval.days')
</h4>
<br />
<table style="width: 100%; border: 0; background-color: lightgrey">
<!-- Create a row to display the bars-->
<tr valign="bottom" style="background-color: white; padding: 1px">
#foreach ($issueBarHeight in $issueBarHeights)
<td height="$maxHeight" align="center">
#if ($issueBarHeight > 0)
<img src="${baseurl}/images/bluepixel.gif" width="12" height="$issueBarHeight">
#end
</td>
#end
</tr>
<!-- Have one row for the issue count -->
<tr style="background-color: #eee; padding: 1px">
#foreach ($count in $openCount)
<td align="center"><b>$count</b></td>
#end
</tr>
<!-- And one row to display the date -->
<tr style="background-color: #eee; padding: 1px">
#foreach ($date in $dates)
<td align="center"><b>$date</b></td>
#end
</tr>
</table>
</div>
- Добавьте в файл свойств следующие текстовые строки:
report.issuecreation.label = Issue Creation Report
report.issuecreation.name = Issue Creation Report
report.issuecreation.projectid.name = Project
report.issuecreation.projectid.description = Select the project to display report on.
report.issuecreation.description = Report displaying a histogram of issues opened over a specified period.
report.issuecreation.startdate = Start Date
report.issuecreation.startdate.description = Graph all issues created after this date.
report.issuecreation.enddate = End Date
report.issuecreation.enddate.description = Graph all issues created before this date.
report.issuecreation.interval = Interval
report.issuecreation.interval.days = days
report.issuecreation.interval.description = Specify the interval (in days) for the report.
report.issuecreation.startdate.required = A valid "Start Date" is required to generate this report.
report.issuecreation.enddate.required = A valid "End Date" is required to generate this report.
report.issuecreation.interval.invalid = The interval must be a number greater than 0.
report.issuecreation.before.startdate = The "End Date" must be after the "Start Date".
report.issuecreation.error = Error occurred generating Issue Creation Report.
report.issuecreation.projectid.invalid = Please select a valid project.
report.issuecreation.default.interval = The interval specified is invalid - using default interval.
report.issuecreation.duration = Duration
report.issuecreation.project = Project
- Сохраните и закройте файл.
Шаг 10. Просмотрите готовый отчет
Теперь попробуйте отчет о создании и посмотрите, что у нас есть.
- Запустите экземпляр Jira, используя команду atlas-run или, если вы оставите Jira, просто перестройте приложение с помощью команды atlas-package. Как уже упоминалось, QuickReload автоматически переустанавливает ваше приложение для вас.
- В нижней части страницы резюме проекта щелкните ссылку «Отчет о создании».
-
Даты поставки, которые следуют этим критериям:
- Даты задают диапазон, который охватывает дату, когда вы создали задачи.
- Интервал достаточно длинный, чтобы дать вам несколько интервалов времени чтобы взглянуть(помните, что интервал по умолчанию составляет 3 дня).
- Нажмите "Далее.
На этот раз у нас есть отчет.
РИСУНОК
Поздравляю, вот и все!
Имейте удовольствие!
Следующие шаги
Наш отчет для вывода пользовательского интерфейса довольно прост. В качестве следующего шага попробуйте улучшить вывод отчета. Вы можете использовать отчеты, поставляемые с Jira в качестве модели.
Для получения дополнительной информации по темам, приведенным в этом учебнике, см.
- Модуль плагина отчета.
- Настройка плагинов с настраиваемыми параметрами объекта.
По материалам Atlassian JIRA Server Developer Creating a Jira report