Skip to content

Commit

Permalink
Paginate tag posts
Browse files Browse the repository at this point in the history
  • Loading branch information
samwilson committed Mar 24, 2021
1 parent 8d5d75c commit cd508b9
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 7 deletions.
5 changes: 5 additions & 0 deletions assets/css/app.less
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ form {
a.button { text-decoration: underline; }
}

nav.pagination {
text-align: center;
margin: 1em 0;
}

@import "flash";
@import "post";
@import "security";
Expand Down
1 change: 1 addition & 0 deletions public/build/app.48bfff7a.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion public/build/app.68d557f9.css

This file was deleted.

2 changes: 1 addition & 1 deletion public/build/entrypoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
],
"css": [
"/build/1.10bbd395.css",
"/build/app.68d557f9.css"
"/build/app.48bfff7a.css"
]
},
"map": {
Expand Down
2 changes: 1 addition & 1 deletion public/build/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"build/0.b868e304.js": "/build/0.b868e304.js",
"build/1.10bbd395.css": "/build/1.10bbd395.css",
"build/1.d9793a66.js": "/build/1.d9793a66.js",
"build/app.css": "/build/app.68d557f9.css",
"build/app.css": "/build/app.48bfff7a.css",
"build/app.js": "/build/app.2d2d6330.js",
"build/map.css": "/build/map.f3a81ea1.css",
"build/map.js": "/build/map.09d99380.js",
Expand Down
29 changes: 26 additions & 3 deletions src/Controller/TagController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Controller;

use App\Repository\PostRepository;
use App\Repository\TagRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -14,7 +15,7 @@ class TagController extends AbstractController
/**
* @Route("/tags/{ids}", name="tags")
*/
public function tags(TagRepository $tagRepository, $ids = '')
public function tags(TagRepository $tagRepository, string $ids = '')
{
return $this->render('tag/index.html.twig', [
'tags' => $tagRepository->findAllOrderedByCount($this->getUser()),
Expand All @@ -23,15 +24,37 @@ public function tags(TagRepository $tagRepository, $ids = '')

/**
* @Route("/T{id}", name="tag_view", requirements={"id"="\d+"})
* @Route("/T{id}/page-{pageNum}", name="tag_view_page", requirements={"id"="\d+", "pageNum"="\d+"})
*/
public function viewTag(TagRepository $tagRepository, $id): Response
{
public function viewTag(
Request $request,
TagRepository $tagRepository,
int $id,
int $pageNum = 1
): Response {
$tag = $tagRepository->find($id);
if (!$tag) {
throw $this->createNotFoundException();
}
$postCount = $tagRepository->countPosts($tag, $this->getUser());
$pageCount = ceil($postCount / 10);
if ($pageNum > $pageCount) {
// Redirect to last page.
return $this->redirectToRoute('tag_view_page', ['id' => $tag->getId(), 'pageNum' => $pageCount]);
}
if (
$pageNum === 1 && $request->get('_route') === 'tag_view_page'
|| $pageNum < 1
) {
// Ensure only one form of URL for page 1, and avoid page 0.
return $this->redirectToRoute('tag_view', ['id' => $tag->getId()]);
}
return $this->render('tag/view.html.twig', [
'tag' => $tag,
'posts' => $tagRepository->findPosts($tag, $this->getUser(), $pageNum),
'post_count' => $postCount,
'page_count' => $pageCount,
'page_num' => $pageNum,
]);
}

Expand Down
47 changes: 47 additions & 0 deletions src/Repository/TagRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Exception;

/**
* @method Tag|null find($id, $lockMode = null, $lockVersion = null)
Expand All @@ -36,6 +40,49 @@ public function getFromIdsString(string $ids): array
];
}

public function countPosts(Tag $tag, ?User $user = null): int
{
return $this->getPostsQueryBuilder($tag, $user)
->select('COUNT(p)')
->getQuery()
->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
}

/**
* @param Tag $tag
* @param User|null $user
* @param int|null $pageNum
* @return array<Post>
*/
public function findPosts(Tag $tag, ?User $user = null, ?int $pageNum = 1): array
{
$pageSize = 10;
return $this->getPostsQueryBuilder($tag, $user)
->setMaxResults($pageSize)
->setFirstResult(($pageNum - 1) * $pageSize)
->getQuery()
->getResult();
}

private function getPostsQueryBuilder(Tag $tag, ?User $user = null): QueryBuilder
{
if (!$tag->getId()) {
throw new Exception('Tag not loaded.');
}
$groupList = $user ? $user->getGroupIdList() : false;
if (!$groupList) {
$groupList = UserGroup::PUBLIC;
}
return $this->getEntityManager()
->createQueryBuilder()
->select('p')
->from(Post::class, 'p')
->join('p.tags', 't', Join::WITH, 't.id = :tag_id')
->setParameter('tag_id', $tag->getId())
->andWhere('p.view_group IN (' . $groupList . ')')
->orderBy('p.date', 'DESC');
}

public function findAllOrderedByCount(?User $user)
{
$groupList = $user ? $user->getGroupIdList() : UserGroup::PUBLIC;
Expand Down
18 changes: 17 additions & 1 deletion templates/tag/view.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@

{{ tag.description|markdownToHtml }}

{% include 'post/_post_list.html.twig' with {posts: tag.posts} %}
<nav class="pagination">
{% if page_num == 2 %}
<a href="{{ path('tag_view', {id: tag.id}) }}">&larr; Prev</a>
{% elseif page_num > 1 %}
<a href="{{ path('tag_view_page', {id: tag.id, pageNum: page_num - 1}) }}">&larr; Prev</a>
{% else %}
<span>&larr; Prev</span>
{% endif %}
| Page {{ page_num }} of {{ page_count }} ({{ post_count }} posts) |
{% if page_num < page_count %}
<a href="{{ path('tag_view_page', {id: tag.id, pageNum: page_num + 1}) }}">Next &rarr;</a>
{% else %}
<span>Next &rarr;</span>
{% endif %}
</nav>

{% include 'post/_post_list.html.twig' with {posts: posts} %}

{% endblock %}
39 changes: 39 additions & 0 deletions tests/Repository/RepositoryTestBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace App\Tests\Repository;

use App\Entity\Post;
use App\Kernel;
use App\Repository\PostRepository;
use App\Repository\TagRepository;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

abstract class RepositoryTestBase extends KernelTestCase
{

/**
* @var \Doctrine\ORM\EntityManager
*/
protected $entityManager;

protected static function getKernelClass()
{
return Kernel::class;
}

protected function setUp(): void
{
// Set a fake clock time of 2020-11-15 07:36:41 and register all our classes that use the time() function.
ClockMock::withClockMock(1605425801);
ClockMock::register(self::class);
ClockMock::register(PostRepository::class);
ClockMock::register(TagRepository::class);
ClockMock::register(Post::class);

$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
}
47 changes: 47 additions & 0 deletions tests/Repository/TagRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Tests\Repository;

use App\Entity\Post;
use App\Entity\Tag;
use App\Entity\User;
use App\Entity\UserGroup;
use App\Repository\PostRepository;
use App\Repository\TagRepository;
use Symfony\Component\HttpFoundation\Request;

class TagRepositoryTest extends RepositoryTestBase
{

public function testGetPostList()
{
/** @var TagRepository $tagRepo */
$tagRepo = $this->entityManager->getRepository(Tag::class);
/** @var PostRepository $postRepo */
$postRepo = $this->entityManager->getRepository(Post::class);

$tag = new Tag();
$tag->setTitle('Tag 1');
$this->entityManager->persist($tag);
$privateGroup = new UserGroup();
$privateGroup->setName('Private');
$this->entityManager->persist($privateGroup);
$this->entityManager->flush();

// Create two posts tagged the same, one of which is private.
$publicPost = new Post();
$publicPostData = ['tags' => 'Tag 1', 'view_group' => UserGroup::PUBLIC, 'author' => 1];
$postRepo->saveFromRequest($publicPost, new Request([], $publicPostData));
$privatePost = new Post();
$privatePostData = ['tags' => 'Tag 1', 'view_group' => $privateGroup->getId(), 'author' => 1];
$postRepo->saveFromRequest($privatePost, new Request([], $privatePostData));

// Make sure the user is only in the public group.
$user = new User();

// Check that only one post is returned.
$this->assertSame(1, $tagRepo->countPosts($tag, $user));
$posts = $tagRepo->findPosts($tag, $user);
$this->assertGreaterThan(0, $posts[0]->getId());
}
}

0 comments on commit cd508b9

Please sign in to comment.