Merge branch 'master' into feature_posts
This commit is contained in:
commit
41960f21c7
|
@ -18,6 +18,7 @@ class AppKernel extends Kernel
|
||||||
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
|
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
|
||||||
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
|
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
|
||||||
new Misd\GuzzleBundle\MisdGuzzleBundle(),
|
new Misd\GuzzleBundle\MisdGuzzleBundle(),
|
||||||
|
new Ob\HighchartsBundle\ObHighchartsBundle(),
|
||||||
new Skobkin\Bundle\PointToolsBundle\SkobkinPointToolsBundle(),
|
new Skobkin\Bundle\PointToolsBundle\SkobkinPointToolsBundle(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
34
app/DoctrineMigrations/Version20151001210600.php
Normal file
34
app/DoctrineMigrations/Version20151001210600.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Application\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes user login unique index. Temporary fix for user renaming.
|
||||||
|
*/
|
||||||
|
class Version20151001210600 extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema)
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||||
|
|
||||||
|
$this->addSql('DROP INDEX users.uniq_338adfc4aa08cb10');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
*/
|
||||||
|
public function down(Schema $schema)
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
|
||||||
|
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_338adfc4aa08cb10 ON users.users (login)');
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li><a href="{{ path('index') }}"><span class="glyphicon glyphicon-home"></span> {{ 'Main'|trans }}</a></li>
|
<li><a href="{{ path('index') }}"><span class="glyphicon glyphicon-home"></span> {{ 'Main'|trans }}</a></li>
|
||||||
<li><a href="{{ path('users_top') }}"><span class="glyphicon glyphicon-stats"></span> {{ 'Top'|trans }}</a></li>
|
<li><a href="{{ path('users_top') }}"><span class="glyphicon glyphicon-stats"></span> {{ 'Top'|trans }}</a></li>
|
||||||
|
<li><a href="{{ path('events_last') }}"><span class="glyphicon glyphicon-th-list"></span> {{ 'Last'|trans }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li><a href="https://bitbucket.org/skobkin/point-tools/issues?status=new&status=open" target="_blank"><span class="glyphicon glyphicon-envelope"></span> {{ 'Report a bug'|trans }}</a></li>
|
<li><a href="https://bitbucket.org/skobkin/point-tools/issues?status=new&status=open" target="_blank"><span class="glyphicon glyphicon-envelope"></span> {{ 'Report a bug'|trans }}</a></li>
|
||||||
|
@ -57,4 +58,5 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'counters.html.twig' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
28
app/Resources/views/counters.html.twig
Normal file
28
app/Resources/views/counters.html.twig
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!-- Yandex.Metrika counter -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function (d, w, c) {
|
||||||
|
(w[c] = w[c] || []).push(function() {
|
||||||
|
try {
|
||||||
|
w.yaCounter32849312 = new Ya.Metrika({
|
||||||
|
id:32849312,
|
||||||
|
clickmap:true,
|
||||||
|
trackLinks:true,
|
||||||
|
accurateTrackBounce:true
|
||||||
|
});
|
||||||
|
} catch(e) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var n = d.getElementsByTagName("script")[0],
|
||||||
|
s = d.createElement("script"),
|
||||||
|
f = function () { n.parentNode.insertBefore(s, n); };
|
||||||
|
s.type = "text/javascript";
|
||||||
|
s.async = true;
|
||||||
|
s.src = "https://mc.yandex.ru/metrika/watch.js";
|
||||||
|
|
||||||
|
if (w.opera == "[object Opera]") {
|
||||||
|
d.addEventListener("DOMContentLoaded", f, false);
|
||||||
|
} else { f(); }
|
||||||
|
})(document, window, "yandex_metrika_callbacks");
|
||||||
|
</script>
|
||||||
|
<noscript><div><img src="https://mc.yandex.ru/watch/32849312" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
|
<!-- /Yandex.Metrika counter -->
|
|
@ -638,20 +638,20 @@ class SymfonyRequirements extends RequirementCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addRecommendation(
|
$this->addRecommendation(
|
||||||
class_exists('Locale'),
|
extension_loaded('intl'),
|
||||||
'intl extension should be available',
|
'intl extension should be available',
|
||||||
'Install and enable the <strong>intl</strong> extension (used for validators).'
|
'Install and enable the <strong>intl</strong> extension (used for validators).'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (class_exists('Collator')) {
|
if (extension_loaded('intl')) {
|
||||||
|
// in some WAMP server installations, new Collator() returns null
|
||||||
$this->addRecommendation(
|
$this->addRecommendation(
|
||||||
null !== new Collator('fr_FR'),
|
null !== new Collator('fr_FR'),
|
||||||
'intl extension should be correctly configured',
|
'intl extension should be correctly configured',
|
||||||
'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.'
|
'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.'
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (class_exists('Locale')) {
|
// check for compatible ICU versions (only done when you have the intl extension)
|
||||||
if (defined('INTL_ICU_VERSION')) {
|
if (defined('INTL_ICU_VERSION')) {
|
||||||
$version = INTL_ICU_VERSION;
|
$version = INTL_ICU_VERSION;
|
||||||
} else {
|
} else {
|
||||||
|
@ -670,6 +670,14 @@ class SymfonyRequirements extends RequirementCollection
|
||||||
'intl ICU version should be at least 4+',
|
'intl ICU version should be at least 4+',
|
||||||
'Upgrade your <strong>intl</strong> extension with a newer ICU version (4+).'
|
'Upgrade your <strong>intl</strong> extension with a newer ICU version (4+).'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->addPhpIniRecommendation(
|
||||||
|
'intl.error_level',
|
||||||
|
create_function('$cfgValue', 'return (int) $cfgValue === 0;'),
|
||||||
|
true,
|
||||||
|
'intl.error_level should be 0 in php.ini',
|
||||||
|
'Set "<strong>intl.error_level</strong>" to "<strong>0</strong>" in php.ini<a href="#phpini">*</a> to inhibit the messages when an error occurs in ICU functions.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$accelerator =
|
$accelerator =
|
||||||
|
|
|
@ -42,9 +42,9 @@ foreach ($symfonyRequirements->getRecommendations() as $req) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($checkPassed) {
|
if ($checkPassed) {
|
||||||
echo_block('success', 'OK', 'Your system is ready to run Symfony2 projects', true);
|
echo_block('success', 'OK', 'Your system is ready to run Symfony2 projects');
|
||||||
} else {
|
} else {
|
||||||
echo_block('error', 'ERROR', 'Your system is not ready to run Symfony2 projects', true);
|
echo_block('error', 'ERROR', 'Your system is not ready to run Symfony2 projects');
|
||||||
|
|
||||||
echo_title('Fix the following mandatory requirements', 'red');
|
echo_title('Fix the following mandatory requirements', 'red');
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.3",
|
"php": ">=5.3.3",
|
||||||
"symfony/symfony": "2.7.x-dev",
|
"symfony/symfony": "2.7.*",
|
||||||
"doctrine/orm": "~2.2,>=2.2.3,<2.5",
|
"doctrine/orm": "~2.2,>=2.2.3,<2.5",
|
||||||
"doctrine/dbal": "<2.5",
|
"doctrine/dbal": "<2.5",
|
||||||
"doctrine/doctrine-bundle": "~1.4",
|
"doctrine/doctrine-bundle": "~1.4",
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
"sensio/framework-extra-bundle": "~3.0,>=3.0.2",
|
"sensio/framework-extra-bundle": "~3.0,>=3.0.2",
|
||||||
"incenteev/composer-parameter-handler": "~2.0",
|
"incenteev/composer-parameter-handler": "~2.0",
|
||||||
"misd/guzzle-bundle": "~1.0",
|
"misd/guzzle-bundle": "~1.0",
|
||||||
"doctrine/migrations": "1.0.*@dev",
|
"ob/highcharts-bundle": "^1.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "2.1.*@dev"
|
"doctrine/doctrine-migrations-bundle": "^1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"sensio/generator-bundle": "~2.3"
|
"sensio/generator-bundle": "~2.3"
|
||||||
|
|
497
composer.lock
generated
497
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -128,7 +128,7 @@ class UpdateSubscriptionsCommand extends ContainerAwareCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo move to the config
|
// @todo move to the config
|
||||||
usleep(200000);
|
usleep(500000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Controller;
|
||||||
|
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||||
|
use Skobkin\Bundle\PointToolsBundle\Entity\SubscriptionEvent;
|
||||||
|
use Skobkin\Bundle\PointToolsBundle\Entity\User;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class ApiController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns last user subscribers log
|
||||||
|
*
|
||||||
|
* @param $login
|
||||||
|
* @ParamConverter("user", class="SkobkinPointToolsBundle:User")
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function lastUserSubscribersByIdAction(User $user)
|
||||||
|
{
|
||||||
|
$qb = $this->getDoctrine()->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->createQueryBuilder('se');
|
||||||
|
$qb
|
||||||
|
->select(['se', 'sub'])
|
||||||
|
->innerJoin('se.subscriber', 'sub')
|
||||||
|
->where($qb->expr()->eq('se.author', ':author'))
|
||||||
|
->orderBy('se.date', 'desc')
|
||||||
|
->setParameter('author', $user)
|
||||||
|
->setMaxResults(20)
|
||||||
|
;
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
/** @var SubscriptionEvent $event */
|
||||||
|
foreach ($qb->getQuery()->getResult() as $event) {
|
||||||
|
$data[] = [
|
||||||
|
'user' => $event->getSubscriber()->getLogin(),
|
||||||
|
'action' => $event->getAction(),
|
||||||
|
'datetime' => $event->getDate()->format('d.m.Y H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Controller;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
|
||||||
|
class EventsController extends Controller
|
||||||
|
{
|
||||||
|
public function lastAction()
|
||||||
|
{
|
||||||
|
/** @var EntityManager $em */
|
||||||
|
$em = $this->getDoctrine()->getManager();
|
||||||
|
|
||||||
|
return $this->render('SkobkinPointToolsBundle:Events:last.html.twig', [
|
||||||
|
'last_events' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(20),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,40 +13,12 @@ class MainController extends Controller
|
||||||
/** @var EntityManager $em */
|
/** @var EntityManager $em */
|
||||||
$em = $this->getDoctrine()->getManager();
|
$em = $this->getDoctrine()->getManager();
|
||||||
|
|
||||||
/** @var QueryBuilder $qb */
|
|
||||||
$qb = $em->getRepository('SkobkinPointToolsBundle:User')->createQueryBuilder('u');
|
|
||||||
|
|
||||||
// All users in the system count
|
|
||||||
$usersCount = $qb->select('COUNT(u)')->getQuery()->getSingleScalarResult();
|
|
||||||
|
|
||||||
$qb = $em->getRepository('SkobkinPointToolsBundle:Subscription')->createQueryBuilder('s');
|
|
||||||
|
|
||||||
// Service subscribers count
|
|
||||||
$subscribersCount = $qb
|
|
||||||
->select('COUNT(s)')
|
|
||||||
->innerJoin('s.author', 'a')
|
|
||||||
->where('a.login = :login')
|
|
||||||
->setParameter('login', $this->container->getParameter('point_login'))
|
|
||||||
->getQuery()->getSingleScalarResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
$qb = $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->createQueryBuilder('se');
|
|
||||||
|
|
||||||
$now = new \DateTime();
|
|
||||||
|
|
||||||
$eventsCount = $qb
|
|
||||||
->select('COUNT(se)')
|
|
||||||
->where('se.date > :time')
|
|
||||||
->setParameter('time', $now->sub(new \DateInterval('PT24H')))
|
|
||||||
->getQuery()->getSingleScalarResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
return $this->render('SkobkinPointToolsBundle:Main:index.html.twig', [
|
return $this->render('SkobkinPointToolsBundle:Main:index.html.twig', [
|
||||||
'users_count' => $usersCount,
|
'users_count' => $em->getRepository('SkobkinPointToolsBundle:User')->getUsersCount(),
|
||||||
'subscribers_count' => $subscribersCount,
|
'subscribers_count' => $em->getRepository('SkobkinPointToolsBundle:Subscription')->getUserSubscribersCountById($this->container->getParameter('point_id')),
|
||||||
'events_count' => $eventsCount,
|
'events_count' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastDayEventsCount(),
|
||||||
'service_login' => $this->container->getParameter('point_login'),
|
'service_login' => $this->container->getParameter('point_login'),
|
||||||
|
'last_events' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(10),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
namespace Skobkin\Bundle\PointToolsBundle\Controller;
|
namespace Skobkin\Bundle\PointToolsBundle\Controller;
|
||||||
|
|
||||||
use Doctrine\ORM\QueryBuilder;
|
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use Skobkin\Bundle\PointToolsBundle\Entity\TopUserDTO;
|
use Skobkin\Bundle\PointToolsBundle\Entity\TopUserDTO;
|
||||||
use Skobkin\Bundle\PointToolsBundle\Entity\User;
|
use Skobkin\Bundle\PointToolsBundle\Entity\User;
|
||||||
use Skobkin\Bundle\PointToolsBundle\Service\UserApi;
|
use Skobkin\Bundle\PointToolsBundle\Service\UserApi;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Ob\HighchartsBundle\Highcharts\Highchart;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
|
@ -17,16 +17,11 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function showAction($login)
|
public function showAction($login)
|
||||||
{
|
{
|
||||||
/** @var QueryBuilder $qb */
|
/** @var EntityManager $em */
|
||||||
$qb = $this->getDoctrine()->getManager()->getRepository('SkobkinPointToolsBundle:User')->createQueryBuilder('u');
|
$em = $this->getDoctrine()->getManager();
|
||||||
|
|
||||||
$user = $qb
|
/** @var User $user */
|
||||||
->select('u')
|
$user = $em->getRepository('SkobkinPointToolsBundle:User')->findUserByLogin($login);
|
||||||
->where('LOWER(u.login) = LOWER(:login)')
|
|
||||||
->setMaxResults(1)
|
|
||||||
->setParameter('login', $login)
|
|
||||||
->getQuery()->getOneOrNullResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
throw $this->createNotFoundException('User ' . $login . ' not found.');
|
throw $this->createNotFoundException('User ' . $login . ' not found.');
|
||||||
|
@ -34,56 +29,23 @@ class UserController extends Controller
|
||||||
|
|
||||||
$userApi = $this->container->get('skobkin_point_tools.api_user');
|
$userApi = $this->container->get('skobkin_point_tools.api_user');
|
||||||
|
|
||||||
$qb = $this->getDoctrine()->getManager()->getRepository('SkobkinPointToolsBundle:User')->createQueryBuilder('u');
|
|
||||||
|
|
||||||
$subscribers = $qb
|
|
||||||
->select('u')
|
|
||||||
->innerJoin('u.subscriptions', 's')
|
|
||||||
->where('s.author = :author')
|
|
||||||
->orderBy('u.login', 'asc')
|
|
||||||
->setParameter('author', $user->getId())
|
|
||||||
->getQuery()->getResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
$qb = $this->getDoctrine()->getManager()->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->createQueryBuilder('se');
|
|
||||||
|
|
||||||
$subscriptionsEvents = $qb
|
|
||||||
->select()
|
|
||||||
->where('se.author = :author')
|
|
||||||
->orderBy('se.date', 'desc')
|
|
||||||
->setMaxResults(10)
|
|
||||||
->setParameter('author', $user)
|
|
||||||
->getQuery()->getResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
return $this->render('SkobkinPointToolsBundle:User:show.html.twig', [
|
return $this->render('SkobkinPointToolsBundle:User:show.html.twig', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'subscribers' => $subscribers,
|
'subscribers' => $em->getRepository('SkobkinPointToolsBundle:User')->findUserSubscribersById($user->getId()),
|
||||||
'log' => $subscriptionsEvents,
|
'log' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getUserLastSubscribersEventsById($user, 10),
|
||||||
'avatar_url' => $userApi->getAvatarUrl($user, UserApi::AVATAR_SIZE_LARGE),
|
'avatar_url' => $userApi->getAvatarUrl($user, UserApi::AVATAR_SIZE_LARGE),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function topAction()
|
public function topAction()
|
||||||
{
|
{
|
||||||
/** @var EntityManager $em */
|
$topUsers = $this->getDoctrine()->getManager()->getRepository('SkobkinPointToolsBundle:User')->getTopUsers();
|
||||||
$em = $this->getDoctrine()->getManager();
|
|
||||||
|
|
||||||
/** @var QueryBuilder $qb */
|
$topChart = $this->createTopUsersGraph($topUsers);
|
||||||
$qb = $em->getRepository('SkobkinPointToolsBundle:Subscription')->createQueryBuilder('s');
|
|
||||||
|
|
||||||
/** @var TopUserDTO[] $topUsers */
|
|
||||||
$topUsers = $qb
|
|
||||||
->select(['COUNT(s.subscriber) as cnt', 'NEW SkobkinPointToolsBundle:TopUserDTO(a.login, COUNT(s.subscriber))'])
|
|
||||||
->innerJoin('s.author', 'a')
|
|
||||||
->orderBy('cnt', 'desc')
|
|
||||||
->groupBy('a.id')
|
|
||||||
->setMaxResults(30)
|
|
||||||
->getQuery()->getResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
return $this->render('@SkobkinPointTools/User/top.html.twig', [
|
return $this->render('@SkobkinPointTools/User/top.html.twig', [
|
||||||
'top_users' => $topUsers
|
'top_users' => $topUsers,
|
||||||
|
'top_chart' => $topChart,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,4 +61,47 @@ class UserController extends Controller
|
||||||
}
|
}
|
||||||
return $this->redirectToRoute('user_show', ['login' => $login]);
|
return $this->redirectToRoute('user_show', ['login' => $login]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TopUserDTO[] $topUsers
|
||||||
|
* @return Highchart
|
||||||
|
*/
|
||||||
|
private function createTopUsersGraph(array $topUsers = [])
|
||||||
|
{
|
||||||
|
$translator = $this->container->get('translator');
|
||||||
|
|
||||||
|
$chartData = [
|
||||||
|
'titles' => [],
|
||||||
|
'subscribers' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Preparing chart data
|
||||||
|
foreach ($topUsers as $user) {
|
||||||
|
$chartData['titles'][] = $user->login;
|
||||||
|
$chartData['subscribers'][] = $user->subscribersCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart
|
||||||
|
$series = [[
|
||||||
|
'name' => $translator->trans('Subscribers'),
|
||||||
|
'data' => $chartData['subscribers'],
|
||||||
|
]];
|
||||||
|
|
||||||
|
// Initializing chart
|
||||||
|
$ob = new Highchart();
|
||||||
|
$ob->chart->renderTo('top-chart');
|
||||||
|
$ob->chart->type('bar');
|
||||||
|
$ob->title->text($translator->trans('Top users'));
|
||||||
|
$ob->xAxis->title(['text' => null]);
|
||||||
|
$ob->xAxis->categories($chartData['titles']);
|
||||||
|
$ob->yAxis->title(['text' => $translator->trans('amount')]);
|
||||||
|
$ob->plotOptions->bar([
|
||||||
|
'dataLabels' => [
|
||||||
|
'enabled' => true
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$ob->series($series);
|
||||||
|
|
||||||
|
return $ob;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ use Doctrine\ORM\Mapping as ORM;
|
||||||
/**
|
/**
|
||||||
* Subscription
|
* Subscription
|
||||||
*
|
*
|
||||||
* @ORM\Table(name="subscriptions.subscriptions", uniqueConstraints={
|
* @ORM\Table(name="subscriptions.subscriptions", schema="subscriptions", uniqueConstraints={
|
||||||
* @ORM\UniqueConstraint(name="subscription_unique", columns={"author_id", "subscriber_id"})}
|
* @ORM\UniqueConstraint(name="subscription_unique", columns={"author_id", "subscriber_id"})}
|
||||||
* )
|
* )
|
||||||
* @ORM\Entity
|
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Entity\SubscriptionRepository")
|
||||||
*/
|
*/
|
||||||
class Subscription
|
class Subscription
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,12 +7,12 @@ use Doctrine\ORM\Mapping as ORM;
|
||||||
/**
|
/**
|
||||||
* SubscriptionEvent
|
* SubscriptionEvent
|
||||||
*
|
*
|
||||||
* @ORM\Table(name="subscriptions.log", indexes={
|
* @ORM\Table(name="subscriptions.log", schema="subscriptions", indexes={
|
||||||
* @ORM\Index(name="author_idx", columns={"author_id"}),
|
* @ORM\Index(name="author_idx", columns={"author_id"}),
|
||||||
* @ORM\Index(name="subscriber_idx", columns={"subscriber_id"}),
|
* @ORM\Index(name="subscriber_idx", columns={"subscriber_id"}),
|
||||||
* @ORM\Index(name="date_idx", columns={"date"})
|
* @ORM\Index(name="date_idx", columns={"date"})
|
||||||
* })
|
* })
|
||||||
* @ORM\Entity
|
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Entity\SubscriptionEventRepository")
|
||||||
* @ORM\HasLifecycleCallbacks
|
* @ORM\HasLifecycleCallbacks
|
||||||
*/
|
*/
|
||||||
class SubscriptionEvent
|
class SubscriptionEvent
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
|
|
||||||
|
class SubscriptionEventRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getLastDayEventsCount()
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('se');
|
||||||
|
|
||||||
|
$now = new \DateTime();
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(se)')
|
||||||
|
->where('se.date > :time')
|
||||||
|
->setParameter('time', $now->sub(new \DateInterval('PT24H')))
|
||||||
|
->getQuery()->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User $user
|
||||||
|
* @param integer $limit
|
||||||
|
* @return SubscriptionEvent[]
|
||||||
|
*/
|
||||||
|
public function getUserLastSubscribersEventsById(User $user, $limit)
|
||||||
|
{
|
||||||
|
if (!is_int($limit)) {
|
||||||
|
throw new \InvalidArgumentException('$limit must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->createQueryBuilder('se');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->select(['se', 's'])
|
||||||
|
->join('se.subscriber', 's')
|
||||||
|
->where('se.author = :author')
|
||||||
|
->orderBy('se.date', 'desc')
|
||||||
|
->setMaxResults($limit)
|
||||||
|
->setParameter('author', $user)
|
||||||
|
->getQuery()->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last $limit subscriptions
|
||||||
|
*
|
||||||
|
* @param integer $limit
|
||||||
|
* @return SubscriptionEvent[]
|
||||||
|
*/
|
||||||
|
public function getLastSubscriptionEvents($limit)
|
||||||
|
{
|
||||||
|
if (!is_int($limit)) {
|
||||||
|
throw new \InvalidArgumentException('$limit must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->createQueryBuilder('se');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->select()
|
||||||
|
->orderBy('se.date', 'desc')
|
||||||
|
->setMaxResults($limit)
|
||||||
|
->getQuery()
|
||||||
|
->setFetchMode('SkobkinPointToolsBundle:SubscriptionEvent', 'author', ClassMetadata::FETCH_EAGER)
|
||||||
|
->setFetchMode('SkobkinPointToolsBundle:SubscriptionEvent', 'subscriber', ClassMetadata::FETCH_EAGER)
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
|
||||||
|
class SubscriptionRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param integer $id
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getUserSubscribersCountById($id)
|
||||||
|
{
|
||||||
|
if (!is_int($id)) {
|
||||||
|
throw new \InvalidArgumentException('$id must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->createQueryBuilder('s');
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(s)')
|
||||||
|
->innerJoin('s.author', 'a')
|
||||||
|
->where('a.id = :id')
|
||||||
|
->setParameter('id', $id)
|
||||||
|
->getQuery()->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ use Doctrine\ORM\Mapping as ORM;
|
||||||
/**
|
/**
|
||||||
* User
|
* User
|
||||||
*
|
*
|
||||||
* @ORM\Table(name="users.users")
|
* @ORM\Table(name="users.users", schema="users")
|
||||||
* @ORM\Entity
|
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Entity\UserRepository")
|
||||||
* @ORM\HasLifecycleCallbacks
|
* @ORM\HasLifecycleCallbacks
|
||||||
*/
|
*/
|
||||||
class User
|
class User
|
||||||
|
@ -25,7 +25,7 @@ class User
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
* @ORM\Column(name="login", type="string", length=255, nullable=false, unique=true)
|
* @ORM\Column(name="login", type="string", length=255, nullable=false)
|
||||||
*/
|
*/
|
||||||
private $login;
|
private $login;
|
||||||
|
|
||||||
|
@ -71,8 +71,17 @@ class User
|
||||||
private $newSubscriberEvents;
|
private $newSubscriberEvents;
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @param string $login
|
||||||
|
* @param string $name
|
||||||
|
*/
|
||||||
|
public function __construct($id = null, $login = null, $name = null)
|
||||||
{
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
$this->login = $login;
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
$this->subscribers = new ArrayCollection();
|
$this->subscribers = new ArrayCollection();
|
||||||
$this->subscriptions = new ArrayCollection();
|
$this->subscriptions = new ArrayCollection();
|
||||||
$this->newSubscriberEvents = new ArrayCollection();
|
$this->newSubscriberEvents = new ArrayCollection();
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
|
||||||
|
class UserRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Case-insensitive user search
|
||||||
|
*
|
||||||
|
* @param string $login
|
||||||
|
* @return User[]
|
||||||
|
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||||
|
*/
|
||||||
|
public function findUserByLogin($login)
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('u');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->select('u')
|
||||||
|
->where('LOWER(u.login) = LOWER(:login)')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->setParameter('login', $login)
|
||||||
|
->getQuery()->getOneOrNullResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getUsersCount()
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('u');
|
||||||
|
|
||||||
|
return $qb->select('COUNT(u)')->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param integer $id
|
||||||
|
* @return User[]
|
||||||
|
*/
|
||||||
|
public function findUserSubscribersById($id)
|
||||||
|
{
|
||||||
|
if (!is_int($id)) {
|
||||||
|
throw new \InvalidArgumentException('$id must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->createQueryBuilder('u');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->select('u')
|
||||||
|
->innerJoin('u.subscriptions', 's')
|
||||||
|
->where('s.author = :author')
|
||||||
|
->orderBy('u.login', 'asc')
|
||||||
|
->setParameter('author', $id)
|
||||||
|
->getQuery()->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return TopUserDTO[]
|
||||||
|
*/
|
||||||
|
public function getTopUsers($limit = 30)
|
||||||
|
{
|
||||||
|
if (!is_int($limit)) {
|
||||||
|
throw new \InvalidArgumentException('$limit must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: refactor query
|
||||||
|
$qb = $this->getEntityManager()->getRepository('SkobkinPointToolsBundle:Subscription')->createQueryBuilder('s');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->select(['COUNT(s.subscriber) as cnt', 'NEW SkobkinPointToolsBundle:TopUserDTO(a.login, COUNT(s.subscriber))'])
|
||||||
|
->innerJoin('s.author', 'a')
|
||||||
|
->orderBy('cnt', 'desc')
|
||||||
|
->groupBy('a.id')
|
||||||
|
->setMaxResults($limit)
|
||||||
|
->getQuery()->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
last_user_events:
|
||||||
|
path: /user/id/{id}/events/subscribers
|
||||||
|
defaults: { _controller: SkobkinPointToolsBundle:Api:lastUserSubscribersById, _format: json }
|
||||||
|
requirements:
|
||||||
|
id: \d+
|
|
@ -15,4 +15,12 @@ user_show:
|
||||||
|
|
||||||
users_top:
|
users_top:
|
||||||
path: /top
|
path: /top
|
||||||
defaults: { _controller: SkobkinPointToolsBundle:User:top }
|
defaults: { _controller: SkobkinPointToolsBundle:User:top }
|
||||||
|
|
||||||
|
events_last:
|
||||||
|
path: /last
|
||||||
|
defaults: { _controller: SkobkinPointToolsBundle:Events:last }
|
||||||
|
|
||||||
|
skobkin_point_tools:
|
||||||
|
resource: "@SkobkinPointToolsBundle/Resources/config/api/routing.yml"
|
||||||
|
prefix: /api/v1
|
||||||
|
|
|
@ -5,6 +5,7 @@ Toggle navigation: Переключить навигацию
|
||||||
Main: Главная
|
Main: Главная
|
||||||
Top: Топ
|
Top: Топ
|
||||||
Report a bug: Сообщить об ошибке
|
Report a bug: Сообщить об ошибке
|
||||||
|
Last: Последнее
|
||||||
|
|
||||||
# Подвал
|
# Подвал
|
||||||
Source code: Исходный код
|
Source code: Исходный код
|
||||||
|
@ -13,6 +14,9 @@ Source code: Исходный код
|
||||||
All users: Всего пользователей
|
All users: Всего пользователей
|
||||||
Subscribed users: Подписчиков сервиса
|
Subscribed users: Подписчиков сервиса
|
||||||
24 hours events: Событий за сутки
|
24 hours events: Событий за сутки
|
||||||
|
Author: Автор
|
||||||
|
Subscriber: Подписчик
|
||||||
|
Last events: Последние события
|
||||||
|
|
||||||
Username: Имя пользователя
|
Username: Имя пользователя
|
||||||
Search: Поиск
|
Search: Поиск
|
||||||
|
@ -29,4 +33,5 @@ No log data found: Лог отсутствует
|
||||||
|
|
||||||
# Топ пользователей
|
# Топ пользователей
|
||||||
Top users: Популярные пользователи
|
Top users: Популярные пользователи
|
||||||
Subscribers count: Подписчиков
|
Subscribers count: Подписчиков
|
||||||
|
amount: Количество
|
|
@ -0,0 +1,55 @@
|
||||||
|
{% extends "::base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{# TODO classes #}
|
||||||
|
<div class="last-subscriptions-log">
|
||||||
|
{% if last_events|length > 0 %}
|
||||||
|
<div class="panel-group" id="accordion-log">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading" id="heading-subscriptions-log">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a data-toggle="collapse" data-parent="#accordion-log" aria-expanded="true" href="#collapse-log">
|
||||||
|
<span class="glyphicon glyphicon-collapse-down"></span> {{ 'Last events'|trans }}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse-log" class="panel-collapse collapse in" aria-labelledby="heading-subscriptions-log">
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>{{ 'Subscriber'|trans }}</td>
|
||||||
|
<td>{{ 'Author'|trans }}</td>
|
||||||
|
<td>{{ 'Action'|trans }}</td>
|
||||||
|
<td>{{ 'Date'|trans }}</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for event in last_events %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url('user_show', {login: event.subscriber.login}) }}">@{{ event.subscriber.login }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url('user_show', {login: event.author.login}) }}">@{{ event.author.login }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="glyphicon {% if event.action == 'subscribe' %}glyphicon-plus{% elseif event.action == 'unsubscribe' %}glyphicon-minus{% endif %}"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{# Use DateTime helper: https://sonata-project.org/bundles/intl/master/doc/reference/datetime.html #}
|
||||||
|
{{ event.date|date('d F Y H:i:s') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning" role="alert">{{ 'No log data found'|trans }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-8 col-sm-3"><span class="glyphicon glyphicon-list"></span> {{ '24 hours events'|trans }}</div>
|
<div class="col-xs-8 col-sm-3"><span class="glyphicon glyphicon-list"></span> {{ '24 hours events'|trans }}</div>
|
||||||
<div class="col-xs-4 col-sm-2">{{ events_count }}</div>
|
<div class="col-xs-4 col-sm-2"><a href="{{ url('events_last') }}">{{ events_count }}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div class="panel-heading" id="heading-subscribers">
|
<div class="panel-heading" id="heading-subscribers">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a data-toggle="collapse" data-parent="#accordion-subscribers" aria-expanded="false" href="#collapse-subscribers">
|
<a data-toggle="collapse" data-parent="#accordion-subscribers" aria-expanded="false" href="#collapse-subscribers">
|
||||||
<span class="glyphicon glyphicon-collapse-down"></span> {{ 'Subscribers'|trans }}
|
<span class="glyphicon glyphicon-collapse-down"></span> {{ 'Subscribers'|trans }} ({{ subscribers|length }})
|
||||||
</a>
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
<span class="glyphicon {% if event.action == 'subscribe' %}glyphicon-plus{% elseif event.action == 'unsubscribe' %}glyphicon-minus{% endif %}"></span>
|
<span class="glyphicon {% if event.action == 'subscribe' %}glyphicon-plus{% elseif event.action == 'unsubscribe' %}glyphicon-minus{% endif %}"></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{# Use DateTime helper: https://sonata-project.org/bundles/intl/master/doc/reference/datetime.html #}
|
{# @todo Use DateTime helper: https://sonata-project.org/bundles/intl/master/doc/reference/datetime.html #}
|
||||||
{{ event.date|date('d F Y H:i:s') }}
|
{{ event.date|date('d F Y H:i:s') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -2,23 +2,17 @@
|
||||||
|
|
||||||
{% block header_title %}Top @ Point Tools{% endblock %}
|
{% block header_title %}Top @ Point Tools{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block head_js %}
|
||||||
<h1>{{ 'Top users'|trans }}</h1>
|
{{ parent() }}
|
||||||
|
<script src="//yastatic.net/jquery/2.1.4/jquery.min.js"></script>
|
||||||
<table class="table table-striped">
|
<script src="//code.highcharts.com/4.0.1/highcharts.js"></script>
|
||||||
<thead>
|
<script src="//code.highcharts.com/4.0.1/modules/exporting.js"></script>
|
||||||
<tr>
|
{% endblock %}
|
||||||
<td>{{ 'User'|trans }}</td>
|
|
||||||
<td>{{ 'Subscribers count'|trans }}</td>
|
{% block content %}
|
||||||
</tr>
|
<script type="text/javascript">
|
||||||
</thead>
|
{{ chart(top_chart) }}
|
||||||
<tbody>
|
</script>
|
||||||
{% for user in top_users %}
|
|
||||||
<tr>
|
<div id="top-chart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div>
|
||||||
<td><a href="{{ url('user_show', {login: user.login}) }}">@{{ user.login }}</a></td>
|
|
||||||
<td>{{ user.subscribersCount }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Service\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
class ApiException extends \Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Service\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidResponseException extends ApiException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Service\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionManagerException extends \Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Skobkin\Bundle\PointToolsBundle\Service\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundException extends ApiException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $login;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
* @param int $userId
|
||||||
|
*/
|
||||||
|
public function __construct($message = 'User not found', $code = 0, \Exception $previous = null, $userId = null, $login = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
$this->userId = $userId;
|
||||||
|
$this->login = $login;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns ID of user which was not found
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getUserId()
|
||||||
|
{
|
||||||
|
return $this->userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLogin()
|
||||||
|
{
|
||||||
|
return $this->login;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,13 @@ namespace Skobkin\Bundle\PointToolsBundle\Service;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Guzzle\Http\Exception\ClientErrorResponseException;
|
||||||
use Guzzle\Service\Client;
|
use Guzzle\Service\Client;
|
||||||
use Skobkin\Bundle\PointToolsBundle\Entity\User;
|
use Skobkin\Bundle\PointToolsBundle\Entity\User;
|
||||||
|
use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\ApiException;
|
||||||
|
use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\InvalidResponseException;
|
||||||
|
use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\UserNotFoundException;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic Point.im user API functions from /api/user/*
|
* Basic Point.im user API functions from /api/user/*
|
||||||
|
@ -20,19 +25,25 @@ class UserApi extends AbstractApi
|
||||||
/**
|
/**
|
||||||
* @var string Base URL for user avatars
|
* @var string Base URL for user avatars
|
||||||
*/
|
*/
|
||||||
protected $avatarsBaseUrl = 'point.im/avatar/';
|
protected $avatarsBaseUrl = '//point.im/avatar/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var EntityManager
|
* @var EntityManager
|
||||||
*/
|
*/
|
||||||
protected $em;
|
protected $em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EntityRepository
|
||||||
|
*/
|
||||||
|
protected $userRepository;
|
||||||
|
|
||||||
|
|
||||||
public function __construct(Client $httpClient, $https = true, $baseUrl = null, EntityManagerInterface $entityManager)
|
public function __construct(Client $httpClient, $https = true, $baseUrl = null, EntityManagerInterface $entityManager)
|
||||||
{
|
{
|
||||||
parent::__construct($httpClient, $https, $baseUrl);
|
parent::__construct($httpClient, $https, $baseUrl);
|
||||||
|
|
||||||
$this->em = $entityManager;
|
$this->em = $entityManager;
|
||||||
|
$this->userRepository = $this->em->getRepository('SkobkinPointToolsBundle:User');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName()
|
public function getName()
|
||||||
|
@ -45,21 +56,33 @@ class UserApi extends AbstractApi
|
||||||
*
|
*
|
||||||
* @param string $login
|
* @param string $login
|
||||||
* @return User[]
|
* @return User[]
|
||||||
|
* @throws ApiException
|
||||||
|
* @throws InvalidResponseException
|
||||||
|
* @throws UserNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getUserSubscribersByLogin($login)
|
public function getUserSubscribersByLogin($login)
|
||||||
{
|
{
|
||||||
$usersList = $this->getGetRequestData('/api/user/' . $login . '/subscribers', [], true);
|
try {
|
||||||
|
$usersList = $this->getGetRequestData('/api/user/'.urlencode($login).'/subscribers', [], true);
|
||||||
|
} catch (ClientErrorResponseException $e) {
|
||||||
|
if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
|
||||||
|
throw new UserNotFoundException('User not found', 0, $e, null, $login);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$users = $this->getUsersFromList($usersList);
|
return $this->getUsersFromList($usersList);
|
||||||
|
|
||||||
return $users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user subscribers by user id
|
* Get user subscribers by user id
|
||||||
*
|
*
|
||||||
* @param int $id
|
* @param $id
|
||||||
* @return User[]
|
* @return User[]
|
||||||
|
* @throws ApiException
|
||||||
|
* @throws InvalidResponseException
|
||||||
|
* @throws UserNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getUserSubscribersById($id)
|
public function getUserSubscribersById($id)
|
||||||
{
|
{
|
||||||
|
@ -67,57 +90,222 @@ class UserApi extends AbstractApi
|
||||||
throw new \InvalidArgumentException('$id must be an integer');
|
throw new \InvalidArgumentException('$id must be an integer');
|
||||||
}
|
}
|
||||||
|
|
||||||
$usersList = $this->getGetRequestData('/api/user/id/' . (int) $id . '/subscribers', [], true);
|
try {
|
||||||
|
$usersList = $this->getGetRequestData('/api/user/id/'.(int) $id.'/subscribers', [], true);
|
||||||
|
} catch (ClientErrorResponseException $e) {
|
||||||
|
if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
|
||||||
|
throw new UserNotFoundException('User not found', 0, $e, $id);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$users = $this->getUsersFromList($usersList);
|
return $this->getUsersFromList($usersList);
|
||||||
|
|
||||||
return $users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Get user subscriptions by user login
|
||||||
|
*
|
||||||
|
* @param string $login
|
||||||
* @return User[]
|
* @return User[]
|
||||||
|
* @throws ApiException
|
||||||
|
* @throws InvalidResponseException
|
||||||
|
* @throws UserNotFoundException
|
||||||
|
*/
|
||||||
|
public function getUserSubscriptionsByLogin($login)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$usersList = $this->getGetRequestData('/api/user/'.urlencode($login).'/subscriptions', [], true);
|
||||||
|
} catch (ClientErrorResponseException $e) {
|
||||||
|
if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
|
||||||
|
throw new UserNotFoundException('User not found', 0, $e, null, $login);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getUsersFromList($usersList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user subscriptions by user id
|
||||||
|
*
|
||||||
|
* @param $id
|
||||||
|
* @return User[]
|
||||||
|
* @throws ApiException
|
||||||
|
* @throws InvalidResponseException
|
||||||
|
* @throws UserNotFoundException
|
||||||
|
*/
|
||||||
|
public function getUserSubscriptionsById($id)
|
||||||
|
{
|
||||||
|
if (!is_numeric($id)) {
|
||||||
|
throw new \InvalidArgumentException('$id must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$usersList = $this->getGetRequestData('/api/user/id/'.(int) $id.'/subscriptions', [], true);
|
||||||
|
} catch (ClientErrorResponseException $e) {
|
||||||
|
if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
|
||||||
|
throw new UserNotFoundException('User not found', 0, $e, $id);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getUsersFromList($usersList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single user by login
|
||||||
|
*
|
||||||
|
* @param string $login
|
||||||
|
* @return User
|
||||||
|
* @throws UserNotFoundException
|
||||||
|
* @throws ClientErrorResponseException
|
||||||
|
*/
|
||||||
|
public function getUserByLogin($login)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$userInfo = $this->getGetRequestData('/api/user/login/'.urlencode($login), [], true);
|
||||||
|
} catch (ClientErrorResponseException $e) {
|
||||||
|
if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
|
||||||
|
throw new UserNotFoundException('User not found', 0, $e, null, $login);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getUserFromUserInfo($userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single user by id
|
||||||
|
*
|
||||||
|
* @param $id
|
||||||
|
* @return User
|
||||||
|
* @throws UserNotFoundException
|
||||||
|
* @throws ClientErrorResponseException
|
||||||
|
*/
|
||||||
|
public function getUserById($id)
|
||||||
|
{
|
||||||
|
if (!is_numeric($id)) {
|
||||||
|
throw new \InvalidArgumentException('$id must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userInfo = $this->getGetRequestData('/api/user/id/'.(int) $id, [], true);
|
||||||
|
} catch (ClientErrorResponseException $e) {
|
||||||
|
if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
|
||||||
|
throw new UserNotFoundException('User not found', 0, $e, $id);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getUserFromUserInfo($userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and updates or create new user from API response data
|
||||||
|
*
|
||||||
|
* @param array $userInfo
|
||||||
|
* @return User
|
||||||
|
* @throws ApiException
|
||||||
|
* @throws InvalidResponseException
|
||||||
|
*/
|
||||||
|
public function getUserFromUserInfo(array $userInfo)
|
||||||
|
{
|
||||||
|
if (!is_array($userInfo)) {
|
||||||
|
throw new \InvalidArgumentException('$userInfo must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo Return ID existance check when @ap-Codkelden will fix this API behaviour
|
||||||
|
if (array_key_exists('id', $userInfo) && array_key_exists('login', $userInfo) && array_key_exists('name', $userInfo) && is_numeric($userInfo['id'])) {
|
||||||
|
/** @var User $user */
|
||||||
|
if (null === ($user = $this->userRepository->find($userInfo['id']))) {
|
||||||
|
// Creating new user
|
||||||
|
$user = new User($userInfo['id']);
|
||||||
|
$this->em->persist($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updating data
|
||||||
|
$user
|
||||||
|
->setLogin($userInfo['login'])
|
||||||
|
->setName($userInfo['name'])
|
||||||
|
;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->em->flush($user);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new ApiException(sprintf('Error while flushing changes for [%d] %s: %s', $user->getId(), $user->getLogin(), $e->getMessage()), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidResponseException('Invalid API response. Mandatory fields do not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get array of User objects from API response containing user list
|
||||||
|
*
|
||||||
|
* @param array $users
|
||||||
|
* @return User[]
|
||||||
|
* @throws ApiException
|
||||||
|
* @throws InvalidResponseException
|
||||||
*/
|
*/
|
||||||
private function getUsersFromList(array $users = [])
|
private function getUsersFromList(array $users = [])
|
||||||
{
|
{
|
||||||
/** @var EntityRepository $userRepo */
|
if (!is_array($users)) {
|
||||||
$userRepo = $this->em->getRepository('SkobkinPointToolsBundle:User');
|
throw new \InvalidArgumentException('$users must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var User[] $resultUsers */
|
||||||
$resultUsers = [];
|
$resultUsers = [];
|
||||||
|
|
||||||
foreach ($users as $userData) {
|
foreach ($users as $userInfo) {
|
||||||
if (array_key_exists('id', $userData) && array_key_exists('login', $userData) && array_key_exists('name', $userData) && is_numeric($userData['id'])) {
|
if (array_key_exists('id', $userInfo) && array_key_exists('login', $userInfo) && array_key_exists('name', $userInfo) && is_numeric($userInfo['id'])) {
|
||||||
|
|
||||||
// @todo Optimize with prehashed id's list
|
// @todo Optimize with prehashed id's list
|
||||||
$user = $userRepo->find($userData['id']);
|
if (null === ($user = $this->userRepository->find($userInfo['id']))) {
|
||||||
|
$user = new User((int) $userInfo['id']);
|
||||||
if (!$user) {
|
|
||||||
$user = new User();
|
|
||||||
$user->setId((int) $userData['id']);
|
|
||||||
$this->em->persist($user);
|
$this->em->persist($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updating data
|
// Updating data
|
||||||
if ($user->getLogin() !== $userData['login']) {
|
$user
|
||||||
$user->setLogin($userData['login']);
|
->setLogin($userInfo['login'])
|
||||||
}
|
->setName($userInfo['name'])
|
||||||
if ($user->getName() !== $userData['name']) {
|
;
|
||||||
$user->setName($userData['name']);
|
|
||||||
|
try {
|
||||||
|
$this->em->flush($user);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new ApiException(sprintf('Error while flushing changes for [%d] %s: %s', $user->getId(), $user->getLogin(), $e->getMessage()), 0, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
$resultUsers[] = $user;
|
$resultUsers[] = $user;
|
||||||
|
} else {
|
||||||
|
throw new InvalidResponseException('Invalid API response. Mandatory fields do not exist.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
return $resultUsers;
|
return $resultUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $login
|
* Creates avatar with specified size URL for user
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param int $size
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getAvatarUrl(User $user, $size)
|
public function getAvatarUrl(User $user, $size)
|
||||||
{
|
{
|
||||||
return ($this->useHttps ? 'https://' : 'http://') . $this->avatarsBaseUrl . $user->getLogin() . '/' . $size;
|
if (!in_array($size, [self::AVATAR_SIZE_SMALL, self::AVATAR_SIZE_MEDIUM, self::AVATAR_SIZE_LARGE], true)) {
|
||||||
|
throw new \InvalidArgumentException('Avatar size must be one of restricted variants. See UserApi class AVATAR_SIZE_* constants.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->avatarsBaseUrl.urlencode($user->getLogin()).'/'.$size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue