<?php
|
namespace JVBase\managers;
|
|
|
use DateTime;
|
use JVBase\registrar\Registrar;
|
use JVBase\managers\CustomTable;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class ApprovalManager {
|
protected array $tables =[];
|
protected int $expiresAt = 21; //days for request
|
protected int $requiredVotes = 3; //Number of votes before a term is approved
|
public function __construct()
|
{
|
$this->defineTables();
|
if (empty($this->tables)) {
|
return;
|
}
|
}
|
|
protected function defineTables():void
|
{
|
$types = Registrar::getFeatured('approve_new');
|
foreach ($types as $type) {
|
$requests = CustomTable::for("approval_{$type}_requests");
|
|
$requests->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'user_id' => "{$requests->getUserIDType()} NOT NULL",
|
'name' => 'varchar(255) NOT NULL',
|
'parent_id' => 'bigint(20) unsigned DEFAULT 0',
|
'status' => "ENUM('pending', 'approved', 'rejected', 'appealed', 'expired') DEFAULT 'pending'",
|
'required_approvals'=> 'int unsigned DEFAULT 3',
|
'approvals' => 'int unsigned DEFAULT 0',
|
'rejections'=> 'int unsigned DEFAULT 0',
|
'expires_at' => 'datetime DEFAULT NULL',
|
'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP',
|
'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
'approved_by' => 'json DEFAULT NULL',
|
'rejected_by' => 'json DEFAULT NULL',
|
'created_item_id' => 'bigint(20) unsigned DEFAULT NULL',
|
]);
|
$requests->setKeys([
|
['key' => 'PRIMARY', 'value' => 'id'],
|
['key' => 'UNIQUE', 'value' => '`unique_request` (`user_id`, `name`)'],
|
'`name` (`name`)',
|
'`name_parent` (`name`, `parent_id`)',
|
'`status` (`status`)',
|
'`expiring_requests` (`status`, `expires_at`)'
|
]);
|
$base = BASE;
|
$requests->setConstraints([
|
"CONSTRAINTS `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
|
REFERENCES `{$requests->getUserTable()}` (`ID`) ON DELETE CASCADE",
|
"CONSTRAINTS `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`)
|
REFERENCES `{$requests->getTermTable()}` (`term_id`) ON DELETE CASCADE"
|
]);
|
$requests->defineTable();
|
|
$votes = CustomTable::for("approval_{$type}_votes");
|
|
$votes->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'request_id' => 'bigint(20) unsigned NOT NULL',
|
'user_id' => "{$votes->getUserIDType()} NOT NULL",
|
'vote' => "ENUM('approve', 'reject', 'dismiss') NOT NULL",
|
'notes' => 'text DEFAULT NULL',
|
'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP',
|
'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
|
]);
|
|
$votes->setKeys([
|
['key' => 'PRIMARY', 'value' => 'id'],
|
['key' => 'UNIQUE', 'value' => '`unique_vote` (`request_id`, `user_id`)'],
|
'`user_votes` (`user_id`)'
|
]);
|
|
$votes->setConstraints([
|
"CONSTRAINT `{$base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
|
REFERENCES `{$requests->getFullTableName()} (`id`) ON DELETE CASCADE",
|
"CONSTRAINT `{$base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
|
REFERENCES `{$votes->getUserTable()}` (`ID`) ON DELETE CASCADE"
|
]);
|
|
$votes->defineTable();
|
|
$this->tables[$type] = [
|
'requests' => $requests,
|
'votes' => $votes
|
];
|
}
|
}
|
|
public function canApprove(int $userID, string $type):bool
|
{
|
$type = jvbNoBase($type);
|
if (!array_key_exists($type, $this->tables)) {
|
return false;
|
}
|
|
return user_can($userID, "approve_{$type}");
|
}
|
protected function requests(string $type):?CustomTable
|
{
|
$type = jvbNoBase($type);
|
if (!array_key_exists($type, $this->tables)){
|
return null;
|
}
|
return $this->tables[$type]['requests']??null;
|
}
|
|
protected function votes(string $type):?CustomTable
|
{
|
$type = jvbNoBase($type);
|
if (!array_key_exists($type, $this->tables)){
|
return null;
|
}
|
return $this->tables[$type]['votes']??null;
|
}
|
|
public function response(bool $success, string $msg = ''):array
|
{
|
$return = ['success' => $success];
|
if ($msg !== '') {
|
$return['message'] = $msg;
|
}
|
return $return;
|
}
|
|
public function createApproval(int $userID, string $type, string $name, int $parentID = 0):array
|
{
|
if (!$this->canApprove($userID, $type)) {
|
return $this->response(false, 'Cannot create request');
|
}
|
$type = jvbNoBase($type);
|
if (!array_key_exists($type, $this->tables)) {
|
$this->response(false, 'Invalid type');
|
}
|
$table = $this->requests($type);
|
if (!$table) {
|
return $this->response(false, 'Invalid type');
|
}
|
|
//Check for existing request
|
$existing = $table->get([
|
'name' => sanitize_text_field($name)
|
]);
|
if (!$existing) {
|
$this->approve($userID, $existing->id);
|
return $this->response(true, 'Existing request - added your vote!');
|
}
|
|
if ($parentID > 0) {
|
$parent = get_term($parentID, jvbCheckBase($type));
|
if (!$parent || is_wp_error($parent)) {
|
$parent = 0;
|
}
|
}
|
|
$date = new DateTime('+' . $this->expiresAt . ' days');
|
$expires = $date->format('Y-m-d H:i:s');
|
|
|
$result = $table->create([
|
'user_id' => $userID,
|
'name' => sanitize_text_field($name),
|
'parent_id' => $parentID,
|
'expires_at'=> $expires
|
]);
|
|
return $this->response((bool)$result, $result?'Request created successfully' : 'Request could not be created');
|
}
|
|
public function markApproval(int $request_id, int $userID, string $type, string $vote, ?string $note = null):array
|
{
|
if (!$this->canApprove($userID, $type)) {
|
return $this->response(false, 'Sorry, you cannot do this.');
|
}
|
$requests = $this->requests($type);
|
if (!$requests) {
|
return $this->response(false, 'Invalid type');
|
}
|
$request = $requests->get(['request_id' => $request_id]);
|
if (!$request) {
|
return $this->response(false, 'Invalid request id');
|
}
|
|
$votes = $this->votes($type);
|
if (!$votes) {
|
return $this->response(false, 'Invalid type');
|
}
|
|
if (!in_array($vote, ['approve', 'reject', 'dismiss'])) {
|
return $this->response(false, 'Invalid vote');
|
}
|
|
$response = $votes->findOrCreate([
|
'request_id' => $request_id,
|
'user_id' => $userID
|
], [
|
'vote' => 'approve',
|
'notes' => $note ? sanitize_text_field($note) : $note
|
]);
|
|
if (!$response) {
|
return $this->response(false, 'Could not store vote for some reason.');
|
}
|
|
switch ($vote) {
|
case 'reject':
|
$request['rejections']++;
|
break;
|
case 'approve':
|
$request['approvals']++;
|
break;
|
default:
|
return $this->response(true, 'Successfully dismissed approval request.');
|
}
|
|
if ($request['rejections'] === $request['required_approvals']) {
|
$this->finalizeDenial($request_id, $type);
|
} else if ($request['approvals'] === $request['required_approvals']) {
|
$this->finalizeApproval($request_id, $type);
|
}
|
$updated = $requests->update([
|
'rejections'=> $request['rejections'],
|
'approvals' => $request['approvals']
|
],
|
[
|
'request_id' => $request_id
|
]);
|
|
if ($updated) {
|
return $this->response(true, 'Successfully voted');
|
}
|
return $this->response(false, 'Could not finalize vote for some reason');
|
}
|
|
public function approveRequest(int $request_id, int $userID, string $type, ?string $note = null):array
|
{
|
return $this->markApproval($request_id, $userID, $type, 'approve', $note);
|
}
|
|
public function denyRequest(int $request_id, int $userID, string $type, string $note = ''):array
|
{
|
return $this->markApproval($request_id, $userID, $type, 'reject', $note);
|
}
|
|
public function dismissRequest(int $request_id, int $userID, string $type, string $note = ''):array
|
{
|
return $this->markApproval($request_id, $userID, $type, 'dismiss', $note);
|
}
|
|
public function isApproved(int $request_id, string $type):bool
|
{
|
$table = $this->requests($type);
|
if (!$table) {
|
return false;
|
}
|
$request = $table->get(['request_id' => $request_id]);
|
if (!$request) {
|
return false;
|
}
|
$votes = $this->votes($type);
|
if (!$votes) {
|
return false;
|
}
|
$total = $votes->getMany([
|
'request_id' => $request_id,
|
'status' => ['IN' => ['approve', 'reject']]
|
]);
|
$approvals = count(array_filter($total, function($item) { return $item->status === 'approve'; }));
|
$denials = count(array_filter($total, function($item) { return $item->status === 'reject'; }));
|
if ($denials === $this->requiredVotes) {
|
return false;
|
}
|
return $approvals === $this->requiredVotes;
|
}
|
|
public function finalizeApproval(int $request_id, string $type):void
|
{
|
$requests = $this->requests($type);
|
$request = $requests->get(['request_id' => $request_id]);
|
if (!$request){
|
error_log('Could not finalize denial for request: '.$request_id);
|
return;
|
}
|
|
$updatedID = null;
|
$registrar = Registrar::getInstance($type);
|
if ($registrar->getType() === 'term') {
|
$term = wp_insert_term($request['name'], $registrar->getBased(), ['parent' => $request['parent_id']]);
|
if (!is_wp_error($term)) {
|
$updatedID = $term['term_id'];
|
}
|
} elseif ($registrar->getType() === 'user') {
|
|
}
|
|
$updates = [
|
'status' => 'approved'
|
];
|
if ($updatedID) {
|
$updates['created_item_id'] = $updatedID;
|
}
|
$updated = $requests->update(
|
$updates,
|
[
|
'request_id' => $request_id
|
]);
|
|
|
JVB()->notification()->addNotification($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.');
|
}
|
public function finalizeDenial(int $request_id, string $type):void
|
{
|
$requests = $this->requests($type);
|
$request = $requests->get(['request_id' => $request_id]);
|
if (!$request){
|
error_log('Could not finalize denial for request: '.$request_id);
|
return;
|
}
|
$updated = $requests->update([
|
'status' => 'rejected'
|
],
|
[
|
'request_id' => $request_id
|
]);
|
|
JVB()->notification()->addNotification($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.');
|
}
|
|
public function getApprovalRequests(int $userID, ?string $type = null):array
|
{
|
$return = [];
|
if ($type) {
|
if (!array_key_exists($type, $this->tables)) {
|
return $return;
|
}
|
$requests = $this->requests($type);
|
$active = $requests->pluck('request_id', [
|
'status' => 'pending'
|
]);
|
$votes = $this->votes($type);
|
$voted = $votes->pluck('request_id',[
|
'user_id' => $userID
|
]);
|
|
$all = array_diff($active, $voted);
|
$allRequests = $requests->getMany([
|
'where' => [
|
'request_id' => ['IN' => $all]
|
]
|
]);
|
|
$return = array_map(function ($request) use ($type) {
|
return $this->format_for_notification($request, $type);
|
}, $allRequests);
|
} else {
|
foreach ($this->tables as $type => $tables) {
|
$requests = $tables['requests'];;
|
$active = $requests->pluck('request_id', [
|
'status' => 'pending',
|
]);
|
$votes = $tables['votes'];
|
$voted = $votes->pluck('request_id', [
|
'user_id' => $userID
|
]);
|
|
$all = array_diff($active, $voted);
|
$allRequests = $requests->getMany([
|
'where' => [
|
'request_id' => ['IN' => $all]
|
]
|
]);
|
$return = array_merge($return, array_map(function ($request) use ($type) {
|
return $this->format_for_notification($request, $type);
|
}, $allRequests));
|
}
|
}
|
|
return $return;
|
}
|
|
protected function format_for_notification(array $request, string $type): array
|
{
|
$registrar = Registrar::getInstance($type);
|
$message = '';
|
$user = get_userdata($request['user_id']);
|
if ($user) {
|
$message = $user->first_name;
|
} else {
|
$message = 'Someone';
|
}
|
$message .= ' thinks we need a new '.$registrar->getSingular().'.
|
They think: "'.$request['name'].'"
|
';
|
if ($request['parent_id'] > 0) {
|
$parent = get_term($request['parent_id'], $registrar->getBased());
|
if ($parent && !is_wp_error($parent)) {
|
$message .= 'And nest it under: '.$parent->name.'
|
';
|
}
|
}
|
$message .= '
|
What say you?';
|
return [
|
'id' => $request['id'],
|
'type' => $type.'_new',
|
'message' => $message,
|
'created_at'=> $request['created_at'],
|
'status' => 'pending',
|
'requires_action' => true,
|
'action_taken' => false,
|
'icon' => $registrar->getIcon(),
|
'priority' => 'normal',
|
'target' => [
|
'id' => $request['id'],
|
'type' => 'approval'
|
],
|
'actions' => [
|
'approve' => 'Approve',
|
'deny' => 'Deny',
|
'dismiss' => 'Dismiss'
|
]
|
];
|
}
|
}
|