Баг-трекер

This commit is contained in:
n1rwana 2022-08-20 20:30:04 +03:00
parent d1b878a5a4
commit 7e357e1708
12 changed files with 994 additions and 0 deletions

View file

@ -0,0 +1,83 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\{RowModel};
use openvk\Web\Models\Entities\{User, BugtrackerProduct};
use openvk\Web\Models\Repositories\{Users, BugtrackerProducts};
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Util\DateTime;
class BugReport extends RowModel
{
protected $tableName = "bugs";
function getId(): int
{
return $this->getRecord()->id;
}
function getReporter(): ?User
{
return (new Users)->get($this->getRecord()->reporter);
}
function getName(): string
{
return $this->getRecord()->title;
}
function getCanonicalName(): string
{
return $this->getName();
}
function getText(): string
{
return $this->getRecord()->text;
}
function getProduct(): ?BugtrackerProduct
{
return (new BugtrackerProducts)->get($this->getRecord()->product_id);
}
function getStatus(): string
{
$list = ["Открыт", "На рассмотрении", "В работе", "Исправлен", "Закрыт", "Требует корректировки", "Заблокирован", "Отклонён"];
$status_id = $this->getRecord()->status;
return $list[$status_id];
}
function getRawStatus(): ?int
{
return $this->getRecord()->status;
}
function getPriority(): string
{
$list = ["Пожелание", "Низкий", "Средний", "Высокий", "Критический", "Уязвимость"];
$priority_id = $this->getRecord()->priority;
return $list[$priority_id];
}
function getRawPriority(): ?int
{
return $this->getRecord()->priority;
}
function getDevice(): string
{
return $this->getRecord()->device;
}
function getReproducedCount(): ?int
{
return $this->getRecord()->reproduced;
}
function getCreationDate(): DateTime
{
return new DateTime($this->getRecord()->created);
}
}

View file

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\{RowModel};
use openvk\Web\Models\Entities\{User, BugtrackerProduct};
use openvk\Web\Models\Repositories\{Users, BugtrackerProducts};
use Chandler\Database\DatabaseConnection as DB;
class BugReportComment extends RowModel
{
protected $tableName = "bt_comments";
function getId(): int
{
return $this->getRecord()->id;
}
function getAuthor(): ?User
{
return (new Users)->get($this->getRecord()->author);
}
function isModer(): bool
{
return (bool) $this->getRecord()->is_moder;
}
function isHidden(): bool
{
return (bool) $this->getRecord()->is_hidden;
}
function getText(): string
{
return $this->getRecord()->text;
}
function getLabel(): string
{
return $this->getRecord()->label;
}
}

View file

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\{RowModel};
use openvk\Web\Models\Entities\{User};
use openvk\Web\Models\Repositories\{Users};
class BugtrackerProduct extends RowModel
{
protected $tableName = "bt_products";
function getId(): int
{
return $this->getRecord()->id;
}
function getCreator(): ?User
{
return (new Users)->get($this->getRecord()->creator_id);
}
function getName(): string
{
return $this->getRecord()->title;
}
function getCanonicalName(): string
{
return $this->getName();
}
function getDescription(): ?string
{
return $this->getRecord()->description;
}
function isClosed(): ?bool
{
return (bool) $this->getRecord()->closed;
}
}

View file

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\{BugReport, BugReportComment};
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
class BugtrackerComments
{
private $context;
private $comments;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->comments = $this->context->table("bt_comments");
}
private function toComment(?ActiveRow $ar)
{
return is_null($ar) ? NULL : new BugReportComment($ar);
}
function get(int $id): ?BugReportComment
{
return $this->toComment($this->comments->get($id));
}
function getByReport(?BugReport $report): \Traversable
{
foreach($this->comments->where(["report" => $report->getId()])->order("id ASC") as $comment)
yield new BugReportComment($comment);
}
}

View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\{BugtrackerProduct};
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
class BugtrackerProducts
{
private $context;
private $products;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->products = $this->context->table("bt_products");
}
private function toProduct(?ActiveRow $ar)
{
return is_null($ar) ? NULL : new BugtrackerProduct($ar);
}
function get(int $id): ?BugtrackerProduct
{
return $this->toProduct($this->products->get($id));
}
function getAll(): \Traversable
{
foreach($this->products->order("id ASC") as $product)
yield new BugtrackerProduct($product);
}
function getOpen(): \Traversable
{
foreach($this->products->where(["closed" => 0])->order("id ASC") as $product)
yield new BugtrackerProduct($product);
}
}

View file

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\{BugReport};
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
class BugtrackerReports
{
private $context;
private $reports;
private $reportsPerPage = 5;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->reports = $this->context->table("bugs");
}
private function toReport(?ActiveRow $ar)
{
return is_null($ar) ? NULL : new BugReport($ar);
}
function get(int $id): ?BugReport
{
return $this->toReport($this->reports->get($id));
}
function getAllReports(int $page = 1): \Traversable
{
foreach($this->reports->where(["deleted" => NULL])->order("created DESC")->page($page, 5) as $report)
yield new BugReport($report);
}
function getReports(int $product_id = 0, int $page = 1): \Traversable
{
foreach($this->reports->where(["deleted" => NULL, "product_id" => $product_id])->order("created DESC")->page($page, 5) as $report)
yield new BugReport($report);
}
function getReportsCount(): int
{
return sizeof($this->reports->where(["deleted" => NULL]));
}
function getByReporter(int $reporter_id, int $page = 1): \Traversable
{
foreach($this->reports->where(["deleted" => NULL, "reporter" => $reporter_id])->order("created DESC")->page($page, 5) as $report)
yield new BugReport($report);
}
function getCountByReporter(int $reporter_id)
{
return sizeof($this->reports->where(["deleted" => NULL, "reporter" => $reporter_id]));
}
function getSuccCountByReporter(int $reporter_id)
{
return sizeof($this->reports->where(["deleted" => NULL, "reporter" => $reporter_id, "status" => "<= 4"]));
}
}

View file

@ -0,0 +1,197 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use Chandler\Session\Session;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\{BugtrackerProducts, BugtrackerReports, BugtrackerComments, Users};
use openvk\Web\Models\Entities\{BugtrackerProduct, BugReport};
final class BugtrackerPresenter extends OpenVKPresenter
{
private $reports;
private $products;
private $comments;
function __construct(BugtrackerReports $reports, BugtrackerProducts $products, BugtrackerComments $comments)
{
$this->reports = $reports;
$this->products = $products;
$this->comments = $comments;
parent::__construct();
}
function renderIndex(): void
{
$this->assertUserLoggedIn();
$this->template->mode = in_array($this->queryParam("act"), ["list", "show", "products", "new_product", "reporter", "new"]) ? $this->queryParam("act") : "list";
$this->template->user = $this->user;
$this->template->all_products = $this->products->getAll();
$this->template->open_products = $this->products->getOpen();
if($this->template->mode === "reporter") {
$this->template->reporter = (new Users)->get((int) $this->queryParam("id"));
$this->template->reporter_stats = [$this->reports->getCountByReporter((int) $this->queryParam("id")), $this->reports->getSuccCountByReporter((int) $this->queryParam("id"))];
$this->template->page = (int) ($this->queryParam("p") ?? 1);
$this->template->iterator = $this->reports->getByReporter((int) $this->queryParam("id"));
$this->template->count = $this->reports->getCountByReporter((int) $this->queryParam("id"));
} else {
$this->template->page = (int) ($this->queryParam("p") ?? 1);
$this->template->count = $this->reports->getReportsCount(0);
$this->template->iterator = $this->reports->getAllReports($this->template->page);
}
$this->template->canAdminBugTracker = $this->user->identity->getChandlerUser()->can("admin")->model('openvk\Web\Models\Repositories\BugtrackerReports')->whichBelongsTo(NULL);
}
function renderView(int $id): void
{
$this->assertUserLoggedIn();
$this->template->user = $this->user;
if ($this->reports->get($id)) {
$this->template->bug = $this->reports->get($id);
$this->template->reporter = $this->template->bug->getReporter();
$this->template->comments = $this->comments->getByReport($this->template->bug);
$this->template->canAdminBugTracker = $this->user->identity->getChandlerUser()->can("admin")->model('openvk\Web\Models\Repositories\BugtrackerReports')->whichBelongsTo(NULL);
} else {
$this->flashFail("err", "Отчёт не найден. Возможно, он был удалён.");
}
}
function renderChangeStatus(int $report_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$status = $this->postParam("status");
$comment = $this->postParam("text");
$list = ["Открыт", "На рассмотрении", "В работе", "Исправлен", "Закрыт", "Требует корректировки", "Заблокирован", "Отклонён"];
$report = (new BugtrackerReports)->get($report_id);
$report->setStatus($status);
$report->save();
$this->createComment($report, $comment, "Новый статус отчёта — $list[$status]", TRUE);
$this->flashFail("succ", "Изменения сохранены", "Новый статус отчёта — $list[$status]");
}
function renderChangePriority(int $report_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$priority = $this->postParam("priority");
$comment = $this->postParam("text");
$list = ["Пожелание", "Низкий", "Средний", "Высокий", "Критический", "Уязвимость"];
$report = (new BugtrackerReports)->get($report_id);
$report->setPriority($priority);
$report->save();
$this->createComment($report, $comment, "Новый приоритет отчёта — $list[$priority]", TRUE);
$this->flashFail("succ", "Изменения сохранены", "Новый приоритет отчёта — $list[$priority]");
}
function createComment(?BugReport $report, string $text, string $label = "", bool $is_moder = FALSE, bool $is_hidden = FALSE)
{
$moder = $this->user->identity->getChandlerUser()->can("admin")->model('openvk\Web\Models\Repositories\BugtrackerReports')->whichBelongsTo(NULL);
if (!$text && !$label)
$this->flashFail("err", "Ошибка", "Комментарий не может быть пустым.");
if ($report->getRawStatus() == 6 && !$moder)
$this->flashFail("err", "Ошибка доступа");
DB::i()->getContext()->table("bt_comments")->insert([
"report" => $report->getId(),
"author" => $this->user->identity->getId(),
"is_moder" => $moder === $is_moder,
"is_hidden" => $moder === $is_hidden,
"text" => $text,
"label" => $label
]);
$this->flashFail("succ", "Успех", "Комментарий отправлен.");
}
function renderAddComment(int $report_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$text = $this->postParam("text");
$is_moder = (bool) $this->postParam("is_moder");
$is_hidden = (bool) $this->postParam("is_hidden");
$this->createComment($this->reports->get($report_id), $text, "", $is_moder, $is_hidden);
}
function renderCreate(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$title = $this->postParam("title");
$text = $this->postParam("text");
$priority = $this->postParam("priority");
$product = $this->postParam("product");
$device = $this->postParam("device");
if (!$title || !$text || !$priority || !$product || !$device)
$this->flashFail("err", "Ошибка", "Заполнены не все поля");
$id = DB::i()->getContext()->table("bugs")->insert([
"reporter" => $this->user->identity->getId(),
"title" => $title,
"text" => $text,
"product_id" => $product,
"device" => $device,
"priority" => $priority,
"created" => time()
]);
$this->redirect("/bug$id");
}
function renderCreateProduct(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$moder = $this->user->identity->getChandlerUser()->can("admin")->model('openvk\Web\Models\Repositories\BugtrackerReports')->whichBelongsTo(NULL);
if (!$moder)
$this->flashFail("err", "Ошибка доступа");
$title = $this->postParam("title");
$description = $this->postParam("description");
DB::i()->getContext()->table("bt_products")->insert([
"creator_id" => $this->user->identity->getId(),
"title" => $title,
"description" => $description
]);
$this->redirect("/bugtracker?act=products");
}
function renderReproduced(int $report_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$report = (new BugtrackerReports)->get($report_id);
if ($report->getReporter()->getId() === $this->user->identity->getId())
$this->flashFail("err", "Ошибка доступа");
DB::i()->getContext()->table("bugs")->where("id", $report_id)->update("reproduced", $report->getReproducedCount() + 1);
$this->flashFail("succ", "Успех", "Вы отметили, что у Вас получилось воспроизвести этот баг.");
}
}

View file

@ -0,0 +1,279 @@
{extends "../@layout.xml"}
{block title}Баг-трекер{/block}
{block header}
Баг-трекер
{/block}
{block content}
<div class="tabs">
<div n:attr='id => $mode === "list" ? "activetabs" : false' class="tab">
<a n:attr='id => $mode === "list" ? "act_tab_a" : false' href="/bugtracker">
Отчёты
</a>
</div>
<div n:attr='id => $mode === "products" ? "activetabs" : false' class="tab">
<a n:attr='id => $mode === "products" ? "act_tab_a" : false' href="/bugtracker?act=products">
Продукты
</a>
</div>
<div n:attr='id => $mode === "reporter" ? "activetabs" : false' class="tab">
<a n:attr='id => $mode === "reporter" ? "act_tab_a" : false' href="/bugtracker?act=reporter&id={$user->id}">
Карточка тестировщика
</a>
</div>
<div n:if='in_array($mode, ["list", "new"])' n:attr='id => $mode === "new" ? "activetabs" : false' class="tab" style="float: right;">
<a n:attr='id => $mode === "new" ? "act_tab_a" : false' href="/bugtracker?act=new">
Создать
</a>
</div>
<div n:if='in_array($mode, ["products", "new_product"])' n:attr='id => $mode === "new_product" ? "activetabs" : false' class="tab" style="float: right;">
<a n:attr='id => $mode === "new_product" ? "act_tab_a" : false' href="/bugtracker?act=new_product">
Создать
</a>
</div>
</div>
</div>
<br><br>
{if $mode === "list"}
{if $count < 1}
{include "../components/nothing.xml"}
{/if}
<table border="0" style="font-size: 11px; width: 100%;" class="post">
<tbody>
<tr n:foreach="$iterator as $bug">
<td width="54" >
<center>
<img src="/assets/packages/static/openvk/img/note_icon.png">
</center>
</td>
<td width="92%" valign="top">
<div class="post-author" href="#">
<a href="/bug{$bug->getId()}">
<b>{$bug->getCanonicalName()}</b>
<span>#{$bug->getId()}</span>
</a>
</div>
<div class="post-content" style="padding: 4px 0; font-size: 11px;">
<table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr>
<td class="label"><span class="nobold">Продукт:</span></td>
<td class="data"><a href="#">{$bug->getProduct()->getCanonicalName()}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Отправил: </span></td>
<td class="data">
<a href="/bugtracker?act=reporter&id={$bug->getReporter()->getId()}">{$bug->getReporter()->getCanonicalName()}</a>
</td>
</tr>
<tr>
<td class="label"><span class="nobold">Воспроизвелось:</span></td>
<td class="data"><a href="#">{tr("participants", $bug->getReproducedCount())}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Статус:</span></td>
<td class="data"><a href="#">{$bug->getStatus()}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Приоритет:</span></td>
<td class="data"><a href="#">{$bug->getPriority()}</a></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div style="padding: 8px;">
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($iterator),
"perPage" => 5,
"atBottom" => true,
]}
</div>
{elseif $mode === "new"}
<form method="post" action="/bugtracker/create">
<input name="title" type="text" placeholder="Название отчёта">
<br><br>
<textarea placeholder="Описание бага" name="text" style="width: 100%;resize: vertical;"></textarea>
<br><br>
<select name="product">
<option disabled>Продукт</option>
<option n:foreach="$open_products as $product" value="{$product->getId()}">{$product->getCanonicalName()}</option>
</select>
<br><br>
<h4>Приоритет</h4>
<table>
<tbody>
<tr>
<td><input type="radio" name="priority" value="0"></td>
<td><label for="priority_1">Пожелание</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="1"></td>
<td><label for="priority_2">Низкий</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="2"></td>
<td><label for="priority_3">Средний</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="3"></td>
<td><label for="priority_4">Высокий</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="4"></td>
<td><label for="priority_5">Критический</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="5"></td>
<td><label for="priority_6">Уязвимость</label></td>
</tr>
</tbody>
</table>
<br>
<input type="text" name="device" placeholder="Устройство">
<br>
<br>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_write}" class="button" style="float: right;" />
<br><br>
</form>
{elseif $mode === "products"}
<table border="0" style="font-size: 11px; width: 100%;" class="post">
<tbody>
<tr n:foreach="$all_products as $product">
<td width="54" >
<center>
<img src="/assets/packages/static/openvk/img/note_icon.png">
</center>
</td>
<td width="92%" valign="top">
<div class="post-author" href="#">
<a href="#">
<b>{$product->getCanonicalName()}</b>
<span>#{$product->getId()}</span>
</a>
</div>
<div class="post-content" style="padding: 4px 0; font-size: 11px;">
<table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr>
<td class="label"><span class="nobold">Название:</span></td>
<td class="data">
<a href="#">{$product->getCanonicalName()}</a>
<b n:if="$product->isClosed()">(закрыт)</b>
</td>
</tr>
<tr n:if="$canAdminBugTracker">
<td class="label"><span class="nobold">Создал: </span></td>
<td class="data">
<a href="/bugtracker?act=reporter&id={$product->getCreator()->getId()}">{$product->getCreator()->getCanonicalName()}</a>
</td>
</tr>
<tr>
<td class="label"><span class="nobold">Дата создания:</span></td>
<td class="data"><a href="#">123</a></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
{elseif $mode === "new_product"}
<form method="post" action="/bugtracker/createProduct">
<input type="text" name="title" placeholder="Название">
<br><br>
<textarea placeholder="Описание продукта" name="description" style="width: 100%; resize: vertical;"></textarea>
<input type="hidden" name="hash" value="{$csrfToken}" />
<br><br>
<input type="submit" value="Создать" class="button" style="float: right;" />
<br><br>
</form>
{elseif $mode === "reporter"}
<div n:if="$reporter">
<div class="avatar-list-item" style="padding: 8px;">
<div class="avatar">
<a href="{$reporter->getURL()}">
<img class="ava" src="{$reporter->getAvatarURL()}">
</a>
</div>
<div class="info" style="width: 92%">
<a href="{$reporter->getURL()}" class="title">{$reporter->getCanonicalName()}</a>
<div>тестировщик отправил {$reporter_stats[0]} отчётов, из них {$reporter_stats[1]} имеют положительный статус</div>
</div>
</div>
<table border="0" style="font-size: 11px; width: 100%;" class="post">
<tbody>
<tr n:foreach="$iterator as $bug">
<td width="54" >
<center>
<img src="/assets/packages/static/openvk/img/note_icon.png">
</center>
</td>
<td width="92%" valign="top">
<div class="post-author" href="#">
<a href="/bug{$bug->getId()}">
<b>{$bug->getCanonicalName()}</b>
<span>#{$bug->getId()}</span>
</a>
</div>
<div class="post-content" style="padding: 4px 0; font-size: 11px;">
<table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr>
<td class="label"><span class="nobold">Продукт:</span></td>
<td class="data"><a href="#">{$bug->getProduct()->getCanonicalName()}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Отправил: </span></td>
<td class="data">
<a href="/bugtracker?act=reporter&id={$bug->getReporter()->getId()}">{$bug->getReporter()->getCanonicalName()}</a>
</td>
</tr>
<tr>
<td class="label"><span class="nobold">Воспроизвелось:</span></td>
<td class="data"><a href="#">{tr("participants", $bug->getReproducedCount())}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Статус:</span></td>
<td class="data"><a href="#">{$bug->getStatus()}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Приоритет:</span></td>
<td class="data"><a href="#">{$bug->getPriority()}</a></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div style="padding: 8px;">
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($iterator),
"perPage" => 5,
"atBottom" => true,
]}
</div>
</div>
<div n:if="!$reporter">
<center>Пользователь не найден.</center>
</div>
{/if}
{/block}

View file

@ -0,0 +1,107 @@
{extends "../@layout.xml"}
{block title}Отчёт #{$bug->getId()}{/block}
{block header}
<a href="/bugtracker">Баг-трекер</a>
» Отчёт #{$bug->getId()}
{/block}
{block content}
{if $bug}
<h4>{$bug->getCanonicalName()}</h4>
<div class="avatar-list-item" style="padding: 8px;">
<div class="avatar">
<a href="/bugtracker?act=reporter&id={$reporter->getId()}">
<img class="ava" src="{$reporter->getAvatarURL()}">
</a>
</div>
<div class="info" style="width: 92%">
<a href="/bugtracker?act=reporter&id={$reporter->getId()}" class="title">{$reporter->getCanonicalName()}</a>
<div class="subtitle">создано: {$bug->getCreationDate()}</div>
</div>
</div>
<hr color="#DAE1E8" size="1">
{$bug->getText()}
<hr color="#DAE1E8" size="1">
<table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0" style="width: 100%;">
<tbody>
<tr>
<td class="label"><span class="nobold">Отправил: </span></td>
<td class="data">
<a href="/bugtracker?act=reporter&id={$bug->getReporter()->getId()}">{$bug->getReporter()->getCanonicalName()}</a>
</td>
</tr>
<tr>
<td class="label"><span class="nobold">Воспроизвелось:</span></td>
<td class="data"><a href="#">{tr("participants", $bug->getReproducedCount())}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Статус:</span></td>
<td class="data"><a href="#" n:attr='onClick => $canAdminBugTracker ? "showBtStatusChangeDialog({$bug->getId()}, \"{$csrfToken}\");" : false'>{$bug->getStatus()}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Приоритет:</span></td>
<td class="data"><a href="#" n:attr='onClick => $canAdminBugTracker ? "showBtPriorityChangeDialog({$bug->getId()}, \"{$csrfToken}\");" : false'>{$bug->getPriority()}</a></td>
</tr>
<tr>
<td class="label"><span class="nobold">Устройство:</span></td>
<td class="data"><a href="#">{$bug->getDevice()}</a></td>
</tr>
</tbody>
</table>
<hr color="#DAE1E8" size="1">
<button n:if="$canAdminBugTracker" class="button" onClick="showBtStatusChangeDialog({$bug->getId()}, {$csrfToken})">Изменить статус</button>
<button n:if="$canAdminBugTracker" class="button" onClick="showBtPriorityChangeDialog({$bug->getId()}, {$csrfToken})">Изменить приоритет</button>
<a n:if="$bug->getReporter()->getId() !== $user->identity->getId()" class="button" href="/bug{$bug->getId()}/reproduce">Воспроизвелось</a>
{if sizeof($comments) > 0}
<hr color="#DAE1E8" size="1">
<div n:foreach="$comments as $comment">
<div n:if="!$comment->isHidden() OR $comment->isHidden() AND $canAdminBugTracker" class="avatar-list-item" style="padding: 8px;">
<div class="avatar">
<a href="/bugtracker?act=reporter&id={$comment->getAuthor()->getId()}">
<img class="ava" src="{$comment->isModer() ? 'https://vk.com/images/support15_specagent.png' : $comment->getAuthor()->getAvatarURL()}">
</a>
</div>
<div class="info" style="width: 90%;">
<a n:attr='href => $comment->isModer() ? false : "/bugtracker?act=reporter&id={$comment->getAuthor()->getId()}"' class="title">
{$comment->isModer() ? "Модератор" : $comment->getAuthor()->getCanonicalName()}
<a n:if="$comment->isModer() AND $canAdminBugTracker" href="{$comment->getAuthor()->getURL()}">
(<b>{$comment->getAuthor()->getCanonicalName()}</b>)
</a>
</a>
<b n:if="$comment->isHidden() AND $canAdminBugTracker">(скрытый комментарий)</b>
<br>
<b n:if="$comment->getLabel()" class="post-author" style="display: inline-block; border-top: 0;">{$comment->getLabel()}</b>
<div>
{$comment->getText()}
</div>
</div>
</div>
<hr color="#DAE1E8" size="1">
</div>
{/if}
<form n:if="$bug->getRawStatus() != 6 OR $bug->getRawStatus() == 6 AND $canAdminBugTracker" method="post" action="/bug{$bug->getId()}/addComment">
<textarea name="text" style="width: 100%;resize: vertical;"></textarea><br />
<div style="float: right;">
<div n:if="$canAdminBugTracker" style="display: inline;">
<input id="is_moder" type="checkbox" name="is_moder">
<label for="is_moder">От лица модератора</label>
<input id="is_hidden" type="checkbox" name="is_hidden">
<label for="is_hidden">Скрытый комментарий</label>
</div>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_write}" class="button" />
</div>
</form>
{else}
<div>
Отчёт не найден. Возможно, он был удалён.
</div>
{/if}
{/block}

4
Web/di.yml Normal file → Executable file
View file

@ -16,6 +16,7 @@ services:
- openvk\Web\Presenters\UnknownTextRouteStrategyPresenter
- openvk\Web\Presenters\NotificationPresenter
- openvk\Web\Presenters\SupportPresenter
- openvk\Web\Presenters\BugtrackerPresenter
- openvk\Web\Presenters\AdminPresenter
- openvk\Web\Presenters\GiftsPresenter
- openvk\Web\Presenters\MessengerPresenter
@ -30,6 +31,9 @@ services:
- openvk\Web\Models\Repositories\Videos
- openvk\Web\Models\Repositories\Notes
- openvk\Web\Models\Repositories\Tickets
- openvk\Web\Models\Repositories\BugtrackerReports
- openvk\Web\Models\Repositories\BugtrackerProducts
- openvk\Web\Models\Repositories\BugtrackerComments
- openvk\Web\Models\Repositories\Messages
- openvk\Web\Models\Repositories\Restores
- openvk\Web\Models\Repositories\Verifications

16
Web/routes.yml Normal file → Executable file
View file

@ -31,6 +31,22 @@ routes:
handler: "Comment->makeComment"
- url: "/support/delete/{num}"
handler: "Support->delete"
- url: "/bugtracker"
handler: "Bugtracker->index"
- url: "/bug{num}"
handler: "Bugtracker->view"
- url: "/bug{num}/setStatus"
handler: "Bugtracker->changeStatus"
- url: "/bug{num}/setPriority"
handler: "Bugtracker->changePriority"
- url: "/bug{num}/addComment"
handler: "Bugtracker->addComment"
- url: "/bugtracker/create"
handler: "Bugtracker->create"
- url: "/bugtracker/createProduct"
handler: "Bugtracker->createProduct"
- url: "/bug{num}/reproduce"
handler: "Bugtracker->reproduced"
- url: "/language"
handler: "About->language"
- url: "/language/{text}.js"

94
Web/static/js/openvk.cls.js Normal file → Executable file
View file

@ -439,3 +439,97 @@ $(document).on("scroll", () => {
}, 250);
}
})
function showBtStatusChangeDialog(report, hash) {
MessageBox("Измененить статус", `<form action="/bug${report}/setStatus" method="post" id="status_change_dialog">
<table>
<tbody>
<tr>
<td><input type="radio" name="status" value="0"></td>
<td><label for="status_1">Открыт</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="1"></td>
<td><label for="status_2">На рассмотрении</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="2"></td>
<td><label for="status_3">В работе</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="3"></td>
<td><label for="status_3">Исправлен</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="4"></td>
<td><label for="status_3">Закрыт</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="5"></td>
<td><label for="status_3">Требует корректировки</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="6"></td>
<td><label for="status_3">Заблокирован</label></td>
</tr>
<tr>
<td><input type="radio" name="status" value="7"></td>
<td><label for="status_3">Отклонён</label></td>
</tr>
</tbody>
</table>
<br>
<h4>Вы можете прокомментировать изменение статуса</h4>
<textarea name="text" style="width: 100%;resize: vertical;"></textarea>
<input type="hidden" name="hash" value="${hash}" />
</form>
`, ["Сохранить", tr("cancel")], [
() => {
$("#status_change_dialog").submit();
},
Function.noop
]);
}
function showBtPriorityChangeDialog(report, hash) {
MessageBox("Измененить приоритет", `<form action="/bug${report}/setPriority" method="post" id="priority_change_dialog">
<table>
<tbody>
<tr>
<td><input type="radio" name="priority" value="0"></td>
<td><label for="priority_1">Пожелание</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="1"></td>
<td><label for="priority_2">Низкий</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="2"></td>
<td><label for="priority_3">Средний</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="3"></td>
<td><label for="priority_4">Высокий</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="4"></td>
<td><label for="priority_5">Критический</label></td>
</tr>
<tr>
<td><input type="radio" name="priority" value="5"></td>
<td><label for="priority_6">Уязвимость</label></td>
</tr>
</tbody>
</table>
<br>
<h4>Вы можете прокомментировать изменение приоритета</h4>
<textarea name="text" style="width: 100%;resize: vertical;"></textarea>
<input type="hidden" name="hash" value="${hash}" />
</form>
`, ["Сохранить", tr("cancel")], [
() => {
$("#priority_change_dialog").submit();
},
Function.noop
]);
}