diff --git a/Controller/ApiController.php b/Controller/ApiController.php index c73c4c3..c9afe0b 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -42,6 +42,7 @@ use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; use phpOMS\Model\Message\FormValidation; use phpOMS\Utils\Parser\Markdown\Markdown; +use Modules\Profile\Models\Profile; /** * Task class. @@ -153,7 +154,7 @@ final class ApiController extends Controller $question->setLanguage((string) $request->getData('language')); $question->setCategory(new NullQACategory((int) $request->getData('category'))); $question->setStatus((int) $request->getData('status')); - $question->createdBy = new NullAccount($request->header->account); + $question->createdBy = new Profile(new NullAccount($request->header->account)); if (!empty($tags = $request->getDataJson('tags'))) { foreach ($tags as $tag) { @@ -246,7 +247,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->isAccepted = false; $answer->setStatus((int) $request->getData('status')); $answer->createdBy = new NullAccount($request->header->account); @@ -278,6 +279,44 @@ final class ApiController extends Controller return []; } + /** + * Api method to create a answer + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiChangeAnsweredStatus(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + { + $old = clone QAAnswerMapper::get((int) $request->getData('id')); + $new = $this->updateAnsweredStatusFromRequest($request); + $this->updateModel($request->header->account, $old, $new, QAAnswerMapper::class, 'answer', $request->getOrigin()); + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Answer', 'Answer successfully updated.', $new); + } + + /** + * Method to create category from request. + * + * @param RequestAbstract $request Request + * + * @return QAAnswer + * + * @since 1.0.0 + */ + public function updateAnsweredStatusFromRequest(RequestAbstract $request) : QAAnswer + { + $answer = QAAnswerMapper::get((int) $request->getData('id')); + $answer->isAccepted = $request->getData('accepted', 'bool') ?? false; + + return $answer; + } + /** * Api method to create a category * @@ -301,18 +340,9 @@ final class ApiController extends Controller } $category = $this->createQACategoryFromRequest($request); + $category->setL11n($request->getData('name'), $request->getData('language')); $this->createModel($request->header->account, $category, QACategoryMapper::class, 'category', $request->getOrigin()); - $l11nRequest = new HttpRequest($request->uri); - $l11nRequest->setData('category', $category->getId()); - $l11nRequest->setData('name', $request->getData('name')); - $l11nRequest->setData('language', $request->getData('language')); - - $l11nQACategory = $this->createQACategoryL11nFromRequest($l11nRequest); - $this->createModel($request->header->account, $l11nQACategory, QACategoryL11nMapper::class, 'tag_l11n', $request->getOrigin()); - - $category->setName($l11nQACategory); - $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Category', 'Category successfully created.', $category); } @@ -439,7 +469,7 @@ final class ApiController extends Controller * * @since 1.0.0 */ - private function apiChangeQAQuestionVote(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + public function apiChangeQAQuestionVote(RequestAbstract $request, ResponseAbstract $response, $data = null) : void { if (!empty($val = $this->validateQuestionVote($request))) { $response->set('qa_question_vote', new FormValidation($val)); @@ -448,12 +478,13 @@ final class ApiController extends Controller return; } - $questionVote = QAQuestionVoteMapper::findVote((int) $reqeust->getData('id'), $request->header->account); + $questionVote = QAQuestionVoteMapper::findVote((int) $request->getData('id'), $request->header->account); - if ($questionVote instanceof NullQAQuestionVote) { + if ($questionVote === false || $questionVote instanceof NullQAQuestionVote || $questionVote === null) { $new = new QAQuestionVote(); $new->score = (int) $request->getData('type'); - $new->createdBy = $request->header->account; + $new->question = (int) $request->getData('id'); + $new->createdBy = new NullAccount($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); @@ -500,7 +531,7 @@ final class ApiController extends Controller * * @since 1.0.0 */ - private function apiChangeQAAnswerVote(RequestAbstract $request, ResponseAbstract $response, $data = null) : void + public function apiChangeQAAnswerVote(RequestAbstract $request, ResponseAbstract $response, $data = null) : void { if (!empty($val = $this->validateAnswerVote($request))) { $response->set('qa_answer_vote', new FormValidation($val)); @@ -509,12 +540,13 @@ final class ApiController extends Controller return; } - $answerVote = QAAnswerVoteMapper::findVote((int) $reqeust->getData('id'), $request->header->account); + $answerVote = QAAnswerVoteMapper::findVote((int) $request->getData('id'), $request->header->account); - if ($answerVote instanceof NullQAAnswerVote) { + if ($answerVote === false || $answerVote instanceof NullQAAnswerVote || $answerVote === null) { $new = new QAAnswerVote(); $new->score = (int) $request->getData('type'); - $new->createdBy = $request->header->account; + $new->answer = (int) $request->getData('id'); + $new->createdBy = new NullAccount($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); diff --git a/Models/NullQAAnswerVote.php b/Models/NullQAAnswerVote.php index cd6f88d..7c8cc0b 100644 --- a/Models/NullQAAnswerVote.php +++ b/Models/NullQAAnswerVote.php @@ -12,7 +12,7 @@ */ declare(strict_types=1); -namespace Modules\QAs\Models; +namespace Modules\QA\Models; /** * Null model diff --git a/Models/NullQAQuestionVote.php b/Models/NullQAQuestionVote.php index 44881a2..a44476f 100644 --- a/Models/NullQAQuestionVote.php +++ b/Models/NullQAQuestionVote.php @@ -12,7 +12,7 @@ */ declare(strict_types=1); -namespace Modules\QAs\Models; +namespace Modules\QA\Models; /** * Null model diff --git a/Models/QAAnswer.php b/Models/QAAnswer.php index e8602e7..626ddfd 100755 --- a/Models/QAAnswer.php +++ b/Models/QAAnswer.php @@ -14,8 +14,8 @@ 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\Profile\Models\Profile; /** * Answer class. @@ -78,10 +78,10 @@ class QAAnswer implements \JsonSerializable /** * Created by. * - * @var Account + * @var Profile * @since 1.0.0 */ - public Account $createdBy; + public Profile $createdBy; /** * Created at. @@ -91,6 +91,14 @@ class QAAnswer implements \JsonSerializable */ public \DateTimeImmutable $createdAt; + /** + * Votes. + * + * @var array + * @since 1.0.0 + */ + private array $votes = []; + /** * Constructor. * @@ -99,7 +107,7 @@ class QAAnswer implements \JsonSerializable public function __construct() { $this->createdAt = new \DateTimeImmutable('now'); - $this->createdBy = new NullAccount(); + $this->createdBy = new NullProfile(); $this->question = new NullQAQuestion(); } @@ -207,6 +215,27 @@ class QAAnswer implements \JsonSerializable $this->isAccepted = $accepted; } + public function getVoteScore() : int + { + $score = 0; + foreach ($this->votes as $vote) { + $score += $vote->score; + } + + return $score; + } + + public function getAccountVoteScore(int $account) : int + { + foreach ($this->votes as $vote) { + if ($vote->createdBy->getId() === $account) { + return $vote->score; + } + } + + return 0; + } + /** * Is the answer accepted * diff --git a/Models/QAAnswerMapper.php b/Models/QAAnswerMapper.php index 7268807..0fc5d4e 100755 --- a/Models/QAAnswerMapper.php +++ b/Models/QAAnswerMapper.php @@ -14,7 +14,7 @@ declare(strict_types=1); namespace Modules\QA\Models; -use Modules\Admin\Models\AccountMapper; +use Modules\Profile\Models\ProfileMapper; use phpOMS\DataStorage\Database\DataMapperAbstract; /** @@ -52,8 +52,9 @@ final class QAAnswerMapper extends DataMapperAbstract */ protected static array $belongsTo = [ 'createdBy' => [ - 'mapper' => AccountMapper::class, + 'mapper' => ProfileMapper::class, 'external' => 'qa_answer_created_by', + 'by' => 'account' ], 'question' => [ 'mapper' => QAQuestionMapper::class, @@ -61,6 +62,21 @@ final class QAAnswerMapper extends DataMapperAbstract ], ]; + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + protected static array $hasMany = [ + 'votes' => [ + 'mapper' => QAAnswerVoteMapper::class, + 'table' => 'qa_answer_vote', + 'self' => 'qa_answer_vote_answer', + 'external' => null, + ], + ]; + /** * Primary table. * diff --git a/Models/QAAnswerVote.php b/Models/QAAnswerVote.php index cea80a5..b763dd5 100644 --- a/Models/QAAnswerVote.php +++ b/Models/QAAnswerVote.php @@ -57,7 +57,7 @@ class QAAnswerVote * @var int * @since 1.0.0 */ - public int $comment = 0; + public int $answer = 0; /** * Score diff --git a/Models/QAAnswerVoteMapper.php b/Models/QAAnswerVoteMapper.php index 1a6db3c..e853f8b 100644 --- a/Models/QAAnswerVoteMapper.php +++ b/Models/QAAnswerVoteMapper.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Modules\QA\Models; use phpOMS\DataStorage\Database\DataMapperAbstract; +use Modules\Admin\Models\AccountMapper; /** * Mapper class. @@ -35,11 +36,24 @@ final class QAAnswerVoteMapper extends DataMapperAbstract 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_answer' => ['name' => 'qa_answer_vote_answer', 'type' => 'int', 'internal' => 'answer', '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], ]; + /** + * Belongs to. + * + * @var array + * @since 1.0.0 + */ + protected static array $belongsTo = [ + 'createdBy' => [ + 'mapper' => AccountMapper::class, + 'external' => 'qa_answer_vote_created_by', + ], + ]; + /** * Primary table. * @@ -63,4 +77,16 @@ final class QAAnswerVoteMapper extends DataMapperAbstract * @since 1.0.0 */ protected static string $primaryField = 'qa_answer_vote_id'; + + public static function findVote(int $question, int $account) + { + $depth = 3; + $query = self::getQuery(); + $query->where(self::$table . '_' . $depth . '.qa_answer_vote_created_by', '=', $account) + ->andWhere(self::$table . '_' . $depth . '.qa_answer_vote_answer', '=', $question); + + $results = self::getAllByQuery($query); + + return \reset($results); + } } diff --git a/Models/QACategory.php b/Models/QACategory.php index a47e215..5d0257e 100755 --- a/Models/QACategory.php +++ b/Models/QACategory.php @@ -79,7 +79,7 @@ class QACategory implements \JsonSerializable * * @since 1.0.0 */ - public function getName() : string + public function getL11n() : string { return $this->name instanceof QACategoryL11n ? $this->name->getName() : $this->name; } @@ -93,7 +93,7 @@ class QACategory implements \JsonSerializable * * @since 1.0.0 */ - public function setName($name, string $lang = ISO639x1Enum::_EN) : void + public function setL11n($name, string $lang = ISO639x1Enum::_EN) : void { if ($name instanceof QACategoryL11n) { $this->name = $name; diff --git a/Models/QAQuestion.php b/Models/QAQuestion.php index 200f284..014dc23 100755 --- a/Models/QAQuestion.php +++ b/Models/QAQuestion.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace Modules\QA\Models; -use Modules\Admin\Models\NullAccount; use Modules\Profile\Models\NullProfile; use Modules\Tag\Models\Tag; use Modules\Profile\Models\Profile; @@ -117,6 +116,14 @@ class QAQuestion implements \JsonSerializable */ private array $answers = []; + /** + * Votes. + * + * @var array + * @since 1.0.0 + */ + private array $votes = []; + /** * Constructor. * @@ -286,6 +293,32 @@ class QAQuestion implements \JsonSerializable $this->tags = $tags; } + public function getAnswerCount() : int + { + return \count($this->answers); + } + + public function getVoteScore() : int + { + $score = 0; + foreach ($this->votes as $vote) { + $score += $vote->score; + } + + return $score; + } + + public function getAccountVoteScore(int $account) : int + { + foreach ($this->votes as $vote) { + if ($vote->createdBy->getId() === $account) { + return $vote->score; + } + } + + return 0; + } + /** * Get answers * diff --git a/Models/QAQuestionMapper.php b/Models/QAQuestionMapper.php index f63ed0a..a1e147e 100755 --- a/Models/QAQuestionMapper.php +++ b/Models/QAQuestionMapper.php @@ -59,6 +59,12 @@ final class QAQuestionMapper extends DataMapperAbstract 'self' => 'qa_answer_question', 'external' => null, ], + 'votes' => [ + 'mapper' => QAQuestionVoteMapper::class, + 'table' => 'qa_question_vote', + 'self' => 'qa_question_vote_question', + 'external' => null, + ], 'tags' => [ 'mapper' => TagMapper::class, 'table' => 'qa_tag', diff --git a/Models/QAQuestionVote.php b/Models/QAQuestionVote.php index 074f47e..d348369 100644 --- a/Models/QAQuestionVote.php +++ b/Models/QAQuestionVote.php @@ -57,7 +57,7 @@ class QAQuestionVote * @var int * @since 1.0.0 */ - public int $comment = 0; + public int $question = 0; /** * Score diff --git a/Models/QAQuestionVoteMapper.php b/Models/QAQuestionVoteMapper.php index 73b50b1..054f98e 100644 --- a/Models/QAQuestionVoteMapper.php +++ b/Models/QAQuestionVoteMapper.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Modules\QA\Models; use phpOMS\DataStorage\Database\DataMapperAbstract; +use Modules\Admin\Models\AccountMapper; /** * Mapper class. @@ -35,11 +36,24 @@ final class QAQuestionVoteMapper extends DataMapperAbstract 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_question' => ['name' => 'qa_question_vote_question', 'type' => 'int', 'internal' => 'question', '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], ]; + /** + * Belongs to. + * + * @var array + * @since 1.0.0 + */ + protected static array $belongsTo = [ + 'createdBy' => [ + 'mapper' => AccountMapper::class, + 'external' => 'qa_question_vote_created_by', + ], + ]; + /** * Primary table. * @@ -63,4 +77,16 @@ final class QAQuestionVoteMapper extends DataMapperAbstract * @since 1.0.0 */ protected static string $primaryField = 'qa_question_vote_id'; + + public static function findVote(int $question, int $account) + { + $depth = 3; + $query = self::getQuery(); + $query->where(self::$table . '_' . $depth . '.qa_question_vote_created_by', '=', $account) + ->andWhere(self::$table . '_' . $depth . '.qa_question_vote_question', '=', $question); + + $results = self::getAllByQuery($query); + + return \reset($results); + } } diff --git a/Theme/Backend/qa-dashboard.tpl.php b/Theme/Backend/qa-dashboard.tpl.php index c770bb1..1d0328e 100755 --- a/Theme/Backend/qa-dashboard.tpl.php +++ b/Theme/Backend/qa-dashboard.tpl.php @@ -12,27 +12,46 @@ */ declare(strict_types=1); +use phpOMS\Uri\UriFactory; +use Modules\Media\Models\NullMedia; + $questions = $this->getData('questions'); echo $this->getData('nav')->render(); ?>
-
+
-
- getAnswers()); ?> +
+
+ getAnswerCount(); ?> + Answers +
+
+ getVoteScore(); ?> + Score +
-
-
- getTags(); foreach ($tags as $tag) : ?> - printHtml($tag->getTitle()); ?> - +
diff --git a/Theme/Backend/qa-question.tpl.php b/Theme/Backend/qa-question.tpl.php index 9c7cb6a..079a08a 100755 --- a/Theme/Backend/qa-question.tpl.php +++ b/Theme/Backend/qa-question.tpl.php @@ -13,37 +13,94 @@ declare(strict_types=1); use phpOMS\Uri\UriFactory; +use Modules\Media\Models\NullMedia; +/** \Modules\QA\Models\QAQuestion $question */ $question = $this->getData('question'); -$answers = $question->getAnswers(); + +/** \Modules\QA\Models\QAAnswer[] $answers */ +$answers = $question->getAnswers(); echo $this->getData('nav')->render(); ?> -
+
-
-
printHtml($question->name); ?>
-
- question; ?> +
+
+
+
+ + getVoteScore(); ?> + Score + +
+
+ getAnswerCount(); ?> + Answers +
+
-
- createdBy->image !== null) : ?> - <?= $this->getHtml('AccountImage', '0', '0'); ?> - -
-
+
+
printHtml($question->name); ?>
+
+
+ question; ?> +
+
+ +
+
-
+
-
-
- answer; ?>printHtml($answer->createdAt->format('Y-m-d')); ?>createdBy->getId(); ?>getStatus(); ?>printHtml((string) $answer->isAccepted()); ?> +
+
+
+
+ + getVoteScore(); ?> + Score + +
+
+ + printHtml($answer->isAccepted ? 'Accepted' : 'Accept'); ?> +
+
-
+
+
+
+ answer; ?> +
+
+ +
+
diff --git a/Theme/Backend/styles.css b/Theme/Backend/styles.css index 6f80943..6644906 100755 --- a/Theme/Backend/styles.css +++ b/Theme/Backend/styles.css @@ -1,16 +1,65 @@ -.qa-list .score { - padding: 10px; - margin-right: 10px; - border: 1px solid #ccc; } - .qa-list .score.closed { - background: #f00; } - .qa-list .score.open { - background: #ff0; } - .qa-list .score.done { - background: #0f0; } -.qa-list .scores { - flex-basis: min-content; } .qa-list .title a { - font-size: 2rem; } -.qa-list .tags { - margin-top: 10px; } + font-size: 2rem; + margin-left: 1rem; } +.qa-list .counter-area { + flex-direction: row; } +.qa-list .counter-container + .counter-container { + margin-left: 10px; } + +.qa-question-view { + display: flex; + flex-direction: row; } + .qa-question-view .counter-area { + flex-direction: column; + margin: 1rem 10px 0 0; + min-width: 50px; } + .qa-question-view .counter-container + .counter-container { + margin-top: 10px; } + +.qa .qa-accept { + font-size: 2rem; + color: #ccc; + cursor: pointer; } +.qa .accepted .qa-accept { + color: #81e27d; } +.qa .accepted .portlet { + border-top: 3px solid #81e27d; } +.qa .qa-vote { + font-size: 2rem; + color: #ccc; + cursor: pointer; } + .qa .qa-vote.voted { + color: #81e27d; } +.qa .counter-area { + display: flex; } + .qa .counter-area .counter-container { + display: flex; + flex-direction: column; + align-items: center; } + .qa .counter-area .counter { + width: 2.3rem; + height: 2.8rem; + border: 1px solid #ccc; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: center; + background: #fff; } + .qa .counter-area .text { + font-size: .8rem; } +.qa .score.closed { + background: #f00; } +.qa .score.open { + background: #f8ffa8; } +.qa .score.done { + background: #81e27d; } +.qa .qa-portlet-foot { + display: flex; + flex-direction: row; + align-items: top; } + .qa .qa-portlet-foot .account-info { + display: flex; + align-items: center; + margin-left: auto; } + .qa .qa-portlet-foot .account-info .name { + margin-right: 10px; } diff --git a/Theme/Backend/styles.scss b/Theme/Backend/styles.scss index 000395c..d269598 100755 --- a/Theme/Backend/styles.scss +++ b/Theme/Backend/styles.scss @@ -1,33 +1,114 @@ .qa-list { + .title { + a { + font-size: 2rem; + margin-left: 1rem; + } + } + + .counter-area { + flex-direction: row; + } + + .counter-container+.counter-container { + margin-left: 10px; + } +} + +.qa-question-view { + display: flex; + flex-direction: row; + + .counter-area { + flex-direction: column; + margin: 1rem 10px 0 0; + min-width: 50px; + } + + .counter-container+.counter-container { + margin-top: 10px; + } +} + +.qa { + .qa-accept { + font-size: 2rem; + color: #ccc; + cursor: pointer; + } + + .accepted { + .qa-accept { + color: #81e27d; + } + + .portlet { + border-top: 3px solid #81e27d; + } + } + + .qa-vote { + font-size: 2rem; + color: #ccc; + cursor: pointer; + + &.voted { + color: #81e27d; + } + } + + .counter-area { + display: flex; + + .counter-container { + display: flex; + flex-direction: column; + align-items: center; + } + + .counter { + width: 2.3rem; + height: 2.8rem; + border: 1px solid #ccc; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + } + + .text { + font-size: .8rem; + } + } + .score { &.closed { background: #f00; } &.open { - background: #ff0; + background: #f8ffa8; } &.done { - background: #0f0; - } - - padding: 10px; - margin-right: 10px; - border: 1px solid #ccc; - } - - .scores { - flex-basis: min-content; - } - - .title { - a { - font-size: 2rem; + background: #81e27d; } } - .tags { - margin-top: 10px; + .qa-portlet-foot { + display: flex; + flex-direction: row; + align-items: top; + + .account-info { + display: flex; + align-items: center; + margin-left: auto; + + .name { + margin-right: 10px; + } + } } -} +} \ No newline at end of file