Fixed login remembering

This commit is contained in:
Marcin Kurczewski 2014-09-23 20:45:59 +02:00
parent c33817e4ab
commit 7b0d907acc
22 changed files with 357 additions and 204 deletions

9
TODO
View file

@ -121,9 +121,6 @@ refactors:
- (idea) keep denormalized data in separate tables, i.e. tag usages in
tag_meta or post favorite count in post_meta, so that it's obvious what
can be recalculated on demand and what is real data
- change AbstractSearchService to AbstractSearchParser
- embed AbstractSearchService into Dao (it sucks to include XSearchService
together with Dao)
- simplify template loading in presenters - right now template loading
requires _, util and promise:
promise.wait(util.loadTemplate('blah'))
@ -131,6 +128,12 @@ refactors:
template = _.template(html); });
- change content spinner to nprogress:
http://ricostacruz.com/nprogress/
- add fetchUsers to PostSearchFilter: when AbstractDao fetches entities
with query decorated by PostDao::decorateQueryFromFilter, call
PostDao::decorateEntitiesWithFilter that will optimally load users and
inject them using posts' lazy loaders.
- make view proxies less greedy, e.g. favs should be fetched only in post
view, not for each of 40 posts in post list. (same goes to other things)
miscellaneous:
- fix mouse trap hotkeys when leaving page

View file

@ -45,7 +45,7 @@ App.Presenters.LoginPresenter = function(
var userNameOrEmail = $el.find('[name=user]').val();
var password = $el.find('[name=password]').val();
var remember = $el.find('[name=remember]').val();
var remember = $el.find('[name=remember]').is(':checked');
if (userNameOrEmail.length === 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');

View file

@ -38,11 +38,11 @@ final class PostController extends AbstractController
{
$formData = new \Szurubooru\FormData\SearchFormData($this->inputReader);
$searchResult = $this->postService->getFiltered($formData);
$entities = $this->postViewProxy->fromArray($searchResult->entities);
$entities = $this->postViewProxy->fromArray($searchResult->getEntities());
return [
'data' => $entities,
'pageSize' => $searchResult->filter->pageSize,
'totalRecords' => $searchResult->totalRecords];
'pageSize' => $searchResult->getPageSize(),
'totalRecords' => $searchResult->getTotalRecords()];
}
public function createPost()

View file

@ -48,11 +48,11 @@ final class UserController extends AbstractController
$formData = new \Szurubooru\FormData\SearchFormData($this->inputReader);
$searchResult = $this->userService->getFiltered($formData);
$entities = $this->userViewProxy->fromArray($searchResult->entities);
$entities = $this->userViewProxy->fromArray($searchResult->getEntities());
return [
'data' => $entities,
'pageSize' => $searchResult->filter->pageSize,
'totalRecords' => $searchResult->totalRecords];
'pageSize' => $searchResult->getPageSize(),
'totalRecords' => $searchResult->getTotalRecords()];
}
public function createUser()

View file

@ -54,14 +54,9 @@ abstract class AbstractDao implements ICrudDao
public function findAll()
{
$entities = [];
$query = $this->fpdo->from($this->tableName);
foreach ($query as $arrayEntity)
{
$entity = $this->entityConverter->toEntity($arrayEntity);
$entities[$entity->getId()] = $entity;
}
return $entities;
$arrayEntities = iterator_to_array($query);
return $this->arrayToEntities($arrayEntities);
}
public function findById($entityId)
@ -74,6 +69,46 @@ abstract class AbstractDao implements ICrudDao
return $this->findBy($this->getIdColumn(), $entityIds);
}
public function findFiltered(\Szurubooru\SearchServices\AbstractSearchFilter $searchFilter)
{
$orderByString = $this->compileOrderBy($searchFilter->order);
$query = $this->fpdo
->from($this->tableName)
->orderBy($orderByString);
$this->decorateQueryFromFilter($query, $searchFilter);
return $this->arrayToEntities(iterator_to_array($query));
}
public function findFilteredAndPaged(\Szurubooru\SearchServices\AbstractSearchFilter $searchFilter, $pageNumber, $pageSize)
{
$orderByString = $this->compileOrderBy($searchFilter->order);
$query = $this->fpdo
->from($this->tableName)
->orderBy($orderByString)
->limit($pageSize)
->offset($pageSize * ($pageNumber - 1));
$this->decorateQueryFromFilter($query, $searchFilter);
$entities = $this->arrayToEntities(iterator_to_array($query));
$query = $this->fpdo
->from($this->tableName)
->select('COUNT(1) AS c');
$totalRecords = intval(iterator_to_array($query)[0]['c']);
$pagedSearchResult = new \Szurubooru\SearchServices\PagedSearchResult();
$pagedSearchResult->setSearchFilter($searchFilter);
$pagedSearchResult->setEntities($entities);
$pagedSearchResult->setTotalRecords($totalRecords);
$pagedSearchResult->setPageNumber($pageNumber);
$pagedSearchResult->setPageSize($pageSize);
return $pagedSearchResult;
}
public function deleteAll()
{
foreach ($this->findAll() as $entity)
@ -153,4 +188,27 @@ abstract class AbstractDao implements ICrudDao
protected function beforeDelete(\Szurubooru\Entities\Entity $entity)
{
}
protected function decorateQueryFromFilter($query, \Szurubooru\SearchServices\AbstractSearchFilter $filter)
{
}
protected function arrayToEntities(array $arrayEntities)
{
$entities = [];
foreach ($arrayEntities as $arrayEntity)
{
$entity = $this->entityConverter->toEntity($arrayEntity);
$entities[$entity->getId()] = $entity;
}
return $entities;
}
private function compileOrderBy($order)
{
$orderByString = '';
foreach ($order as $orderColumn => $orderDir)
$orderByString .= $orderColumn . ' ' . ($orderDir === \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_DESC ? 'DESC' : 'ASC') . ', ';
return substr($orderByString, 0, -2);
}
}

View file

@ -14,4 +14,3 @@ class SearchResult
$this->totalRecords = $totalRecords;
}
}

View file

@ -1,134 +0,0 @@
<?php
namespace Szurubooru\Dao\Services;
abstract class AbstractSearchService
{
const ORDER_DESC = -1;
const ORDER_ASC = 1;
private $tableName;
private $entityConverter;
private $fpdo;
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Dao\AbstractDao $dao)
{
$this->tableName = $dao->getTableName();
$this->entityConverter = $dao->getEntityConverter();
$this->fpdo = new \FluentPDO($databaseConnection->getPDO());
}
public function getFiltered(
\Szurubooru\Dao\SearchFilter $searchFilter)
{
list ($basicTokens, $complexTokens) = $this->tokenize($searchFilter->query);
$filter = [];
if ($basicTokens)
$this->decorateFilterWithBasicTokens($filter, $basicTokens);
if ($complexTokens)
$this->decorateFilterWithComplexTokens($filter, $complexTokens);
$order = $this->getOrder($searchFilter->order);
$pageSize = min(100, max(1, $searchFilter->pageSize));
$pageNumber = max(1, $searchFilter->pageNumber) - 1;
//todo: clean up
$orderByString = '';
foreach ($order as $orderColumn => $orderDir)
{
$orderByString .= $orderColumn . ' ' . ($orderDir === self::ORDER_DESC ? 'DESC' : 'ASC') . ', ';
}
$orderByString = substr($orderByString, 0, -2);
$query = $this->fpdo
->from($this->tableName)
->orderBy($orderByString)
->limit($pageSize)
->offset($pageSize * $pageNumber);
$entities = [];
foreach ($query as $arrayEntity)
{
$entity = $this->entityConverter->toEntity($arrayEntity);
$entities[] = $entity;
}
$query = $this->fpdo
->from($this->tableName)
->select('COUNT(1) AS c');
$totalRecords = intval(iterator_to_array($query)[0]['c']);
return new \Szurubooru\Dao\SearchResult($searchFilter, $entities, $totalRecords);
}
protected function decorateFilterWithBasicTokens($filter, $basicTokens)
{
throw new \BadMethodCallException('Not supported');
}
protected function decorateFilterWithComplexTokens($filter, $complexTokens)
{
throw new \BadMethodCallException('Not supported');
}
protected function getOrderColumn($token)
{
throw new \BadMethodCallException('Not supported');
}
protected function getDefaultOrderColumn()
{
return 'id';
}
protected function getDefaultOrderDir()
{
return self::ORDER_DESC;
}
private function getOrder($query)
{
$order = [];
$tokens = array_filter(preg_split('/\s+/', $query));
foreach ($tokens as $token)
{
$token = preg_split('/,|\s+/', $token);
$orderToken = $token[0];
$orderDir = (count($token) === 2 and $token[1] === 'desc') ? self::ORDER_DESC : self::ORDER_ASC;
$orderColumn = $this->getOrderColumn($orderToken);
if ($orderColumn === null)
throw new \InvalidArgumentException('Invalid search order token: ' . $orderToken);
$order[$orderColumn] = $orderDir;
}
$defaultOrderColumn = $this->getDefaultOrderColumn();
$defaultOrderDir = $this->getDefaultOrderDir();
if ($defaultOrderColumn)
$order[$defaultOrderColumn] = $defaultOrderDir;
return $order;
}
private function tokenize($query)
{
$basicTokens = [];
$complexTokens = [];
$tokens = array_filter(preg_split('/\s+/', $query));
foreach ($tokens as $token)
{
if (strpos($token, ':') !== false)
{
list ($key, $value) = explode(':', $token, 1);
$complexTokens[$key] = $value;
}
else
{
$basicTokens[] = $token;
}
}
return [$basicTokens, $complexTokens];
}
}

View file

@ -1,12 +0,0 @@
<?php
namespace Szurubooru\Dao\Services;
class PostSearchService extends AbstractSearchService
{
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Dao\PostDao $postDao)
{
parent::__construct($databaseConnection, $postDao);
}
}

View file

@ -1,23 +0,0 @@
<?php
namespace Szurubooru\Dao\Services;
class UserSearchService extends AbstractSearchService
{
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Dao\UserDao $userDao)
{
parent::__construct($databaseConnection, $userDao);
}
protected function getOrderColumn($token)
{
if ($token === 'name')
return 'name';
if (in_array($token, ['registrationDate', 'registrationTime', 'registered', 'joinDate', 'joinTime', 'joined']))
return 'registrationTime';
return null;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace Szurubooru\SearchServices;
abstract class AbstractSearchFilter
{
const ORDER_ASC = 1;
const ORDER_DESC = -1;
public $order;
public function __construct()
{
$this->order = [];
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Szurubooru\SearchServices;
class NamedSearchToken extends SearchToken
{
public $key = false;
}

View file

@ -0,0 +1,61 @@
<?php
namespace Szurubooru\SearchServices;
class PagedSearchResult
{
public $pageNumber;
public $pageSize;
public $searchFilter;
public $entities;
public $totalRecords;
public function setSearchFilter(AbstractSearchFilter $searchFilter = null)
{
$this->searchFilter = $searchFilter;
}
public function getSearchFilter()
{
return $this->searchFilter;
}
public function setPageNumber($pageNumber)
{
$this->pageNumber = $pageNumber;
}
public function getPageNumber()
{
return $this->pageNumber;
}
public function setPageSize($pageSize)
{
$this->pageSize = $pageSize;
}
public function getPageSize()
{
return $this->pageSize;
}
public function setEntities(array $entities)
{
$this->entities = $entities;
}
public function getEntities()
{
return $this->entities;
}
public function setTotalRecords($totalRecords)
{
$this->totalRecords = $totalRecords;
}
public function getTotalRecords()
{
return $this->totalRecords;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace Szurubooru\SearchServices\Parsers;
abstract class AbstractSearchParser
{
public function createFilterFromFormData(\Szurubooru\FormData\SearchFormData $formData)
{
$filter = $this->createFilter();
$filter->order = $this->getOrder($formData->order);
$tokens = $this->tokenize($formData->query);
foreach ($tokens as $token)
{
if ($token instanceof \Szurubooru\SearchServices\NamedSearchToken)
$this->decorateFilterFromNamedToken($filter, $token);
elseif ($token instanceof \Szurubooru\SearchService\SearchToken)
$this->decorateFilterFromToken($filter, $token);
else
throw new \RuntimeException('Invalid search token type');
}
return $filter;
}
protected abstract function createFilter();
protected abstract function decorateFilterFromToken($filter, $token);
protected abstract function decorateFilterFromNamedToken($filter, $namedToken);
protected abstract function getOrderColumn($token);
protected function getDefaultOrderColumn()
{
return 'id';
}
protected function getDefaultOrderDir()
{
return \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_DESC;
}
private function getOrder($query)
{
$order = [];
$tokens = array_filter(preg_split('/\s+/', $query));
foreach ($tokens as $token)
{
$token = preg_split('/,|\s+/', $token);
$orderToken = $token[0];
$orderDir = (count($token) === 2 and $token[1] === 'desc')
? \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_DESC
: \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_ASC;
$orderColumn = $this->getOrderColumn($orderToken);
if ($orderColumn === null)
throw new \InvalidArgumentException('Invalid search order token: ' . $orderToken);
$order[$orderColumn] = $orderDir;
}
$defaultOrderColumn = $this->getDefaultOrderColumn();
$defaultOrderDir = $this->getDefaultOrderDir();
if ($defaultOrderColumn)
$order[$defaultOrderColumn] = $defaultOrderDir;
return $order;
}
private function tokenize($query)
{
$searchTokens = [];
foreach (array_filter(preg_split('/\s+/', $query)) as $tokenText)
{
$negated = false;
if (substr($tokenText, 0, 1) === '-')
{
$negated = true;
$tokenText = substr($tokenText, 1);
}
if (strpos($tokenText, ':') !== false)
{
$searchToken = new \Szurubooru\SearchServices\NamedSearchToken();
list ($searchToken->key, $searchToken->value) = explode(':', $tokenText, 1);
}
else
{
$searchToken = new \Szurubooru\SearchServices\SearchToken();
$searchToken->value = $tokenText;
}
$searchToken->negated = $negated;
$searchTokens[] = $searchToken;
}
return $searchTokens;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Szurubooru\SearchServices\Parsers;
class PostSearchParser extends AbstractSearchParser
{
protected function createFilter()
{
return new \Szurubooru\SearchServices\PostSearchFilter;
}
protected function decorateFilterFromToken($filter, $token)
{
throw new \BadMethodCallException('Not supported');
}
protected function decorateFilterFromNamedToken($filter, $namedToken)
{
throw new \BadMethodCallException('Not supported');
}
protected function getOrderColumn($token)
{
throw new \BadMethodCallException('Not supported');
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Szurubooru\SearchServices\Parsers;
class UserSearchParser extends AbstractSearchParser
{
protected function createFilter()
{
return new \Szurubooru\SearchServices\UserSearchFilter;
}
protected function decorateFilterFromToken($filter, $token)
{
throw new \BadMethodCallException('Not supported');
}
protected function decorateFilterFromNamedToken($filter, $namedToken)
{
throw new \BadMethodCallException('Not supported');
}
protected function getOrderColumn($token)
{
if ($token === 'name')
return 'name';
if (in_array($token, ['registrationDate', 'registrationTime', 'registered', 'joinDate', 'joinTime', 'joined']))
return 'registrationTime';
return null;
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Szurubooru\SearchServices;
class PostSearchFilter extends AbstractSearchFilter
{
}

View file

@ -0,0 +1,8 @@
<?php
namespace Szurubooru\SearchServices;
class SearchToken
{
public $negated = false;
public $value;
}

View file

@ -0,0 +1,6 @@
<?php
namespace Szurubooru\SearchServices;
class UserSearchFilter extends AbstractSearchFilter
{
}

View file

@ -7,7 +7,7 @@ class PostService
private $validator;
private $transactionManager;
private $postDao;
private $postSearchService;
private $postSearchParser;
private $timeService;
private $authService;
private $fileService;
@ -18,7 +18,7 @@ class PostService
\Szurubooru\Validator $validator,
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Dao\PostDao $postDao,
\Szurubooru\Dao\Services\PostSearchService $postSearchService,
\Szurubooru\SearchServices\Parsers\PostSearchParser $postSearchParser,
\Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\FileService $fileService,
@ -28,7 +28,7 @@ class PostService
$this->validator = $validator;
$this->transactionManager = $transactionManager;
$this->postDao = $postDao;
$this->postSearchService = $postSearchService;
$this->postSearchParser = $postSearchParser;
$this->timeService = $timeService;
$this->authService = $authService;
$this->fileService = $fileService;
@ -66,8 +66,8 @@ class PostService
$transactionFunc = function() use ($formData)
{
$this->validator->validate($formData);
$searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->posts->postsPerPage, $formData);
return $this->postSearchService->getFiltered($searchFilter);
$searchFilter = $this->postSearchParser->createFilterFromFormData($formData);
return $this->postDao->findFilteredAndPaged($searchFilter, $formData->pageNumber, $this->config->posts->postsPerPage);
};
return $this->transactionManager->rollback($transactionFunc);
}

View file

@ -7,7 +7,7 @@ class UserService
private $validator;
private $transactionManager;
private $userDao;
private $userSearchService;
private $userSearchParser;
private $passwordService;
private $emailService;
private $fileService;
@ -20,7 +20,7 @@ class UserService
\Szurubooru\Validator $validator,
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Dao\UserDao $userDao,
\Szurubooru\Dao\Services\UserSearchService $userSearchService,
\Szurubooru\SearchServices\Parsers\UserSearchParser $userSearchParser,
\Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\EmailService $emailService,
\Szurubooru\Services\FileService $fileService,
@ -32,7 +32,7 @@ class UserService
$this->validator = $validator;
$this->transactionManager = $transactionManager;
$this->userDao = $userDao;
$this->userSearchService = $userSearchService;
$this->userSearchParser = $userSearchParser;
$this->passwordService = $passwordService;
$this->emailService = $emailService;
$this->fileService = $fileService;
@ -87,8 +87,8 @@ class UserService
$transactionFunc = function() use ($formData)
{
$this->validator->validate($formData);
$searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->users->usersPerPage, $formData);
return $this->userSearchService->getFiltered($searchFilter);
$searchFilter = $this->userSearchParser->createFilterFromFormData($formData);
return $this->userDao->findFilteredAndPaged($searchFilter, $formData->pageNumber, $this->config->users->usersPerPage);
};
return $this->transactionManager->rollback($transactionFunc);
}

View file

@ -7,7 +7,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
private $validatorMock;
private $transactionManagerMock;
private $postDaoMock;
private $postSearchServiceMock;
private $postSearchParserMock;
private $authServiceMock;
private $timeServiceMock;
private $fileServiceMock;
@ -19,7 +19,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->transactionManagerMock = $this->mockTransactionManager();
$this->postDaoMock = $this->mock(\Szurubooru\Dao\PostDao::class);
$this->postSearchServiceMock = $this->mock(\Szurubooru\Dao\Services\PostSearchService::class);
$this->postSearchParserMock = $this->mock(\Szurubooru\SearchServices\Parsers\PostSearchParser::class);
$this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
@ -178,7 +178,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->validatorMock,
$this->transactionManagerMock,
$this->postDaoMock,
$this->postSearchServiceMock,
$this->postSearchParserMock,
$this->authServiceMock,
$this->timeServiceMock,
$this->fileServiceMock,

View file

@ -7,7 +7,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
private $validatorMock;
private $transactionManagerMock;
private $userDaoMock;
private $userSearchServiceMock;
private $userSearchParserMock;
private $passwordServiceMock;
private $emailServiceMock;
private $fileServiceMock;
@ -22,7 +22,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->transactionManagerMock = $this->mockTransactionManager();
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class);
$this->userSearchService = $this->mock(\Szurubooru\Dao\Services\UserSearchService::class);
$this->userSearchParserMock = $this->mock(\Szurubooru\SearchServices\Parsers\UserSearchParser::class);
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
$this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class);
$this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
@ -72,7 +72,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$mockUser = new \Szurubooru\Entities\User;
$mockUser->setName('user');
$expected = [$mockUser];
$this->userSearchService->method('getFiltered')->willReturn($expected);
$this->userDaoMock->method('getFiltered')->willReturn($expected);
$this->configMock->set('users/usersPerPage', 1);
$searchFormData = new \Szurubooru\FormData\SearchFormData;
@ -292,7 +292,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->validatorMock,
$this->transactionManagerMock,
$this->userDaoMock,
$this->userSearchService,
$this->userSearchParserMock,
$this->passwordServiceMock,
$this->emailServiceMock,
$this->fileServiceMock,