diff --git a/Admin/Install/db.json b/Admin/Install/db.json index 773248b..9286639 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -119,15 +119,15 @@ "primary": true, "autoincrement": true }, - "qa_question_vote_src": { - "name": "qa_question_vote_src", + "qa_question_vote_question": { + "name": "qa_question_vote_question", "type": "INT", "null": false, "foreignTable": "qa_question", "foreignKey": "qa_question_id" }, - "qa_question_vote_dst": { - "name": "qa_question_vote_dst", + "qa_question_vote_created_by": { + "name": "qa_question_vote_created_by", "type": "INT", "null": false, "foreignTable": "account", @@ -137,6 +137,11 @@ "name": "qa_question_vote_score", "type": "TINYINT", "null": false + }, + "qa_question_vote_created_at": { + "name": "qa_question_vote_created_at", + "type": "DATETIME", + "null": false } } }, @@ -229,15 +234,15 @@ "primary": true, "autoincrement": true }, - "qa_answer_vote_src": { - "name": "qa_answer_vote_src", + "qa_answer_vote_answer": { + "name": "qa_answer_vote_answer", "type": "INT", "null": false, "foreignTable": "qa_answer", "foreignKey": "qa_answer_id" }, - "qa_answer_vote_dst": { - "name": "qa_answer_vote_dst", + "qa_answer_vote_created_by": { + "name": "qa_answer_vote_created_by", "type": "INT", "null": false, "foreignTable": "account", @@ -247,6 +252,11 @@ "name": "qa_answer_vote_score", "type": "TINYINT", "null": false + }, + "qa_answer_vote_created_at": { + "name": "qa_answer_vote_created_at", + "type": "DATETIME", + "null": false } } } diff --git a/Admin/Routes/Web/Api.php b/Admin/Routes/Web/Api.php new file mode 100644 index 0000000..9762ce1 --- /dev/null +++ b/Admin/Routes/Web/Api.php @@ -0,0 +1,91 @@ + [ + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiQACategoryCreate', + 'verb' => RouteVerb::PUT, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::CATEGORY, + ], + ], + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiQACategoryUpdate', + 'verb' => RouteVerb::SET, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::CATEGORY, + ], + ], + ], + '^.*/qa/question(\?.*|$)' => [ + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiQAQuestionCreate', + 'verb' => RouteVerb::PUT, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::QUESTION, + ], + ], + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiQuestionUpdate', + 'verb' => RouteVerb::SET, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::QUESTION, + ], + ], + ], + '^.*/qa/question/vote(\?.*|$)' => [ + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiChangeQAQuestionVote', + 'verb' => RouteVerb::PUT | RouteVerb::SET, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::VOTE, + ], + ], + ], + '^.*/qa/answer(\?.*|$)' => [ + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiQAAnswerCreate', + 'verb' => RouteVerb::PUT, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::ANSWER, + ], + ], + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiAnswerUpdate', + 'verb' => RouteVerb::SET, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::ANSWER, + ], + ], + ], + '^.*/qa/answer/vote(\?.*|$)' => [ + [ + 'dest' => '\Modules\QA\Controller\ApiController:apiChangeQAAnswerVote', + 'verb' => RouteVerb::PUT | RouteVerb::SET, + 'permission' => [ + 'module' => ApiController::MODULE_NAME, + 'type' => PermissionType::CREATE, + 'state' => PermissionState::VOTE, + ], + ], + ], +]; diff --git a/Controller/ApiController.php b/Controller/ApiController.php index 649d9cb..c73c4c3 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -27,6 +27,12 @@ use Modules\QA\Models\QACategoryMapper; use Modules\QA\Models\QAQuestion; use Modules\QA\Models\QAQuestionMapper; use Modules\QA\Models\QAQuestionStatus; +use Modules\QA\Models\QAQuestionVote; +use Modules\QA\Models\QAQuestionVoteMapper; +use Modules\QA\Models\QAAnswerVote; +use Modules\QA\Models\QAAnswerVoteMapper; +use Modules\QA\Models\NullQAQuestionVote; +use Modules\QA\Models\NullQAAnswerVote; use Modules\Tag\Models\NullTag; use phpOMS\Message\Http\HttpRequest; use phpOMS\Message\Http\HttpResponse; @@ -47,6 +53,57 @@ use phpOMS\Utils\Parser\Markdown\Markdown; */ final class ApiController extends Controller { + /** + * Api method to create a question + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiQuestionUpdate(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + { + } + + /** + * Api method to create a question + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiQACategoryUpdate(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + { + } + + /** + * Api method to create a question + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAnswerUpdate(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + { + } + /** * Api method to create a question * @@ -189,6 +246,7 @@ final class ApiController extends Controller $answer->answerRaw = (string) $request->getData('plain'); $answer->answer = Markdown::parse((string) ($request->getData('plain') ?? '')); $answer->question = new NullQAQuestion((int) $request->getData('question')); + $answer->isAccepted = (bool) ($request->getData('accepted') ?? false); $answer->setStatus((int) $request->getData('status')); $answer->createdBy = new NullAccount($request->header->account); @@ -367,4 +425,126 @@ final class ApiController extends Controller return $l11nQACategory; } + + /** + * Api method to change question vote + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + private function apiChangeQAQuestionVote(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + { + if (!empty($val = $this->validateQuestionVote($request))) { + $response->set('qa_question_vote', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $questionVote = QAQuestionVoteMapper::findVote((int) $reqeust->getData('id'), $request->header->account); + + if ($questionVote instanceof NullQAQuestionVote) { + $new = new QAQuestionVote(); + $new->score = (int) $request->getData('type'); + $new->createdBy = $request->header->account; + + $this->createModel($request->header->account, $new, QAQuestionVoteMapper::class, 'qa_question_vote', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Vote', 'Sucessfully voted.', $new); + } else { + $new = clone $questionVote; + $new->score = (int) $request->getData('type'); + + $this->updateModel($request->header->account, $questionVote, $new, QAQuestionVoteMapper::class, 'qa_question_vote', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Vote', 'Vote successfully changed.', $new); + } + } + + /** + * Validate question vote request + * + * @param RequestAbstract $request Request + * + * @return array Returns the validation array of the request + * + * @since 1.0.0 + */ + private function validateQuestionVote(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = ($request->getData('id') === null)) + || ($val['type'] = ($request->getData('type', 'int') < -1 || $request->getData('type') > 1)) + ) { + return $val; + } + + return []; + } + + /** + * Api method to change answer vote + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + private function apiChangeQAAnswerVote(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + { + if (!empty($val = $this->validateAnswerVote($request))) { + $response->set('qa_answer_vote', new FormValidation($val)); + $response->header->status = RequestStatusCode::R_400; + + return; + } + + $answerVote = QAAnswerVoteMapper::findVote((int) $reqeust->getData('id'), $request->header->account); + + if ($answerVote instanceof NullQAAnswerVote) { + $new = new QAAnswerVote(); + $new->score = (int) $request->getData('type'); + $new->createdBy = $request->header->account; + + $this->createModel($request->header->account, $new, QAAnswerVoteMapper::class, 'qa_answer_vote', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Vote', 'Sucessfully voted.', $new); + } else { + $new = clone $answerVote; + $new->score = (int) $request->getData('type'); + + $this->updateModel($request->header->account, $answerVote, $new, QAAnswerVoteMapper::class, 'qa_answer_vote', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Vote', 'Vote successfully changed.', $new); + } + } + + /** + * Validate answer vote request + * + * @param RequestAbstract $request Request + * + * @return array Returns the validation array of the request + * + * @since 1.0.0 + */ + private function validateAnswerVote(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = ($request->getData('id') === null)) + || ($val['type'] = ($request->getData('type', 'int') < -1 || $request->getData('type') > 1)) + ) { + return $val; + } + + return []; + } } diff --git a/Models/NullQAAnswerVote.php b/Models/NullQAAnswerVote.php new file mode 100644 index 0000000..cd6f88d --- /dev/null +++ b/Models/NullQAAnswerVote.php @@ -0,0 +1,38 @@ +id = $id; + } +} diff --git a/Models/NullQAQuestionVote.php b/Models/NullQAQuestionVote.php new file mode 100644 index 0000000..44881a2 --- /dev/null +++ b/Models/NullQAQuestionVote.php @@ -0,0 +1,38 @@ +id = $id; + } +} diff --git a/Models/PermissionState.php b/Models/PermissionState.php index c900591..3877516 100755 --- a/Models/PermissionState.php +++ b/Models/PermissionState.php @@ -29,4 +29,10 @@ abstract class PermissionState extends Enum public const QA = 1; public const QUESTION = 2; + + public const ANSWER = 3; + + public const VOTE = 4; + + public const CATEGORY = 5; } diff --git a/Models/QAAnswer.php b/Models/QAAnswer.php index 50a85d8..e8602e7 100755 --- a/Models/QAAnswer.php +++ b/Models/QAAnswer.php @@ -35,6 +35,12 @@ class QAAnswer implements \JsonSerializable */ protected int $id = 0; + /** + * Status. + * + * @var int + * @since 1.0.0 + */ private int $status = QAAnswerStatus::ACTIVE; /** @@ -67,7 +73,7 @@ class QAAnswer implements \JsonSerializable * @var bool * @since 1.0.0 */ - private bool $isAccepted = false; + public bool $isAccepted = false; /** * Created by. diff --git a/Models/QAAnswerVote.php b/Models/QAAnswerVote.php new file mode 100644 index 0000000..cea80a5 --- /dev/null +++ b/Models/QAAnswerVote.php @@ -0,0 +1,80 @@ +createdBy = new NullAccount(); + $this->createdAt = new \DateTimeImmutable(); + } +} diff --git a/Models/QAAnswerVoteMapper.php b/Models/QAAnswerVoteMapper.php new file mode 100644 index 0000000..1a6db3c --- /dev/null +++ b/Models/QAAnswerVoteMapper.php @@ -0,0 +1,66 @@ + + * @since 1.0.0 + */ + protected static array $columns = [ + 'qa_answer_vote_id' => ['name' => 'qa_answer_vote_id', 'type' => 'int', 'internal' => 'id'], + 'qa_answer_vote_score' => ['name' => 'qa_answer_vote_score', 'type' => 'int', 'internal' => 'score'], + 'qa_answer_vote_answer' => ['name' => 'qa_answer_vote_comment', 'type' => 'int', 'internal' => 'comment', 'readonly' => true], + 'qa_answer_vote_created_by' => ['name' => 'qa_answer_vote_created_by', 'type' => 'int', 'internal' => 'createdBy', 'readonly' => true], + 'qa_answer_vote_created_at' => ['name' => 'qa_answer_vote_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + protected static string $table = 'qa_answer_vote'; + + /** + * Created at. + * + * @var string + * @since 1.0.0 + */ + protected static string $createdAt = 'qa_answer_vote_created_at'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + protected static string $primaryField = 'qa_answer_vote_id'; +} diff --git a/Models/QAQuestion.php b/Models/QAQuestion.php index 2768813..200f284 100755 --- a/Models/QAQuestion.php +++ b/Models/QAQuestion.php @@ -14,9 +14,10 @@ declare(strict_types=1); namespace Modules\QA\Models; -use Modules\Admin\Models\Account; use Modules\Admin\Models\NullAccount; +use Modules\Profile\Models\NullProfile; use Modules\Tag\Models\Tag; +use Modules\Profile\Models\Profile; /** * Task class. @@ -87,10 +88,10 @@ class QAQuestion implements \JsonSerializable /** * Created by. * - * @var Account + * @var Profile * @since 1.0.0 */ - public Account $createdBy; + public Profile $createdBy; /** * Created at. @@ -124,7 +125,7 @@ class QAQuestion implements \JsonSerializable public function __construct() { $this->createdAt = new \DateTimeImmutable('now'); - $this->createdBy = new NullAccount(); + $this->createdBy = new NullProfile(); } /** diff --git a/Models/QAQuestionMapper.php b/Models/QAQuestionMapper.php index e7bf321..f63ed0a 100755 --- a/Models/QAQuestionMapper.php +++ b/Models/QAQuestionMapper.php @@ -14,7 +14,7 @@ declare(strict_types=1); namespace Modules\QA\Models; -use Modules\Admin\Models\AccountMapper; +use Modules\Profile\Models\ProfileMapper; use Modules\Tag\Models\TagMapper; use phpOMS\DataStorage\Database\DataMapperAbstract; @@ -88,8 +88,9 @@ final class QAQuestionMapper extends DataMapperAbstract */ protected static array $belongsTo = [ 'createdBy' => [ - 'mapper' => AccountMapper::class, - 'external' => 'qa_question_created_by', + 'mapper' => ProfileMapper::class, + 'external' => 'qa_question_created_by', + 'by' => 'account' ], ]; diff --git a/Models/QAQuestionVote.php b/Models/QAQuestionVote.php new file mode 100644 index 0000000..074f47e --- /dev/null +++ b/Models/QAQuestionVote.php @@ -0,0 +1,80 @@ +createdBy = new NullAccount(); + $this->createdAt = new \DateTimeImmutable(); + } +} diff --git a/Models/QAQuestionVoteMapper.php b/Models/QAQuestionVoteMapper.php new file mode 100644 index 0000000..73b50b1 --- /dev/null +++ b/Models/QAQuestionVoteMapper.php @@ -0,0 +1,66 @@ + + * @since 1.0.0 + */ + protected static array $columns = [ + 'qa_question_vote_id' => ['name' => 'qa_question_vote_id', 'type' => 'int', 'internal' => 'id'], + 'qa_question_vote_score' => ['name' => 'qa_question_vote_score', 'type' => 'int', 'internal' => 'score'], + 'qa_question_vote_question' => ['name' => 'qa_question_vote_comment', 'type' => 'int', 'internal' => 'comment', 'readonly' => true], + 'qa_question_vote_created_by' => ['name' => 'qa_question_vote_created_by', 'type' => 'int', 'internal' => 'createdBy', 'readonly' => true], + 'qa_question_vote_created_at' => ['name' => 'qa_question_vote_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + protected static string $table = 'qa_question_vote'; + + /** + * Created at. + * + * @var string + * @since 1.0.0 + */ + protected static string $createdAt = 'qa_question_vote_created_at'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + protected static string $primaryField = 'qa_question_vote_id'; +} diff --git a/Theme/Backend/qa-question.tpl.php b/Theme/Backend/qa-question.tpl.php index 951bef0..9c7cb6a 100755 --- a/Theme/Backend/qa-question.tpl.php +++ b/Theme/Backend/qa-question.tpl.php @@ -41,7 +41,7 @@ echo $this->getData('nav')->render();
- printHtml($answer->getAnswer()); ?>printHtml($answer->createdAt->format('Y-m-d')); ?>createdBy->getId(); ?>getStatus(); ?>printHtml((string) $answer->isAccepted()); ?> + answer; ?>printHtml($answer->createdAt->format('Y-m-d')); ?>createdBy->getId(); ?>getStatus(); ?>printHtml((string) $answer->isAccepted()); ?>