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}
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ {include "../components/paginator.xml", conf => (object) [
+ "page" => $page,
+ "count" => $count,
+ "amount" => sizeof($iterator),
+ "perPage" => 5,
+ "atBottom" => true,
+ ]}
+
+
+{elseif $mode === "new"}
+
+
+{elseif $mode === "products"}
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+{elseif $mode === "new_product"}
+
+
+{elseif $mode === "reporter"}
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ {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()}
+
+
+
+{$bug->getText()}
+
+
+
+
+
+Воспроизвелось
+{if sizeof($comments) > 0}
+
+
+{/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