Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
10 changed files with 584 additions and 16 deletions
Showing only changes of commit d798a057c7 - Show all commits

View File

@ -153,6 +153,9 @@ return function(array $configArray = []) {
$container->set('anime-collection-model', function($container) { $container->set('anime-collection-model', function($container) {
return new Model\AnimeCollection($container); return new Model\AnimeCollection($container);
}); });
$container->set('manga-collection-model', function($container) {
return new Model\MangaCollection($container);
});
// Miscellaneous Classes // Miscellaneous Classes
$container->set('auth', function($container) { $container->set('auth', function($container) {

View File

@ -1,6 +1,6 @@
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<main> <main>
<h2>Add Anime to your Collection</h2> <h2>Add <?= ucfirst($collection_type) ?> to your Collection</h2>
<form action="<?= $action_url ?>" method="post"> <form action="<?= $action_url ?>" method="post">
<section> <section>
<div class="cssload-loader" hidden="hidden"> <div class="cssload-loader" hidden="hidden">
@ -8,7 +8,7 @@
<div class="cssload-inner cssload-two"></div> <div class="cssload-inner cssload-two"></div>
<div class="cssload-inner cssload-three"></div> <div class="cssload-inner cssload-three"></div>
</div> </div>
<label for="search">Search for anime by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" name="search" /></label> <label for="search">Search for <?= $collection_type ?> by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" name="search" /></label>
<section id="series_list" class="media-wrap"> <section id="series_list" class="media-wrap">
</section> </section>
</section> </section>
@ -39,5 +39,5 @@
</table> </table>
</form> </form>
</main> </main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/anime_collection') ?>"></script> <script defer="defer" src="<?= $urlGenerator->assetUrl("js.php/g/{$collection_type}_collection") ?>"></script>
<?php endif ?> <?php endif ?>

View File

@ -32,6 +32,12 @@ $extraSegment = $lastSegment === 'list' ? '/list' : '';
) ?>] ) ?>]
<?php else: ?> <?php else: ?>
<?= $whose . ucfirst($url_type) . ' Collection' ?> <?= $whose . ucfirst($url_type) . ' Collection' ?>
<?php if($config->get("show_{$other_type}_collection")): ?>
[<?= $helper->a(
$url->generate("{$other_type}.collection.view") . $extraSegment,
ucfirst($other_type) . ' Collection'
) ?>]
<?php endif ?>
[<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>] [<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>]
[<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>] [<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>]
<?php endif ?> <?php endif ?>

View File

@ -0,0 +1,54 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddMangaCollectionTables extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
// Create manga_set table
$manga_set = $this->table('manga_set', ['id' => FALSE, 'primary_key' => ['hummingbird_id']]);
$manga_set->addColumn('hummingbird_id', 'biginteger')
->addColumn('slug', 'string', ['comment' => "URL slug used for image caching and generating links"])
->addColumn('title', 'string')
->addColumn('alternate_title', 'string', ['null' => TRUE])
->addColumn('media_id', 'integer', ['default' => 3, 'null' => TRUE])
->addColumn('show_type', 'string', ['default' => 'TV', 'null' => TRUE, 'comment' => "TV Series/OVA/etc"])
->addColumn('age_rating', 'string', ['default' => 'PG13', 'null' => TRUE])
->addColumn('cover_image', 'string', ['null' => TRUE])
->addColumn('episode_count', 'integer', ['null' => TRUE])
->addColumn('episode_length', 'integer', ['null' => TRUE])
->addColumn('notes', 'text', ['null' => TRUE])
->addForeignKey('media_id', 'media', 'id')
->create();
// Create genre_manga_set_link table
$genre_manga_set_link = $this->table('genre_manga_set_link', ['id' => FALSE, 'primary_key' => ['hummingbird_id', 'genre_id']]);
$genre_manga_set_link->addColumn('hummingbird_id', 'biginteger')
->addColumn('genre_id', 'integer')
->addForeignKey('hummingbird_id', 'manga_set', 'hummingbird_id')
->addForeignKey('genre_id', 'genres', 'id')
->create();
}
}

View File

@ -0,0 +1,184 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\{
manga as mangaModel,
mangaCollection as mangaCollectionModel
};
use Aviat\AnimeClient\UrlGenerator;
use Aviat\Ion\Di\ContainerInterface;
/**
* Controller for manga collection pages
*/
class MangaCollection extends BaseController {
/**
* The manga collection model
* @var mangaCollectionModel $mangaCollectionModel
*/
private $mangaCollectionModel;
/**
* The manga API model
* @var mangaModel $mangaModel
*/
private $mangaModel;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->mangaModel = $container->get('manga-model');
$this->mangaCollectionModel = $container->get('manga-collection-model');
$this->baseData = array_merge($this->baseData, [
'collection_type' => 'manga',
'menu_name' => 'manga-collection',
'url_type' => 'manga',
'other_type' => 'anime',
'config' => $this->config,
]);
}
/**
* Search for manga
*
* @return void
*/
public function search()
{
$queryParams = $this->request->getQueryParams();
$query = $queryParams['query'];
$this->outputJSON($this->mangaModel->search($query));
}
/**
* Show the manga collection page
*
* @param string $view
* @return void
*/
public function index($view)
{
$viewMap = [
'' => 'cover',
'list' => 'list'
];
$data = $this->mangaCollectionModel->getCollection();
$this->outputHTML('collection/' . $viewMap[$view], [
'title' => $this->config->get('whose_list') . "'s Manga Collection",
'sections' => $data,
'genres' => $this->mangaCollectionModel->getGenreList()
]);
}
/**
* Show the manga collection add/edit form
*
* @param integer|null $id
* @return void
*/
public function form($id = NULL)
{
$this->setSessionRedirect();
$action = (is_null($id)) ? "Add" : "Edit";
$urlAction = strtolower($action);
$this->outputHTML('collection/' . $urlAction, [
'action' => $action,
'action_url' => $this->url->generate("manga.collection.{$urlAction}.post"),
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s manga Collection",
$action
),
'media_items' => $this->mangaCollectionModel->getMediaTypeList(),
'item' => ($action === "Edit") ? $this->mangaCollectionModel->get($id) : []
]);
}
/**
* Update a collection item
*
* @return void
*/
public function edit()
{
$data = $this->request->getParsedBody();
if (array_key_exists('hummingbird_id', $data))
{
$this->mangaCollectionModel->update($data);
$this->setFlashMessage('Successfully updated collection item.', 'success');
}
else
{
$this->setFlashMessage('Failed to update collection item', 'error');
}
$this->sessionRedirect();
}
/**
* Add a collection item
*
* @return void
*/
public function add()
{
$data = $this->request->getParsedBody();
if (array_key_exists('id', $data))
{
$this->mangaCollectionModel->add($data);
$this->setFlashMessage('Successfully added collection item', 'success');
}
else
{
$this->setFlashMessage('Failed to add collection item.', 'error');
}
$this->sessionRedirect();
}
/**
* Remove a collection item
*
* @return void
*/
public function delete()
{
$data = $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data))
{
$this->redirect("/manga-collection/view", 303);
}
$this->mangaCollectionModel->delete($data);
$this->setFlashMessage("Successfully removed manga from collection.", 'success');
$this->redirect("/manga-collection/view", 303);
}
}
// End of CollectionController.php

View File

@ -26,6 +26,23 @@ use PDO;
*/ */
class AnimeCollection extends Collection { class AnimeCollection extends Collection {
/**
* Anime API Model
* @var object $animeModel
*/
protected $animeModel;
/**
* Create the collection model
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->animeModel = $container->get('anime-model');
}
/** /**
* Get collection from the database, and organize by media type * Get collection from the database, and organize by media type
* *

View File

@ -25,14 +25,8 @@ use PDOException;
* Base model for anime and manga collections * Base model for anime and manga collections
*/ */
class Collection extends DB { class Collection extends DB {
use ContainerAware;
/** use ContainerAware;
* Anime API Model
* @var object $animeModel
*/
protected $animeModel;
/** /**
* Whether the database is valid for querying * Whether the database is valid for querying
@ -46,7 +40,7 @@ class Collection extends DB {
* @param ContainerInterface $container * @param ContainerInterface $container
*/ */
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
try try
@ -58,7 +52,6 @@ class Collection extends DB {
//$this->validDatabase = FALSE; //$this->validDatabase = FALSE;
//return FALSE; //return FALSE;
} }
$this->animeModel = $container->get('anime-model');
// Is database valid? If not, set a flag so the // Is database valid? If not, set a flag so the
// app can be run without a valid database // app can be run without a valid database

View File

@ -0,0 +1,304 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use PDO;
/**
* Model for getting anime collection data
*/
class MangaCollection extends Collection {
/**
* Manga API Model
* @var object $mangaModel
*/
protected $mangaModel;
/**
* Create the collection model
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->mangaModel = $container->get('manga-model');
}
/**
* Get collection from the database, and organize by media type
*
* @return array
*/
public function getCollection()
{
$rawCollection = $this->getCollectionFromDatabase();
$collection = [];
foreach ($rawCollection as $row)
{
if (array_key_exists($row['media'], $collection))
{
$collection[$row['media']][] = $row;
}
else
{
$collection[$row['media']] = [$row];
}
}
return $collection;
}
/**
* Get list of media types
*
* @return array
*/
public function getMediaTypeList()
{
$output = [];
$query = $this->db->select('id, type')
->from('media')
->get();
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $row)
{
$output[$row['id']] = $row['type'];
}
return $output;
}
/**
* Get item from collection for editing
*
* @param int $id
* @return array
*/
public function getCollectionEntry($id)
{
$query = $this->db->from('anime_set')
->where('hummingbird_id', (int)$id)
->get();
return $query->fetch(PDO::FETCH_ASSOC);
}
/**
* Get full collection from the database
*
* @return array
*/
private function getCollectionFromDatabase()
{
if ( ! $this->validDatabase)
{
return [];
}
$query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type,
age_rating, episode_count, episode_length, cover_image, notes, media.type as media')
->from('manga_set a')
->join('media', 'media.id=a.media_id', 'inner')
->order_by('media')
->order_by('title')
->get();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Add an item to the anime collection
*
* @param array $data
* @return void
*/
public function add($data)
{
$anime = (object)$this->mangaModel->getMangaById($data['id']);
$this->db->set([
'hummingbird_id' => $data['id'],
'slug' => $anime->slug,
'title' => array_shift($anime->titles),
'alternate_title' => implode('<br />', $anime->titles),
'show_type' => $anime->show_type,
'age_rating' => $anime->age_rating,
'cover_image' => $anime->cover_image,
'episode_count' => $anime->episode_count,
'episode_length' => $anime->episode_length,
'media_id' => $data['media_id'],
'notes' => $data['notes']
])->insert('manga_set');
$this->updateGenre($data['id']);
}
/**
* Update a collection item
*
* @param array $data
* @return void
*/
public function update($data)
{
// If there's no id to update, don't update
if ( ! array_key_exists('hummingbird_id', $data))
{
return;
}
$id = $data['hummingbird_id'];
unset($data['hummingbird_id']);
$this->db->set($data)
->where('hummingbird_id', $id)
->update('manga_set');
}
/**
* Remove a collection item
*
* @param array $data
* @return void
*/
public function delete($data)
{
// If there's no id to update, don't delete
if ( ! array_key_exists('hummingbird_id', $data))
{
return;
}
$this->db->where('hummingbird_id', $data['hummingbird_id'])
->delete('genre_manga_set_link');
$this->db->where('hummingbird_id', $data['hummingbird_id'])
->delete('manga_set');
}
/**
* Get the details of a collection item
*
* @param int $kitsuId
* @return array
*/
public function get($kitsuId)
{
$query = $this->db->from('manga_set')
->where('hummingbird_id', $kitsuId)
->get();
return $query->fetch(PDO::FETCH_ASSOC);
}
/**
* Update genre information for selected manga
*
* @param int $mangaId The current manga
* @return void
*/
private function updateGenre($mangaId)
{
$genreInfo = $this->getGenreData();
extract($genreInfo);
// Get api information
$manga = $this->mangaModel->getMangaById($mangaId);
foreach ($anime['genres'] as $genre)
{
// Add genres that don't currently exist
if ( ! in_array($genre, $genres))
{
$this->db->set('genre', $genre)
->insert('genres');
$genres[] = $genre;
}
// Update link table
// Get id of genre to put in link table
$flippedGenres = array_flip($genres);
$insertArray = [
'hummingbird_id' => $mangaId,
'genre_id' => $flippedGenres[$genre]
];
if (array_key_exists($mangaId, $links))
{
if ( ! in_array($flippedGenres[$genre], $links[$mangaId]))
{
$this->db->set($insertArray)->insert('genre_manga_set_link');
}
}
else
{
$this->db->set($insertArray)->insert('genre_manga_set_link');
}
}
}
/**
* Get list of existing genres
*
* @return array
*/
private function getGenreData()
{
$genres = [];
$links = [];
// Get existing genres
$query = $this->db->select('id, genre')
->from('genres')
->get();
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $genre)
{
$genres[$genre['id']] = $genre['genre'];
}
// Get existing link table entries
$query = $this->db->select('hummingbird_id, genre_id')
->from('genre_manga_set_link')
->get();
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $link)
{
if (array_key_exists($link['hummingbird_id'], $links))
{
$links[$link['hummingbird_id']][] = $link['genre_id'];
}
else
{
$links[$link['hummingbird_id']] = [$link['genre_id']];
}
}
return [
'genres' => $genres,
'links' => $links
];
}
}
// End of MangaCollectionModel.php

View File

@ -22,7 +22,8 @@ use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller\{ use Aviat\AnimeClient\Controller\{
Anime as AnimeController, Anime as AnimeController,
Character as CharacterController, Character as CharacterController,
Collection as CollectionController, AnimeCollection as AnimeCollectionController,
MangaCollection as MangaCollectionController,
Manga as MangaController Manga as MangaController
}; };
@ -71,7 +72,11 @@ class ControllerTest extends AnimeClientTestCase {
); );
$this->assertInstanceOf( $this->assertInstanceOf(
'Aviat\AnimeClient\Controller', 'Aviat\AnimeClient\Controller',
new CollectionController($this->container) new AnimeCollectionController($this->container)
);
$this->assertInstanceOf(
'Aviat\AnimeClient\Controller',
new MangaCollectionController($this->container)
); );
} }

View File

@ -227,7 +227,8 @@ class DispatcherTest extends AnimeClientTestCase {
'expected' => [ 'expected' => [
'anime' => 'Aviat\AnimeClient\Controller\Anime', 'anime' => 'Aviat\AnimeClient\Controller\Anime',
'manga' => 'Aviat\AnimeClient\Controller\Manga', 'manga' => 'Aviat\AnimeClient\Controller\Manga',
'collection' => 'Aviat\AnimeClient\Controller\Collection', 'anime-collection' => 'Aviat\AnimeClient\Controller\AnimeCollection',
'manga-collection' => 'Aviat\AnimeClient\Controller\MangaCollection',
'character' => 'Aviat\AnimeClient\Controller\Character', 'character' => 'Aviat\AnimeClient\Controller\Character',
'index' => 'Aviat\AnimeClient\Controller\Index', 'index' => 'Aviat\AnimeClient\Controller\Index',
] ]
@ -248,7 +249,8 @@ class DispatcherTest extends AnimeClientTestCase {
'expected' => [ 'expected' => [
'anime' => 'Aviat\AnimeClient\Controller\Anime', 'anime' => 'Aviat\AnimeClient\Controller\Anime',
'manga' => 'Aviat\AnimeClient\Controller\Manga', 'manga' => 'Aviat\AnimeClient\Controller\Manga',
'collection' => 'Aviat\AnimeClient\Controller\Collection', 'anime-collection' => 'Aviat\AnimeClient\Controller\AnimeCollection',
'manga-collection' => 'Aviat\AnimeClient\Controller\MangaCollection',
'character' => 'Aviat\AnimeClient\Controller\Character', 'character' => 'Aviat\AnimeClient\Controller\Character',
'index' => 'Aviat\AnimeClient\Controller\Index', 'index' => 'Aviat\AnimeClient\Controller\Index',
] ]