Added proof of concept for pagination and search
This commit is contained in:
parent
48016bea13
commit
ee9fde5402
28 changed files with 643 additions and 23 deletions
|
@ -10,3 +10,4 @@ minPasswordLength = 5
|
||||||
[users]
|
[users]
|
||||||
minUserNameLength = 1
|
minUserNameLength = 1
|
||||||
maxUserNameLength = 32
|
maxUserNameLength = 32
|
||||||
|
usersPerPage = 20
|
||||||
|
|
|
@ -23,3 +23,7 @@ a {
|
||||||
color: #6a2;
|
color: #6a2;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #7b3;
|
||||||
|
}
|
||||||
|
|
27
public_html/css/pager.css
Normal file
27
public_html/css/pager.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.pager {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager li {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager li a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.4em 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager li:hover a {
|
||||||
|
background: #efa;
|
||||||
|
}
|
||||||
|
.pager li.active a {
|
||||||
|
background: #faffca;
|
||||||
|
}
|
27
public_html/css/user-list.css
Normal file
27
public_html/css/user-list.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#user-list {
|
||||||
|
min-width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-list ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-list ul.order {
|
||||||
|
margin: -0.5em -0.5em 0.5em -0.5em;
|
||||||
|
}
|
||||||
|
#user-list ul.order li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-list ul.order li.active a {
|
||||||
|
background: #faffca;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-list ul.order a {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
}
|
|
@ -12,8 +12,10 @@
|
||||||
<link rel="stylesheet" type="text/css" href="/css/forms.css"/>
|
<link rel="stylesheet" type="text/css" href="/css/forms.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/css/messages.css"/>
|
<link rel="stylesheet" type="text/css" href="/css/messages.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/css/top-navigation.css"/>
|
<link rel="stylesheet" type="text/css" href="/css/top-navigation.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/pager.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/css/login-form.css"/>
|
<link rel="stylesheet" type="text/css" href="/css/login-form.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="/css/registration-form.css"/>
|
<link rel="stylesheet" type="text/css" href="/css/registration-form.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/user-list.css"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -31,6 +33,7 @@
|
||||||
<script type="text/javascript" src="/js/Auth.js"></script>
|
<script type="text/javascript" src="/js/Auth.js"></script>
|
||||||
<script type="text/javascript" src="/js/Util.js"></script>
|
<script type="text/javascript" src="/js/Util.js"></script>
|
||||||
<script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script>
|
<script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/Presenters/PagedCollectionPresenter.js"></script>
|
||||||
<script type="text/javascript" src="/js/Presenters/LoginPresenter.js"></script>
|
<script type="text/javascript" src="/js/Presenters/LoginPresenter.js"></script>
|
||||||
<script type="text/javascript" src="/js/Presenters/LogoutPresenter.js"></script>
|
<script type="text/javascript" src="/js/Presenters/LogoutPresenter.js"></script>
|
||||||
<script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script>
|
<script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script>
|
||||||
|
|
114
public_html/js/Presenters/PagedCollectionPresenter.js
Normal file
114
public_html/js/Presenters/PagedCollectionPresenter.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
var App = App || {};
|
||||||
|
App.Presenters = App.Presenters || {};
|
||||||
|
|
||||||
|
App.Presenters.PagedCollectionPresenter = function(api, util) {
|
||||||
|
|
||||||
|
var searchOrder;
|
||||||
|
var searchQuery;
|
||||||
|
var pageNumber;
|
||||||
|
var baseUri;
|
||||||
|
var backendUri;
|
||||||
|
var renderCallback;
|
||||||
|
|
||||||
|
var template;
|
||||||
|
var pageSize;
|
||||||
|
var totalRecords;
|
||||||
|
|
||||||
|
function init(args) {
|
||||||
|
parseSearchArgs(args.searchArgs);
|
||||||
|
baseUri = args.baseUri;
|
||||||
|
backendUri = args.backendUri;
|
||||||
|
renderCallback = args.renderCallback;
|
||||||
|
|
||||||
|
util.loadTemplate('pager').then(function(html) {
|
||||||
|
template = _.template(html);
|
||||||
|
//renderCallback({entities: [], totalRecords: 0});
|
||||||
|
|
||||||
|
changePage(pageNumber);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(newPageNumber) {
|
||||||
|
pageNumber = newPageNumber;
|
||||||
|
|
||||||
|
api.get(backendUri, {
|
||||||
|
order: searchOrder,
|
||||||
|
query: searchQuery,
|
||||||
|
page: pageNumber
|
||||||
|
}).then(function(response) {
|
||||||
|
totalRecords = response.json.totalRecords;
|
||||||
|
pageSize = response.json.pageSize;
|
||||||
|
renderCallback({
|
||||||
|
entities: response.json.data,
|
||||||
|
totalRecords: response.json.totalRecords});
|
||||||
|
}).catch(function(response) {
|
||||||
|
console.log(Error(response.json && response.json.error || response));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function render($target) {
|
||||||
|
var totalPages = Math.ceil(totalRecords / pageSize);
|
||||||
|
var pages = [1, totalPages];
|
||||||
|
var pagesAroundCurrent = 2;
|
||||||
|
for (var i = -pagesAroundCurrent; i <= pagesAroundCurrent; i ++)
|
||||||
|
if (pageNumber + i >= 1 && pageNumber + i <= totalPages)
|
||||||
|
pages.push(pageNumber + i);
|
||||||
|
if (pageNumber - pagesAroundCurrent - 1 == 2)
|
||||||
|
pages.push(2);
|
||||||
|
if (pageNumber + pagesAroundCurrent + 1 == totalPages - 1)
|
||||||
|
pages.push(totalPages - 1);
|
||||||
|
|
||||||
|
pages = pages.sort(function(a, b) { return a - b; }).filter(function(item, pos) {
|
||||||
|
return !pos || item != pages[pos - 1];
|
||||||
|
});
|
||||||
|
|
||||||
|
$target.html(template({
|
||||||
|
pages: pages,
|
||||||
|
pageNumber: pageNumber,
|
||||||
|
link: getPageChangeLink,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchQueryChangeLink(newSearchQuery) {
|
||||||
|
return util.compileComplexRouteArgs(baseUri, {
|
||||||
|
page: 1,
|
||||||
|
order: searchOrder,
|
||||||
|
query: newSearchQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchOrderChangeLink(newSearchOrder) {
|
||||||
|
return util.compileComplexRouteArgs(baseUri, {
|
||||||
|
page: 1,
|
||||||
|
order: newSearchOrder,
|
||||||
|
query: searchQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageChangeLink(newPageNumber) {
|
||||||
|
return util.compileComplexRouteArgs(baseUri, {
|
||||||
|
page: newPageNumber,
|
||||||
|
order: searchOrder,
|
||||||
|
query: searchQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSearchArgs(searchArgs) {
|
||||||
|
var args = util.parseComplexRouteArgs(searchArgs);
|
||||||
|
pageNumber = parseInt(args.page) || 1;
|
||||||
|
searchOrder = args.order;
|
||||||
|
searchQuery = args.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
render: render,
|
||||||
|
changePage: changePage,
|
||||||
|
getSearchQueryChangeLink: getSearchQueryChangeLink,
|
||||||
|
getSearchOrderChangeLink: getSearchOrderChangeLink,
|
||||||
|
getPageChangeLink: getPageChangeLink
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
App.DI.register('pagedCollectionPresenter', App.Presenters.PagedCollectionPresenter);
|
|
@ -1,19 +1,56 @@
|
||||||
var App = App || {};
|
var App = App || {};
|
||||||
App.Presenters = App.Presenters || {};
|
App.Presenters = App.Presenters || {};
|
||||||
|
|
||||||
App.Presenters.UserListPresenter = function(jQuery, topNavigationPresenter, appState) {
|
App.Presenters.UserListPresenter = function(
|
||||||
|
jQuery,
|
||||||
|
util,
|
||||||
|
router,
|
||||||
|
pagedCollectionPresenter,
|
||||||
|
topNavigationPresenter) {
|
||||||
|
|
||||||
var $el = jQuery('#content');
|
var $el = jQuery('#content');
|
||||||
|
var template;
|
||||||
|
var userList = [];
|
||||||
|
var activeSearchOrder;
|
||||||
|
|
||||||
function init() {
|
function init(args) {
|
||||||
topNavigationPresenter.select('users');
|
topNavigationPresenter.select('users');
|
||||||
|
activeSearchOrder = util.parseComplexRouteArgs(args.searchArgs).order;
|
||||||
|
|
||||||
|
util.loadTemplate('user-list').then(function(html) {
|
||||||
|
template = _.template(html);
|
||||||
|
|
||||||
|
pagedCollectionPresenter.init({
|
||||||
|
searchArgs: args.searchArgs,
|
||||||
|
baseUri: '#/users',
|
||||||
|
backendUri: '/users',
|
||||||
|
renderCallback: function updateCollection(data) {
|
||||||
|
userList = data.entities;
|
||||||
render();
|
render();
|
||||||
|
}});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
$el.html('Logged in: ' + appState.get('loggedIn'));
|
$el.html(template({
|
||||||
|
userList: userList,
|
||||||
|
}));
|
||||||
|
$el.find('.order a').click(orderLinkClicked);
|
||||||
|
$el.find('.order [data-order="' + activeSearchOrder + '"]').parent('li').addClass('active');
|
||||||
|
console.log(activeSearchOrder);
|
||||||
|
|
||||||
|
var $pager = $el.find('.pager');
|
||||||
|
pagedCollectionPresenter.render($pager);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function orderLinkClicked(e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
var $orderLink = jQuery(this);
|
||||||
|
activeSearchOrder = $orderLink.attr('data-order');
|
||||||
|
router.navigate(pagedCollectionPresenter.getSearchOrderChangeLink(activeSearchOrder));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: init,
|
init: init,
|
||||||
render: render
|
render: render
|
||||||
|
|
|
@ -23,7 +23,8 @@ App.Router = function(jQuery, util) {
|
||||||
inject('#/logout', function() { return App.DI.get('logoutPresenter'); });
|
inject('#/logout', function() { return App.DI.get('logoutPresenter'); });
|
||||||
inject('#/register', function() { return App.DI.get('registrationPresenter'); });
|
inject('#/register', function() { return App.DI.get('registrationPresenter'); });
|
||||||
inject('#/users', function() { return App.DI.get('userListPresenter'); });
|
inject('#/users', function() { return App.DI.get('userListPresenter'); });
|
||||||
inject('#/users/:userName', function() { return App.DI.get('userPresenter'); });
|
inject('#/users/:searchArgs', function() { return App.DI.get('userListPresenter'); });
|
||||||
|
inject('#/user/:userName', function() { return App.DI.get('userPresenter'); });
|
||||||
setRoot('#/users');
|
setRoot('#/users');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,30 @@ App.Util = (function(jQuery) {
|
||||||
|
|
||||||
var templateCache = {};
|
var templateCache = {};
|
||||||
|
|
||||||
|
function parseComplexRouteArgs(args) {
|
||||||
|
var result = {};
|
||||||
|
args = (args || '').split(/;/);
|
||||||
|
for (var i = 0; i < args.length; i ++) {
|
||||||
|
var arg = args[i];
|
||||||
|
if (!arg)
|
||||||
|
continue;
|
||||||
|
kv = arg.split(/=/);
|
||||||
|
result[kv[0]] = kv[1];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileComplexRouteArgs(baseUri, args) {
|
||||||
|
var result = baseUri + '/';
|
||||||
|
_.each(args, function(v, k) {
|
||||||
|
if (typeof(v) == 'undefined')
|
||||||
|
return;
|
||||||
|
result += k + '=' + v + ';'
|
||||||
|
});
|
||||||
|
result = result.slice(0, -1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function loadTemplate(templateName) {
|
function loadTemplate(templateName) {
|
||||||
return loadTemplateFromCache(templateName)
|
return loadTemplateFromCache(templateName)
|
||||||
|| loadTemplateFromDOM(templateName)
|
|| loadTemplateFromDOM(templateName)
|
||||||
|
@ -62,6 +86,8 @@ App.Util = (function(jQuery) {
|
||||||
loadTemplate: loadTemplate,
|
loadTemplate: loadTemplate,
|
||||||
initPresenter : initPresenter,
|
initPresenter : initPresenter,
|
||||||
initContentPresenter: initContentPresenter,
|
initContentPresenter: initContentPresenter,
|
||||||
|
parseComplexRouteArgs: parseComplexRouteArgs,
|
||||||
|
compileComplexRouteArgs: compileComplexRouteArgs,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
15
public_html/templates/pager.tpl
Normal file
15
public_html/templates/pager.tpl
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<ul class="pager">
|
||||||
|
|
||||||
|
<% _.each(pages, function(page) { %>
|
||||||
|
<% if (page == pageNumber) { %>
|
||||||
|
<li class="active">
|
||||||
|
<% } else { %>
|
||||||
|
<li>
|
||||||
|
<% } %>
|
||||||
|
<a href="<%= link(page) %>">
|
||||||
|
<%= page %>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
|
||||||
|
</ul>
|
|
@ -12,7 +12,7 @@
|
||||||
</li>
|
</li>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<li class="my-account">
|
<li class="my-account">
|
||||||
<a href="#/users/<%= user.name %>"><%= user.name %></a>
|
<a href="#/user/<%= user.name %>"><%= user.name %></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="logout">
|
<li class="logout">
|
||||||
<a href="#/logout">Logout</a>
|
<a href="#/logout">Logout</a>
|
||||||
|
|
24
public_html/templates/user-list.tpl
Normal file
24
public_html/templates/user-list.tpl
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<div id="user-list">
|
||||||
|
<ul class="order">
|
||||||
|
<li>
|
||||||
|
<a data-order="name">Sort A→Z</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a data-order="name,desc">Sort Z→A</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a data-order="registrationTime">Sort old→new</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a data-order="registrationTime,desc">Sort new→old</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<% _.each(userList, function(user) { %>
|
||||||
|
<div class="user">
|
||||||
|
User name: <%= user.name %>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
|
||||||
|
<div class="pager"></div>
|
||||||
|
</div>
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Szurubooru;
|
namespace Szurubooru;
|
||||||
|
|
||||||
final class Config extends \ArrayObject
|
class Config extends \ArrayObject
|
||||||
{
|
{
|
||||||
public function __construct(array $configPaths = [])
|
public function __construct(array $configPaths = [])
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,15 +17,36 @@ final class UserController extends AbstractController
|
||||||
public function registerRoutes(\Szurubooru\Router $router)
|
public function registerRoutes(\Szurubooru\Router $router)
|
||||||
{
|
{
|
||||||
$router->post('/api/users', [$this, 'register']);
|
$router->post('/api/users', [$this, 'register']);
|
||||||
$router->get('/api/users', [$this, 'getAll']);
|
$router->get('/api/users', [$this, 'getFiltered']);
|
||||||
$router->get('/api/users/:id', [$this, 'getById']);
|
$router->get('/api/users/:id', [$this, 'getById']);
|
||||||
$router->put('/api/users/:id', [$this, 'update']);
|
$router->put('/api/users/:id', [$this, 'update']);
|
||||||
$router->delete('/api/users/:id', [$this, 'delete']);
|
$router->delete('/api/users/:id', [$this, 'delete']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFiltered()
|
||||||
|
{
|
||||||
|
//todo: move this to form data constructor
|
||||||
|
$searchFormData = new \Szurubooru\FormData\SearchFormData;
|
||||||
|
$searchFormData->query = $this->inputReader->query;
|
||||||
|
$searchFormData->order = $this->inputReader->order;
|
||||||
|
$searchFormData->pageNumber = $this->inputReader->page;
|
||||||
|
$searchResult = $this->userService->getFiltered($searchFormData);
|
||||||
|
$entities = array_map(function($user) { return new \Szurubooru\ViewProxies\User($user); }, $searchResult->entities);
|
||||||
|
return [
|
||||||
|
'data' => $entities,
|
||||||
|
'pageSize' => $searchResult->filter->pageSize,
|
||||||
|
'totalRecords' => $searchResult->totalRecords];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById($id)
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
$input = new \Szurubooru\FormData\RegistrationFormData;
|
$input = new \Szurubooru\FormData\RegistrationFormData;
|
||||||
|
//todo: move this to form data constructor
|
||||||
$input->name = $this->inputReader->userName;
|
$input->name = $this->inputReader->userName;
|
||||||
$input->password = $this->inputReader->password;
|
$input->password = $this->inputReader->password;
|
||||||
$input->email = $this->inputReader->email;
|
$input->email = $this->inputReader->email;
|
||||||
|
@ -40,16 +61,6 @@ final class UserController extends AbstractController
|
||||||
throw new \BadMethodCallException('Not implemented');
|
throw new \BadMethodCallException('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAll()
|
|
||||||
{
|
|
||||||
throw new \BadMethodCallException('Not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getById($id)
|
|
||||||
{
|
|
||||||
throw new \BadMethodCallException('Not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete($id)
|
public function delete($id)
|
||||||
{
|
{
|
||||||
throw new \BadMethodCallException('Not implemented');
|
throw new \BadMethodCallException('Not implemented');
|
||||||
|
|
|
@ -19,6 +19,16 @@ abstract class AbstractDao implements ICrudDao
|
||||||
$this->entityName = $entityName;
|
$this->entityName = $entityName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCollection()
|
||||||
|
{
|
||||||
|
return $this->collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityConverter()
|
||||||
|
{
|
||||||
|
return $this->entityConverter;
|
||||||
|
}
|
||||||
|
|
||||||
public function save(&$entity)
|
public function save(&$entity)
|
||||||
{
|
{
|
||||||
$arrayEntity = $this->entityConverter->toArray($entity);
|
$arrayEntity = $this->entityConverter->toArray($entity);
|
||||||
|
|
21
src/Dao/SearchFilter.php
Normal file
21
src/Dao/SearchFilter.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Dao;
|
||||||
|
|
||||||
|
class SearchFilter
|
||||||
|
{
|
||||||
|
public $order;
|
||||||
|
public $query;
|
||||||
|
public $pageNumber;
|
||||||
|
public $pageSize;
|
||||||
|
|
||||||
|
public function __construct($pageSize, \Szurubooru\FormData\SearchFormData $searchFormData = null)
|
||||||
|
{
|
||||||
|
$this->pageSize = $pageSize;
|
||||||
|
if ($searchFormData)
|
||||||
|
{
|
||||||
|
$this->query = $searchFormData->query;
|
||||||
|
$this->order = $searchFormData->order;
|
||||||
|
$this->pageNumber = $searchFormData->pageNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/Dao/SearchResult.php
Normal file
17
src/Dao/SearchResult.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Dao;
|
||||||
|
|
||||||
|
class SearchResult
|
||||||
|
{
|
||||||
|
public $filter;
|
||||||
|
public $entities;
|
||||||
|
public $totalRecords;
|
||||||
|
|
||||||
|
public function __construct(SearchFilter $filter, $entities, $totalRecords)
|
||||||
|
{
|
||||||
|
$this->filter = $filter;
|
||||||
|
$this->entities = $entities;
|
||||||
|
$this->totalRecords = $totalRecords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
122
src/Dao/Services/AbstractSearchService.php
Normal file
122
src/Dao/Services/AbstractSearchService.php
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Dao\Services;
|
||||||
|
|
||||||
|
abstract class AbstractSearchService
|
||||||
|
{
|
||||||
|
const ORDER_DESC = -1;
|
||||||
|
const ORDER_ASC = 1;
|
||||||
|
|
||||||
|
private $collection;
|
||||||
|
private $entityConverter;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
\Szurubooru\Dao\AbstractDao $dao)
|
||||||
|
{
|
||||||
|
$this->collection = $dao->getCollection();
|
||||||
|
$this->entityConverter = $dao->getEntityConverter();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
$cursor = $this->collection->find($filter);
|
||||||
|
$totalRecords = $cursor->count();
|
||||||
|
$cursor->sort($order);
|
||||||
|
$cursor->skip($pageSize * $pageNumber);
|
||||||
|
$cursor->limit($pageSize);
|
||||||
|
|
||||||
|
$entities = [];
|
||||||
|
foreach ($cursor as $arrayEntity)
|
||||||
|
$entities []= $this->entityConverter->toEntity($arrayEntity);
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (count($token) == 2)
|
||||||
|
{
|
||||||
|
$orderDir = $token[1] == 'desc' ? self::ORDER_DESC : self::ORDER_ASC;
|
||||||
|
$orderToken = $token[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$orderDir = self::ORDER_ASC;
|
||||||
|
$orderToken = $token;
|
||||||
|
}
|
||||||
|
$orderColumn = $this->getOrderColumn($token[0]);
|
||||||
|
if ($orderColumn === null)
|
||||||
|
throw new \InvalidArgumentException('Invalid search order token: ' . $token);
|
||||||
|
$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];
|
||||||
|
}
|
||||||
|
}
|
31
src/Dao/Services/UserSearchService.php
Normal file
31
src/Dao/Services/UserSearchService.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Dao\Services;
|
||||||
|
|
||||||
|
class UserSearchService extends AbstractSearchService
|
||||||
|
{
|
||||||
|
public function __construct(\Szurubooru\Dao\UserDao $userDao)
|
||||||
|
{
|
||||||
|
parent::__construct($userDao);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getOrderColumn($token)
|
||||||
|
{
|
||||||
|
switch ($token)
|
||||||
|
{
|
||||||
|
case 'name':
|
||||||
|
return 'name';
|
||||||
|
|
||||||
|
case 'registrationDate':
|
||||||
|
case 'registrationTime':
|
||||||
|
case 'registered':
|
||||||
|
case 'joinDate':
|
||||||
|
case 'joinTime':
|
||||||
|
case 'joined':
|
||||||
|
return 'registrationTime';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,7 +3,8 @@ namespace Szurubooru\Dao;
|
||||||
|
|
||||||
class UserDao extends AbstractDao implements ICrudDao
|
class UserDao extends AbstractDao implements ICrudDao
|
||||||
{
|
{
|
||||||
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
|
public function __construct(
|
||||||
|
\Szurubooru\DatabaseConnection $databaseConnection)
|
||||||
{
|
{
|
||||||
parent::__construct($databaseConnection, 'users', '\Szurubooru\Entities\User');
|
parent::__construct($databaseConnection, 'users', '\Szurubooru\Entities\User');
|
||||||
}
|
}
|
||||||
|
|
9
src/FormData/SearchFormData.php
Normal file
9
src/FormData/SearchFormData.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\FormData;
|
||||||
|
|
||||||
|
class SearchFormData
|
||||||
|
{
|
||||||
|
public $query;
|
||||||
|
public $order;
|
||||||
|
public $pageNumber;
|
||||||
|
}
|
|
@ -30,6 +30,8 @@ class HttpHelper
|
||||||
|
|
||||||
public function getRequestUri()
|
public function getRequestUri()
|
||||||
{
|
{
|
||||||
return $_SERVER['REQUEST_URI'];
|
$requestUri = $_SERVER['REQUEST_URI'];
|
||||||
|
$requestUri = preg_replace('/\?.*$/', '', $requestUri);
|
||||||
|
return $requestUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,26 +3,40 @@ namespace Szurubooru\Services;
|
||||||
|
|
||||||
class UserService
|
class UserService
|
||||||
{
|
{
|
||||||
|
private $config;
|
||||||
private $validator;
|
private $validator;
|
||||||
private $userDao;
|
private $userDao;
|
||||||
|
private $userSearchService;
|
||||||
private $passwordService;
|
private $passwordService;
|
||||||
private $emailService;
|
private $emailService;
|
||||||
private $timeService;
|
private $timeService;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
\Szurubooru\Config $config,
|
||||||
\Szurubooru\Validator $validator,
|
\Szurubooru\Validator $validator,
|
||||||
\Szurubooru\Dao\UserDao $userDao,
|
\Szurubooru\Dao\UserDao $userDao,
|
||||||
|
\Szurubooru\Dao\Services\UserSearchService $userSearchService,
|
||||||
\Szurubooru\Services\PasswordService $passwordService,
|
\Szurubooru\Services\PasswordService $passwordService,
|
||||||
\Szurubooru\Services\EmailService $emailService,
|
\Szurubooru\Services\EmailService $emailService,
|
||||||
\Szurubooru\Services\TimeService $timeService)
|
\Szurubooru\Services\TimeService $timeService)
|
||||||
{
|
{
|
||||||
|
$this->config = $config;
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
$this->userDao = $userDao;
|
$this->userDao = $userDao;
|
||||||
|
$this->userSearchService = $userSearchService;
|
||||||
$this->passwordService = $passwordService;
|
$this->passwordService = $passwordService;
|
||||||
$this->emailService = $emailService;
|
$this->emailService = $emailService;
|
||||||
$this->timeService = $timeService;
|
$this->timeService = $timeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFiltered(\Szurubooru\FormData\SearchFormData $formData)
|
||||||
|
{
|
||||||
|
$pageSize = intval($this->config->users->usersPerPage);
|
||||||
|
$this->validator->validateNumber($formData->page);
|
||||||
|
$searchFilter = new \Szurubooru\Dao\SearchFilter($pageSize, $formData);
|
||||||
|
return $this->userSearchService->getFiltered($searchFilter);
|
||||||
|
}
|
||||||
|
|
||||||
public function register(\Szurubooru\FormData\RegistrationFormData $formData)
|
public function register(\Szurubooru\FormData\RegistrationFormData $formData)
|
||||||
{
|
{
|
||||||
$this->validator->validateUserName($formData->name);
|
$this->validator->validateUserName($formData->name);
|
||||||
|
|
|
@ -10,6 +10,10 @@ class Validator
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateNumber(&$subject) {
|
||||||
|
$subject = intval($subject);
|
||||||
|
}
|
||||||
|
|
||||||
public function validateNonEmpty($subject, $subjectName = 'Object')
|
public function validateNonEmpty($subject, $subjectName = 'Object')
|
||||||
{
|
{
|
||||||
if (!$subject)
|
if (!$subject)
|
||||||
|
|
|
@ -8,3 +8,5 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
|
||||||
return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock();
|
return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
|
67
tests/Dao/Services/UserSearchServiceTest.php
Normal file
67
tests/Dao/Services/UserSearchServiceTest.php
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
namespace Szurubooru\Tests\Dao\Services;
|
||||||
|
|
||||||
|
class UserSearchServiceTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
|
||||||
|
{
|
||||||
|
private $userDao;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNothing()
|
||||||
|
{
|
||||||
|
$searchFilter = new \Szurubooru\Dao\SearchFilter(1);
|
||||||
|
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [], 0);
|
||||||
|
|
||||||
|
$userSearchService = $this->getUserSearchService();
|
||||||
|
$actual = $userSearchService->getFiltered($searchFilter);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSorting()
|
||||||
|
{
|
||||||
|
$user1 = new \Szurubooru\Entities\User();
|
||||||
|
$user1->name = 'reginald';
|
||||||
|
$user1->registrationTime = date('c', mktime(3, 2, 1));
|
||||||
|
$user2 = new \Szurubooru\Entities\User();
|
||||||
|
$user2->name = 'beartato';
|
||||||
|
$user2->registrationTime = date('c', mktime(1, 2, 3));
|
||||||
|
|
||||||
|
$this->userDao->save($user1);
|
||||||
|
$this->userDao->save($user2);
|
||||||
|
|
||||||
|
$userSearchService = $this->getUserSearchService();
|
||||||
|
$searchFilter = new \Szurubooru\Dao\SearchFilter(1);
|
||||||
|
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user2], 2);
|
||||||
|
$actual = $userSearchService->getFiltered($searchFilter);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
|
||||||
|
$searchFilter->order = 'name,asc';
|
||||||
|
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user2], 2);
|
||||||
|
$actual = $userSearchService->getFiltered($searchFilter);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
|
||||||
|
$searchFilter->order = 'name,desc';
|
||||||
|
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user1], 2);
|
||||||
|
$actual = $userSearchService->getFiltered($searchFilter);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
|
||||||
|
$searchFilter->order = 'registrationTime,desc';
|
||||||
|
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user1], 2);
|
||||||
|
$actual = $userSearchService->getFiltered($searchFilter);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
|
||||||
|
$searchFilter->order = 'registrationTime';
|
||||||
|
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user2], 2);
|
||||||
|
$actual = $userSearchService->getFiltered($searchFilter);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUserSearchService()
|
||||||
|
{
|
||||||
|
return new \Szurubooru\Dao\Services\UserSearchService($this->userDao);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
|
||||||
{
|
{
|
||||||
public function testRetrievingByValidName()
|
public function testRetrievingByValidName()
|
||||||
{
|
{
|
||||||
$userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection);
|
$userDao = $this->getUserDao();
|
||||||
|
|
||||||
$user = new \Szurubooru\Entities\User();
|
$user = new \Szurubooru\Entities\User();
|
||||||
$user->name = 'test';
|
$user->name = 'test';
|
||||||
|
@ -19,7 +19,7 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
|
||||||
|
|
||||||
public function testRetrievingByInvalidName()
|
public function testRetrievingByInvalidName()
|
||||||
{
|
{
|
||||||
$userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection);
|
$userDao = $this->getUserDao();
|
||||||
|
|
||||||
$actual = $userDao->getByName('rubbish');
|
$actual = $userDao->getByName('rubbish');
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
|
||||||
|
|
||||||
public function testCheckingUserPresence()
|
public function testCheckingUserPresence()
|
||||||
{
|
{
|
||||||
$userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection);
|
$userDao = $this->getUserDao();
|
||||||
|
|
||||||
$this->assertFalse($userDao->hasAnyUsers());
|
$this->assertFalse($userDao->hasAnyUsers());
|
||||||
|
|
||||||
|
@ -38,4 +38,9 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
|
||||||
|
|
||||||
$this->assertTrue($userDao->hasAnyUsers());
|
$this->assertTrue($userDao->hasAnyUsers());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getUserDao()
|
||||||
|
{
|
||||||
|
return new \Szurubooru\Dao\UserDao($this->databaseConnection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,44 @@ namespace Szurubooru\Tests\Services;
|
||||||
|
|
||||||
final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
|
final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
|
||||||
{
|
{
|
||||||
|
private $configMock;
|
||||||
private $validatorMock;
|
private $validatorMock;
|
||||||
private $userDaoMock;
|
private $userDaoMock;
|
||||||
|
private $userSearchServiceMock;
|
||||||
private $passwordServiceMock;
|
private $passwordServiceMock;
|
||||||
private $emailServiceMock;
|
private $emailServiceMock;
|
||||||
private $timeServiceMock;
|
private $timeServiceMock;
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
|
$this->configMock = $this->mock(\Szurubooru\Config::class);
|
||||||
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
|
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
|
||||||
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class);
|
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class);
|
||||||
|
$this->userSearchService = $this->mock(\Szurubooru\Dao\Services\UserSearchService::class);
|
||||||
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
|
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
|
||||||
$this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class);
|
$this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class);
|
||||||
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
|
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGettingFilteredUsers()
|
||||||
|
{
|
||||||
|
$mockUser = new \Szurubooru\Entities\User();
|
||||||
|
$mockUser->name = 'user';
|
||||||
|
$expected = [$mockUser];
|
||||||
|
$this->userSearchService->method('getFiltered')->willReturn($expected);
|
||||||
|
|
||||||
|
$this->configMock->users = new \StdClass;
|
||||||
|
$this->configMock->users->usersPerPage = 1;
|
||||||
|
$searchFormData = new \Szurubooru\FormData\SearchFormData;
|
||||||
|
$searchFormData->query = '';
|
||||||
|
$searchFormData->order = 'joined';
|
||||||
|
$searchFormData->page = 2;
|
||||||
|
|
||||||
|
$userService = $this->getUserService();
|
||||||
|
$actual = $userService->getFiltered($searchFormData);
|
||||||
|
$this->assertEquals($expected, $actual);
|
||||||
|
}
|
||||||
|
|
||||||
public function testValidRegistration()
|
public function testValidRegistration()
|
||||||
{
|
{
|
||||||
$formData = new \Szurubooru\FormData\RegistrationFormData;
|
$formData = new \Szurubooru\FormData\RegistrationFormData;
|
||||||
|
@ -76,8 +99,10 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
|
||||||
private function getUserService()
|
private function getUserService()
|
||||||
{
|
{
|
||||||
return new \Szurubooru\Services\UserService(
|
return new \Szurubooru\Services\UserService(
|
||||||
|
$this->configMock,
|
||||||
$this->validatorMock,
|
$this->validatorMock,
|
||||||
$this->userDaoMock,
|
$this->userDaoMock,
|
||||||
|
$this->userSearchService,
|
||||||
$this->passwordServiceMock,
|
$this->passwordServiceMock,
|
||||||
$this->emailServiceMock,
|
$this->emailServiceMock,
|
||||||
$this->timeServiceMock);
|
$this->timeServiceMock);
|
||||||
|
|
Loading…
Reference in a new issue