Вывод комментариев в отчёте дейли со ссылками на задачи в redmine

This commit is contained in:
Pavel Gnedov 2022-12-15 19:18:31 +07:00
parent e71491b637
commit 9b17d703ed
7 changed files with 225 additions and 31 deletions

View file

@ -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,
);
},
);
}
}

View file

@ -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('');
}
}

View 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;
}
}

View file

@ -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);
}
}

View file

@ -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,7 +42,8 @@ export class SetDailyEccmUserCommentBotHandlerService
msg.chat.id,
);
const redmineUserId = userMetaInfo.user_id;
const data = this.parseDate(msg.text);
const data = this.parseData(msg.text);
if (data) {
this.logger.debug(
`Setting user comment for daily eccm ` +
`by redmineUserId = ${redmineUserId}, ` +
@ -55,34 +55,45 @@ export class SetDailyEccmUserCommentBotHandlerService
date: data.date,
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 {
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 };
}
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;
}
return { date: date, msg: msgWithoutCommand };
text = text.replace(dateRaw, '').trim();
dateRaw = date.toFormat(ISO_DATE_FORMAT);
return { date: dateRaw, msg: text };
}
}

View file

@ -6,7 +6,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"target": "es2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",

View 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>