diff --git a/Web/Models/Entities/Topic.php b/Web/Models/Entities/Topic.php new file mode 100644 index 00000000..938eb383 --- /dev/null +++ b/Web/Models/Entities/Topic.php @@ -0,0 +1,86 @@ +isPostedOnBehalfOfGroup()) + return $this->getClub(); + + return parent::getOwner($real); + } + + function getClub(): Club + { + return (new Clubs)->get($this->getRecord()->group); + } + + function getTitle(): string + { + return $this->getRecord()->title; + } + + function isClosed(): bool + { + return (bool) $this->getRecord()->closed; + } + + function isPinned(): bool + { + return (bool) $this->getRecord()->pinned; + } + + function getPrettyId(): string + { + return $this->getRecord()->group . "_" . $this->getVirtualId(); + } + + function isPostedOnBehalfOfGroup(): bool + { + return ($this->getRecord()->flags & 0b10000000) > 0; + } + + function isDeleted(): bool + { + return (bool) $this->getRecord()->deleted; + } + + function canBeModifiedBy(User $user): bool + { + return $this->getOwner(false)->getId() === $user->getId() || $this->club->canBeModifiedBy($user); + } + + function getLastComment(): ?Comment + { + $array = iterator_to_array($this->getLastComments(1)); + return isset($array[0]) ? $array[0] : NULL; + } + + function getUpdateTime(): DateTime + { + $lastComment = $this->getLastComment(); + if(!is_null($lastComment)) + return $lastComment->getPublicationTime(); + else + return $this->getEditTime() ?? $this->getPublicationTime(); + } + + function deleteTopic(): void + { + $this->setDeleted(1); + $this->unwire(); + $this->save(); + } +} diff --git a/Web/Models/Repositories/Topics.php b/Web/Models/Repositories/Topics.php new file mode 100644 index 00000000..23b854d4 --- /dev/null +++ b/Web/Models/Repositories/Topics.php @@ -0,0 +1,73 @@ +context = DatabaseConnection::i()->getContext(); + $this->topics = $this->context->table("topics"); + } + + private function toTopic(?ActiveRow $ar): ?Topic + { + return is_null($ar) ? NULL : new Topic($ar); + } + + function get(int $id): ?Topic + { + return $this->toTopic($this->topics->get($id)); + } + + function getTopicById(int $club, int $topic): ?Topic + { + return $this->toTopic($this->topics->where(["group" => $club, "virtual_id" => $topic, "deleted" => 0])->fetch()); + } + + function getClubTopics(Club $club, int $page = 1, ?int $perPage = NULL): \Traversable + { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + + // Get pinned topics first + $query = "SELECT `id` FROM `topics` WHERE `pinned` = 1 AND `group` = ? AND `deleted` = 0 UNION SELECT `id` FROM `topics` WHERE `pinned` = 0 AND `group` = ? AND `deleted` = 0"; + $query .= " LIMIT " . $perPage . " OFFSET " . ($page - 1) * $perPage; + + foreach(DatabaseConnection::i()->getConnection()->query($query, $club->getId(), $club->getId()) as $topic) { + $topic = $this->get($topic->id); + if(!$topic) continue; + + yield $topic; + } + } + + function getClubTopicsCount(Club $club): int + { + return sizeof($this->topics->where([ + "group" => $club->getId(), + "deleted" => false + ])); + } + + function find(Club $club, string $query): \Traversable + { + return new Util\EntityStream("Topic", $this->topics->where("title LIKE ? AND group = ? AND deleted = 0", "%$query%", $club->getId())); + } + + function getLastTopics(Club $club, ?int $count = NULL): \Traversable + { + $topics = $this->topics->where([ + "group" => $club->getId(), + "deleted" => false + ])->page(1, $count ?? OPENVK_DEFAULT_PER_PAGE)->order("created DESC"); + + foreach($topics as $topic) + yield $this->toTopic($topic); + } +} diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index c904c295..c114ba5d 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -1,16 +1,17 @@ "openvk\\Web\\Models\\Repositories\\Posts", - "photos" => "openvk\\Web\\Models\\Repositories\\Photos", - "videos" => "openvk\\Web\\Models\\Repositories\\Videos", - "notes" => "openvk\\Web\\Models\\Repositories\\Notes", + "posts" => "openvk\\Web\\Models\\Repositories\\Posts", + "photos" => "openvk\\Web\\Models\\Repositories\\Photos", + "videos" => "openvk\\Web\\Models\\Repositories\\Videos", + "notes" => "openvk\\Web\\Models\\Repositories\\Notes", + "topics" => "openvk\\Web\\Models\\Repositories\\Topics", ]; function renderLike(int $id): void @@ -37,6 +38,9 @@ final class CommentPresenter extends OpenVKPresenter $repo = new $repoClass; $entity = $repo->get($eId); if(!$entity) $this->notFound(); + + if($entity instanceof Topic && $entity->isClosed()) + $this->notFound(); $flags = 0; if($this->postParam("as_group") === "on") diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index a253ea72..1a7b1932 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo}; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; final class GroupPresenter extends OpenVKPresenter { @@ -28,6 +28,8 @@ final class GroupPresenter extends OpenVKPresenter $this->template->club = $club; $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); + $this->template->topics = (new Topics)->getLastTopics($club, 3); + $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); } } @@ -204,6 +206,7 @@ final class GroupPresenter extends OpenVKPresenter $club->setShortcode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode")); $club->setWall(empty($this->postParam("wall")) ? 0 : 1); $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); + $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $website = $this->postParam("website") ?? ""; if(empty($website)) diff --git a/Web/Presenters/TopicsPresenter.php b/Web/Presenters/TopicsPresenter.php new file mode 100644 index 00000000..de27fd38 --- /dev/null +++ b/Web/Presenters/TopicsPresenter.php @@ -0,0 +1,191 @@ +topics = $topics; + $this->clubs = $clubs; + + parent::__construct(); + } + + function renderBoard(int $id): void + { + $this->assertUserLoggedIn(); + + $club = $this->clubs->get($id); + if(!$club) + $this->notFound(); + + $this->template->club = $club; + $page = (int) ($this->queryParam("p") ?? 1); + + $query = $this->queryParam("query"); + if($query) { + $results = $this->topics->find($club, $query); + $this->template->topics = $results->page($page); + $this->template->count = $results->size(); + } else { + $this->template->topics = $this->topics->getClubTopics($club, $page); + $this->template->count = $this->topics->getClubTopicsCount($club); + } + + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $page, + "amount" => NULL, + "perPage" => OPENVK_DEFAULT_PER_PAGE, + ]; + } + + function renderTopic(int $clubId, int $topicId): void + { + $this->assertUserLoggedIn(); + + $topic = $this->topics->getTopicById($clubId, $topicId); + if(!$topic) + $this->notFound(); + + $this->template->topic = $topic; + $this->template->club = $topic->getClub(); + $this->template->count = $topic->getCommentsCount(); + $this->template->page = (int) ($this->queryParam("p") ?? 1); + $this->template->comments = iterator_to_array($topic->getComments($this->template->page)); + } + + function renderCreate(int $clubId): void + { + $this->assertUserLoggedIn(); + + $club = $this->clubs->get($clubId); + if(!$club) + $this->notFound(); + + if(!$club->isEveryoneCanCreateTopics() && !$club->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->willExecuteWriteAction(); + $title = $this->postParam("title"); + + if(!$title) + $this->flashFail("err", tr("failed_to_create_topic"), tr("no_title_specified")); + + $flags = 0; + if($this->postParam("as_group") === "on") + $flags |= 0b10000000; + + $topic = new Topic; + $topic->setGroup($club->getId()); + $topic->setOwner($this->user->id); + $topic->setTitle(ovk_proc_strtr($title, 127)); + $topic->setCreated(time()); + $topic->setFlags($flags); + $topic->save(); + + // TODO move to trait + try { + $photo = NULL; + $video = NULL; + if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { + $album = NULL; + if($wall > 0 && $wall === $this->user->id) + $album = (new Albums)->getUserWallAlbum($wallOwner); + + $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album); + } + + if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { + $video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"]); + } + } catch(ISE $ex) { + $this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); + $this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY); + } + + if(!empty($this->postParam("text")) || $photo || $video) { + try { + $comment = new Comment; + $comment->setOwner($this->user->id); + $comment->setModel(get_class($topic)); + $comment->setTarget($topic->getId()); + $comment->setContent($this->postParam("text")); + $comment->setCreated(time()); + $comment->setFlags($flags); + $comment->save(); + } catch (\LengthException $ex) { + $this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); + $this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY); + } + + if(!is_null($photo)) + $comment->attach($photo); + + if(!is_null($video)) + $comment->attach($video); + } + + $this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY); + } + + $this->template->club = $club; + $this->template->graffiti = (bool) ovkGetQuirk("comments.allow-graffiti"); + } + + function renderEdit(int $clubId, int $topicId): void + { + $this->assertUserLoggedIn(); + + $topic = $this->topics->getTopicById($clubId, $topicId); + if(!$topic) + $this->notFound(); + + if(!$topic->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->willExecuteWriteAction(); + $title = $this->postParam("title"); + + if(!$title) + $this->flashFail("err", tr("failed_to_change_topic"), tr("no_title_specified")); + + $topic->setTitle(ovk_proc_strtr($title, 127)); + $topic->setClosed(empty($this->postParam("close")) ? 0 : 1); + $topic->setPinned(empty($this->postParam("pin")) ? 0 : 1); + $topic->save(); + + $this->flash("succ", tr("changes_saved"), tr("topic_changes_saved_comment")); + $this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY); + } + + $this->template->topic = $topic; + $this->template->club = $topic->getClub(); + } + + function renderDelete(int $clubId, int $topicId): void + { + $this->assertUserLoggedIn(); + $this->assertNoCSRF(); + + $topic = $this->topics->getTopicById($clubId, $topicId); + if(!$topic) + $this->notFound(); + + if(!$topic->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + $this->willExecuteWriteAction(); + $topic->deleteTopic(); + + $this->redirect("/board" . $topic->getClub()->getId(), static::REDIRECT_TEMPORARY); + } +} diff --git a/Web/Presenters/templates/Group/Edit.xml b/Web/Presenters/templates/Group/Edit.xml index 093d7aa1..ecf17393 100644 --- a/Web/Presenters/templates/Group/Edit.xml +++ b/Web/Presenters/templates/Group/Edit.xml @@ -69,7 +69,7 @@ -
+ {tr("results", $count)} +
+{/block} + +{block actions} + +{/block} + +{* BEGIN ELEMENTS DESCRIPTION *} + +{block link|strip|stripHtml} + /topic{$x->getPrettyId()} +{/block} + +{block preview} + +{/block} + +{block name} + {$x->getTitle()} + +{/block} + +{block description} +