bizley/yii2-podium

View on GitHub
src/models/Thread.php

Summary

Maintainability
F
5 days
Test Coverage
<?php

namespace bizley\podium\models;

use bizley\podium\log\Log;
use bizley\podium\models\db\ThreadActiveRecord;
use bizley\podium\Podium;
use bizley\podium\PodiumCache;
use bizley\podium\rbac\Rbac;
use cebe\markdown\GithubMarkdown;
use Exception;
use Yii;
use yii\data\ActiveDataProvider;
use yii\db\Expression;

/**
 * Thread model
 *
 * @author Paweł Bizley Brzozowski <pawel@positive.codes>
 * @since 0.1
 *
 * @property string $parsedPost
 */
class Thread extends ThreadActiveRecord
{
    /**
     * Color classes.
     */
    const CLASS_DEFAULT = 'default';
    const CLASS_EDITED  = 'warning';
    const CLASS_NEW     = 'success';

    /**
     * Icon classes.
     */
    const ICON_HOT    = 'fire';
    const ICON_LOCKED = 'lock';
    const ICON_NEW    = 'leaf';
    const ICON_NO_NEW = 'comment';
    const ICON_PINNED = 'pushpin';

    /**
     * Returns first post to see.
     * @return Post
     */
    public function firstToSee()
    {
        if ($this->firstNewNotSeen) {
            return $this->firstNewNotSeen;
        }
        if ($this->firstEditedNotSeen) {
            return $this->firstEditedNotSeen;
        }
        return $this->latest;
    }

    /**
     * Searches for thread.
     * @param int $forumId
     * @return ActiveDataProvider
     */
    public function search($forumId = null, $filters = null)
    {
        $query = static::find();
        if ($forumId) {
            $query->where(['forum_id' => (int)$forumId]);
        }
        if (!empty($filters)) {
            $loggedId = User::loggedId();
            if (!empty($filters['pin']) && $filters['pin'] == 1) {
                $query->andWhere(['pinned' => 1]);
            }
            if (!empty($filters['lock']) && $filters['lock'] == 1) {
                $query->andWhere(['locked' => 1]);
            }
            if (!empty($filters['hot']) && $filters['hot'] == 1) {
                $query->andWhere(['>=', 'posts', Podium::getInstance()->podiumConfig->get('hot_minimum')]);
            }
            if (!empty($filters['new']) && $filters['new'] == 1 && !Podium::getInstance()->user->isGuest) {
                $query->joinWith(['threadView tvn' => function ($q) use ($loggedId) {
                    $q->onCondition(['tvn.user_id' => $loggedId]);
                    $q->andWhere(['or',
                            new Expression('tvn.new_last_seen < new_post_at'),
                            ['tvn.new_last_seen' => null]
                        ]);
                }], false);
            }
            if (!empty($filters['edit']) && $filters['edit'] == 1 && !Podium::getInstance()->user->isGuest) {
                $query->joinWith(['threadView tve' => function ($q) use ($loggedId) {
                    $q->onCondition(['tve.user_id' => $loggedId]);
                    $q->andWhere(['or',
                            new Expression('tve.edited_last_seen < edited_post_at'),
                            ['tve.edited_last_seen' => null]
                        ]);
                }], false);
            }
        }

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'pagination' => false,
        ]);
        $dataProvider->sort->defaultOrder = [
            'pinned' => SORT_DESC,
            'updated_at' => SORT_DESC,
            'id' => SORT_ASC
        ];

        return $dataProvider;
    }

    /**
     * Searches for threads created by user of given ID.
     * @param int $userId
     * @return ActiveDataProvider
     */
    public function searchByUser($userId)
    {
        $query = static::find();
        $query->where(['author_id' => $userId]);
        if (Podium::getInstance()->user->isGuest) {
            $query->joinWith(['forum' => function ($q) {
                $q->where([Forum::tableName() . '.visible' => 1]);
            }]);
        }

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'pagination' => false,
        ]);
        $dataProvider->sort->defaultOrder = [
            'updated_at' => SORT_DESC,
            'id' => SORT_ASC
        ];

        return $dataProvider;
    }

    /**
     * Returns proper icon for thread.
     * @return string
     */
    public function getIcon()
    {
        $icon   = self::ICON_NO_NEW;
        $append = false;

        if ($this->locked) {
            $icon = self::ICON_LOCKED;
            $append = true;
        } elseif ($this->pinned) {
            $icon = self::ICON_PINNED;
            $append = true;
        } elseif ($this->posts >= Podium::getInstance()->podiumConfig->get('hot_minimum')) {
            $icon = self::ICON_HOT;
            $append = true;
        }
        if ($this->userView) {
            if ($this->new_post_at > $this->userView->new_last_seen) {
                if (!$append) {
                    $icon = self::ICON_NEW;
                }
            } elseif ($this->edited_post_at > $this->userView->edited_last_seen) {
                if (!$append) {
                    $icon = self::ICON_NEW;
                }
            }
        } else {
            if (!$append) {
                $icon = self::ICON_NEW;
            }
        }
        return $icon;
    }

    /**
     * Returns proper description for thread.
     * @return string
     */
    public function getDescription()
    {
        $description = Yii::t('podium/view', 'No New Posts');
        $append = false;

        if ($this->locked) {
            $description = Yii::t('podium/view', 'Locked Thread');
            $append = true;
        } elseif ($this->pinned) {
            $description = Yii::t('podium/view', 'Pinned Thread');
            $append = true;
        } elseif ($this->posts >= Podium::getInstance()->podiumConfig->get('hot_minimum')) {
            $description = Yii::t('podium/view', 'Hot Thread');
            $append = true;
        }
        if ($this->userView) {
            if ($this->new_post_at > $this->userView->new_last_seen) {
                if (!$append) {
                    $description = Yii::t('podium/view', 'New Posts');
                } else {
                    $description .= ' (' . Yii::t('podium/view', 'New Posts') . ')';
                }
            } elseif ($this->edited_post_at > $this->userView->edited_last_seen) {
                if (!$append) {
                    $description = Yii::t('podium/view', 'Edited Posts');
                } else {
                    $description = ' (' . Yii::t('podium/view', 'Edited Posts') . ')';
                }
            }
        } else {
            if (!$append) {
                $description = Yii::t('podium/view', 'New Posts');
            } else {
                $description .= ' (' . Yii::t('podium/view', 'New Posts') . ')';
            }
        }
        return $description;
    }

    /**
     * Returns proper CSS class for thread.
     * @return string
     */
    public function getCssClass()
    {
        $class = self::CLASS_DEFAULT;

        if ($this->userView) {
            if ($this->new_post_at > $this->userView->new_last_seen) {
                $class = self::CLASS_NEW;
            } elseif ($this->edited_post_at > $this->userView->edited_last_seen) {
                $class = self::CLASS_EDITED;
            }
        } else {
            $class = self::CLASS_NEW;
        }
        return $class;
    }

    /**
     * Checks if user is this thread moderator.
     * @param int $userId
     * @return bool
     */
    public function isMod($userId = null)
    {
        if (User::can(Rbac::ROLE_ADMIN)) {
            return true;
        }
        if (in_array($userId, $this->forum->getMods())) {
            return true;
        }
        return false;
    }

    /**
     * Performs thread delete with parent forum counters update.
     * @return bool
     * @since 0.2
     */
    public function podiumDelete()
    {
        $transaction = Thread::getDb()->beginTransaction();
        try {
            if (!$this->delete()) {
                throw new Exception('Thread deleting error!');
            }
            $this->forum->updateCounters([
                'threads' => -1,
                'posts'   => -$this->postsCount
            ]);
            $transaction->commit();
            PodiumCache::clearAfter('threadDelete');
            Log::info('Thread deleted', $this->id, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs thread posts delete with parent forum counters update.
     * @param array $posts posts IDs
     * @return bool
     * @throws Exception
     * @since 0.2
     */
    public function podiumDeletePosts($posts)
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            foreach ($posts as $post) {
                if (!is_numeric($post) || $post < 1) {
                    throw new Exception('Incorrect post ID');
                }
                $nPost = Post::find()
                            ->where([
                                'id' => $post,
                                'thread_id' => $this->id,
                                'forum_id' => $this->forum->id
                            ])
                            ->limit(1)
                            ->one();
                if (!$nPost) {
                    throw new Exception('No post of given ID found');
                }
                if (!$nPost->delete()) {
                    throw new Exception('Post deleting error!');
                }
            }
            $wholeThread = false;
            if ($this->postsCount) {
                $this->updateCounters(['posts' => -count($posts)]);
                $this->forum->updateCounters(['posts' => -count($posts)]);
            } else {
                $wholeThread = true;
                if (!$this->delete()) {
                    throw new Exception('Thread deleting error!');
                }
                $this->forum->updateCounters(['posts' => -count($posts), 'threads' => -1]);
            }
            $transaction->commit();
            PodiumCache::clearAfter('postDelete');
            Log::info('Posts deleted', null, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs thread lock / unlock.
     * @return bool
     * @since 0.2
     */
    public function podiumLock()
    {
        $this->locked = !$this->locked;
        if ($this->save()) {
            Log::info($this->locked ? 'Thread locked' : 'Thread unlocked', $this->id, __METHOD__);
            return true;
        }
        return false;
    }

    /**
     * Performs thread move with counters update.
     * @param int $target new parent forum's ID
     * @return bool
     * @since 0.2
     */
    public function podiumMoveTo($target = null)
    {
        $newParent = Forum::find()->where(['id' => $target])->limit(1)->one();
        if (empty($newParent)) {
            Log::error('No parent forum of given ID found', $this->id, __METHOD__);
            return false;
        }

        $postsCount = $this->postsCount;
        $transaction = Forum::getDb()->beginTransaction();
        try {
            $this->forum->updateCounters(['threads' => -1, 'posts' => -$postsCount]);
            $newParent->updateCounters(['threads' => 1, 'posts' => $postsCount]);
            $this->forum_id = $newParent->id;
            $this->category_id = $newParent->category_id;
            if (!$this->save()) {
                throw new Exception('Thread saving error!');
            }
            Post::updateAll(['forum_id' => $newParent->id], ['thread_id' => $this->id]);
            $transaction->commit();
            PodiumCache::clearAfter('threadMove');
            Log::info('Thread moved', $this->id, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs thread posts move with counters update.
     * @param int $target new parent thread ID
     * @param array $posts IDs of posts to move
     * @param string $name new thread name if $target = 0
     * @param type $forum new thread parent forum if $target = 0
     * @return bool
     * @throws Exception
     * @since 0.2
     */
    public function podiumMovePostsTo($target = null, $posts = [], $name = null, $forum = null)
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            if ($target == 0) {
                $parent = Forum::find()->where(['id' => $forum])->limit(1)->one();
                if (empty($parent)) {
                    throw new Exception('No parent forum of given ID found');
                }
                $newThread = new Thread();
                $newThread->name = $name;
                $newThread->posts = 0;
                $newThread->views = 0;
                $newThread->category_id = $parent->category_id;
                $newThread->forum_id = $parent->id;
                $newThread->author_id = User::loggedId();
                if (!$newThread->save()) {
                    throw new Exception('Thread saving error!');
                }
            } else {
                $newThread = Thread::find()->where(['id' => $target])->limit(1)->one();
                if (empty($newThread)) {
                    throw new Exception('No thread of given ID found');
                }
            }
            if (empty($newThread)) {
                throw new Exception('No target thread selected!');
            }
            foreach ($posts as $post) {
                if (!is_numeric($post) || $post < 1) {
                    throw new Exception('Incorrect post ID');
                }
                $newPost = Post::find()
                            ->where([
                                'id'        => $post,
                                'thread_id' => $this->id,
                                'forum_id'  => $this->forum->id
                            ])
                            ->limit(1)
                            ->one();
                if (empty($newPost)) {
                    throw new Exception('No post of given ID found');
                }
                $newPost->thread_id = $newThread->id;
                $newPost->forum_id = $newThread->forum_id;
                if (!$newPost->save()) {
                    throw new Exception('Post saving error!');
                }
            }
            $wholeThread = false;
            if ($this->postCount) {
                $this->updateCounters(['posts' => -count($posts)]);
                $this->forum->updateCounters(['posts' => -count($posts)]);
            } else {
                $wholeThread = true;
                if (!$this->delete()) {
                    throw new Exception('Thread deleting error!');
                }
                $this->forum->updateCounters(['posts' => -count($posts), 'threads' => -1]);
            }
            $newThread->updateCounters(['posts' => count($posts)]);
            $newThread->forum->updateCounters(['posts' => count($posts)]);
            $transaction->commit();
            PodiumCache::clearAfter('postMove');
            Log::info('Posts moved', null, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs new thread with first post creation and subscription.
     * Saves thread poll.
     * @return bool
     * @since 0.2
     */
    public function podiumNew()
    {
        $transaction = static::getDb()->beginTransaction();
        try {
            if (!$this->save()) {
                throw new Exception('Thread saving error!');
            }

            $loggedIn = User::loggedId();

            if ($this->pollAdded && Podium::getInstance()->podiumConfig->get('allow_polls')) {
                $poll = new Poll();
                $poll->thread_id = $this->id;
                $poll->question = $this->pollQuestion;
                $poll->votes = $this->pollVotes;
                $poll->hidden = $this->pollHidden;
                $poll->end_at = !empty($this->pollEnd) ? Podium::getInstance()->formatter->asTimestamp($this->pollEnd . ' 23:59:59') : null;
                $poll->author_id = $loggedIn;
                if (!$poll->save()) {
                    throw new Exception('Poll saving error!');
                }

                foreach ($this->pollAnswers as $answer) {
                    $pollAnswer = new PollAnswer();
                    $pollAnswer->poll_id = $poll->id;
                    $pollAnswer->answer = $answer;
                    if (!$pollAnswer->save()) {
                        throw new Exception('Poll Answer saving error!');
                    }
                }
                Log::info('Poll added', $poll->id, __METHOD__);
            }

            $this->forum->updateCounters(['threads' => 1]);

            $post = new Post();
            $post->content = $this->post;
            $post->thread_id = $this->id;
            $post->forum_id = $this->forum_id;
            $post->author_id = $loggedIn;
            $post->likes = 0;
            $post->dislikes = 0;
            if (!$post->save()) {
                throw new Exception('Post saving error!');
            }

            $post->markSeen();
            $this->forum->updateCounters(['posts' => 1]);
            $this->updateCounters(['posts' => 1]);

            $this->touch('new_post_at');
            $this->touch('edited_post_at');

            if ($this->subscribe) {
                $subscription = new Subscription();
                $subscription->user_id = $loggedIn;
                $subscription->thread_id = $this->id;
                $subscription->post_seen = Subscription::POST_SEEN;
                if (!$subscription->save()) {
                    throw new Exception('Subscription saving error!');
                }
            }

            $transaction->commit();
            PodiumCache::clearAfter('newThread');
            Log::info('Thread added', $this->id, __METHOD__);
            return true;
        } catch (Exception $e) {
            $transaction->rollBack();
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Performs thread pin / unpin.
     * @return bool
     * @since 0.2
     */
    public function podiumPin()
    {
        $this->pinned = !$this->pinned;
        if ($this->save()) {
            Log::info($this->pinned ? 'Thread pinned' : 'Thread unpinned', $this->id, __METHOD__);
            return true;
        }
        return false;
    }

    /**
     * Performs marking all unread threads as seen for user.
     * @return bool
     * @throws Exception
     * @since 0.2
     */
    public static function podiumMarkAllSeen()
    {
        try {
            $loggedId = User::loggedId();
            if (empty($loggedId)) {
                throw new Exception('User ID missing');
            }
            $updateBatch = [];
            $threadsPrevMarked = Thread::find()->joinWith(['threadView' => function ($q) use ($loggedId) {
                $q->onCondition(['user_id' => $loggedId]);
                $q->andWhere(['or',
                        new Expression('new_last_seen < new_post_at'),
                        new Expression('edited_last_seen < edited_post_at'),
                    ]);
            }], false);
            $time = time();
            foreach ($threadsPrevMarked->each() as $thread) {
                $updateBatch[] = $thread->id;
            }
            if (!empty($updateBatch)) {
                Podium::getInstance()->db->createCommand()->update(ThreadView::tableName(), [
                    'new_last_seen' => $time,
                    'edited_last_seen' => $time
                ], ['thread_id' => $updateBatch, 'user_id' => $loggedId])->execute();
            }

            $insertBatch = [];
            $threadsNew = Thread::find()->joinWith(['threadView' => function ($q) use ($loggedId) {
                $q->onCondition(['user_id' => $loggedId]);
                $q->andWhere(['new_last_seen' => null]);
            }], false);
            foreach ($threadsNew->each() as $thread) {
                $insertBatch[] = [$loggedId, $thread->id, $time, $time];
            }
            if (!empty($insertBatch)) {
                Podium::getInstance()->db->createCommand()->batchInsert(ThreadView::tableName(), [
                    'user_id', 'thread_id', 'new_last_seen', 'edited_last_seen'
                ], $insertBatch)->execute();
            }
            return true;
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Returns post Markdown-parsed if WYSIWYG editor is switched off.
     * @return string
     * @since 0.6
     */
    public function getParsedPost()
    {
        if (Podium::getInstance()->podiumConfig->get('use_wysiwyg') == '0') {
            $parser = new GithubMarkdown();
            $parser->html5 = true;
            return $parser->parse($this->post);
        }
        return $this->post;
    }
}