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

Сегодня задали вопрос: -а как выводить тикеты определённого пользователя, типа как персональный блог?

Ну, тут минимум 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, которая и должна выводиться в таком случае. Конечно, вы можете добавить собственные дополнительные услоия\проверки и расширить мой пример как угодно.

Задача решена. С этого момента у меня в разделе вопросов можно кликать на имя юзера и получать все его тикеты. Например: bezumkin.ru/user/topics/bezumkin/.

Данный пример является класическим роутером дружественных url. Приблительно так же работают и friendly urls самого MODX.

Следующая заметка
Полезный сниппет для блога.
Предыдущая заметка
Тег spoiler для Jevix


Комментарии ()

  1. Andrei Kilin 18 декабря 2012, 15:22 # 0
    1. Andrei Kilin 18 декабря 2012, 15:46 # 0
      Дзен наступит, если еще сделать ссылки на сами тикеты в такой форме:
      http://bezumkin.ru/help/Hanami/467
      .
      1. Василий Наумкин 18 декабря 2012, 17:09 # 0
        Вы уже знаете всё нужное для этого.
        1. Andrei Kilin 18 декабря 2012, 17:34 # 0
          дада, это не вопрос был, а предложение по функционалу сюда.
      2. Бохонов Андрей 26 октября 2013, 00:30 # 0
        Большое спасибо за статью!
        Появился вопрос, если добавить случайные символы в конец url то сервер отвечает кодом 200 вместо вывода 404 ошибки

        Например ссылка на комментарии bezumkin-а — код 200
        http://bezumkin.ru/user/comments/bezumkin/

        добавили к ссылке 123 — так же код 200 (почему не 404 ошибка?)
        http://bezumkin.ru/user/comments/bezumkin/123

        Может кто-нибудь сможет подсказать как это исправить?
        1. Andrei Kilin 28 октября 2013, 12:06 # +1
          404 ошибка в том и том случае, просто она плагином обрабатывается. В обоих случаях урл подпадает под условие проверок первых 3-х секций. Если ты хочешь 404, когда в конце лишнее написано — проверяй 4-ую секцию на отсутсвие знаков. Если в 4-ой пусто тогда форвард, если нет то форвард надо пропустить и будет 404
        2. Даниил 13 января 2014, 08:27 # 0
          Что то не хотят у меня урлы работать(( Сниппет getPageWrapper создал, плагин redirectToUser тоже создал (именно плагин же должен быть, не сниппет, верно?), на странице, на которой предполагается вывод, вызываю так:
          [[!getPageWrapper?  &element=`getTickets` &parents=`8` &includeContent=`1`]]
          Ссылка с именем пользователя на эту страничку (ее id у меня 69) следующая
          <a href="[[~69]]/[[+username]]">[[+fullname]]</a>
          В чем может быть ошибка?
          1. Даниил 13 января 2014, 08:28 # 0
            Если пишу через обычный GET имя пользователя вручную в адресной строке — работает.
            1. Василий Наумкин 13 января 2014, 08:55 # 0
              У меня здесь старый код, в котором показан общий принцип.

              Попробуй вот такой плагин:
              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
              	}
              }
              1. Даниил 13 января 2014, 09:04 # 0
                Увы и ах, не хочет даже так срабатывать… А в настройках плагина разве не нужно выставлять никаких системных событий?
                1. Василий Наумкин 13 января 2014, 09:05 # 0
                  Естественно нужно, в первой строке же проверка OnPageNotFound.
                  1. Даниил 13 января 2014, 09:31 # 0
                    Все, заработало! Ура!))))) Спасибо большущее!
                    На самом деле это я протупил и не посмотрел в код детально изначально, у меня адрес страницы с getPageWrapper был произвольный, а надо было чтоб он был адрес-сайта/user/topics (ну либо адрес-сайта/user/comments — во втором случае) в соответствие с условием
                    $matches[0] == 'user' && $matches[1] == 'topics' && $user = $modx->getObject('modUser', array('username' => @$matches[2]))
                    1. Василий Наумкин 13 января 2014, 10:34 # 0
                      На здоровье!
                2. Николай 07 февраля 2014, 08:37 # 0
                  А как правильно должен выглядеть сниппет getPageWrapper, чтобы работал с этим плагином?
                  У меня заработал только когда добавил в плагин
                  $_REQUEST['username'] = $matches[2];
                  но это как-то странно по моему
              2. Александр Наумов 13 января 2014, 15:04 # 0
                Василий, объясни, пожалуйста, почему у тебя не срабатывает запрос?
                http://bezumkin.ru/user/topics/?username=bezumkin
                1. Василий Наумкин 13 января 2014, 15:07 # 0
                  Сниппет реагирует на uid — bezumkin.ru/user/topics/?uid=2

                  А плагин переводит username в uid.
                  1. Александр Наумов 13 января 2014, 16:03 # 0
                    Получается у тебя в добавок по ссылке bezumkin.ru/user/topics/ работает какой-то сниппет, который выводит надпись: Подходящих записей не найдено.?
                    1. Василий Наумкин 13 января 2014, 16:39 # 0
                      Обычный фильтр вывода default, при вызове сниппета.
                      1. Александр Наумов 13 января 2014, 16:43 # 0
                        Спасибо, понял!
                2. Чикин Артур 13 января 2014, 16:33 # 0
                  Василий, скрести ссылки, что бы получилось вот так:

                  1. Василий Наумкин 13 января 2014, 16:39 # 0
                    Один раздел доступен только для юзеров, а другой для всех.

                    Их нельзя скрещивать.
                    1. Чикин Артур 13 января 2014, 16:54 # 0
                      Блин было бы гораздо удобнее. Так сказать появилась бы иллюзия единого кабинета.
                      1. Даниил 14 января 2014, 12:02 # 0
                        Ну да, пусть все видят две вкладки, а юзеры четыре.
                        1. Василий Наумкин 14 января 2014, 13:06 # 0
                          Это разные разделы сайта: личный кабинет и публичный профиль. В личный кабинет без авторизации даже зайти нельзя, в профиль индексируют поисковики.

                          Вы если не понимаете — не нужно давать советов. Я вроде не дурной, осознаю, что делаю.
                          1. Даниил 14 января 2014, 13:13 # 0
                            После озвучивания конкретных названий разделов стал понимать логику твоих мыслей. Да, согласен — объединять нельзя.
                  2. Даниил 14 января 2014, 12:00 # 0
                    Что-то не пойму как должен использоваться TicketComments через getPageWrapper… Комментарии это же по сути не ресурсы. Можно пример такого вызова?
                    1. Александр Наумов 14 января 2014, 14:30 # 0
                      [[!getPageWrapper?
                      	&element=`pdoResources`
                      	&class=`TicketComment`
                      	&sortby=``
                      	&sortdir=`DESC`
                      	&limit=`10`
                      	&tpl=`tpl.Tickets.comments`
                      ]]
                      В чанке tpl.Tickets.comments можно выводить
                      Текст комментария — [[+text]],
                      Полное имя — [[+name]],
                      И дату создания — [[+createdon]]
                      1. Даниил 14 января 2014, 15:01 # 0
                        Спасибо, Александр! Я уже пробовал pdoResources подставлять, но что-то видимо все-таки не так делал, сейчас попробую этот вызов.
                        1. Александр Наумов 14 января 2014, 15:27 # 0
                          Я тоже кучу разных вариантов перепробовал и заработало, только тогда, когда поставил &sortby=``, так и не пойму почему без этого параметра не работает.
                          1. Василий Наумкин 14 января 2014, 16:55 # 0
                            Потому что иначе сортировка по первичному ключу, а он у комментов на два поля, то есть массив.

                            Через это ошибка в запросе pdo. Смотрите логи, это полезно.
                            1. Александр Наумов 14 января 2014, 17:34 # 0
                              Спасибо, буду знать!
                        2. Даниил 15 января 2014, 03:59 # 0
                          При таком выводе интересно несколько моментов, да [[+text]], [[+name]] и [[+createdon]] в чанке выводят все как положено. Фото пользователя из соцсети, через которую он залогинен также выводится без проблем, через конструкцию [[+createdby:userinfo=`photo`]]. А как вывести имя тикета, к которому относится комментарий и граватар пользователя в чанке tpl.Tickets.comments?
                          1. Даниил 15 января 2014, 07:55 # 0
                            И как еще можно вывести на одной странице общее число публикаций и комментариев? Для того раздела где находимся понятное дело [[+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]]
                            Как бы это правильнее сделать, чтоб не запускать целый «завод», ради производства одной «зубочистки»?
                            1. Василий Наумкин 15 января 2014, 08:11 # 0
                              Я написал свой сниппет:
                              <?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.
                              1. Даниил 15 января 2014, 08:57 # 0
                                Огого как тут все круто оказывается) Спасибо, Василий, сейчас буду пробовать!

                                А аватар у меня кстать легко вот так вывелся:
                                [[!Profile? &user=`[[!+userid]]` &prefix=`user.`]]
                                [[+user.gravatar]]?s=64&d=[[++site_url]]/assets/img/system/no-photo200.jpg
                                
                                1. Даниил 15 января 2014, 08:59 # 0
                                  Твой сниппет кстати можно включить даже в стандартный набор Tickets, я думаю многим такой функционал был бы полез!
                            2. Даниил 15 января 2014, 07:15 # 0
                              Оказалось у меня 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`
                              ]]
                          2. Даниил 16 января 2014, 08:33 # 0
                            Не пойму, почему при моем вызове 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`
                            ]]
                            1. Василий Наумкин 16 января 2014, 08:53 # 0
                              &includeTVs=`id`
                              Имя ТВ не должно совпадать с именем поля ресурса.
                              1. Даниил 16 января 2014, 09:00 # 0
                                А, да то я уже извращался, если вот так делаю тоже не выбирает:
                                [[!getPage:default=`У этого пользователя еще нет комментариев`?
                                	&element=`pdoResources`
                                	&class=`TicketComment`
                                	&where=`{"createdby":"[[!+userid]]"}`
                                	&sortby=``
                                	&sortdir=`DESC`
                                	&limit=`10`
                                    &tpl=`tpl.Tickets.comments`
                                ]]
                                1. Даниил 16 января 2014, 10:49 # 0
                                  Добавил параметр select в котором перечислил все нужные мне поля, в том числе и id, так попадает в выборку

                                  &select=`id,name,text,thread,createdon,createdby`
                                  Странно, что без select никак его не получается вытащить. Это как то связано с тем что у поля id в таблице AUTO_INCREMENT стоит? По крайней мере больше я никаких отличий этого поля от других не нашел…
                            2. Даниил 16 января 2014, 12:16 # 0
                              А чем мы можем получить информацию о тикете находясь на произвольной странице, но зная id тикета?
                              1. Даниил 16 января 2014, 21:25 # 0
                                Может быть можно для этих целей как-то использовать pdoField? Единственное не могу понять как переключить его логику работы с ресурсов на комментарии((
                                1. Даниил 17 января 2014, 15:59 # 0
                                  Маленький модификатор threadid который из id тикета получает resource, т.е. на выходе получаем id ресурса, являющегося тикетом
                                  if ($thread = $modx->getObject('TicketThread', array('id' => $input))) {
                                  	echo $thread->get('resource');
                                  }
                                  Применяем в чанке tpl.Tickets.comments (о котором выше говорили) так
                                  [[+thread:threadid]]
                                  Соответственно отсюда можем уже и pagetitle тикета узнать, к которому относится комментарий и pagetitle деда ну и т.д.
                                  1. Даниил 17 января 2014, 18:13 # 0
                                    Сам задал вопрос, сам нашел ответ, поразительно как действует отсутствие внешних подсказок :DDD Вот только времени очень много лишнего тратится на понимание…
                                    1. Василий Наумкин 17 января 2014, 18:39 # 0
                                      Зато потом экономится.

                                      Иначе так и будешь постоянно ждать подсказок.
                                      1. Даниил 17 января 2014, 18:53 # 0
                                        Да я ж не против никогда учиться, у меня этот веб весь хоть и сидит в печенках уже за несколько лет, но узнавать что-то новое и осваивать всегда приятно и интересно… Тут просто ситуация другая, когда над тобой висит заказчик проекта и орет «быстрее-быстрее» и ты понимаешь, что да, надо бы действительно быстрее, т.к. обещал сайт за 3 недели сделать, а уже полтора месяца «пилишь» его и при этом тратишь 2 дня на то чтоб только понять как вытянуть этот самый pagetitle, вот это расстраивает… Но я не в коем случае никого не виню, тебе, Василий, вообще отдельная и пожизненная благодарность за то, чем мы пользуемся все, и со временем я представляю как проблематично у тебя, тут нас таких вопрошающих не одна сотня, а ты один. Эт я сам дурак что не могу чего-то быстро сообразить, матчасть учить мне больше надо
                                        1. Василий Наумкин 17 января 2014, 19:18 # 0
                                          Ну так нужно оценивать свои силы и не обещать за 3 недели =)
                                          1. Даниил 17 января 2014, 19:22 # 0
                                            А если не обещать, то никто и не закажет, кушать то хочется ;)
                              Добавление новых комментариев отключено.