diff --git a/Web/Models/Entities/BugReport.php b/Web/Models/Entities/BugReport.php new file mode 100644 index 00000000..d07011c7 --- /dev/null +++ b/Web/Models/Entities/BugReport.php @@ -0,0 +1,83 @@ +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); + } +} \ No newline at end of file diff --git a/Web/Models/Entities/BugReportComment.php b/Web/Models/Entities/BugReportComment.php new file mode 100644 index 00000000..6c777b4d --- /dev/null +++ b/Web/Models/Entities/BugReportComment.php @@ -0,0 +1,41 @@ +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; + } +} \ No newline at end of file diff --git a/Web/Models/Entities/BugtrackerProduct.php b/Web/Models/Entities/BugtrackerProduct.php new file mode 100644 index 00000000..a89c8596 --- /dev/null +++ b/Web/Models/Entities/BugtrackerProduct.php @@ -0,0 +1,40 @@ +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; + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/BugtrackerComments.php b/Web/Models/Repositories/BugtrackerComments.php new file mode 100644 index 00000000..cec614ed --- /dev/null +++ b/Web/Models/Repositories/BugtrackerComments.php @@ -0,0 +1,33 @@ +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); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/BugtrackerProducts.php b/Web/Models/Repositories/BugtrackerProducts.php new file mode 100644 index 00000000..1803880a --- /dev/null +++ b/Web/Models/Repositories/BugtrackerProducts.php @@ -0,0 +1,39 @@ +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); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/BugtrackerReports.php b/Web/Models/Repositories/BugtrackerReports.php new file mode 100644 index 00000000..46be8df0 --- /dev/null +++ b/Web/Models/Repositories/BugtrackerReports.php @@ -0,0 +1,61 @@ +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"])); + } +} \ No newline at end of file diff --git a/Web/Presenters/BugtrackerPresenter.php b/Web/Presenters/BugtrackerPresenter.php new file mode 100644 index 00000000..262f8041 --- /dev/null +++ b/Web/Presenters/BugtrackerPresenter.php @@ -0,0 +1,197 @@ +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", "Успех", "Вы отметили, что у Вас получилось воспроизвести этот баг."); + } +} \ No newline at end of file diff --git a/Web/Presenters/templates/Bugtracker/Index.xml b/Web/Presenters/templates/Bugtracker/Index.xml new file mode 100644 index 00000000..faea620d --- /dev/null +++ b/Web/Presenters/templates/Bugtracker/Index.xml @@ -0,0 +1,279 @@ +{extends "../@layout.xml"} + +{block title}Баг-трекер{/block} + +{block header} +Баг-трекер +{/block} + +{block content} +
+
+ + Отчёты + +
+
+ + Продукты + +
+
+ + Карточка тестировщика + +
+
+ + Создать + +
+
+ + Создать + +
+
+ +

+{if $mode === "list"} +{if $count < 1} + {include "../components/nothing.xml"} +{/if} + + + + + + + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Продукт:{$bug->getProduct()->getCanonicalName()}
Отправил: + {$bug->getReporter()->getCanonicalName()} +
Воспроизвелось:{tr("participants", $bug->getReproducedCount())}
Статус:{$bug->getStatus()}
Приоритет:{$bug->getPriority()}
+
+
+
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($iterator), + "perPage" => 5, + "atBottom" => true, + ]} +
+ +{elseif $mode === "new"} +
+ +

+ +

+ +

+

Приоритет

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + +

+
+ +{elseif $mode === "products"} + + + + + + + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + +
Название: + {$product->getCanonicalName()} + (закрыт) +
Создал: + {$product->getCreator()->getCanonicalName()} +
Дата создания:123
+
+
+ +{elseif $mode === "new_product"} +
+ +

+ + + +

+ +

+
+ +{elseif $mode === "reporter"} +
+
+
+ + + +
+
+ {$reporter->getCanonicalName()} +
тестировщик отправил {$reporter_stats[0]} отчётов, из них {$reporter_stats[1]} имеют положительный статус
+
+
+ + + + + + + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Продукт:{$bug->getProduct()->getCanonicalName()}
Отправил: + {$bug->getReporter()->getCanonicalName()} +
Воспроизвелось:{tr("participants", $bug->getReproducedCount())}
Статус:{$bug->getStatus()}
Приоритет:{$bug->getPriority()}
+
+
+
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($iterator), + "perPage" => 5, + "atBottom" => true, + ]} +
+
+
+
Пользователь не найден.
+
+{/if} +{/block} \ No newline at end of file diff --git a/Web/Presenters/templates/Bugtracker/View.xml b/Web/Presenters/templates/Bugtracker/View.xml new file mode 100644 index 00000000..f26a26d8 --- /dev/null +++ b/Web/Presenters/templates/Bugtracker/View.xml @@ -0,0 +1,107 @@ +{extends "../@layout.xml"} + +{block title}Отчёт #{$bug->getId()}{/block} + +{block header} +Баг-трекер +» Отчёт #{$bug->getId()} +{/block} + +{block content} +{if $bug} +

{$bug->getCanonicalName()}

+ +
+
+ + + +
+
+ {$reporter->getCanonicalName()} +
создано: {$bug->getCreationDate()}
+
+
+
+{$bug->getText()} +
+ + + + + + + + + + + + + + + + + + + + + + + +
Отправил: + {$bug->getReporter()->getCanonicalName()} +
Воспроизвелось:{tr("participants", $bug->getReproducedCount())}
Статус:{$bug->getStatus()}
Приоритет:{$bug->getPriority()}
Устройство:{$bug->getDevice()}
+
+ + +Воспроизвелось +{if sizeof($comments) > 0} +
+
+
+
+ + + +
+
+ + {$comment->isModer() ? "Модератор" : $comment->getAuthor()->getCanonicalName()} + + ({$comment->getAuthor()->getCanonicalName()}) + + + (скрытый комментарий) +
+ +
+ {$comment->getText()} +
+
+
+
+
+{/if} +
+
+
+
+ + + + + +
+ + + + +
+
+{else} +
+ Отчёт не найден. Возможно, он был удалён. +
+{/if} + +{/block} \ No newline at end of file diff --git a/Web/di.yml b/Web/di.yml old mode 100644 new mode 100755 index 1e47bbc1..3807476d --- a/Web/di.yml +++ b/Web/di.yml @@ -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 diff --git a/Web/routes.yml b/Web/routes.yml old mode 100644 new mode 100755 index 55225df3..d7c654ed --- a/Web/routes.yml +++ b/Web/routes.yml @@ -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" diff --git a/Web/static/js/openvk.cls.js b/Web/static/js/openvk.cls.js old mode 100644 new mode 100755 index 7532a1ad..47ffd360 --- a/Web/static/js/openvk.cls.js +++ b/Web/static/js/openvk.cls.js @@ -439,3 +439,97 @@ $(document).on("scroll", () => { }, 250); } }) + +function showBtStatusChangeDialog(report, hash) { + MessageBox("Измененить статус", `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Вы можете прокомментировать изменение статуса

+ + +
+ `, ["Сохранить", tr("cancel")], [ + () => { + $("#status_change_dialog").submit(); + }, + Function.noop + ]); +} + +function showBtPriorityChangeDialog(report, hash) { + MessageBox("Измененить приоритет", `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Вы можете прокомментировать изменение приоритета

+ + +
+ `, ["Сохранить", tr("cancel")], [ + () => { + $("#priority_change_dialog").submit(); + }, + Function.noop + ]); +} \ No newline at end of file