Skip to content

Commit

Permalink
Merge pull request #321 from biblioverse/refactor-reading-status
Browse files Browse the repository at this point in the history
Refactor reading status and reading list into enums
  • Loading branch information
SergioMendolia authored Feb 2, 2025
2 parents cc4ee69 + 398aeb1 commit c41b393
Show file tree
Hide file tree
Showing 48 changed files with 711 additions and 345 deletions.
10 changes: 2 additions & 8 deletions config/packages/biblioverse_typesense.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ biblioverse_typesense:
type: string
facet: true
optional: true
entity_attribute: ageCategory
entity_attribute: ageCategoryLabel
user.read:
name: read
optional: true
Expand All @@ -76,13 +76,7 @@ biblioverse_typesense:
default_sorting_field: sortable_id
symbols_to_index: ['+', '#', '@', '_']
token_separators: [' ', '-', "'"]
# finders:
# books_autocomplete:
# finder_parameters:
# query_by: title,serie,extension,authors,tags,summary
# limit: 100
# num_typos: 2

when@test:
biblioverse_typesense:
auto_update: false
auto_update: false
38 changes: 38 additions & 0 deletions migrations/Version20241218154423.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241218154423 extends AbstractMigration
{
public function getDescription(): string
{
return 'Migrate to Enums for readstatus and readinglist';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE book_interaction ADD read_status VARCHAR(255) DEFAULT \'rs-not-started\' NOT NULL, ADD reading_list VARCHAR(255) DEFAULT \'rl-undefined\' NOT NULL');

$this->addSql('UPDATE book_interaction SET read_status = \'rs-finished\' WHERE finished =1');
$this->addSql('UPDATE book_interaction SET read_status = \'rs-started\' WHERE finished =0 and read_pages > 0');
$this->addSql('UPDATE book_interaction SET read_status = \'rs-not-started\' WHERE finished =0 and read_pages = 0');

$this->addSql('UPDATE book_interaction SET reading_list = \'rl-ignored\' WHERE hidden =1');
$this->addSql('UPDATE book_interaction SET reading_list = \'rl-to-read\' WHERE hidden =0 and favorite = 1');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE book_interaction DROP read_status, DROP reading_list');
}
}
31 changes: 31 additions & 0 deletions migrations/Version20241218154713.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241218154713 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add interaction rating';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE book_interaction ADD rating INT DEFAULT NULL');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE book_interaction DROP rating');
}
}
32 changes: 32 additions & 0 deletions migrations/Version20250202073734.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250202073734 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('DELETE FROM book_interaction WHERE book_id is null or user_id is null');
$this->addSql('ALTER TABLE book_interaction CHANGE user_id user_id INT NOT NULL, CHANGE book_id book_id INT NOT NULL');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE book_interaction CHANGE user_id user_id INT DEFAULT NULL, CHANGE book_id book_id INT DEFAULT NULL');
}
}
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ parameters:
# - '#Asserted type (.*) for (.*) with type (.*) does not narrow down the type.#'
errorFormat: symplify
typeAliases:
ReadingStateCriteria: "array{'book':int, 'readPages': int|null, 'finished': boolean}"
ReadingStateCriteria: "array{'book':int, 'readPages': int|null, 'readStatus': App\\Enum\\ReadStatus}"
9 changes: 5 additions & 4 deletions src/Controller/AutocompleteGroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

namespace App\Controller;

use App\Entity\User;
use App\Enum\AgeCategory;
use App\Repository\BookRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
* @phpstan-type GroupType array{ item:string, slug:string, bookCount:int, booksFinished:int, lastBookIndex:int }
*/
class AutocompleteGroupController extends AbstractController
{
#[Route('/autocomplete/group/{type}', name: 'app_autocomplete_group')]
public function index(Request $request, BookRepository $bookRepository, string $type): Response
public function index(Request $request, BookRepository $bookRepository, string $type, TranslatorInterface $translator): Response
{
$query = $request->get('query');
if (!is_string($query)) {
Expand All @@ -25,8 +26,8 @@ public function index(Request $request, BookRepository $bookRepository, string $
$json = ['results' => []];

if ($type === 'ageCategory') {
foreach (User::AGE_CATEGORIES as $ageCategory => $ageCategoryId) {
$json['results'][] = ['value' => $ageCategoryId, 'text' => $ageCategory];
foreach (AgeCategory::cases() as $ageCategory) {
$json['results'][] = ['value' => $ageCategory->value, 'text' => $translator->trans(AgeCategory::getLabel($ageCategory))];
}

return new JsonResponse($json);
Expand Down
16 changes: 10 additions & 6 deletions src/Controller/BookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Entity\Book;
use App\Entity\BookInteraction;
use App\Entity\User;
use App\Enum\ReadStatus;
use App\Repository\BookRepository;
use App\Repository\ShelfRepository;
use App\Security\Voter\BookVoter;
Expand Down Expand Up @@ -199,15 +200,18 @@ public function read(

$page = (int) max(1, $page);

if (!$interaction->isFinished() && $interaction->getReadPages() < $page) {
if ($interaction->getReadStatus() !== ReadStatus::Finished && $interaction->getReadPages() < $page) {
$interaction->setReadPages($page);
}
$manager->persist($interaction);
$manager->flush();

if (!$interaction->isFinished() && $page === $book->getPageNumber()) {
$interaction->setFinished(true);
$interaction->setFinishedDate(new \DateTime());
if ($interaction->getReadStatus() !== ReadStatus::Finished && $page === $book->getPageNumber()) {
$interaction->setReadStatus(ReadStatus::Finished);

// TODO Add next unread in serie to reading list?
$interaction->setFinishedDate(new \DateTimeImmutable());
$this->addFlash('success', 'Book finished! Congratulations!');
$manager->flush();

Expand Down Expand Up @@ -390,9 +394,9 @@ public function relocate(Request $request, Book $book, BookFileSystemManagerInte
$book = $fileSystemManager->renameFiles($book);
$entityManager->persist($book);
$entityManager->flush();
$this->addFlash('success', 'Files relocated');
$this->addFlash('success', 'Book relocated.');
} catch (\Exception $e) {
$this->addFlash('danger', 'Error while relocating files: '.$e->getMessage());
$this->addFlash('danger', 'Error during relocation: '.$e->getMessage());
}

return $this->redirect($request->headers->get('referer') ?? '/');
Expand Down
3 changes: 2 additions & 1 deletion src/Controller/DefaultController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Controller;

use App\Enum\ReadStatus;
use App\Repository\BookInteractionRepository;
use App\Repository\BookRepository;
use App\Service\BookFileSystemManagerInterface;
Expand Down Expand Up @@ -61,7 +62,7 @@ public function readingList(BookInteractionRepository $bookInteractionRepository
'finished' => [],
];
foreach ($readList as $bookInteraction) {
if ($bookInteraction->isFinished()) {
if ($bookInteraction->getReadStatus() === ReadStatus::Finished) {
$statuses['finished'][] = $bookInteraction->getBook();
} else {
$statuses['unread'][] = $bookInteraction->getBook();
Expand Down
4 changes: 2 additions & 2 deletions src/Controller/GroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public function groups(Request $request, string $type, string $letter = 'a'): Re
}

$group = match ($search) {
'' => array_filter($group, static fn (mixed $item) => str_contains(strtolower($item['item']), strtolower($search))),
default => array_filter($group, static fn (mixed $item) => str_starts_with(strtolower($item['item']), $letter)),
default => array_filter($group, static fn (mixed $item) => str_contains(strtolower($item['item']), strtolower($search))),
'' => array_filter($group, static fn (mixed $item) => str_starts_with(strtolower($item['item']), $letter)),
};

return $this->render('group/index.html.twig', [
Expand Down
3 changes: 0 additions & 3 deletions src/Controller/OPDS/OpdsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,6 @@ public function readinglist(BookInteractionRepository $bookInteractionRepository

$feeds = [];
foreach ($books as $bookInteraction) {
if ($bookInteraction->getBook() === null) {
continue;
}
$feeds[] = $this->opds->convertBookToOpdsEntry($bookInteraction->getBook());
}
$opds->feeds($feeds);
Expand Down
2 changes: 1 addition & 1 deletion src/Controller/SerieController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#[Route('/series')]
class SerieController extends AbstractController
{
#[Route('/{name}', name: 'app_serie_detail')]
#[Route('/{name}', name: 'app_serie_detail', requirements: ['name' => '.+'])]
public function detail(string $name, BookRepository $bookRepository, BookInteractionService $bookInteractionService, ShelfRepository $shelfRepository): Response
{
$books = $bookRepository->findBySerie($name);
Expand Down
1 change: 0 additions & 1 deletion src/Controller/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public function index(UserRepository $userRepository): Response
{
return $this->render('user/index.html.twig', [
'users' => $userRepository->findAll(),
'age_categories' => array_flip(User::AGE_CATEGORIES),
]);
}

Expand Down
3 changes: 2 additions & 1 deletion src/DataFixtures/BookFixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\DataFixtures;

use App\Entity\Book;
use App\Enum\AgeCategory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
Expand Down Expand Up @@ -91,7 +92,7 @@ public function load(ObjectManager $manager): void
}

if (array_key_exists('ageCategory', $bookData)) {
$book->setAgeCategory($bookData['ageCategory']);
$book->setAgeCategory(AgeCategory::tryFrom($bookData['ageCategory']));
}

if (array_key_exists('tags', $bookData)) {
Expand Down
3 changes: 2 additions & 1 deletion src/DataFixtures/UserFixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\DataFixtures;

use App\Entity\User;
use App\Enum\AgeCategory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
Expand Down Expand Up @@ -43,7 +44,7 @@ public function load(ObjectManager $manager): void
$user->setUsername(self::CHILD_USERNAME);
$user->setBirthday(new \DateTimeImmutable('1990-01-01'));
$user->setLanguage('en');
$user->setMaxAgeCategory(1);
$user->setMaxAgeCategory(AgeCategory::TenPlus);
$user->setPassword($this->passwordHasher->hashPassword($user, self::CHILD_PASSWORD));
$user->setRoles(['ROLE_USER']);

Expand Down
31 changes: 17 additions & 14 deletions src/Entity/Book.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace App\Entity;

use App\Enum\AgeCategory;
use App\Enum\ReadingList;
use App\Enum\ReadStatus;
use App\Repository\BookRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
Expand Down Expand Up @@ -109,8 +112,8 @@ class Book
#[ORM\ManyToMany(targetEntity: Shelf::class, mappedBy: 'books', cascade: ['persist'])]
private Collection $shelves;

#[ORM\Column(nullable: true)]
private ?int $ageCategory = null;
#[ORM\Column(enumType: AgeCategory::class, nullable: true)]
private ?AgeCategory $ageCategory = null;

/**
* @var Collection<int, KoboSyncedBook>
Expand Down Expand Up @@ -432,10 +435,7 @@ public function addBookInteraction(BookInteraction $bookInteraction): static

public function removeBookInteraction(BookInteraction $bookInteraction): static
{
// set the owning side to null (unless already changed)
if ($this->bookInteractions->removeElement($bookInteraction) && $bookInteraction->getBook() === $this) {
$bookInteraction->setBook(null);
}
$this->bookInteractions->removeElement($bookInteraction);

return $this;
}
Expand Down Expand Up @@ -504,18 +504,23 @@ public function removeShelf(Shelf $shelf): static
return $this;
}

public function getAgeCategory(): ?int
public function getAgeCategory(): ?AgeCategory
{
return $this->ageCategory;
}

public function setAgeCategory(?int $ageCategory): static
public function setAgeCategory(?AgeCategory $ageCategory): static
{
$this->ageCategory = $ageCategory;

return $this;
}

public function getAgeCategoryLabel(): ?string
{
return $this->ageCategory?->label() ?? 'enum.agecategories.notset';
}

public function getUuid(): string
{
if ($this->uuid === null) {
Expand Down Expand Up @@ -598,17 +603,15 @@ public function getUsers(): object

foreach ($this->getBookInteractions() as $interaction) {
$user = $interaction->getUser();
if ($user === null) {
continue;
}

$userId = $user->getId();
if ($interaction->isFinished()) {
if ($interaction->getReadStatus() === ReadStatus::Finished) {
$return['read'][] = $userId;
}
if ($interaction->isFavorite()) {
if ($interaction->getReadingList() === ReadingList::ToRead) {
$return['favorite'][] = $userId;
}
if ($interaction->isHidden()) {
if ($interaction->getReadingList() === ReadingList::Ignored) {
$return['hidden'][] = $userId;
}
}
Expand Down
Loading

0 comments on commit c41b393

Please sign in to comment.