Merged in feature_username_search_autocomplete (pull request #9)

Feature username search autocomplete
This commit is contained in:
Alexey Eschenko 2016-12-11 01:58:24 +03:00
commit ddfc1bce26
11 changed files with 214 additions and 87 deletions

View file

@ -19,12 +19,8 @@
{% endblock %}
{% block head_js %}
{# HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries #}
{# Who uses IE??? #}
<!--[if lt IE 9]>
<script src="//oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="//oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script src="//yastatic.net/jquery/2.2.3/jquery.min.js"></script>
<script src="//yastatic.net/bootstrap/3.3.6/js/bootstrap.min.js"></script>
{% endblock %}
</head>
@ -46,8 +42,6 @@
{# Footer JavaScripts for faster loading #}
{% block footer_js %}
<script src="//yastatic.net/jquery/2.1.3/jquery.min.js"></script>
<script src="//yastatic.net/bootstrap/3.3.1/js/bootstrap.min.js"></script>
{% endblock %}
{%- endblock %}
</body>

View file

@ -35,6 +35,9 @@ sensio_framework_extra:
# Twig Configuration
twig:
form:
resources:
- bootstrap_3_layout.html.twig
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
@ -82,4 +85,4 @@ swiftmailer:
knp_markdown:
parser:
service: markdown.parser.point
service: markdown.parser.point

97
composer.lock generated
View file

@ -4,7 +4,6 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "68f896fc6d55e4454cb5f2d9be639e6f",
"content-hash": "15efa608e772ff5c6381daec33cf9114",
"packages": [
{
@ -73,7 +72,7 @@
"docblock",
"parser"
],
"time": "2015-08-31 12:32:49"
"time": "2015-08-31T12:32:49+00:00"
},
{
"name": "doctrine/cache",
@ -143,7 +142,7 @@
"cache",
"caching"
],
"time": "2015-12-31 16:37:02"
"time": "2015-12-31T16:37:02+00:00"
},
{
"name": "doctrine/collections",
@ -209,7 +208,7 @@
"collections",
"iterator"
],
"time": "2015-04-14 22:21:58"
"time": "2015-04-14T22:21:58+00:00"
},
{
"name": "doctrine/common",
@ -282,7 +281,7 @@
"persistence",
"spl"
],
"time": "2015-12-25 13:18:31"
"time": "2015-12-25T13:18:31+00:00"
},
{
"name": "doctrine/dbal",
@ -345,7 +344,7 @@
"persistence",
"queryobject"
],
"time": "2016-01-05 22:18:20"
"time": "2016-01-05T22:18:20+00:00"
},
{
"name": "doctrine/doctrine-bundle",
@ -424,7 +423,7 @@
"orm",
"persistence"
],
"time": "2016-01-10 17:21:44"
"time": "2016-01-10T17:21:44+00:00"
},
{
"name": "doctrine/doctrine-cache-bundle",
@ -512,7 +511,7 @@
"cache",
"caching"
],
"time": "2016-01-26 17:28:51"
"time": "2016-01-26T17:28:51+00:00"
},
{
"name": "doctrine/doctrine-migrations-bundle",
@ -570,7 +569,7 @@
"migrations",
"schema"
],
"time": "2015-11-04 13:45:30"
"time": "2015-11-04T13:45:30+00:00"
},
{
"name": "doctrine/inflector",
@ -637,7 +636,7 @@
"singularize",
"string"
],
"time": "2015-11-06 14:35:42"
"time": "2015-11-06T14:35:42+00:00"
},
{
"name": "doctrine/instantiator",
@ -691,7 +690,7 @@
"constructor",
"instantiate"
],
"time": "2015-06-14 21:17:01"
"time": "2015-06-14T21:17:01+00:00"
},
{
"name": "doctrine/lexer",
@ -745,7 +744,7 @@
"lexer",
"parser"
],
"time": "2014-09-09 13:34:57"
"time": "2014-09-09T13:34:57+00:00"
},
{
"name": "doctrine/migrations",
@ -818,7 +817,7 @@
"database",
"migrations"
],
"time": "2016-03-14 12:29:11"
"time": "2016-03-14T12:29:11+00:00"
},
{
"name": "doctrine/orm",
@ -891,7 +890,7 @@
"database",
"orm"
],
"time": "2015-08-31 13:19:01"
"time": "2015-08-31T13:19:01+00:00"
},
{
"name": "guzzle/guzzle",
@ -986,7 +985,8 @@
"rest",
"web service"
],
"time": "2015-03-18 18:23:50"
"abandoned": "guzzlehttp/guzzle",
"time": "2015-03-18T18:23:50+00:00"
},
{
"name": "incenteev/composer-parameter-handler",
@ -1037,7 +1037,7 @@
"keywords": [
"parameters management"
],
"time": "2015-06-03 08:27:03"
"time": "2015-06-03T08:27:03+00:00"
},
{
"name": "jdorn/sql-formatter",
@ -1087,7 +1087,7 @@
"highlight",
"sql"
],
"time": "2014-01-12 16:20:24"
"time": "2014-01-12T16:20:24+00:00"
},
{
"name": "jms/metadata",
@ -1139,7 +1139,7 @@
"xml",
"yaml"
],
"time": "2014-07-12 07:13:19"
"time": "2014-07-12T07:13:19+00:00"
},
{
"name": "jms/parser-lib",
@ -1174,7 +1174,7 @@
"Apache2"
],
"description": "A library for easily creating recursive-descent parsers.",
"time": "2012-11-18 18:08:43"
"time": "2012-11-18T18:08:43+00:00"
},
{
"name": "jms/serializer",
@ -1247,7 +1247,7 @@
"serialization",
"xml"
],
"time": "2015-10-27 09:24:41"
"time": "2015-10-27T09:24:41+00:00"
},
{
"name": "jms/serializer-bundle",
@ -1317,7 +1317,7 @@
"serialization",
"xml"
],
"time": "2015-11-10 12:26:42"
"time": "2015-11-10T12:26:42+00:00"
},
{
"name": "knplabs/knp-markdown-bundle",
@ -1378,7 +1378,7 @@
"knplabs",
"markdown"
],
"time": "2015-12-15 20:41:45"
"time": "2015-12-15T20:41:45+00:00"
},
{
"name": "kriswallsmith/assetic",
@ -1456,7 +1456,7 @@
"compression",
"minification"
],
"time": "2015-08-31 19:07:16"
"time": "2015-08-31T19:07:16+00:00"
},
{
"name": "michelf/php-markdown",
@ -1507,7 +1507,7 @@
"keywords": [
"markdown"
],
"time": "2015-12-24 01:37:31"
"time": "2015-12-24T01:37:31+00:00"
},
{
"name": "misd/guzzle-bundle",
@ -1579,7 +1579,8 @@
"rest",
"web service"
],
"time": "2014-12-01 08:29:51"
"abandoned": true,
"time": "2014-12-01T08:29:51+00:00"
},
{
"name": "monolog/monolog",
@ -1655,7 +1656,7 @@
"logging",
"psr-3"
],
"time": "2015-08-31 09:17:37"
"time": "2015-08-31T09:17:37+00:00"
},
{
"name": "ob/highcharts-bundle",
@ -1710,7 +1711,7 @@
"marcaube",
"ob"
],
"time": "2014-08-04 23:56:54"
"time": "2014-08-04T23:56:54+00:00"
},
{
"name": "ocramius/proxy-manager",
@ -1773,7 +1774,7 @@
"proxy pattern",
"service proxies"
],
"time": "2015-08-09 04:28:19"
"time": "2015-08-09T04:28:19+00:00"
},
{
"name": "phpcollection/phpcollection",
@ -1823,7 +1824,7 @@
"sequence",
"set"
],
"time": "2014-03-11 13:46:42"
"time": "2014-03-11T13:46:42+00:00"
},
{
"name": "phpoption/phpoption",
@ -1873,7 +1874,7 @@
"php",
"type"
],
"time": "2015-07-25 16:39:46"
"time": "2015-07-25T16:39:46+00:00"
},
{
"name": "psr/log",
@ -1911,7 +1912,7 @@
"psr",
"psr-3"
],
"time": "2012-12-21 11:40:51"
"time": "2012-12-21T11:40:51+00:00"
},
{
"name": "sensio/distribution-bundle",
@ -1971,7 +1972,7 @@
"configuration",
"distribution"
],
"time": "2015-08-03 10:07:12"
"time": "2015-08-03T10:07:12+00:00"
},
{
"name": "sensio/framework-extra-bundle",
@ -2026,7 +2027,7 @@
"annotations",
"controllers"
],
"time": "2015-08-03 11:59:27"
"time": "2015-08-03T11:59:27+00:00"
},
{
"name": "sensiolabs/security-checker",
@ -2070,7 +2071,7 @@
}
],
"description": "A security checker for your composer.lock",
"time": "2015-08-11 12:11:25"
"time": "2015-08-11T12:11:25+00:00"
},
{
"name": "swiftmailer/swiftmailer",
@ -2123,7 +2124,7 @@
"mail",
"mailer"
],
"time": "2015-06-06 14:19:39"
"time": "2015-06-06T14:19:39+00:00"
},
{
"name": "symfony/assetic-bundle",
@ -2193,7 +2194,7 @@
"compression",
"minification"
],
"time": "2015-09-01 00:05:29"
"time": "2015-09-01T00:05:29+00:00"
},
{
"name": "symfony/monolog-bundle",
@ -2252,7 +2253,7 @@
"log",
"logging"
],
"time": "2015-10-02 11:51:59"
"time": "2015-10-02T11:51:59+00:00"
},
{
"name": "symfony/swiftmailer-bundle",
@ -2309,7 +2310,7 @@
],
"description": "Symfony SwiftmailerBundle",
"homepage": "http://symfony.com",
"time": "2014-12-01 17:44:50"
"time": "2014-12-01T17:44:50+00:00"
},
{
"name": "symfony/symfony",
@ -2431,7 +2432,7 @@
"keywords": [
"framework"
],
"time": "2015-09-25 11:16:52"
"time": "2015-09-25T11:16:52+00:00"
},
{
"name": "twig/extensions",
@ -2483,7 +2484,7 @@
"i18n",
"text"
],
"time": "2015-08-22 16:38:35"
"time": "2015-08-22T16:38:35+00:00"
},
{
"name": "twig/twig",
@ -2544,7 +2545,7 @@
"keywords": [
"templating"
],
"time": "2015-09-22 13:59:32"
"time": "2015-09-22T13:59:32+00:00"
},
{
"name": "zendframework/zend-code",
@ -2596,7 +2597,7 @@
"code",
"zf2"
],
"time": "2016-01-05 05:58:37"
"time": "2016-01-05T05:58:37+00:00"
},
{
"name": "zendframework/zend-eventmanager",
@ -2650,7 +2651,7 @@
"events",
"zf2"
],
"time": "2016-02-18 20:53:00"
"time": "2016-02-18T20:53:00+00:00"
},
{
"name": "zendframework/zend-json",
@ -2704,7 +2705,7 @@
"json",
"zf2"
],
"time": "2014-03-12 16:10:15"
"time": "2014-03-12T16:10:15+00:00"
},
{
"name": "zendframework/zend-stdlib",
@ -2759,7 +2760,7 @@
"stdlib",
"zf2"
],
"time": "2014-03-12 16:10:15"
"time": "2014-03-12T16:10:15+00:00"
}
],
"packages-dev": [
@ -2818,7 +2819,7 @@
"keywords": [
"database"
],
"time": "2015-03-30 12:14:13"
"time": "2015-03-30T12:14:13+00:00"
},
{
"name": "doctrine/doctrine-fixtures-bundle",
@ -2875,7 +2876,7 @@
"Fixture",
"persistence"
],
"time": "2015-11-04 21:23:23"
"time": "2015-11-04T21:23:23+00:00"
},
{
"name": "sensio/generator-bundle",
@ -2923,7 +2924,7 @@
}
],
"description": "This bundle generates code for you",
"time": "2015-03-17 06:36:52"
"time": "2015-03-17T06:36:52+00:00"
}
],
"aliases": [],

View file

@ -3,17 +3,42 @@
namespace Skobkin\Bundle\PointToolsBundle\Controller;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\QueryBuilder;
use Skobkin\Bundle\PointToolsBundle\Form\UserSearchType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class MainController extends Controller
{
public function indexAction()
public function indexAction(Request $request)
{
/** @var EntityManager $em */
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(
new UserSearchType(),
null,
[
'action' => $this->generateUrl('index'),
'method' => 'POST',
]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$login = $form->get('login')->getData();
if (null !== $user = $em->getRepository('SkobkinPointToolsBundle:User')->findOneBy(['login' => $login])) {
return $this->redirectToRoute('user_show', ['login' => $login]);
}
$form->get('login')->addError(new FormError('Login not found'));
}
return $this->render('SkobkinPointToolsBundle:Main:index.html.twig', [
'form' => $form->createView(),
'users_count' => $em->getRepository('SkobkinPointToolsBundle:User')->getUsersCount(),
'subscribers_count' => $em->getRepository('SkobkinPointToolsBundle:Subscription')->getUserSubscribersCountById($this->container->getParameter('point_id')),
'events_count' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastDayEventsCount(),
@ -21,4 +46,20 @@ class MainController extends Controller
'last_events' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(10),
]);
}
public function searchUserAjaxAction($login)
{
$em = $this->getDoctrine()->getManager();
$result = [];
foreach ($em->getRepository('SkobkinPointToolsBundle:User')->findUsersLikeLogin($login) as $user) {
$result[] = [
'login' => $user->getLogin(),
'name' => $user->getName(),
];
}
return new JsonResponse($result);
}
}

View file

@ -50,19 +50,6 @@ class UserController extends Controller
]);
}
/**
* @param Request $request
*/
public function searchUserAction(Request $request)
{
$login = $request->request->get('login');
if (!$login) {
return $this->redirectToRoute('index');
}
return $this->redirectToRoute('user_show', ['login' => $login]);
}
/**
* @param TopUserDTO[] $topUsers
* @return Highchart

View file

@ -0,0 +1,39 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserSearchType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('login')
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'Skobkin\Bundle\PointToolsBundle\Entity\User',
]);
}
/**
* @return string
*/
public function getName()
{
return 'skobkin_bundle_pointtoolsbundle_user_search';
}
}

View file

@ -12,7 +12,7 @@ class UserRepository extends EntityRepository
* Case-insensitive user search
*
* @param string $login
* @return User[]
* @return User|null
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function findUserByLogin($login)
@ -28,6 +28,31 @@ class UserRepository extends EntityRepository
;
}
/**
* Case insensitive user LIKE %login% search
*
* @param string $login
*
* @return User[]
*/
public function findUsersLikeLogin($login)
{
if (empty($login)) {
return [];
}
$qb = $this->createQueryBuilder('u');
return $qb
->where('u.login LIKE :login')
->orderBy('u.login', 'ASC')
->setMaxResults(10)
->setParameter('login', '%'.strtolower($login).'%')
->getQuery()
->getResult()
;
}
/**
* @return integer
*/

View file

@ -2,10 +2,12 @@ index:
path: /
defaults: { _controller: SkobkinPointToolsBundle:Main:index }
user_search:
path: /user/search
defaults: { _controller: SkobkinPointToolsBundle:User:searchUser }
methods: [POST]
user_search_ajax:
path: /ajax/users/search/{login}
defaults: { _controller: SkobkinPointToolsBundle:Main:searchUserAjax, _format: json }
requirements:
login: "[\w-]*"
_format: json
user_show:
path: /user/{login}

View file

@ -1,18 +1,52 @@
{% extends "::base.html.twig" %}
{% block head_js %}
{{ parent() }}
<script src="{{ asset('js/bootstrap3-typeahead.min.js') }}"></script>
{% endblock %}
{% block content %}
<div class="well well-lg">
{# @todo rewrite to Symfony forms #}
<form class="form-inline" method="post" action="{{ path('user_search') }}">
{{ form_start(form, {'attr': {'class': 'form-inline'} }) }}
<div class="form-group">
<label class="sr-only" for="index-input-username">{{ 'Username'|trans }}</label>
<div class="input-group">
<div class="input-group-addon">@</div>
<input name="login" type="text" class="form-control" id="index-input-username" placeholder="username">
</div>
{{ form_errors(form.login) }}
{{ form_widget(form.login, {'attr': {'autocomplete': 'off'}}) }}
<script type="text/javascript">
$(function() {
$field = $('#{{ form.login.vars.id }}');
$field.typeahead({
minLength: 2,
delay: 500,
autoSelect: true,
source: function (query, processCallback) {
$.get('{{ path('user_search_ajax', {'login': ''}) }}' + query, function (data) {
processCallback(data);
});
},
afterSelect: function () {
$field.parents('form').first().submit();
},
displayText: function (item) {
// Crutches to place only login into the field after selecting the item
if (typeof item === 'object') {
return item.login+(item.name ? ' ('+item.name+')' : '');
} else if (typeof item === 'string') {
return item;
}
},
updater: function (item) {
// Crutches to place only login into the field after selecting the item
return item.login;
}
});
});
</script>
<input type="submit" value="{{ 'Search'|trans }}" class="btn btn-default" />
</div>
<button type="submit" class="btn btn-primary">{{ 'Search'|trans }}</button>
</form>
{{ form_end(form) }}
</div>
<div class="container service-stats">

View file

@ -11,7 +11,7 @@ umask(0002);
// Feel free to remove this, extend it, or make something more sophisticated.
if (isset($_SERVER['HTTP_CLIENT_IP'])
|| isset($_SERVER['HTTP_X_FORWARDED_FOR'])
|| !(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1', '192.168.1.2', '192.168.1.14')) || php_sapi_name() === 'cli-server')
|| !(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1')) || php_sapi_name() === 'cli-server')
) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');

1
web/js/bootstrap3-typeahead.min.js vendored Normal file

File diff suppressed because one or more lines are too long