diff --git a/Web/Models/Entities/BugtrackerPrivateProduct.php b/Web/Models/Entities/BugtrackerPrivateProduct.php new file mode 100644 index 00000000..df9c3caf --- /dev/null +++ b/Web/Models/Entities/BugtrackerPrivateProduct.php @@ -0,0 +1,42 @@ +get($this->getId()); + } + + function getName(): string + { + return $this->toProduct()->getName(); + } + + function isClosed(): ?bool + { + return $this->toProduct()->isClosed(); + } + + function getCreator(): ?User + { + return $this->toProduct()->getCreator(); + } + + function isPrivate(): ?bool + { + return true; + } + + function getModerator(): ?User + { + return (new Users)->get($this->getRecord("moderator")); + } +} \ No newline at end of file diff --git a/Web/Models/Entities/BugtrackerProduct.php b/Web/Models/Entities/BugtrackerProduct.php index 264b7f22..5798f007 100644 --- a/Web/Models/Entities/BugtrackerProduct.php +++ b/Web/Models/Entities/BugtrackerProduct.php @@ -1,6 +1,7 @@ getRecord()->created); } + + function isPrivate(): ?bool + { + return (bool) $this->getRecord()->private; + } + + function hasAccess(User $user): bool + { + if ($user->isBtModerator() || !$this->isPrivate()) + return true; + + $check = DB::i()->getContext()->table("bt_products_access")->where([ + "tester" => $user->getId(), + "product" => $this->getId() + ]); + + if (sizeof($check) > 0) + return true; + + return false; + } } \ No newline at end of file diff --git a/Web/Models/Repositories/BugtrackerPrivateProducts.php b/Web/Models/Repositories/BugtrackerPrivateProducts.php new file mode 100644 index 00000000..f09dce86 --- /dev/null +++ b/Web/Models/Repositories/BugtrackerPrivateProducts.php @@ -0,0 +1,58 @@ +context = DatabaseConnection::i()->getContext(); + $this->private_products = $this->context->table("bt_products_access"); + } + + private function toPrivateProduct(?ActiveRow $ar) + { + return is_null($ar) ? NULL : new BugtrackerPrivateProduct($ar); + } + + function get(int $id): ?BugtrackerPrivateProduct + { + return $this->toPrivateProduct($this->private_products->get($id)); + } + + function getAll(int $page = 1): \Traversable + { + $products = $this->private_products + ->order("created DESC") + ->page($page, 5); + + foreach($products as $product) + yield (new BugtrackerProducts)->get($product->product); + } + + function getForUser(User $user, int $page = 1): \Traversable + { + + $products = $this->private_products + ->where(["tester" => $user->getId()]) + ->order("id ASC") + ->page($page, 5); + + foreach($products as $product) + yield (new BugtrackerProducts)->get($product->product); + } + + function getCount(User $user): ?int + { + if ($user->isBtModerator()) + return sizeof($this->getAll()); + + return sizeof($this->private_products->where(["tester" => $user->getId()])); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/BugtrackerProducts.php b/Web/Models/Repositories/BugtrackerProducts.php index 0bcebd5a..f74d20c3 100644 --- a/Web/Models/Repositories/BugtrackerProducts.php +++ b/Web/Models/Repositories/BugtrackerProducts.php @@ -1,6 +1,8 @@ context = DatabaseConnection::i()->getContext(); - $this->products = $this->context->table("bt_products"); + $this->products = $this->context->table("bt_products"); } private function toProduct(?ActiveRow $ar) @@ -27,18 +30,106 @@ class BugtrackerProducts function getAll(int $page = 1): \Traversable { - foreach($this->products->order("id ASC")->page($page, 5) as $product) + $products = $this->products + ->where(["private" => 0]) + ->order("created DESC") + ->page($page, 5); + + foreach($products as $product) yield new BugtrackerProduct($product); } - function getOpen(int $page = 1): \Traversable + function getOpen(int $page = 1, bool $private = FALSE): \Traversable { - foreach($this->products->where(["closed" => 0])->order("id ASC")->page($page, 5) as $product) + $products = $this->products + ->where([ + "closed" => 0, + "private" => $private + ]) + ->order("id ASC") + ->page($page, 5); + + foreach($products as $product) yield new BugtrackerProduct($product); } - function getCount(): ?int + function getClosed(int $page = 1, bool $private = FALSE): \Traversable { - return sizeof($this->products->where(["closed" => 0])); + $products = $this->products + ->where([ + "closed" => 1, + "private" => $private + ]) + ->order("id ASC") + ->page($page, 5); + + foreach($products as $product) + yield new BugtrackerProduct($product); + } + + function getPrivate(int $page = 1): \Traversable + { + $products = $this->products + ->where([ + "private" => 1 + ]) + ->order("id ASC") + ->page($page, 5); + + foreach($products as $product) + yield new BugtrackerProduct($product); + } + + function getPrivateForUser(User $user, int $page = 1): \Traversable + { + if (!$user->isBtModerator()) { + return (new BugtrackerPrivateProducts)->getForUser($user, $page); + } else { + return (new BugtrackerPrivateProducts)->getAll($page); + } + } + + function getFiltered(User $tester, string $type = "all", int $page = 1): \Traversable + { + switch ($type) { + case 'open': + return $this->getOpen($page); + break; + + case 'closed': + return $this->getClosed($page); + break; + + case 'private': + if ($tester->isBtModerator()) + return $this->getPrivate($page); + + return (new BugtrackerPrivateProducts)->getForUser($tester, $page); + break; + + default: + return $this->getAll($page); + break; + } + } + + function getCount(string $filter = "all", User $user = NULL): ?int + { + switch ($filter) { + case 'open': + return sizeof($this->products->where(["closed" => 0, "private" => 0])); + break; + + case 'closed': + return sizeof($this->products->where(["closed" => 1, "private" => 0])); + + case 'private': + return (new BugtrackerPrivateProducts)->getCount($user); + break; + + default: + return sizeof($this->products->where(["private" => 0])); + break; + } } } \ No newline at end of file diff --git a/Web/Models/Repositories/BugtrackerReports.php b/Web/Models/Repositories/BugtrackerReports.php index 22170d5d..4fec172c 100644 --- a/Web/Models/Repositories/BugtrackerReports.php +++ b/Web/Models/Repositories/BugtrackerReports.php @@ -1,6 +1,7 @@ toReport($this->reports->get($id)); } - function getAllReports(int $page = 1): \Traversable + function getAllReports(User $user, int $page = 1): \Traversable { - foreach($this->reports->where(["deleted" => NULL])->order("created DESC")->page($page, 5) as $report) + $reports = $this->reports->where(["deleted" => NULL])->order("created DESC")->page($page, 5); + + foreach($reports as $report) yield new BugReport($report); } - function getReports(int $product_id = 0, int $priority = 0, int $page = 1): \Traversable + function getReports(int $product_id = 0, int $priority = 0, int $page = 1, User $user = NULL): \Traversable { $filter = ["deleted" => NULL]; $product_id && $filter["product_id"] = $product_id; $priority && $filter["priority"] = $priority; + $product = (new BugtrackerProducts)->get($product_id); + if (!$product->hasAccess($user)) + return false; + foreach($this->reports->where($filter)->order("created DESC")->page($page, 5) as $report) yield new BugReport($report); } diff --git a/Web/Presenters/BugtrackerPresenter.php b/Web/Presenters/BugtrackerPresenter.php index d269e662..94f62af3 100644 --- a/Web/Presenters/BugtrackerPresenter.php +++ b/Web/Presenters/BugtrackerPresenter.php @@ -43,15 +43,16 @@ final class BugtrackerPresenter extends OpenVKPresenter break; case 'products': - $this->template->count = $this->products->getCount(); - $this->template->iterator = $this->products->getAll($this->template->page); + $this->template->filter = $this->queryParam("filter") ?? "all"; + $this->template->count = $this->products->getCount($this->template->filter, $this->user->identity); + $this->template->iterator = $this->products->getFiltered($this->user->identity, $this->template->filter, $this->template->page); break; default: - $this->template->count = $this->reports->getReportsCount((int) $this->queryParam("product"), (int) $this->queryParam("priority")); + $this->template->count = $this->reports->getReportsCount((int) $this->queryParam("product"), (int) $this->queryParam("priority"), $this->user->identity); $this->template->iterator = $this->queryParam("product") - ? $this->reports->getReports((int) $this->queryParam("product"), (int) $this->queryParam("priority"), $this->template->page) - : $this->reports->getAllReports($this->template->page); + ? $this->reports->getReports((int) $this->queryParam("product"), (int) $this->queryParam("priority"), $this->template->page, $this->user->identity) + : $this->reports->getAllReports($this->user->identity, $this->template->page); break; } @@ -64,6 +65,9 @@ final class BugtrackerPresenter extends OpenVKPresenter $this->template->user = $this->user; + if (!$this->reports->get($id)->getProduct()->hasAccess($this->template->user->identity)) + $this->flashFail("err", tr("forbidden")); + if ($this->reports->get($id)) { $this->template->bug = $this->reports->get($id); $this->template->reporter = $this->template->bug->getReporter(); @@ -203,19 +207,21 @@ final class BugtrackerPresenter extends OpenVKPresenter $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - $moder = $this->user->identity->isBtModerator(); - - if (!$moder) + if (!$this->user->identity->isBtModerator()) $this->flashFail("err", tr("forbidden")); $title = $this->postParam("title"); $description = $this->postParam("description"); + $is_closed = (bool) $this->postParam("is_closed"); + $is_private = (bool) $this->postParam("is_private"); DB::i()->getContext()->table("bt_products")->insert([ "creator_id" => $this->user->identity->getId(), "title" => $title, "description" => $description, - "created" => time() + "created" => time(), + "closed" => $is_closed, + "private" => $is_private ]); $this->redirect("/bugtracker?act=products"); @@ -235,4 +241,102 @@ final class BugtrackerPresenter extends OpenVKPresenter $this->flashFail("succ", tr("bug_tracker_success"), tr("bug_tracker_reproduced_text")); } + + function renderManageAccess(int $product_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if (!$this->user->identity->isBtModerator()) + $this->flashFail("err", tr("forbidden")); + + $user = (new Users)->get((int) $this->postParam("uid")); + $product = $this->products->get($product_id); + $action = $this->postParam("action"); + + if ($action === "give") { + if (!$product->isPrivate() || $product->hasAccess($user)) + $this->flashFail("err", "Ошибка", $user->getCanonicalName() . " уже имеет доступ к продукту " . $product->getCanonicalName()); + + DB::i()->getContext()->table("bt_products_access")->insert([ + "created" => time(), + "tester" => $user->getId(), + "product" => $product_id, + "moderator" => $this->user->identity->getId() + ]); + + $this->flashFail("succ", "Успех", $user->getCanonicalName() . " теперь имеет доступ к продукту " . $product->getCanonicalName()); + } else { + if ($user->isBtModerator()) + $this->flashFail("err", "Ошибка", "Невозможно забрать доступ к продукту у модератора."); + + if (!$product->hasAccess($user)) + $this->flashFail("err", "Ошибка", $user->getCanonicalName() . " и так не имеет доступа к продукту " . $product->getCanonicalName()); + + DB::i()->getContext()->table("bt_products_access")->where([ + "tester" => $user->getId(), + "product" => $product_id, + ])->delete(); + + $this->flashFail("succ", "Успех", $user->getCanonicalName() . " теперь не имеет доступа к продукту " . $product->getCanonicalName()); + } + } + + function renderManagePrivacy(int $product_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if (!$this->user->identity->isBtModerator()) + $this->flashFail("err", tr("forbidden")); + + $user = (new Users)->get((int) $this->postParam("uid")); + $product = $this->products->get($product_id); + $action = $this->postParam("action"); + + if ($action == "open") { + if (!$product->isPrivate()) + $this->flashFail("err", "Ошибка", "Продукт " . $product->getCanonicalName() . " и так открытый."); + + DB::i()->getContext()->table("bt_products")->where("id", $product_id)->update(["private" => 0]); + + $this->flashFail("succ", "Успех", "Продукт " . $product->getCanonicalName() . " теперь открытый."); + } else { + if ($product->isPrivate()) + $this->flashFail("err", "Ошибка", "Продукт " . $product->getCanonicalName() . " и так приватный."); + + DB::i()->getContext()->table("bt_products")->where("id", $product_id)->update(["private" => 1]); + + $this->flashFail("succ", "Успех", "Продукт " . $product->getCanonicalName() . " теперь приватный."); + } + } + + function renderManageStatus(int $product_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if (!$this->user->identity->isBtModerator()) + $this->flashFail("err", tr("forbidden")); + + $user = (new Users)->get((int) $this->postParam("uid")); + $product = $this->products->get($product_id); + $action = $this->postParam("action"); + + if ($action == "open") { + if (!$product->isClosed()) + $this->flashFail("err", "Ошибка", "Продукт " . $product->getCanonicalName() . " и так открытый."); + + DB::i()->getContext()->table("bt_products")->where("id", $product_id)->update(["closed" => 0]); + + $this->flashFail("succ", "Успех", "Продукт " . $product->getCanonicalName() . " теперь открытый."); + } else { + if ($product->isClosed()) + $this->flashFail("err", "Ошибка", "Продукт " . $product->getCanonicalName() . " и так закрытый."); + + DB::i()->getContext()->table("bt_products")->where("id", $product_id)->update(["closed" => 1]); + + $this->flashFail("succ", "Успех", "Продукт " . $product->getCanonicalName() . " теперь закрытый."); + } + } } \ No newline at end of file diff --git a/Web/Presenters/templates/Bugtracker/Index.xml b/Web/Presenters/templates/Bugtracker/Index.xml index 89e9bb55..ff3e45dc 100644 --- a/Web/Presenters/templates/Bugtracker/Index.xml +++ b/Web/Presenters/templates/Bugtracker/Index.xml @@ -28,7 +28,7 @@ {_create} -
+
{_create} @@ -43,20 +43,23 @@ - @@ -151,10 +206,36 @@ {elseif $mode === "products"} +
+ + + + +
+
+{if $count < 1} + {include "../components/nothing.xml"} +{/if}
+ {var $isHidden = !$bug->getProduct()->hasAccess($user->identity)} +
- +
@@ -86,6 +89,58 @@
{_bug_tracker_product}:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {_bug_tracker_product}: + + AbcdEFGH +
+ {_bug_tracker_sent_by}: + + John Doe +
+ {_bug_tracker_reproduced}: + + ... +
+ {_status}: + + ... +
+ {_bug_tracker_priority}: + + ... +
+ {_created}: + + ... +
-
+
@@ -167,6 +248,10 @@
+
+ {$product->getDescription()} +
+
@@ -186,6 +271,28 @@ + + + +
{_bug_tracker_product_creation_date}: {$product->getCreationTime()}
действия: + + + закрытый + + ・ + + приватный + + ・ + + доступ + + +
@@ -208,7 +315,13 @@

+
+ + + + +


diff --git a/Web/di.yml b/Web/di.yml index 3807476d..b2878983 100755 --- a/Web/di.yml +++ b/Web/di.yml @@ -33,6 +33,7 @@ services: - openvk\Web\Models\Repositories\Tickets - openvk\Web\Models\Repositories\BugtrackerReports - openvk\Web\Models\Repositories\BugtrackerProducts + - openvk\Web\Models\Repositories\BugtrackerPrivateProducts - openvk\Web\Models\Repositories\BugtrackerComments - openvk\Web\Models\Repositories\Messages - openvk\Web\Models\Repositories\Restores diff --git a/Web/routes.yml b/Web/routes.yml index c8814ea3..5f069de5 100755 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -49,6 +49,12 @@ routes: handler: "Bugtracker->createProduct" - url: "/bug{num}/reproduce" handler: "Bugtracker->reproduced" + - url: "/bt_product{num}/manageAccess" + handler: "Bugtracker->manageAccess" + - url: "/bt_product{num}/managePrivacy" + handler: "Bugtracker->managePrivacy" + - url: "/bt_product{num}/manageStatus" + handler: "Bugtracker->manageStatus" - 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 index 2282b266..4b5e6821 100755 --- a/Web/static/js/openvk.cls.js +++ b/Web/static/js/openvk.cls.js @@ -550,4 +550,110 @@ function showBtPriorityChangeDialog(report, currentBalance, hash) { }, Function.noop ]); +} + +function showBtGiveProductAccessDialog(product, hash) { + MessageBox("Выдать доступ", `
+
+ Выдать пользователю ID  + +   доступ к продукту ${product[1]} (#${product[0]}). +
+ +
`, ["Продолжить", tr("cancel")], [ + () => { + $("#give_product_access_dialog").submit(); + }, + Function.noop + ]); +} + +function showBtRevokeProductAccessDialog(product, hash) { + MessageBox("Забрать доступ", `
+
+ Забрать у пользователя ID  + +   доступ к продукту ${product[1]} (#${product[0]}). +
+ +
`, ["Продолжить", tr("cancel")], [ + () => { + $("#revoke_product_access_dialog").submit(); + }, + Function.noop + ]); +} + +function showBtProductAccessDialog(product, hash) { + MessageBox(`Доступ к ${product[1]} (#${product[0]})`, `
+ + + + + + + + + + + +
+
+
+ ID пользователя  + +
+ +
`, ["Продолжить", tr("cancel")], [ + () => { + $("#give_product_access_dialog").submit(); + }, + Function.noop + ]); +} + +function showBtPrivateProductDialog(product, hash) { + MessageBox(`Настройки продукта ${product[1]} (#${product[0]})`, `
+ + + + + + + + + + + +
+ +
`, ["Продолжить", tr("cancel")], [ + () => { + $("#give_product_access_dialog").submit(); + }, + Function.noop + ]); +} + +function showBtProductStatusDialog(product, hash) { + MessageBox(`Статус продукта ${product[1]} (#${product[0]})`, `
+ + + + + + + + + + + +
+ +
`, ["Продолжить", tr("cancel")], [ + () => { + $("#give_product_access_dialog").submit(); + }, + Function.noop + ]); } \ No newline at end of file diff --git a/install/sqls/00030-bug-tracker.sql b/install/sqls/00030-bug-tracker.sql index dbdc854f..ce5ed23d 100644 --- a/install/sqls/00030-bug-tracker.sql +++ b/install/sqls/00030-bug-tracker.sql @@ -29,7 +29,9 @@ CREATE TABLE `bt_products` ( `title` varchar(255) NOT NULL, `description` varchar(10000) NOT NULL, `created` bigint(20) NOT NULL, - `closed` tinyint(1) DEFAULT 0 + `closed` tinyint(1) DEFAULT 0, + `private` tinyint(4) DEFAULT 0, + `deleted` tinyint(1) DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ALTER TABLE `bt_products` @@ -58,4 +60,22 @@ ALTER TABLE `bt_comments` ALTER TABLE `bt_comments` MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT; +COMMIT; + +/* bt_products_access */ + +CREATE TABLE `bt_products_access` ( + `id` bigint(20) NOT NULL, + `created` bigint(20) NOT NULL, + `tester` bigint(20) NOT NULL, + `product` bigint(20) NOT NULL, + `moderator` bigint(20) NOT NULL, + `access` tinyint(1) DEFAULT 1 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `bt_products_access` + ADD PRIMARY KEY (`id`); + +ALTER TABLE `bt_products_access` + MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT; COMMIT; \ No newline at end of file