Вывод комментариев в отчёте дейли со ссылками на задачи в redmine
This commit is contained in:
parent
e71491b637
commit
9b17d703ed
7 changed files with 225 additions and 31 deletions
|
|
@ -35,6 +35,7 @@ import { DailyEccmReportTask } from './reports/daily-eccm.report.task';
|
||||||
import { DailyEccmReportsUserCommentsDatasource } from './couchdb-datasources/daily-eccm-reports-user-comments.datasource';
|
import { DailyEccmReportsUserCommentsDatasource } from './couchdb-datasources/daily-eccm-reports-user-comments.datasource';
|
||||||
import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service';
|
import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service';
|
||||||
import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handlers/set-daily-eccm-user-comment.bot-handler.service';
|
import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handlers/set-daily-eccm-user-comment.bot-handler.service';
|
||||||
|
import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -76,6 +77,7 @@ import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handler
|
||||||
DailyEccmReportsUserCommentsDatasource,
|
DailyEccmReportsUserCommentsDatasource,
|
||||||
DailyEccmUserCommentsService,
|
DailyEccmUserCommentsService,
|
||||||
SetDailyEccmUserCommentBotHandlerService,
|
SetDailyEccmUserCommentBotHandlerService,
|
||||||
|
DailyEccmWithExtraDataService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit {
|
export class AppModule implements OnModuleInit {
|
||||||
|
|
@ -93,6 +95,8 @@ export class AppModule implements OnModuleInit {
|
||||||
private telegramBotService: TelegramBotService,
|
private telegramBotService: TelegramBotService,
|
||||||
private personalNotificationAdapterService: PersonalNotificationAdapterService,
|
private personalNotificationAdapterService: PersonalNotificationAdapterService,
|
||||||
private statusChangeAdapterService: StatusChangeAdapterService,
|
private statusChangeAdapterService: StatusChangeAdapterService,
|
||||||
|
private dailyEccmUserCommentsService: DailyEccmUserCommentsService,
|
||||||
|
private setDailyEccmUserCommentBotHandlerService: SetDailyEccmUserCommentBotHandlerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
|
@ -165,5 +169,19 @@ export class AppModule implements OnModuleInit {
|
||||||
`Save result process success finished, issue_id = ${args.saveResult.current.id}`,
|
`Save result process success finished, issue_id = ${args.saveResult.current.id}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initDailyEccmUserCommentsPipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initDailyEccmUserCommentsPipeline(): void {
|
||||||
|
this.setDailyEccmUserCommentBotHandlerService.$messages.subscribe(
|
||||||
|
(data) => {
|
||||||
|
this.dailyEccmUserCommentsService.setComment(
|
||||||
|
data.userId,
|
||||||
|
data.date,
|
||||||
|
data.comment,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
// TODO: Этот сервис возможно перенести в lib event-emitter
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedminePublicUrlConverter {
|
export class RedminePublicUrlConverter {
|
||||||
private redminePublicUrlPrefix: string;
|
private redminePublicUrlPrefix: string;
|
||||||
|
|
@ -24,4 +26,48 @@ export class RedminePublicUrlConverter {
|
||||||
const url = this.getUrl(issue.id);
|
const url = this.getUrl(issue.id);
|
||||||
return `<a href="${url}">${issue.tracker.name} #${issue.id}</a>`;
|
return `<a href="${url}">${issue.tracker.name} #${issue.id}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMinHtmlHref(issueId: number | string): string {
|
||||||
|
const url = this.getUrl(issueId);
|
||||||
|
return `<a href="${url}">#${issueId}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обогащение текста с идентификаторами задач html ссылками на эти задачи
|
||||||
|
*
|
||||||
|
* Например текст `"Эта ошибка будет решена в рамках задач #123 и #456"`
|
||||||
|
* будет заменён на `"Эта ошибка будет решена в рамках задач
|
||||||
|
* <a href="http://redmine.example.org/issues/123">#123</a> и
|
||||||
|
* <a href="http://redmine.example.org/issues/456">#456</a>"`
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* @param linkGenerator функция замены отдельного идентификатора на html ссылку. По умолчанию
|
||||||
|
* будет использоваться собственная функция this.getMinHtmlHref
|
||||||
|
* @see convert
|
||||||
|
*/
|
||||||
|
enrichTextWithIssues(
|
||||||
|
text: string,
|
||||||
|
linkGenerator?: (issueId: number | string) => string,
|
||||||
|
): string {
|
||||||
|
const generator = linkGenerator
|
||||||
|
? linkGenerator
|
||||||
|
: (issueId) => this.getMinHtmlHref(issueId);
|
||||||
|
|
||||||
|
const regexp = /^\d+/;
|
||||||
|
|
||||||
|
const parts = text.split('#');
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
let part = parts[i];
|
||||||
|
const match = part.match(regexp);
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const issueId = match[0];
|
||||||
|
const replacment = generator(issueId);
|
||||||
|
part = part.replace(new RegExp(`^${issueId}`), replacment);
|
||||||
|
parts[i] = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
src/reports/daily-eccm-with-extra-data.service.ts
Normal file
43
src/reports/daily-eccm-with-extra-data.service.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Timestamped } from '@app/event-emitter/models/timestamped';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import nano from 'nano';
|
||||||
|
import { RedminePublicUrlConverter } from 'src/converters/redmine-public-url.converter';
|
||||||
|
import { DailyEccmUserCommentsService } from './daily-eccm-user-comments.service';
|
||||||
|
import {
|
||||||
|
DailyEccmReport,
|
||||||
|
DailyEccmReportService,
|
||||||
|
} from './daily-eccm.report.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DailyEccmWithExtraDataService {
|
||||||
|
constructor(
|
||||||
|
private dailyEccmReportService: DailyEccmReportService,
|
||||||
|
private dailyEccmUserCommentsService: DailyEccmUserCommentsService,
|
||||||
|
private redminePublicUrlConverter: RedminePublicUrlConverter,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async loadReport(
|
||||||
|
name: string,
|
||||||
|
): Promise<
|
||||||
|
| (DailyEccmReport.Models.Report & nano.DocumentGetResponse & Timestamped)
|
||||||
|
| null
|
||||||
|
> {
|
||||||
|
const baseReportData = await this.dailyEccmReportService.loadReport(name);
|
||||||
|
if (!baseReportData) return null;
|
||||||
|
const userIds = baseReportData.byUsers.map((item) => item.user.id);
|
||||||
|
const userComments = await this.dailyEccmUserCommentsService.loadComments(
|
||||||
|
userIds,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < baseReportData.byUsers.length; i++) {
|
||||||
|
const byUser = baseReportData.byUsers[i];
|
||||||
|
if (userComments[byUser.user.id]) {
|
||||||
|
byUser.dailyMessage =
|
||||||
|
this.redminePublicUrlConverter.enrichTextWithIssues(
|
||||||
|
userComments[byUser.user.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseReportData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Controller, Get, Param, Query, Render } from '@nestjs/common';
|
import { Controller, Get, Param, Query, Render } from '@nestjs/common';
|
||||||
|
import { DailyEccmWithExtraDataService } from './daily-eccm-with-extra-data.service';
|
||||||
import {
|
import {
|
||||||
DailyEccmReport,
|
DailyEccmReport,
|
||||||
DailyEccmReportService,
|
DailyEccmReportService,
|
||||||
|
|
@ -6,7 +7,10 @@ import {
|
||||||
|
|
||||||
@Controller('daily-eccm')
|
@Controller('daily-eccm')
|
||||||
export class DailyEccmReportController {
|
export class DailyEccmReportController {
|
||||||
constructor(private dailyEccmReportService: DailyEccmReportService) {}
|
constructor(
|
||||||
|
private dailyEccmReportService: DailyEccmReportService,
|
||||||
|
private dailyEccmWithExtraDataService: DailyEccmWithExtraDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Render('daily-eccm-report')
|
@Render('daily-eccm-report')
|
||||||
|
|
@ -67,4 +71,19 @@ export class DailyEccmReportController {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/load/:name/extended/raw')
|
||||||
|
async loadExtendedReportRawData(
|
||||||
|
@Param('name') name: string,
|
||||||
|
): Promise<DailyEccmReport.Models.Report> {
|
||||||
|
return await this.dailyEccmWithExtraDataService.loadReport(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/load/:name/extended')
|
||||||
|
@Render('daily-eccm-report-extended')
|
||||||
|
async loadExtendedReport(
|
||||||
|
@Param('name') name: string,
|
||||||
|
): Promise<DailyEccmReport.Models.Report> {
|
||||||
|
return await this.dailyEccmWithExtraDataService.loadReport(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ export class SetDailyEccmUserCommentBotHandlerService
|
||||||
private service: TelegramBotService;
|
private service: TelegramBotService;
|
||||||
private regexp = /\/set_daily_eccm_user_comment (.+)/;
|
private regexp = /\/set_daily_eccm_user_comment (.+)/;
|
||||||
private logger = new Logger(SetDailyEccmUserCommentBotHandlerService.name);
|
private logger = new Logger(SetDailyEccmUserCommentBotHandlerService.name);
|
||||||
private dateParamRegexp = /^date=([\d\-]+) (.+)$/;
|
|
||||||
|
|
||||||
$messages =
|
$messages =
|
||||||
new Subject<SetDailyEccmUserCommentBotHandler.Models.SetDailyEccmUserComment>();
|
new Subject<SetDailyEccmUserCommentBotHandler.Models.SetDailyEccmUserComment>();
|
||||||
|
|
@ -43,7 +42,8 @@ export class SetDailyEccmUserCommentBotHandlerService
|
||||||
msg.chat.id,
|
msg.chat.id,
|
||||||
);
|
);
|
||||||
const redmineUserId = userMetaInfo.user_id;
|
const redmineUserId = userMetaInfo.user_id;
|
||||||
const data = this.parseDate(msg.text);
|
const data = this.parseData(msg.text);
|
||||||
|
if (data) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Setting user comment for daily eccm ` +
|
`Setting user comment for daily eccm ` +
|
||||||
`by redmineUserId = ${redmineUserId}, ` +
|
`by redmineUserId = ${redmineUserId}, ` +
|
||||||
|
|
@ -55,34 +55,45 @@ export class SetDailyEccmUserCommentBotHandlerService
|
||||||
date: data.date,
|
date: data.date,
|
||||||
comment: data.msg,
|
comment: data.msg,
|
||||||
});
|
});
|
||||||
return;
|
} else {
|
||||||
|
this.logger.error(
|
||||||
|
`For some reason, it was not possible to get data from an incoming message - ${msg.text}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHelpMsg(): string {
|
getHelpMsg(): string {
|
||||||
return (
|
return (
|
||||||
`/set_daily_eccm_user_comment ` +
|
`/set_daily_eccm_user_comment ` +
|
||||||
`[дата=yyyy-MM-dd] <комментарий> ` +
|
`[date=yyyy-MM-dd] <комментарий> ` +
|
||||||
`- дополнительный комментарий для дейли`
|
`- дополнительный комментарий для дейли`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseDate(src: string): { date: string; msg: string } | null {
|
private parseData(src: string): { date: string; msg: string } | null {
|
||||||
let msgWithoutCommand: any = src.match(this.regexp);
|
let text = src;
|
||||||
if (!msgWithoutCommand || !msgWithoutCommand[1]) {
|
|
||||||
|
text = text.replace('/set_daily_eccm_user_comment', '').trim();
|
||||||
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
msgWithoutCommand = msgWithoutCommand[1];
|
|
||||||
const msgWithDate: any = msgWithoutCommand.match(this.dateParamRegexp);
|
const dateMatch = text.match(/^date=[\d-]+/);
|
||||||
let date: any = '';
|
if (!dateMatch) {
|
||||||
if (msgWithDate && msgWithDate[1] && msgWithDate[2]) {
|
return { date: DateTime.now().toFormat(ISO_DATE_FORMAT), msg: text };
|
||||||
date = msgWithDate[1];
|
}
|
||||||
if (DateTime.fromFormat(date, ISO_DATE_FORMAT).isValid) {
|
|
||||||
msgWithoutCommand = msgWithDate[2];
|
const datePart = dateMatch[0];
|
||||||
} else {
|
let dateRaw = datePart.replace('date=', '');
|
||||||
date = '';
|
text = text.replace('date=', '').trim();
|
||||||
}
|
const date = DateTime.fromFormat(dateRaw, ISO_DATE_FORMAT);
|
||||||
}
|
if (!date.isValid) {
|
||||||
return { date: date, msg: msgWithoutCommand };
|
this.logger.error(`Wrong date in message - ${src}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
text = text.replace(dateRaw, '').trim();
|
||||||
|
dateRaw = date.toFormat(ISO_DATE_FORMAT);
|
||||||
|
return { date: dateRaw, msg: text };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "es2017",
|
"target": "es2021",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
|
|
||||||
57
views/daily-eccm-report-extended.hbs
Normal file
57
views/daily-eccm-report-extended.hbs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Daily Eccm Report</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<details>
|
||||||
|
<summary>Параметры отчёта</summary>
|
||||||
|
<ul>
|
||||||
|
<li>От - {{this.params.from}}</li>
|
||||||
|
<li>До - {{this.params.to}}</li>
|
||||||
|
<li>Имя отчёта - {{this.params.name}}</li>
|
||||||
|
<li>Имя проекта - {{this.params.project}}</li>
|
||||||
|
<li>Версии - {{this.params.versions}}</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<h1>Отчёт по работникам</h1>
|
||||||
|
{{#each this.byUsers}}
|
||||||
|
|
||||||
|
<h2>{{this.user.firstname}} {{this.user.lastname}}</h2>
|
||||||
|
|
||||||
|
{{#if this.dailyMessage}}
|
||||||
|
<h3>Комментарий</h3>
|
||||||
|
<pre>{{{this.dailyMessage}}}</pre>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<h3>Текущие задачи</h3>
|
||||||
|
<ul>
|
||||||
|
{{#each this.issuesGroupedByStatus}}
|
||||||
|
<li>
|
||||||
|
{{this.status.name}}:
|
||||||
|
<ul>
|
||||||
|
{{#each this.issues}}
|
||||||
|
<li title="{{this.parents}}">{{>redmineIssueAHref issue=this.issue}} (приоритет {{this.issue.priority.name}}; версия {{this.issue.fixed_version.name}}) - {{this.issue.subject}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Активности за период</h3>
|
||||||
|
<ul>
|
||||||
|
{{#each this.activities}}
|
||||||
|
<li title="{{this.parents}}">
|
||||||
|
{{>redmineIssueAHref issue=this.issue}} (приоритет {{this.issue.priority.name}}; версия {{this.issue.fixed_version.name}}; статус {{this.issue.status.name}}) - {{this.issue.subject}}
|
||||||
|
<ul>
|
||||||
|
{{#each this.changes}}
|
||||||
|
<li>{{this.created_on}}: {{this.change_message}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/each}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue