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 @@
-
+ {var $isHidden = !$bug->getProduct()->hasAccess($user->identity)}
+ |
|
-
+
{_bug_tracker_product}: |
@@ -86,6 +89,58 @@
+
+
+
+
+ {_bug_tracker_product}:
+ |
+
+ AbcdEFGH
+ |
+
+
+
+ {_bug_tracker_sent_by}:
+ |
+
+ John Doe
+ |
+
+
+
+ {_bug_tracker_reproduced}:
+ |
+
+ ...
+ |
+
+
+
+ {_status}:
+ |
+
+ ...
+ |
+
+
+
+ {_bug_tracker_priority}:
+ |
+
+ ...
+ |
+
+
+
+ {_created}:
+ |
+
+ ...
+ |
+
+
+
@@ -151,10 +206,36 @@
{elseif $mode === "products"}
+
+
+{if $count < 1}
+ {include "../components/nothing.xml"}
+{/if}
-
+ |
@@ -167,6 +248,10 @@
+
+ {$product->getDescription()}
+
+
@@ -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("Выдать доступ", ``, ["Продолжить", tr("cancel")], [
+ () => {
+ $("#give_product_access_dialog").submit();
+ },
+ Function.noop
+ ]);
+}
+
+function showBtRevokeProductAccessDialog(product, hash) {
+ MessageBox("Забрать доступ", ``, ["Продолжить", tr("cancel")], [
+ () => {
+ $("#revoke_product_access_dialog").submit();
+ },
+ Function.noop
+ ]);
+}
+
+function showBtProductAccessDialog(product, hash) {
+ MessageBox(`Доступ к ${product[1]} (#${product[0]})`, ``, ["Продолжить", 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
| |