Вывод комментариев в отчёте дейли со ссылками на задачи в 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 { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.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({
|
||||
imports: [
|
||||
|
|
@ -76,6 +77,7 @@ import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handler
|
|||
DailyEccmReportsUserCommentsDatasource,
|
||||
DailyEccmUserCommentsService,
|
||||
SetDailyEccmUserCommentBotHandlerService,
|
||||
DailyEccmWithExtraDataService,
|
||||
],
|
||||
})
|
||||
export class AppModule implements OnModuleInit {
|
||||
|
|
@ -93,6 +95,8 @@ export class AppModule implements OnModuleInit {
|
|||
private telegramBotService: TelegramBotService,
|
||||
private personalNotificationAdapterService: PersonalNotificationAdapterService,
|
||||
private statusChangeAdapterService: StatusChangeAdapterService,
|
||||
private dailyEccmUserCommentsService: DailyEccmUserCommentsService,
|
||||
private setDailyEccmUserCommentBotHandlerService: SetDailyEccmUserCommentBotHandlerService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
|
|
@ -165,5 +169,19 @@ export class AppModule implements OnModuleInit {
|
|||
`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 { ConfigService } from '@nestjs/config';
|
||||
|
||||
// TODO: Этот сервис возможно перенести в lib event-emitter
|
||||
|
||||
@Injectable()
|
||||
export class RedminePublicUrlConverter {
|
||||
private redminePublicUrlPrefix: string;
|
||||
|
|
@ -24,4 +26,48 @@ export class RedminePublicUrlConverter {
|
|||
const url = this.getUrl(issue.id);
|
||||
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 { DailyEccmWithExtraDataService } from './daily-eccm-with-extra-data.service';
|
||||
import {
|
||||
DailyEccmReport,
|
||||
DailyEccmReportService,
|
||||
|
|
@ -6,7 +7,10 @@ import {
|
|||
|
||||
@Controller('daily-eccm')
|
||||
export class DailyEccmReportController {
|
||||
constructor(private dailyEccmReportService: DailyEccmReportService) {}
|
||||
constructor(
|
||||
private dailyEccmReportService: DailyEccmReportService,
|
||||
private dailyEccmWithExtraDataService: DailyEccmWithExtraDataService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Render('daily-eccm-report')
|
||||
|
|
@ -67,4 +71,19 @@ export class DailyEccmReportController {
|
|||
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 regexp = /\/set_daily_eccm_user_comment (.+)/;
|
||||
private logger = new Logger(SetDailyEccmUserCommentBotHandlerService.name);
|
||||
private dateParamRegexp = /^date=([\d\-]+) (.+)$/;
|
||||
|
||||
$messages =
|
||||
new Subject<SetDailyEccmUserCommentBotHandler.Models.SetDailyEccmUserComment>();
|
||||
|
|
@ -43,46 +42,58 @@ export class SetDailyEccmUserCommentBotHandlerService
|
|||
msg.chat.id,
|
||||
);
|
||||
const redmineUserId = userMetaInfo.user_id;
|
||||
const data = this.parseDate(msg.text);
|
||||
this.logger.debug(
|
||||
`Setting user comment for daily eccm ` +
|
||||
`by redmineUserId = ${redmineUserId}, ` +
|
||||
`full text - ${msg.text}, ` +
|
||||
`parsed data - ${JSON.stringify(data)}`,
|
||||
);
|
||||
this.$messages.next({
|
||||
userId: redmineUserId,
|
||||
date: data.date,
|
||||
comment: data.msg,
|
||||
});
|
||||
return;
|
||||
const data = this.parseData(msg.text);
|
||||
if (data) {
|
||||
this.logger.debug(
|
||||
`Setting user comment for daily eccm ` +
|
||||
`by redmineUserId = ${redmineUserId}, ` +
|
||||
`full text - ${msg.text}, ` +
|
||||
`parsed data - ${JSON.stringify(data)}`,
|
||||
);
|
||||
this.$messages.next({
|
||||
userId: redmineUserId,
|
||||
date: data.date,
|
||||
comment: data.msg,
|
||||
});
|
||||
} else {
|
||||
this.logger.error(
|
||||
`For some reason, it was not possible to get data from an incoming message - ${msg.text}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getHelpMsg(): string {
|
||||
return (
|
||||
`/set_daily_eccm_user_comment ` +
|
||||
`[дата=yyyy-MM-dd] <комментарий> ` +
|
||||
`[date=yyyy-MM-dd] <комментарий> ` +
|
||||
`- дополнительный комментарий для дейли`
|
||||
);
|
||||
}
|
||||
|
||||
private parseDate(src: string): { date: string; msg: string } | null {
|
||||
let msgWithoutCommand: any = src.match(this.regexp);
|
||||
if (!msgWithoutCommand || !msgWithoutCommand[1]) {
|
||||
private parseData(src: string): { date: string; msg: string } | null {
|
||||
let text = src;
|
||||
|
||||
text = text.replace('/set_daily_eccm_user_comment', '').trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
msgWithoutCommand = msgWithoutCommand[1];
|
||||
const msgWithDate: any = msgWithoutCommand.match(this.dateParamRegexp);
|
||||
let date: any = '';
|
||||
if (msgWithDate && msgWithDate[1] && msgWithDate[2]) {
|
||||
date = msgWithDate[1];
|
||||
if (DateTime.fromFormat(date, ISO_DATE_FORMAT).isValid) {
|
||||
msgWithoutCommand = msgWithDate[2];
|
||||
} else {
|
||||
date = '';
|
||||
}
|
||||
|
||||
const dateMatch = text.match(/^date=[\d-]+/);
|
||||
if (!dateMatch) {
|
||||
return { date: DateTime.now().toFormat(ISO_DATE_FORMAT), msg: text };
|
||||
}
|
||||
return { date: date, msg: msgWithoutCommand };
|
||||
|
||||
const datePart = dateMatch[0];
|
||||
let dateRaw = datePart.replace('date=', '');
|
||||
text = text.replace('date=', '').trim();
|
||||
const date = DateTime.fromFormat(dateRaw, ISO_DATE_FORMAT);
|
||||
if (!date.isValid) {
|
||||
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,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"target": "es2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"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