mirror of
https://github.com/openvk/openvk
synced 2024-12-24 01:21:07 +03:00
Баг-трекер
This commit is contained in:
parent
d1b878a5a4
commit
7e357e1708
12 changed files with 994 additions and 0 deletions
83
Web/Models/Entities/BugReport.php
Normal file
83
Web/Models/Entities/BugReport.php
Normal 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);
|
||||
}
|
||||
}
|
41
Web/Models/Entities/BugReportComment.php
Normal file
41
Web/Models/Entities/BugReportComment.php
Normal 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;
|
||||
}
|
||||
}
|
40
Web/Models/Entities/BugtrackerProduct.php
Normal file
40
Web/Models/Entities/BugtrackerProduct.php
Normal 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;
|
||||
}
|
||||
}
|
33
Web/Models/Repositories/BugtrackerComments.php
Normal file
33
Web/Models/Repositories/BugtrackerComments.php
Normal 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);
|
||||
}
|
||||
}
|
39
Web/Models/Repositories/BugtrackerProducts.php
Normal file
39
Web/Models/Repositories/BugtrackerProducts.php
Normal 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);
|
||||
}
|
||||
}
|
61
Web/Models/Repositories/BugtrackerReports.php
Normal file
61
Web/Models/Repositories/BugtrackerReports.php
Normal 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"]));
|
||||
}
|
||||
}
|
197
Web/Presenters/BugtrackerPresenter.php
Normal file
197
Web/Presenters/BugtrackerPresenter.php
Normal 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", "Успех", "Вы отметили, что у Вас получилось воспроизвести этот баг.");
|
||||
}
|
||||
}
|
279
Web/Presenters/templates/Bugtracker/Index.xml
Normal file
279
Web/Presenters/templates/Bugtracker/Index.xml
Normal 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}
|
107
Web/Presenters/templates/Bugtracker/View.xml
Normal file
107
Web/Presenters/templates/Bugtracker/View.xml
Normal 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
4
Web/di.yml
Normal file → Executable 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
16
Web/routes.yml
Normal file → Executable 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
94
Web/static/js/openvk.cls.js
Normal file → Executable 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
|
||||
]);
|
||||
}
|
Loading…
Reference in a new issue