Давайте напишем поле, с помощью которого можно сделать запись ТК (например, объявление) неактуальной без перехода на страницу редактирования. Сразу скажу, что я не учитель и это не урок.
План такой: автор объявления видит кнопку с произвольным текстом, указываемым в настройках поля. При нажатии на эту кнопку автор выбирает из выпадающего списка причину, по которой объявление больше неактуально. Смена статуса должно происходить без перезагрузки страницы. Начнем.
Любое поле в InstantCMS состоит из 2-х обязательных файлов — файла поля в папке system/fields и файла шаблона в папке templates/шаблон/assets/fields. Начнем со второго.
Наше новое поле будет называться changestatus. По задумке, автор не должен иметь возможности менять статус при добавлении или редактировании записи, поэтому просто создадим пустой файл в папке templates/default/assets/fields с именем changestatus.tpl.php. В этом файле обычно пишется код, который выводит поле в форме. Но нам это не надо, поэтому создаем и закрываем — больше мы к нему возвращаться не будем. Почему файл создан в шаблоне default? Можно создать и в своем шаблоне, но если создать в дефолтном, то он будет работать в любом другом.
Теперь создадим в папке system/fields файл с именем changestatus.php. Здесь будет вся логика. Начнем с объявления класса fieldChangestatus, наследуемого от cmsFormField:
<?php class fieldChangestatus extends cmsFormField { }
Внутри класса напишем свойства поля: название поля, sql, тип в фильтре.
<?php class fieldChangestatus extends cmsFormField { public $title = 'Смена статуса'; public $sql = "VARCHAR(128) NOT NULL DEFAULT 'actual'"; public $filter_type = 'str'; }
Добавим опции. Всего их будет 3: кнопка отключения, текст, если объявление неактуально, и список причин, почему автор делает объявление неактуальным.
<?php class fieldChangestatus extends cmsFormField { public $title = 'Смена статуса'; public $sql = "VARCHAR(128) NOT NULL DEFAULT 'actual'"; public $filter_type = 'str'; public function getOptions(){ return [ new fieldString('btn_off', [ 'title' => 'Текст кнопки отключения', 'default' => 'Неактуально' ]), new fieldText('reason', [ 'title' => 'Причины', 'hint' => 'Оставьте поле пустым, если указывать причину не нужно. По одной причине в каждой строке, например:<br><b>Продал на этом сайте<br>Продал на другом сайте<br>Передумал продавать</b>' ]), new fieldString('not_actual', [ 'title' => 'Текст, если запись неактуальная', 'default' => 'Неактуально' ]) ]; } }
Теперь мы можем добавить наше новое поле в типе контента. Если всё сделано, как написано выше, то при добавлении мы получим нашу форму. Можно ее сразу заполнить и сохранить.
Теперь нам надо вывести поле в записи и списке. За показ в поля в записи отвечает метод parse(), а за вывод в списке записей parseTeaser(), но у нас поле будет выводиться одинаково, поэтому добавим только parse():
public function parse($value){ return $value; }
Должно получиться так:
<?php class fieldChangestatus extends cmsFormField { public $title = 'Смена статуса'; public $sql = "VARCHAR(128) NOT NULL DEFAULT 'actual'"; public $filter_type = 'str'; public function getOptions(){ return [ new fieldString('btn_off', [ 'title' => 'Текст кнопки отключения', 'default' => 'Неактуально' ]), new fieldText('reason', [ 'title' => 'Причины', 'hint' => 'Оставьте поле пустым, если указывать причину не нужно. По одной причине в каждой строке, например:<br><b>Продал на этом сайте<br>Продал на другом сайте<br>Передумал продавать</b>' ]), new fieldString('not_actual', [ 'title' => 'Текст, если запись неактуальная', 'default' => 'Неактуально' ]) ]; } // Ниже добавлен метод parse() public function parse($value){ return $value; } }
Дальше будем работать внутри этого метода. Здесь просто показал, куда его вставлять. Пока мы вернули значение поля. Если посмотреть в записи, то сейчас для всех записей мы будем видеть текст «actual».
А теперь займемся логикой. Во-первых, что мы должны выводить и для кого? Если объявление актуальное, то для автора и админа нам надо показать кнопку, а для всех остальных не показывать ничего. Если неактуальное, то показать какой-то текст. Сначала надо узнать, кто смотрит объявление. Получаем пользователя:
public function parse($value){ $user = cmsUser::getInstance(); return $value; }
Пишем условие, если объявление актуальное, внутри условия пока вернем текст «Актуально»:
public function parse($value){ $user = cmsUser::getInstance(); if ($value == 'actual') { return 'Актуально'; } return ''; }
Смотрим, что получилось:
Теперь добавим внутри этого условия еще одно: если пользователь автор или админ, то покажем текст «Кнопка», а иначе не будем показывать ничего:
if ($value == 'actual') { if ($user->is_admin || $user->id == $this->item['user_id']) { return 'Кнопка'; } return ''; }
Здесь:
- $user->is_admin — пользователь админ,
- $user->id — id пользователя,
- $this->item['user_id'] — id пользователя из записи.
Получается: если (if) админ или id пользователя равно id автора, то...
Вы могли заметить, что я не писал выражение else, потому что return прерывает выполнение скрипта.
Давайте теперь напишем саму кнопку.
public function parse($value){ $user = cmsUser::getInstance(); $cl = $this->item['ctype_name'].'_'.$this->item['id'].'_'.$this->name; // Объявляем переменную $cl if ($value == 'actual') { if ($user->is_admin || $user->id == $this->item['user_id']) { $off_text = $this->options['btn_off'] ? $this->options['btn_off'] : 'Больше неактуально?'; $btn_off = '<div class="changestatus_btn changestatus_off changestatus_off_'.$cl.'">'.$off_text.'</div>'; return $btn_off; } return ''; } return ''; }
Здесь мы добавили div с тремя классами:
- changestatus_btn — класс для всех кнопок (будет же еще кнопка восстановления), скоро добавим для этого класса стили,
- changestatus_off — отдельный класс для кнопки отключения,
- changestatus_off_'.$cl.' — здесь мы сгенерировали класс для кнопки, соответствующей определенной записи.
Переменную $cl я объявил чуть выше. Если так не сделать, то потом, когда мы добавим javascript для этой кнопки, то в списке записей этот скрипт будет применяться только к кнопке первого объявления. На выходе этот класс будет иметь вид примерно такой: changestatus_off_board_14_status.
Перед этим мы получили объявили переменную $off_text, которая содержит текст кнопки из опций поля. Если текст в опциях не указан, то выводится текст «Больше неактуально?».
Вот такая получилась кнопка:
На кнопку не очень похоже, да? Добавим стилей. Для этого в папке templates/default/css создадим файл field-changestatus.css и подключим его внутри условия, где определяли пользователя:
if ($user->is_admin || $user->id == $this->item['user_id']) { cmsTemplate::getInstance()->addCSS('templates/default/css/field-changestatus.css');
Если хотите возразить, сразу скажу, что мне такой способ подключения больше нравится.
Добавим стили для поля и для кнопки в файле field-changestatus.css:
.changestatus_btn{ display: inline-block; vertical-align: top; color: #fff; font-size: 15px; line-height: 18px; padding: 7px 10px; border-radius: 3px; cursor: pointer; transition: all ease .15s; } .changestatus_off{ background: #d00000; } .changestatus_off:hover{ background: #e50000; }
Так вроде лучше.
Но это у нас простой div, а не кнопка. Давайте ее оживим. Допишем ей атрибут onclick:
$btn_off = '<div class="changestatus_btn changestatus_off changestatus_off_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>';
При клике на эту кнопку будет выполняться функция javascript statusOff, ей мы передаем данные, которые содержатся в переменной $data — ее мы объявим ниже. Это имя типа контента, id записи и имя поля.
public function parse($value){ $user = cmsUser::getInstance(); $cl = $this->item['ctype_name'].'_'.$this->item['id'].'_'.$this->name; $data = '\''.$this->item['ctype_name'].'\', '.$this->item['id'].', \''.$this->name.'\''; // объявляем переменную $data if ($value == 'actual') { if ($user->is_admin || $user->id == $this->item['user_id']) { cmsTemplate::getInstance()->addCSS('templates/default/css/field-changestatus.css'); $off_text = $this->options['btn_off'] ? $this->options['btn_off'] : 'Больше неактуально?'; $btn_off = '<div class="changestatus_btn changestatus_off changestatus_off_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>'; return $btn_off; } return ''; } return ''; }
Но у нас нет такой функции. Давайте напишем. В папке templates/default/js создалим файл field-changestatus.js и подключим его по аналогии с подключением css-файла:
if ($user->is_admin || $user->id == $this->item['user_id']) { cmsTemplate::getInstance()->addCSS('templates/default/css/field-changestatus.css'); cmsTemplate::getInstance()->addJS('templates/default/js/field-changestatus.js'); // Подключаем js-файл
В самом файле напишем нашу функцию, в которой отправим запрос ajax к экшену changestatus_off контроллера content с нашими данными. У нас еще нет такого экшена, сейчас создадим. В папке system/controllers/content/actions создадим файл changestatus_off.php. В нем объявим класс actionContentChangestatusOff, наследуемый от системного класса cmsAction. Внутри класса добавим метод run(), в который прилетают наши данные из кнопки:
<?php class actionContentChangestatusOff extends cmsAction { public function run($ctype_name, $item_id, $field_name) { } }
Нам нужно вернуть массив с данными, которые мы сейчас получим. Поэтому сразу объявим переменную $result, которая будет массивом.
Может быть так, что у автора была открыта вкладка долгое время. За это время могло произойти многое. Например, админ сменил владельца. Или пользователь разлогинился в другой вкладке. Давайте сначала проверим, что пользователь имеет право менять статус записи. Если нет, то вернем ошибку, а если да — то будем продолжать. Думаю, стоит разделить ошибку на две части — если пользователь разлогинился и если не имеет прав.
<?php class actionContentChangestatusOff extends cmsAction { public function run($ctype_name, $item_id, $field_name) { $result = []; $user = cmsUser::getInstance(); // Получаем пользователя $item = $this->model->getItemById('con_'.$ctype_name, $item_id); // Получаем запись if (!$user->is_admin && $user->id != $item['user_id']) { // Если это не админ и не автор $result['action'] = 'error'; $result['type'] = 'forbidden'; $result['text'] = 'Вам запрещено это делать!'; if (!$user->is_logged) { // Если разлогинился $result['type'] = 'guest'; $result['text'] = 'Вы вышли из аккаунта!'; } $this->halt(); } $result['action'] = 'ok'; $result['text'] = ''; $this->halt(); } }
Добавим в js-функции условие: если получили ошибку, то выводим текст ошибки:
function statusOff(ctype_name, item_id, field_name) { $.post('/content/changestatus_off/' + ctype_name + '/' + item_id + '/' + field_name).done(function(response) { result = $.parseJSON(response); if (result.action === 'error') { $('.changestatus_off_' + ctype_name + '_' + item_id + '_' + field_name).removeAttr('onclick').addClass('error_btn').html(result.text); } }); }
Разберем строку
$('.changestatus_off_' + ctype_name + '_' + item_id + '_' + field_name).removeAttr('onclick').addClass('error_btn').html(result.text);
В ней мы вот, что делаем с кнопкой:
- удаляем атрибут onclick — removeAttr('onclick')
- добавляем класс error_btn — addClass('error_btn')
- заменяем текст кнопки на сообщение об ошибке.
Только что мы добавили для кнопки новый класс. Давайте добавим для этого класса немного стилей в файле css:
.changestatus_btn.error_btn{ cursor: text!important; background: transparent!important; color: #e50000!important; padding: 7px 0; }
С ошибками разобрались. Теперь нужно что-то сделать, если пользователю можно продолжать. Давайте ему предложим выбрать причину. А если причина не указана или если статус меняет админ, то хотя бы предупреждение, что объявление будет скрыто. Опять работаем в файле system/controllers/content/actions/changestatus_off.php. Получим список причин из опций поля:
<?php class actionContentChangestatusOff extends cmsAction { public function run($ctype_name, $item_id, $field_name) { $result = []; $user = cmsUser::getInstance(); $item = $this->model->getItemById('con_'.$ctype_name, $item_id); if (!$user->is_admin && $user->id != $item['user_id']) { $result['action'] = 'error'; $result['text'] = 'Вам запрещено это делать!'; if (!$user->is_logged) { $result['text'] = 'Вы вышли из аккаунта!'; } $this->halt(); } $result['action'] = 'ok'; $field = $this->model->getItemByField('con_'.$ctype_name.'_fields', 'name', $field_name); // Получаем поле $field_options = $this->model->yamlToArray($field['options']); // Получаем опции поля и конвертируем их и формата yaml в обычный массив $reasons = $field_options['reason'] ? preg_split('/\\r\\n?|\\n/', $field_options['reason']) : ''; // Получаем массив из поля с причинами $reasons_list = []; if ($reasons) { foreach ($reasons as $key => $reason) { $reasons_list[] = '<option value="'.$key.'">'.$reason.'</option>'; // Собираем новый массив, где значения становятся элементами выпадающего списка } } $result['text'] = 'Материал будет помечен, как неактуальный. Вы уверены, что хотите продолжить?'; if ($reasons_list && !$user->is_admin) { $result['text'] = 'Укажите, пожалуйста, причину, по которой материал больше неактуален.'.$list; } $this->halt(); } }
Мы получили текст сообщения. Но его теперь надо где-то вывести. Напишем всплывающее окно. В этом же файле немного изменим наш код:
$result_text = '<p class="changestatus_text">Материал будет помечен, как неактуальный. Вы уверены, что хотите продолжить?</p>'; if ($reasons_list && !$user->is_admin) { $result_text = '<p class="changestatus_text">Укажите, пожалуйста, причину, по которой материал больше неактуален.</p>'.$list; } $ctype = $this->model->getItemByField('content_types', 'name', $ctype_name); $ctype_labels = $this->model->yamlToArray($ctype['labels']); $label = '<p class="changestatus_label">Изменение статуса '.$ctype_labels['two'].'</p> <h3>«'.$item['title'].'»</h3>'; $result['text'] = '<div class="changestatus_modal changestatus_modal_'.$ctype_name.'_'.$item_id.'_'.$field_name.'"><div class="changestatus_modal_body">'.$label.$result_text.'</div></div>';
И в js-файле вызовем это всплывающее окно:
if (result.action === 'ok') { $('body').append(result.text); }
Добавим стилей:
.changestatus_modal{ width: 100%; height: 100%; position: fixed; z-index: 100; background: rgba(0,0,0,.7); animation: changestatus_show ease-out .3s 1; } .changestatus_modal_body{ width: 540px; max-width: calc(100% - 40px); padding:35px 30px; border-radius:3px; position: absolute; background: #fff; left: 50%; top: 50%; transform: translate(-50%, -50%); } .changestatus_modal .changestatus_label{ margin: 0; line-height: 115%; font-weight: bold; } .changestatus_modal h3{ margin-bottom: 20px; line-height: 115%; } .changestatus_modal select{ width: 100%; height: 36px; border: 1px solid #a1a1a1; border-radius: 3px; margin-bottom: 15px; } @keyframes changestatus_show{ from{ opacity:0; } to{ opacity:1; } }
Теперь, если всё нормально, автор получит такое модальное окошко, если в опциях указали причины:
А если пользователь админ или автор, но причин для выбора нет, то такое:
Чего-то не хватает, да? Правильно, кнопок. Во-первых, нужна кнопка отмены, чтобы закрыть это окно. Во-вторых, нужна кнопка для продолжения. Напишем их. Для этого немного изменим код в файле system/controllers/content/actions/changestatus_off.php:
$btn_continue = '<div class="changestatus_btn changestatus_continue" onclick="statusOffContinue(\''.$ctype_name.'\', '.$item_id.', \''.$field_name.'\')">Продолжить</div>'; $btn_cancel = '<div class="changestatus_btn changestatus_cancel" onclick="statusCancel(\''.$ctype_name.'\', '.$item_id.', \''.$field_name.'\')">Отменить</div>'; $buttons = '<div class="changestatus_btns">'.$btn_continue.$btn_cancel.'</div>'; $result['text'] = '<div class="changestatus_modal changestatus_modal_'.$ctype_name.'_'.$item_id.'_'.$field_name.'"><div class="changestatus_modal_body">'.$label.$result_text.$buttons.'</div></div>';
И добавим стилей для кнопок:
.changestatus_modal .changestatus_btns{ margin-top: 10px; display: inline-block; vertical-align: top; } .changestatus_continue{ background: #d00000; margin-right:10px; } .changestatus_continue:hover{ background: #e50000; } .changestatus_cancel{ background: #b4a8ac; } .changestatus_cancel:hover{ background: #a89da0; }
Получается вот так:
Оживим кнопки. Начнем с кнопки отмены — она проще. Пишем в js-файл новую функцию:
function statusCancel(ctype_name, item_id, field_name) { $('.changestatus_modal_' + ctype_name + '_' + item_id + '_' + field_name).remove(); return; }
Здесь всё просто — при нажатии на кнопку мы удаляем модальное окно. А вот следующая функция будет посложнее. В ней мы отправим аякс-запрос к новому экшену. Но передадим не только те данные, которые мы передаем по кругу с самого начала, а еще и выбранный пункт из выпадающего списка. Итак, пишем в js-файле нашу функцию:
function statusOffContinue(ctype_name, item_id, field_name) { var reasons = $('.changestatus_modal_' + ctype_name + '_' + item_id + '_' + field_name + ' select'); if (reasons.length) { var reason = reasons.val(); } else { var reason = 'x'; } $.post('/content/changestatus_off_confirm/' + ctype_name + '/' + item_id + '/' + field_name + '/' + reason).done(function(response) { result = $.parseJSON(response); }); }
Здесь мы проверили, существует ли наш выпадающий список. Если да, то reason — это ключ выбранной причины, а если нет — передаем «х».
Теперь нужно в папке system/controllers/content/actions создать файл changestatus_off_continue.php. В нем точно так же, как и в предыдущем экшене, начинаем с этого:
<?php class actionContentChangestatusOff extends cmsAction { public function run($ctype_name, $item_id, $field_name, $reason_id) { } }
Думаю, стоит опять проверить, может ли пользователь менять статус. Ведь пока была открыта форма, он мог, например, разлогиниться в соседней вкладке. Поэтому сначала просто скопируем часть кода из файла system/controllers/content/actions/changestatus_off.php:
<?php class actionContentChangestatusOff extends cmsAction { public function run($ctype_name, $item_id, $field_name, $reason_id) { $result = []; $user = cmsUser::getInstance(); $item = $this->model->getItemById('con_'.$ctype_name, $item_id); if (!$user->is_admin && $user->id != $item['user_id']) { $result['action'] = 'error'; $result['text'] = 'Вам запрещено это делать!'; if (!$user->is_logged) { $result['text'] = 'Вы вышли из аккаунта!'; } $this->halt(); } } }
И в javascript-функции напишем условие для вывода ошибки:
if (result.action === 'error') { statusCancel(ctype_name, item_id, field_name); $('.changestatus_off_' + ctype_name + '_' + item_id + '_' + field_name).removeAttr('onclick').addClass('error_btn').html(result.text); }
Обратите внимание, что перед тем, как вывести сообщение об ошибке, мы вызвали функцию statusCancel(), которая удаляет со страницы модальное окно с формой.
Продолжаем писать внутри метода run() в файле changestatus_off_continue.php. Сначала напишем, что у нас нет ошибки, а также текст, который выведем после завершения:
$result['action'] = 'ok'; $result['text'] = 'Неактуально';
Дальше разберемся с причинами:
$reason = 'Причина не указана'; // Объявим переменную $reason, которая по-умолчанию будет принимать значение "Причина не указана" if ($reason_id != 'x') { // Если у нас в форме был выпадающий список и значение выбрано $field = $this->model->getItemByField('con_'.$ctype_name.'_fields', 'name', $field_name); $field_options = $this->model->yamlToArray($field['options']); if ($reasons) { $reason = $reasons[$reason_id]; // $reason принимает текст выбранного элемента в списке } } if ($user->is_admin) { // А вот такая будет причина, если статус сменил админ $reason = 'Статус изменен решением администрации сайта'; }
Ну вот и всё, давайте теперь обновим наше поле в БД:
$this->model->update('con_'.$ctype_name, $item_id, [$field_name => $reason]);
И нужно остановить всё и выйти из скрипта:
$this->halt();
В этом файле всё. Теперь в js напишем, что нужно сделать:
if (result.action === 'ok') { statusCancel(ctype_name, item_id, field_name); $('.changestatus_off_' + ctype_name + '_' + item_id + '_' + field_name).removeAttr('onclick').addClass('error_btn').html(result.text); }
Здесь мы удалили форму и в кнопку написали текст «Неактуально». Если присмотреться, то мы увидим, что с ошибкой и без нее мы выполняем одинаковые действия. Поэтому мы уберем все условия и приведем код функции к такому виду:
function statusOffContinue(ctype_name, item_id, field_name) { var reasons = $('.changestatus_modal_' + ctype_name + '_' + item_id + '_' + field_name + ' select'); if (reasons.length) { var reason = reasons.val(); } else { var reason = 'x'; } $.post('/content/changestatus_off_continue/' + ctype_name + '/' + item_id + '/' + field_name + '/' + reason).done(function(response) { result = $.parseJSON(response); statusCancel(ctype_name, item_id, field_name); $('.changestatus_off_' + ctype_name + '_' + item_id + '_' + field_name).removeAttr('onclick').addClass('error_btn').html(result.text); }); }
Теперь вернемся к файлу поля system/fields/shangestatus.php. Сейчас нас интересует метод parse():
public function parse($value){ $user = cmsUser::getInstance(); $cl = $this->item['ctype_name'].'_'.$this->item['id'].'_'.$this->name; $data = '\''.$this->item['ctype_name'].'\', '.$this->item['id'].', \''.$this->name.'\''; if ($value == 'actual') { if ($user->is_admin || $user->id == $this->item['user_id']) { cmsTemplate::getInstance()->addCSS('templates/default/css/field-changestatus.css'); cmsTemplate::getInstance()->addJS('templates/default/js/field-changestatus.js'); $off_text = $this->options['btn_off'] ? $this->options['btn_off'] : 'Больше неактуально?'; $btn_off = '<div class="changestatus_btn changestatus_off changestatus_off_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>'; return $btn_off; } return ''; } return ''; }
Смотрите, мы написали условие, что если значение поля равно «actual», то показываем кнопку, если пользователь админ или автор. Но мы ничего не выводим, если значение поля другое. Давайте это исправим. У нас будет выводиться надпись «Неактуально» или текст из опций поля. А рядом каким-то образом выведем причину. Начнем с надписи. В конце метода последняя строка содержит return ''; Заменим ее на это:
return '<div class="changestatus_off_text">'.($this->options['not_actual'] ? $this->options['not_actual'] : 'Неактуально').'</div>';
И добавим стилей:
.changestatus_off_text{ display: inline-block; vertical-align: top; color: #fff; font-size: 15px; line-height: 18px; padding: 7px 10px; background: #a89da0; font-weight: bold; border-radius:3px; cursor: help; }
Но раньше мы подключали css и js не для всех и не всегда. Эти файлы подключены только если значение поля «actual» и если пользователь автор или админ. Давайте переместим подключение файлов выше, но добавим условие, чтобы они не были подключены тогда, когда это не нужно. После этого метод parse будет выглядеть так:
public function parse($value){ $user = cmsUser::getInstance(); $cl = $this->item['ctype_name'].'_'.$this->item['id'].'_'.$this->name; $data = '\''.$this->item['ctype_name'].'\', '.$this->item['id'].', \''.$this->name.'\''; // Подключаем css и js файлы только если пользователь автор или админ или если значение не равно "actual" if ($value != 'actual' || $user->is_admin || $user->id == $this->item['user_id']) { cmsTemplate::getInstance()->addCSS('templates/default/css/field-changestatus.css'); cmsTemplate::getInstance()->addJS('templates/default/js/field-changestatus.js'); } if ($value == 'actual') { if ($user->is_admin || $user->id == $this->item['user_id']) { $off_text = $this->options['btn_off'] ? $this->options['btn_off'] : 'Больше неактуально?'; $btn_off = '<div class="changestatus_btn changestatus_off changestatus_off_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>'; return $btn_off; } return ''; } return '<div class="changestatus_off_text">'.($this->options['not_actual'] ? $this->options['not_actual'] : 'Не актуально').'</div>'; }
Получилось так:
Теперь сюда же добавим причину:
return '<div class="changestatus_off_text" title="'.$value.'">'.($this->options['not_actual'] ? $this->options['not_actual'] : 'Неактуально').'</div>';
Здесь мы добавили описание причины в title блока. Немного приукрасим. Добавим в js-файл такой скрипт:
function tooltip(target_items, name) { $(target_items).each(function(i) { $('body').append('<div class="' + name + '" id="' + name + i + '">' + $(this).attr('title') + '</div>'); var tooltip = $('#' + name + i); $(this).removeAttr('title').mouseover(function() { tooltip.css({opacity:0.8, display:"none"}).fadeIn(400); }).mousemove(function(kmouse) { tooltip.css({left:kmouse.pageX+15, top:kmouse.pageY+15}); }).mouseout(function() { tooltip.fadeOut(250); }); }); } $(document).ready(function(){ tooltip('.changestatus_off_text', 'tooltip'); });
Скажу честно, я его нашел в интернете лет 10 назад. Объяснять, как он работает, не буду. Скажу только, что мы с помощью этого скрипта преобразуем title блока во всплывающую подсказу (tooltip). Добавим стилей:
.tooltip{ position: absolute; z-index: 999; left: -9999px; background: rgba(0,0,0,.9); color: #f0f0f0; padding: 5px 7px; border-radius: 3px; width: 200px; font-size:13px; }
Теперь это выглядит так:
С этим разобрались.
Но если мы добавили возможность делать запись неактуальной, то как же ее вернуть? А что, если она автоматически будет становиться актуальной после редактирования? Так и запишем. В файле поля system/fields/shangestatus.php напишем новый метод store(), который отвечает за то, что сохраняется в поле при добавлении или редактировании записи. Внутри метода вернем значение 'actual':
public function store($value, $is_submitted, $old_value=null) { return 'actual'; }
Теперь разберемся с фильтром. Так как поле не выводится при добавлении или редактировании записи, то ему можно дать заголовок «Только актуальные».
И напишем два метода для фильтра.
public function applyFilter($model, $value) { return $model->filterEqual($this->name, 'actual'); }
В этом методе мы отфильтровали все записи по значению «actual». Добавим еще один метод — чтобы в форме фильтра показать переключатель:
public function getFilterInput($value = false) { cmsTemplate::getInstance()->addBottom('<script>$(document).ready(function(){$("#f_'.$this->id.' .input-checkbox").on("change", function(){$("#f_'.$this->id.'").toggle();});var state_'.$this->id.' = $("#f_'.$this->id.' .input-checkbox").prop("checked");if(state_'.$this->id.'){$("#f_'.$this->id.'").show();}});</script>'); return '<div class="custom-control custom-switch">'.html_checkbox($this->name, $value, 'actual', ['id'=>$this->id,'class'=>'custom-control-input']).'<label class="custom-control-label" for="'.$this->id.'">'.$this->title.'</label></div>'; }
Ну вот, кажется, и всё. Но наверняка вы захотите использовать поле в фильтрах наборов. Например, показать только актуальные или только неактуальные. Для этого в наборе добавьте фильтр по полю. Если нужно показать только актуальные, то "= actual":
А если неактуальные, то "не содержит actual".
Вот и всё. Крутое поле готово. Осталось упаковать его в архив. Для этого создаем папку changestatus, в ней будет вся наша кухня. Внутри нее создаем дерево папок и файлов:
package
> system
>> fields
>>> changestatus.php
>> system
>>> controllers
>>>> content
>>>>> actions
>>>>>> changestatus_off.php
>>>>>> changestatus_off_continue.php
> templates
>> default
>>> assets
>>>> fields
>>>>> changestatus.tpl.php
>>> css
>>>> field-changestatus.css
>>> js
>>>> field-changestatus.js
Содержимое файлов вы знаете, где брать.
Рядом с папкой package создаем файл manifest.ru.ini с таким содрежимым (или другим):
[info] title = "Поле «Смена статуса»" [version] major = "1" minor = "0" build = "0" date = "20220715" [depends] core = "2.12.2" [author] name = "Нифигассе о-го-гошеньки" url = "https://nifigasse.ru" [description] text[] = "Поле позволяет сделать запись не акутальной без перезагрузки страницы" text[] = "Распространяется БЕСПЛАТНО и без каких-либо ограничений." text[] = "Код полностью открыт. Автор не несет никакой ответственности в связи с использованием вами поля на своих сайтах. Поддержка не оказывается."
Теперь в папке changestatus должно быть два элемента — папка package с файлами и файл манифеста. Выделяем эти два элемента и упаковываем в zip-архив.
Вот такие вот дела. Вроде закончил.
Скачать готовое поле можно в каталоге дополнений.
Ну и домашнее задание)) Можете добавить опции для замены всех текстов, прописанных прямо в файлах. Мне можете не отчитываться — проверять не буду.
А в следующий раз, когда мне будет скучно и я решу сделать нечто подобное, мы с вами напишем небольшой компонент, в котором установим срок публикации записи, и вместо удаления или скрытия, как это сделано в типах контента, будем помечать, что запись перестала быть актуальной. Только я не знаю, когда это будет))
=====================================================
Приняв во внимание рекомендации Fuze, немного изменим наше поле.
Во-первых, в самом начале метода parse() в файле system/fields/changestatus.php добавим такую такое условие:
if (!isset($this->item) || !$this->item['id'] || !$this->item['ctype_name'] || !$this->item['user_id']) { return ''; }
Здесь мы проверили, существует ли массив $this->item и есть ли в нем значения, которые используются данные. Если чего-то нет, то не выводим поле. Я специально сделал не так, как предложил Fuze, чтобы послушать критику по этому поводу. Как говорится, век живи — век учись))
Дальше нам нужно поработать над безопасностью. Сейчас в кнопке мы передаем имена типа контента и поля. А сделаем так: будем передавать id типа контента и поля, в экшенах будем проверять, что получили число. Ну и еще пару проверок в самом начале экшенов.
Делаем так. В файле system/fields/changestatus.php содержится код кнопки, в котором передается значение переменной $data в javascript-функцию statusOff(). Вот этот код:
if (!isset($this->item) || !$this->item['id'] || !$this->item['ctype_name'] || !$this->item['user_id']) { return ''; }
Переменная $data объявлена чуть выше:
$data = '\''.$this->item['ctype_name'].'\', '.$this->item['id'].', \''.$this->field_id.'\'';
Изменим значение переменной на такое:
$data = $this->ctype_id.', '.$this->item['id'].', '.$this->field_id;
Точно так же сделаем и с переменной $cl:
$cl = $this->ctype_id.'_'.$this->item['id'].'_'.$this->field_id;
Но теперь у нас в коде вообще нигде нет $this->item['ctype_name']. Значит можно не проверять, существует ли это значение. Поэтому, в том условии, которое мы только что прописали в начале метода parse(), можно убрать проверку $this->item['ctype_name']:
return ''; }
Теперь открываем файл templates/default/js/field-changestatus.js. Туда пришли наши данные. Можно, в принципе, ничего не менять, всё и так будет работать. Но лучше изменим имена. А то через полгода вернемся к доработке этого поля и будем долго вникать, что за ctype_name, если приходит на самом деле число. В файле делаем замену всех вхождений ctype_name на ctype_id и field_name на field_id. Сохраняем и закрываем файл — дальше будем работать в экшенах.
Начнем с system/controllers/content/actions/changestatus_off.php.
Сразу изменяем названия переменных, которые приходят в этот экшен:
public function run($ctype_id, $item_id, $field_id) {
Теперь проверяем, что получены числа, а не что-то еще:
if ((int)$ctype_id <= 0 || (int)$item_id <= 0 || (int)$field_id <= 0) { $result['action'] = 'error'; $result['text'] = 'Ай-я-яй, как не стыдно?'; $this->halt(); }
Теперь если взломщик подсунет что-то другое, кроме числа, то мы его поругаем. Теперь давайте проверим, что полученные данные настоящие. Посмотрим, есть ли у нас тип контента с полученным id:
$ctype = $this->model->getContentType($ctype_id); if (!$ctype) { $result['action'] = 'error'; $result['text'] = 'Плохо, очень плохо!'; $this->halt(); }
Если есть, то проверим, есть ли у нас поле, с полученным id. И если есть, то наш ли у него тип:
$ctype_name = $ctype['name']; $field = $this->model->getItemById('con_'.$ctype_name.'_fields', $field_id); if (!$field || $field['type'] != 'changestatus') { $result['action'] = 'error'; $result['text'] = 'А ну, марш в угол!'; $this->halt(); }
И объявим переменную $field_name, так как она у нас используется позже:
$field_name = $field['name'];
Дальше у нас идет такой код:
$result = []; $user = cmsUser::getInstance(); $item = $this->model->getItemById('con_'.$ctype_name, $item_id);
Переместим $item вверх:
$item = $this->model->getItemById('con_'.$ctype_name, $item_id); $result = []; $user = cmsUser::getInstance();
И проверим, существует ли запись:
if (!$item) { $result['action'] = 'error'; $result['text'] = 'Ну всё, приплыли...'; $this->halt(); }
Чуть не забыл. Нужно в внизу этого файла немного поменять кнопки и классы:
$btn_continue = '<div class="changestatus_btn changestatus_continue" onclick="statusOffContinue(\''.$ctype_id.'\', '.$item_id.', \''.$field_id.'\')">Продолжить</div>';
$btn_cancel = '<div class="changestatus_btn changestatus_cancel" onclick="statusCancel(\''.$ctype_id.'\', '.$item_id.', \''.$field_id.'\')">Отменить</div>';
$result['text'] = '<div class="changestatus_modal changestatus_modal_'.$ctype_id.'_'.$item_id.'_'.$field_id.'"><div class="changestatus_modal_body">'.$label.$result_text.$buttons.'</div></div>';
В этом файле всё, перейдем к system/controllers/content/actions/changestatus_off_continue.php. Начало там такое же, поэтому сделаем всё то же самое:
<?php class actionContentChangestatusOffContinue extends cmsAction { public function run($ctype_id, $item_id, $field_id, $reason_id) { if ((int)$ctype_id <= 0 || (int)$item_id <= 0 || (int)$field_id <= 0) { $result['action'] = 'error'; $result['text'] = 'Ай-я-яй, как не стыдно?'; $this->halt(); } $ctype = $this->model->getContentType($ctype_id); if (!$ctype) { $result['action'] = 'error'; $result['text'] = 'Плохо, очень плохо!'; $this->halt(); } $ctype_name = $ctype['name']; $field = $this->model->getItemById('con_'.$ctype_name.'_fields', $field_id); if (!$field || $field['type'] != 'changestatus') { $result['action'] = 'error'; $result['text'] = 'А ну, марш в угол!'; $this->halt(); } $field_name = $field['name'];
Но в этот экшен приходит еще значение выпадающего списка с причиной. Проверим и его. Находим этот код:
$reason = 'Причина не указана'; if ($reason_id != 'x') { $field = $this->model->getItemByField('con_'.$ctype_name.'_fields', 'name', $field_name); $field_options = $this->model->yamlToArray($field['options']); if ($reasons) { $reason = $reasons[$reason_id]; } }
И немного его исправляем в этом месте:
if ($reasons) { $reason = $reasons[$reason_id]; }
Проверим, существует ли значение массива с полученным id:
$reason = $reasons[$reason_id]; }
Вот и всё. Опасность, кажется, миновала))
Реклама #
Vlad 2 года назад #
Спасибо за подробное объяснение/старания.
Читать подробный, с объяснениями блог — одно удовольствие!
fincheck 2 года назад #
Ого, с картинками, кодом и пояснениями! От души, это я скоро и кодить научусь?!
Demetre 2 года назад #
Соглашусь читать блоги с детальным разбором всегда очень увлекательнл
Happy 2 года назад #
Спасибо большое , хорошее поле и хороший подход, не останавливайтесь )
Loadырь 2 года назад #
👍 Наконец-то, хоть кто-то объяснил и показал, как это надо делать. Всё оказывается читаемо, легко и просто. Вообще не понимаю, за что тут люди дерут по 500 р. и выше 😂
Fuze 2 года назад #
Безусловный респект за такую большую работу. А так как пост всё же больше познавательный, позволю себе ремарки.
$this->item может не быть. В методе parse стоит проверять на непустой массив. В самом начале можно добавить
Лучше для этого создать директорию со своим контроллером и там уже создавать экшены. Контроллер не обязательно добавлять в базу данных.
Это необходимо делать в самом начале экшена.
По структуре кода — лучше делать вот так. Я бы в экшене сначала проверил авторизацию, все входящие переменные, получил $ctype по $ctype_name — проверил что тип контента есть, потом получил запись — проверил что она есть, потом поле и так далее. Самое главное — все входящие переменные — проверять, получаемые записи — проверять на наличие, прежде чем с ними что-то делать дальше.
В методе getFilterInput лучше всё делать в шаблоне.
Вроде бы всё)
Ну и я всё же должен предупредить, что пользоваться данным полем в продакшене на данный момент небезопасно. Но как пример подхода в разработке полей отличнейший док.
p.s. проверьте ваши другие разработки по ремаркам выше.
&$!#% 2 года назад #
Спасибо за отклик!)) Вас понял. Пока жалоб не было, но всё-таки лучше всё перепроверить. Вот прямо сейчас и займусь))
Хотя некоторые моменты, конечно, не совсем понятны)) Например, в каком случае у записи типа контента может не быть $this->item. И если его нет, то нет и $this->item['id'], чтобы проверить, не пустой ли он.
Сначала, как мы передадим в ctype_name произвольное имя? Ну да, можно подсунуть. Но дальше мы проверим, можно ли юзеру выполнять действие. Если это другой тип контента или другая запись, не принадлежащая юзеру, то он просто получит ошибку. А если вообще левое имя, то 503 ошибку, что таблица не найдена. Ну так это его проблема. Что «хакер» может сделать с этой ошибкой?
И да, в чем всё-таки опасность использования поля в текущем виде?
Я не спорю, просто интересно, почему так)) Чтобы что-то делать, лучше понимать, что делаешь. Тот человек, который меня всему научил, уже давно не на связи.
Fuze 2 года назад #
Навскидку не скажу, но такое может быть, с вами же как-то давно встречали такое.
Проверка empty не даёт нотис, поэтому быстрее и удобнее проверить сразу наличие ключа id.
У вас получение $item идёт до проверок.
Тип контента, запись, поле — всего этого по переданным параметрам может просто не быть в базе. В вашем коде, как я уже писал, пойдут нотисы, если что-то пойдёт не так. Всё нужно проверять, когда работаем с входными данными.
Зачем вообще давать возможность перебирать таблицы на вашем сервере? Мы можем указать существующую таблицу, но не таблицу записи и как минимум в ответе не будет нужных ячеек. Мы можем передать не просто имя типа контента, а например
Вариантов много. И куда может привести SQL инъекция зависит от сообразительности атакующего.
Так я же написал все ошибки) Если бы это был просто грязный код — это одно, но по моему мнению указанные недочёты критичны.
Если интересно, я могу на досуге переделать это поле, снабдив комментариями.
&$!#% 2 года назад #
Да, точно, было что-то такое с несуществующим итемом. Только не могу вспомнить, что именно. Где-то на гитхабе вроде я писал, надо поискать.
Суть вроде понял. Но был бы не против почитать Ваши комментарии в переделанном поле))
Loadырь 2 года назад #
Надо переделать и не просто с комментариями, а с подробным объяснением почему именно так в данном случае, а не иначе. Это будет «весомый пендаль» в сторону саморазвития сторонних разработчиков.
KoRn 2 года назад #
Плюс в карму)
&$!#% 2 года назад #
Приняв во внимание рекомендации Fuze, немного изменим наше поле.
Во-первых, в самом начале метода parse() в файле system/fields/changestatus.php добавим такую такое условие:
Здесь мы проверили, существует ли массив $this->item и есть ли в нем значения, которые используются данные. Если чего-то нет, то не выводим поле. Я специально сделал не так, как предложил Fuze, чтобы послушать критику по этому поводу. Как говорится, век живи — век учись))
Дальше нам нужно поработать над безопасностью. Сейчас в кнопке мы передаем имена типа контента и поля. А сделаем так: будем передавать id типа контента и поля, в экшенах будем проверять, что получили число. Ну и еще пару проверок в самом начале экшенов.
Делаем так. В файле system/fields/changestatus.php содержится код кнопки, в котором передается значение переменной $data в javascript-функцию statusOff(). Вот этот код:
Переменная $data объявлена чуть выше:
Изменим значение переменной на такое:
Точно так же сделаем и с переменной $cl:
Но теперь у нас в коде вообще нигде нет $this->item['ctype_name']. Значит можно не проверять, существует ли это значение. Поэтому, в том условии, которое мы только что прописали в начале метода parse(), можно убрать проверку $this->item['ctype_name']:
Теперь открываем файл templates/default/js/field-changestatus.js. Туда пришли наши данные. Можно, в принципе, ничего не менять, всё и так будет работать. Но лучше изменим имена. А то через полгода вернемся к доработке этого поля и будем долго вникать, что за ctype_name, если приходит на самом деле число. В файле делаем замену всех вхождений ctype_name на ctype_id и field_name на field_id. Сохраняем и закрываем файл — дальше будем работать в экшенах.
Начнем с system/controllers/content/actions/changestatus_off.php.
Сразу изменяем названия переменных, которые приходят в этот экшен:
Теперь проверяем, что получены числа, а не что-то еще:
Теперь если взломщик подсунет что-то другое, кроме числа, то мы его поругаем. Теперь давайте проверим, что полученные данные настоящие. Посмотрим, есть ли у нас тип контента с полученным id:
Если есть, то проверим, есть ли у нас поле, с полученным id. И если есть, то наш ли у него тип:
И объявим переменную $field_name, так как она у нас используется позже:
Дальше у нас идет такой код:
Переместим $item вверх:
И проверим, существует ли запись:
Чуть не забыл. Нужно в внизу этого файла немного поменять кнопки и классы:
В этом файле всё, перейдем к system/controllers/content/actions/changestatus_off_continue.php.
&$!#% 2 года назад #
Начало там такое же, поэтому сделаем всё то же самое:
Но в этот экшен приходит еще значение выпадающего списка с причиной. Проверим и его. Находим этот код:
И немного его исправляем в этом месте:
Проверим, существует ли значение массива с полученным id:
Вот и всё. Кажется, опасность миновала)) Добавил в каталог дополнений исправленную версию.
Loadырь 2 года назад #
Ну так слушайте ))):
Ваш новый код:
Не решает проблему с нотисами. $this->item может сушествовать, но может не содержать id и прочих элементов массива. Как говорил Fuze надо делать так
empty не выдает нотисов при отсутсвии элементов массива, но при этом вернет false если $this->item не существует. То есть вы одним условием проверяете сразу оба варианта.
$this->item['user_id'] можно не проверять, так как оно всегда идет в $this->item, если есть $this->item['id'].
$this->item['ctype_name'] в последних версиях движка тоже нет смысла проверять (оно есть, если есть $this->item['id'] даже в профилях пользователей), но вы его впоследствии отбросили.
То же самое тут
Достаточно так
Дублирующийся код желательно выносить в отдельный метод/файл
&$!#% 2 года назад #
Спасибо за разъяснение, очень полезно!
Значит, вносим последние (но это не точно) правки в наше поле))
В файле system/fields/changestatus.php в методе parse() заменяем
на это:
В файле system/controllers/content/actions/changestatus_off_continue.php заменяем
на это:
Теперь мы понимаем, как надо делать, и как делать не стоит.
А если есть еще замечания и/или возражения, с удовольствием принимаю любую конструктивную критику. Спасибо!))
Fuze 2 года назад #
Как и обещал, вот разбор одного из экшенов.
Код переделанного экшена changestatus_off_continue с комментариями:
Loadырь 2 года назад #
Тут если админ, то код с 96 по 117 строки можно пропустить не выполняя.
Оптимизация 😄
Fuze 2 года назад #
Так чутка грязнее всё же, да и экономия на спичках)