Вывод тикетов пользователя

Сегодня задали вопрос: -а как выводить тикеты определённого пользователя, типа как персональный блог?
Ну, тут минимум 2 варианта: 1. Создать отдельный раздел для юзера. 2. Выделять тикеты юзера из общей кучи.
Первый вариант отметаем сразу по ряду причин: - Нужно дать право юзеру на создание разделов. - Нужно проверять, сколько он их создал (должно же быть не больше одного). - Нужно назначать политики доступа так, чтобы туда мог написать только юзер. - Куча ненужных никому разделов в админке - Еще всякие непредвиденные сложности.
Если вы будуте создавать каждому юзеру блог самостоятельно в админке руками - еще куда ни шло. Но это не удобно + если юзеров много, то админка разрастётся до неприличных размеров.
Поэтому, мы пойдем вторым путём - выводом тикетов юзера по уникальному url. Я предлагаю site_name/section_name/user_name/.
Первым делом, нам нужно подготовить getPage к фильтрации по юзеру. Пиишем сниппет-обёртку getPageWrapper:

if (!empty($_REQUEST['username'])) {
    $username = trim($_REQUEST['username']);
    if ($user = $modx->getObject('modUser', array('username' => $username))) {
        $uid = $user->get('id');
    }
    else {$uid = 0;}
    $scriptProperties['where'] = '{"createdby":'.$uid.'}';
}

return $modx->runSnippet('getPage', $scriptProperties);
В разделе всех тикетов заменяем вызов getPage на getPageWrapper. Теперь, если в массиве $_REQUEST передать username существующего юзера - то выведутся только его тикеты.
Уже можно обращаться к странице site_name/section_name/?username=k07n. Обратите внимание, мы заодно нашли решение распространённого вопроса - а как передать через $_GET данные в getPage? Ничего сложного.
В принципе, всё. Можно делать ссылки на блог юзера и при переходе туда, будут выводиться только его статьи. Но это не красиво, мы ведь хотим дружественные url, да?
Эта задача также решается довольно просто. Пишем плагин redirectToUser:

if ($modx->event->name == 'OnPageNotFound') {
    $request = $_REQUEST['q'].'/';
    preg_match('/(.*?)\/(.*?)(?:\/|\.html)/', $request, $matches); // выбираем имя секции и юзера
    //echo '<pre>';print_r($matches);die; // Можно активировать для отладке
    if (!empty($matches[1]) && !empty($matches[2])) {
        $q = array(
            'alias' => $matches[1]
            ,'class_key' => 'TicketsSection'
            ,'deleted' => 0
            ,'published' => 1
        );
        // Ищем секцию тикетов и выводим её вместе с параметром фильтрации по юзеру
        if ($section = $modx->getObject('TicketsSection', $q)) {
            $section_id = $section->get('id');
            $_REQUEST['username'] = $matches[2];

            $modx->sendForward($section_id);
        }
    }
}
Плагин сработает, если не найдена страница по указанному адресу, затем попытается выделить из адреса запроса имя секции тикетов и юзера. Если секцию тикетов существует, то плагин загрузит ее, поместив в $_REQUEST имя юзера, чтобы обёртка getPage добавила его в условие выборки.
Если где-то в этом процессе произойдет сбой\несоответствие, то работа плагина остановится и вы получите стандартную страницу 404, которая и должна выводиться в таком случае. Конечно, вы можете добавить собственные дополнительные услоия\проверки и расширить мой пример как угодно.
Задача решена. С этого момента у меня в разделе вопросов можно кликать на имя юзера и получать все его тикеты. Например: http://bezumkin.ru/user/topics/bezumkin/.
Данный пример является класическим роутером дружественных url. Приблительно так же работают и friendly urls самого MODX.

49 комментариев

Дзен наступит, если еще сделать ссылки на сами тикеты в такой форме:
http://bezumkin.ru/help/Hanami/467
.
Василий Наумкин
Вы уже знаете всё нужное для этого.
дада, это не вопрос был, а предложение по функционалу сюда.
Бохонов Андрей
Большое спасибо за статью! Появился вопрос, если добавить случайные символы в конец url то сервер отвечает кодом 200 вместо вывода 404 ошибки
Например ссылка на комментарии bezumkin-а - код 200
http://bezumkin.ru/user/comments/bezumkin/
добавили к ссылке 123 - так же код 200 (почему не 404 ошибка?)
http://bezumkin.ru/user/comments/bezumkin/123
Может кто-нибудь сможет подсказать как это исправить?
404 ошибка в том и том случае, просто она плагином обрабатывается. В обоих случаях урл подпадает под условие проверок первых 3-х секций. Если ты хочешь 404, когда в конце лишнее написано - проверяй 4-ую секцию на отсутсвие знаков. Если в 4-ой пусто тогда форвард, если нет то форвард надо пропустить и будет 404
Что то не хотят у меня урлы работать(( Сниппет getPageWrapper создал, плагин redirectToUser тоже создал (именно плагин же должен быть, не сниппет, верно?), на странице, на которой предполагается вывод, вызываю так:
[[!getPageWrapper?  &element=`getTickets` &parents=`8` &includeContent=`1`]]
Ссылка с именем пользователя на эту страничку (ее id у меня 69) следующая
<a href="[[~69]]/[[+username]]">[[+fullname]]</a>
В чем может быть ошибка?
Если пишу через обычный GET имя пользователя вручную в адресной строке - работает.
Василий Наумкин
У меня здесь старый код, в котором показан общий принцип.
Попробуй вот такой плагин:

if ($modx->event->name == 'OnPageNotFound') {
    $q = trim($_REQUEST['q']);
    $matches = explode('/', $q);
    $count = count($matches);

    if ($count < 3) {return;}
    // Редирект на тикеты юзера
    elseif ($matches[0] == 'user' && $matches[1] == 'topics' && $user = $modx->getObject('modUser', array('username' => @$matches[2]))) {
        $_REQUEST['uid'] = $user->id;
        $modx->setPlaceholder('username', $user->username);
        $modx->sendForward(529); // id страницы с getPageWrapper
    }
    // Редирект на комменты юзера
    elseif ($matches[0] == 'user' && $matches[1] == 'comments' && $user = $modx->getObject('modUser', array('username' => @$matches[2]))) {
        $_REQUEST['uid'] = $user->id;
        $modx->setPlaceholder('username', $user->username);
        $modx->sendForward(530); // id страницы с getPageWrapper
    }
}
Увы и ах, не хочет даже так срабатывать... А в настройках плагина разве не нужно выставлять никаких системных событий?
Василий Наумкин
Естественно нужно, в первой строке же проверка OnPageNotFound.
Все, заработало! Ура!))))) Спасибо большущее! На самом деле это я протупил и не посмотрел в код детально изначально, у меня адрес страницы с getPageWrapper был произвольный, а надо было чтоб он был адрес-сайта/user/topics (ну либо адрес-сайта/user/comments - во втором случае) в соответствие с условием
$matches[0] == 'user' && $matches[1] == 'topics' && $user = $modx->getObject('modUser', array('username' => @$matches[2]))
Василий Наумкин
На здоровье!
Николай
А как правильно должен выглядеть сниппет getPageWrapper,чтобы работал с этим плагином? У меня заработал только когда добавил в плагин
$_REQUEST['username'] = $matches[2];
но это как-то странно по моему
Александр Наумов
Василий, объясни, пожалуйста, почему у тебя не срабатывает запрос?
http://bezumkin.ru/user/topics/?username=bezumkin
Василий Наумкин
Сниппет реагирует на uid - bezumkin.ru/user/topics/?uid=2
А плагин переводит username в uid.
Александр Наумов
Получается у тебя в добавок по ссылке http://bezumkin.ru/user/topics/ работает какой-то сниппет, который выводит надпись: Подходящих записей не найдено.?
Василий Наумкин
Обычный фильтр вывода default, при вызове сниппета.
Александр Наумов
Спасибо, понял!
Чикин Артур
Василий, скрести ссылки, что бы получилось вот так:
Василий Наумкин
Один раздел доступен только для юзеров, а другой для всех.
Их нельзя скрещивать.
Чикин Артур
Блин было бы гораздо удобнее. Так сказать появилась бы иллюзия единого кабинета.
Ну да, пусть все видят две вкладки, а юзеры четыре.
Василий Наумкин
Это разные разделы сайта: личный кабинет и публичный профиль. В личный кабинет без авторизации даже зайти нельзя, в профиль индексируют поисковики.
Вы если не понимаете - не нужно давать советов. Я вроде не дурной, осознаю, что делаю.
После озвучивания конкретных названий разделов стал понимать логику твоих мыслей. Да, согласен - объединять нельзя.
Что-то не пойму как должен использоваться TicketComments через getPageWrapper... Комментарии это же по сути не ресурсы. Можно пример такого вызова?
Александр Наумов
[[!getPageWrapper?
    &element=`pdoResources`
    &class=`TicketComment`
    &sortby=``
    &sortdir=`DESC`
    &limit=`10`
    &tpl=`tpl.Tickets.comments`
]]
В чанке tpl.Tickets.comments можно выводить
Спасибо, Александр! Я уже пробовал pdoResources подставлять, но что-то видимо все-таки не так делал, сейчас попробую этот вызов.
Александр Наумов
Я тоже кучу разных вариантов перепробовал и заработало, только тогда, когда поставил &sortby=``, так и не пойму почему без этого параметра не работает.
Василий Наумкин
Потому что иначе сортировка по первичному ключу, а он у комментов на два поля, то есть массив.
Через это ошибка в запросе pdo. Смотрите логи, это полезно.
Александр Наумов
Спасибо, буду знать!
При таком выводе интересно несколько моментов, да [[+text]], [[+name]] и [[+createdon]] в чанке выводят все как положено. Фото пользователя из соцсети, через которую он залогинен также выводится без проблем, через конструкцию [[+createdby:userinfo=photo]]. А как вывести имя тикета, к которому относится комментарий и граватар пользователя в чанке tpl.Tickets.comments?
И как еще можно вывести на одной странице общее число публикаций и комментариев? Для того раздела где находимся понятное дело [[+total]] выводит, а как для другого находясь в этом же разделе?? Я то конечно вывел, но по-моему это большущий костыль... На странице комментариев общее количество публикаций вот так:
[[!Profile? &user=`[[!+userid]]` &prefix=`user.`]]

[[!pdoResources:is=``:then=``:else=``?
    &class=`TicketComment`
    &where=`{"createdby":"[[!+userid]]"}`
    &sortby=``
    &sortdir=`DESC`
    &limit=`0`
    &totalVar=`totalcomments`
]]

[[+totalcomments]]
И, соответственно, на странице публикаций общее количество комментариев тем же способом, только используя getTickets:
[[!Profile? &user=`[[!+userid]]` &prefix=`user.`]]

[[!getTickets:is=``:then=``:else=``?
    &parents=`8`
    &user=`[[!+userid]]`
    &limit=`0`
]]

[[+total]]
Как бы это правильнее сделать, чтоб не запускать целый "завод", ради производства одной "зубочистки"?
Василий Наумкин
Я написал свой сниппет:
<?php
if (empty($_REQUEST['uid'])) {return '';}
$uid = intval($_REQUEST['uid']);

// Limit by specified parents
if (!isset($depth)) {$depth = 10;}
if (!empty($parents)) {
    $pids = array_map('trim', explode(',', $parents));
    $parents = array();
    foreach ($pids as $v) {
        $parents = array_merge($parents, $modx->getChildIds($v, $depth, array('context_key' => $modx->context->key)));
    }
}

// Tickets
$where = array('createdby' => $uid, 'deleted' => 0, 'published' => 1, 'class_key' => 'Ticket', 'privateweb' => 0);
if (!empty($parents)) {$where['parent:IN'] = $parents;}
$q = $modx->newQuery('Ticket', $where);
$topics = $modx->getCount('Ticket', $q);

// Comments
$where = array('createdby' => $uid, 'deleted' => 0);
if (!empty($parents)) {$where['Ticket.parent:IN'] = $parents;}
$q = $modx->newQuery('TicketComment', $where);
$q->leftJoin('TicketThread','Thread','Thread.id = TicketComment.thread');
$q->leftJoin('Ticket','Ticket','Ticket.id = Thread.resource');
if (!$modx->hasPermission('ticket_view_private')) {
    $q->where('privateweb = 0');
}
$comments = $modx->getCount('TicketComment', $q);

// Placeholders
$modx->setPlaceholder('total.topics', "($topics)");
$modx->setPlaceholder('total.comments', "($comments)");

$pdo = $modx->getService('pdoFetch');
if ($user = $pdo->getObject('modUser', $uid)) {
    $profile = $pdo->getObject('modUserProfile', array('internalKey' => $uid));

    $modx->setPlaceholder('username', $user['username']);
    $modx->setPlaceholder('fullname', $profile['fullname']);
    $modx->setPlaceholder('avatar', 'http://www.gravatar.com/avatar/'.md5($profile['email']).'?s=64&d=mm');
}
Выставляет плейсхолдеры total.topics, total.comments, username, fullname и avatar.
Огого как тут все круто оказывается) Спасибо, Василий, сейчас буду пробовать!
А аватар у меня кстать легко вот так вывелся:

[[!Profile? &user=`[[!+userid]]` &prefix=`user.`]]
[[+user.gravatar]]?s=64&d=[[++site_url]]/assets/img/system/no-photo200.jpg
Твой сниппет кстати можно включить даже в стандартный набор Tickets, я думаю многим такой функционал был бы полез!
Оказалось у меня getPageWrapper по какой-то причине не хотел воспринимать имя пользователя, использую теперь getPage без него. Для этого создаю в плагине redirectToUser еще один плейсхолдер, содержащий id юзера. Получается вот такой код:
if ($modx->event->name == 'OnPageNotFound') {
    $q = trim($_REQUEST['q']);
    $matches = explode('/', $q);
    $count = count($matches);

    if ($count < 3) {return;}
    // Редирект на тикеты юзера
    elseif ($matches[0] == 'форум' && $matches[1] == 'темы-пользователя' && $user = $modx->getObject('modUser', array('username' => @$matches[2]))) {
        $_REQUEST['uid'] = $user->id;
        $modx->setPlaceholder('username', $user->username);
        $modx->setPlaceholder('userid', $user->id);
        $modx->sendForward(69); // id страницы с getPageWrapper
    }
    // Редирект на комменты юзера
    elseif ($matches[0] == 'форум' && $matches[1] == 'комментарии-пользователя' && $user = $modx->getObject('modUser', array('username' => @$matches[2]))) {
        $_REQUEST['uid'] = $user->id;
        $modx->setPlaceholder('username', $user->username);
        $modx->setPlaceholder('userid', $user->id);
        $modx->sendForward(76); // id страницы с getPageWrapper
    }
}
А на страницах получаю все необходимые данные через:
[[!Profile? &user=`[[!+userid]]` &prefix=`user.`]]
Соответственно и в вызовы getPage потом подставляю.
Для тикетов:
[[!getPage:default=`Данный пользователь еще не создал ни одной темы`?  
    &element=`getTickets`
    &parents=`8`
    &includeContent=`1`
    &user=`[[!+userid]]`
]]
И для комментариев:
[[!getPage:default=`У этого пользователя еще нет комментариев`?
    &element=`pdoResources`
    &class=`TicketComment`
    &where=`{"createdby":"[[!+userid]]"}`
    &sortby=``
    &sortdir=`DESC`
    &limit=`10`
        &tpl=`tpl.Tickets.comments`
]]
Не пойму, почему при моем вызове thread, parent, text и name попадают в выборку, а id не попадает (вроде в той же самой таблице находится)?
[[!getPage:default=`У этого пользователя еще нет комментариев`?
    &element=`pdoResources`
    &class=`TicketComment`
    &where=`{"createdby":"[[!+userid]]"}`
    &sortby=``
    &sortdir=`DESC`
    &limit=`10`
    &includeTVs=`id`
    &processTVs=`1`
    &tvPrefix=``
    &totalVar=`totalcomments`
    &tpl=`tpl.Tickets.comments`
]]
Василий Наумкин
&includeTVs=`id`
Имя ТВ не должно совпадать с именем поля ресурса.
А, да то я уже извращался, если вот так делаю тоже не выбирает:
[[!getPage:default=`У этого пользователя еще нет комментариев`?
    &element=`pdoResources`
    &class=`TicketComment`
    &where=`{"createdby":"[[!+userid]]"}`
    &sortby=``
    &sortdir=`DESC`
    &limit=`10`
    &tpl=`tpl.Tickets.comments`
]]
Добавил параметр select в котором перечислил все нужные мне поля, в том числе и id, так попадает в выборку
&select=`id,name,text,thread,createdon,createdby`
Странно, что без select никак его не получается вытащить. Это как то связано с тем что у поля id в таблице AUTO_INCREMENT стоит? По крайней мере больше я никаких отличий этого поля от других не нашел...
А чем мы можем получить информацию о тикете находясь на произвольной странице, но зная id тикета?
Может быть можно для этих целей как-то использовать pdoField? Единственное не могу понять как переключить его логику работы с ресурсов на комментарии((
Маленький модификатор threadid который из id тикета получает resource, т.е. на выходе получаем id ресурса, являющегося тикетом
if ($thread = $modx->getObject('TicketThread', array('id' => $input))) {
    echo $thread->get('resource');
}
Применяем в чанке tpl.Tickets.comments (о котором выше говорили) так
[[+thread:threadid]]
Соответственно отсюда можем уже и pagetitle тикета узнать, к которому относится комментарий и pagetitle деда ну и т.д.
Сам задал вопрос, сам нашел ответ, поразительно как действует отсутствие внешних подсказок :DDD Вот только времени очень много лишнего тратится на понимание...
Василий Наумкин
Зато потом экономится.
Иначе так и будешь постоянно ждать подсказок.
Да я ж не против никогда учиться, у меня этот веб весь хоть и сидит в печенках уже за несколько лет, но узнавать что-то новое и осваивать всегда приятно и интересно... Тут просто ситуация другая, когда над тобой висит заказчик проекта и орет "быстрее-быстрее" и ты понимаешь, что да, надо бы действительно быстрее, т.к. обещал сайт за 3 недели сделать, а уже полтора месяца "пилишь" его и при этом тратишь 2 дня на то чтоб только понять как вытянуть этот самый pagetitle, вот это расстраивает... Но я не в коем случае никого не виню, тебе, Василий, вообще отдельная и пожизненная благодарность за то, чем мы пользуемся все, и со временем я представляю как проблематично у тебя, тут нас таких вопрошающих не одна сотня, а ты один. Эт я сам дурак что не могу чего-то быстро сообразить, матчасть учить мне больше надо
Василий Наумкин
Ну так нужно оценивать свои силы и не обещать за 3 недели =)
А если не обещать, то никто и не закажет, кушать то хочется ;)
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
inna
02.11.2024, 12:17:25
ой... по бесплатным урокам по vesp все ссылки битые. А так хотелось...
Ivan CR
24.10.2024, 15:20:54
С днем рождения!!! Класс, что в твоей жизни есть такие интересные достижения.
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!