Недавно мы написали поле для пометки неактуальных записей. Сегодня усложним задачу и сделаем из этого поля компонент.
Что будем делать
- Создадим компонент, перенесем в него наши экшены.
- Немного доработаем само поле, чтобы можно было делать актуальным без редактирования записи.
- Добавим админку и форму опций.
- Будем автоматически помечать записи, как неактуальные, через определенный промежуток времени, если автор не подтвержит акутальность.
- Отправим автору письмо с просьбой подтеврдить актуальность за пару дней до того, как сделаем запись неактуальной.
Начем с создания компонента. Если нам не нужна админка, то можно просто создать папку, в которой добавить файл модели и/или фронтенда и папку actions, куда переместим наши экшены. Приступим. Создадим в папке system/controllers новую папку changestatus, внутри этой папки добавим файл frontend.php. В нем объявим класс changestatus, наследуемый от cmsFrontend:
<?php class changestatus extends cmsFrontend { }
Здесь же создадим файл model.php c таким содержимым:
<?php class modelChangestatus extends cmsModel { }
В этой же папке создаем еще одну папку — actions. В нее перемещаем файлы changestatus_off.php и changestatus_off_continue.php из папки system/controllers/content/actions. Но давайте переименуем эти файлы: changestatus_off.php переименуем в off_start.php, а changestatus_off_continue.php в off_end.php.
Теперь внесем небольшие исправления в этих файлах. В файле off_start.php заменим
<?php class actionContentChangestatusOff extends cmsAction {
на
<?php class actionChangestatusOffStart extends cmsAction {
А в файле off_end.php заменим
<?php class actionContentChangestatusOffContinue extends cmsAction {
на
<?php class actionChangestatusOffEnd extends cmsAction {
Так как теперь наши экшены в другом компоненте, то $this->model не будет работать, если так мы обращаемся модели другого компонента. У нас есть такое в обоих экшенах:
$ctype = $this->model->getContentType($ctype_id);
Метод getContentType() — это метод компонента content. Поэтому здесь мы $this->model заменим на $this->model_content.
$ctype = $this->model_content->getContentType($ctype_id);
Еще у нас в обоих экшенах много повторяющегося кода. Чуть позже мы его перенесем в модель. Но сначала давайте внесем изменения в js-файл, ведь там идет обращение к файлам по старым адресам. Открываем файл templates/default/js/field-changestatus.js и меняем в нем
$.post('/content/changestatus_off/' + ctype_id + '/' + item_id + '/' + field_id).done(function(response) {
на
$.post('/changestatus/off_start/' + ctype_id + '/' + item_id + '/' + field_id).done(function(response) {
и
$.post('/content/changestatus_off_continue/' + ctype_id + '/' + item_id + '/' + field_id + '/' + reason).done(function(response) {
на
$.post('/changestatus/off_end/' + ctype_id + '/' + item_id + '/' + field_id + '/' + reason).done(function(response) {
Чтобы убедиться, что всё работает, не забываем очистить кэш или изменить значение абстрактного счетчика в настройках сайта во вкладке Интерфейс.
Теперь давайте перенесем повторяющийся код из экшенов в модель. Открываем файлы off_start.php и off_end.php и смотрим, что у нас повторяется. А повторяется достаточно много:
if ((int)$ctype_id <= 0 || (int)$item_id <= 0 || (int)$field_id <= 0) { $result['action'] = 'error'; $result['text'] = 'Ай-я-яй, как не стыдно?'; $this->halt(); } $ctype = $this->model_content->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']; $item = $this->model->getItemById('con_'.$ctype_name, $item_id); if (!$item) { $result['action'] = 'error'; $result['text'] = 'Ну всё, приплыли...'; $this->halt(); } $result = []; $user = cmsUser::getInstance(); if (!$user->is_admin && $user->id != $item['user_id']) { $result['action'] = 'error'; $result['text'] = 'Вам запрещено это делать!'; if (!$user->is_logged) { $result['text'] = 'Вы вышли из аккаунта!'; } $this->halt(); }
Сейчас мы сделаем это код немного чище, а также добавим метод в модель, где перед действием будут выполняться все проверки.
Открываем файл model.php и добавляем метод check():
<?php class modelChangestatus extends cmsModel { public function check() { } }
В этот метод мы передадим параметры, которые нужно проверить. В файл off_start.php приходят id типа контента, записи и поля. В файл off_end.php еще и id причины. Вот все эти 4 значения мы передадим в метод check():
<?php class modelChangestatus extends cmsModel { public function check($ctype_id, $item_id, $field_id, $reason_id = null) { } }
Так как id причины у нас есть не всегда, то по-умолчанию пусть будет null.
Приступим к проверкам:
<?php class modelChangestatus extends cmsModel { public function check($ctype_id, $item_id, $field_id, $reason_id = null) { $result = []; // $result - это массив $result['action'] = 'error'; // По-умолчанию ошибка // Id типа контента, записи и поля должны быть числами // Если пришел id причины, то он должен быть числом или иксом // Если хотя бы одно из условий выше не выполняется if (!$err1 || !$err2) { $result['text'] = 'Ай-я-яй, как не стыдно?'; // Такой будет текст ошибки return $result; // Возвращаемся в экшен } $user = cmsUser::getInstance(); // Если пользователь не авторизован, нет смысла продолжать if (!$user->is_logged) { $result['text'] = 'Вы вышли из аккаунта!'; return $result; } $model_content = cmsCore::getModel('content'); // Модель контента $ctype = $model_content->getContentType($ctype_id); // Получаем тип контента по id if (!$ctype) { // Если нет такого типа контента $result['text'] = 'Плохо, очень плохо!'; return $result; } // Получаем поле $field = $model_content->getContentField($ctype['name'], $field_id); // Если нет поля или его тип не changestatus if (!$field || $field['type'] != 'changestatus') { $result['text'] = 'А ну, марш в угол!'; return $result; } // Получаем запись $item = $model_content->getContentItem($ctype['name'], $item_id); // Если нет записи if (!$item) { $result['text'] = 'Ну всё, приплыли...'; return $result; } // Если пользователь не админ или не автора if (!$user->is_admin && $user->id != $item['user_id']) { $result['text'] = 'Вам запрещено это делать!'; return $result; } // Если всё нормально, то возвращаем ok // и данные, которые получали в процессе проверок: // тип контента, запись и поле return [ 'action' => 'ok', 'ctype' => $ctype, 'item' => $item, 'field' => $field ]; } }
Теперь изменим экшены. Начнем с off_start.php:
<?php class actionChangestatusOffStart extends cmsAction { public function run($ctype_id, $item_id, $field_id) { $result = []; // $result - это массив $result['action'] = 'error'; // По-умолчанию ошибка // Отправляем данные в метод check() $check = $this->model->check($ctype_id, $item_id, $field_id); // Если вернулась ошибка при проверке if ($check['action'] == 'error') { // Возвращаем скрипту текстовое сообщение, соответствующее ошибке $result['text'] = $check['text']; return $this->cms_template->renderJSON($result); } // Если проверка пройдена, то меняем ошибку на ok $result['action'] = 'ok'; // Опции поля $field_options = $check['field']['options']; // Если в опциях указаны причины, то делаем из них массив $reasons = $field_options['reason'] ? string_explode_list($field_options['reason']) : ''; $reasons_list = []; // Если у есть причины, то перебераем их и создаем новый массив. // Так как значение с нулевым ключом - это первое значение списка, // то оставим икс в качестве метки, что причина не указана if ($reasons) { foreach ($reasons as $key => $reason) { $reasons_list[] = '<option value="'.$key.'">'.$reason.'</option>'; } } // Такой текст будет в модальном окне по-умолчанию $result_text = '<p class="changestatus_text">Материал будет помечен, как неактуальный. Вы уверены, что хотите продолжить?</p>'; // Если есть причины и пользователь не админ if ($reasons_list && !$this->cms_user->is_admin) { // Делаем из массива с причинами выпадающий список // Меняем текст в модальном окне и к нему добавляем список причин $result_text = '<p class="changestatus_text">Укажите, пожалуйста, причину, по которой материал больше неактуален.</p>'.$list; } // Заголовоки из настроек типа контента. // В стрших версиях InstantCMS не было склонений по падежам. // Но нам нужен родительный падеж. Поэтому мы будем использовать заголовок two (два): // "Изменение статуса объявления/новости/поста" $ctype_labels = $this->model->yamlToArray($check['ctype']['labels']); // Заголовок в модальном окне $label = '<p class="changestatus_label">Изменение статуса '.$ctype_labels['two'].'</p> <h3>«'.$check['item']['title'].'»</h3>'; // Кнопка для продолжения $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>'; // Собираем кнопки вместе $buttons = '<div class="changestatus_btns">'.$btn_continue.$btn_cancel.'</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>'; return $this->cms_template->renderJSON($result); } }
Теперь изменим содержимое файла off_end.php:
<?php class actionChangestatusOffEnd extends cmsAction { public function run($ctype_id, $item_id, $field_id, $reason_id) { $result = []; $result['action'] = 'error'; // Точно так же проходим проверку, но теперь передаем еще и id причины $check = $this->model->check($ctype_id, $item_id, $field_id, $reason_id); if ($check['action'] == 'error') { $result['text'] = $check['text']; return $this->cms_template->renderJSON($result); } $result['action'] = 'ok'; $field_options = $check['field']['options']; $result['text'] = '<div class="changestatus_off_text changestatus_not_reason">'.($field_options['not_actual'] ? $field_options['not_actual'] : 'Неактуально').'</div>'; // По-умолчанию причина не указана $reason = 'Причина не указана'; // Если id не является иксом if ($reason_id != 'x') { //Если причины в опциях есть, то делаем из них массив $reasons = $field_options['reason'] ? string_explode_list($field_options['reason']) : ''; // Если в списке причин есть причина с id, который был передан в этот экшен // то получаем причину $reason = $reasons[$reason_id]; } } // Если это делает админ if ($this->cms_user->is_admin) { // то причина будет такая $reason = 'Статус изменен решением администрации сайта'; } // Получаем имя таблицы в БД $table_name = $this->model_content->getContentTypeTableName($check['ctype']['name']); // Обновляем поле в записи $this->model->update($table_name, $item_id, [ $check['field']['name'] => $reason ]); return $this->cms_template->renderJSON($result); } }
Перенесем обновление поля в модель. Открываем model.php и добавляем метод updField():
public function updField($ctype_name, $item_id, $field_name, $reason) { $this->update('con_'.$ctype_name, $item_id, [ $field_name => $reason ]); return true; }
А в off_end.php вместо
$table_name = $this->model_content->getContentTypeTableName($check['ctype']['name']); $this->model->update($table_name, $item_id, [ $check['field']['name'] => $reason ]);
напишем это:
$this->model->updField($check['ctype']['name'], $item_id, $check['field']['name'], $reason);
Первый пункт выполнили, переходим ко второму. Сейчас, чтобы восстановить запись, нужно открыть форму редактирования и сохранить. Мы это так и оставим, но также добавим кнопку для неактуальной записи, при нажатии на которую он будет восстановлена.
Открываем файл поля system/fields/changestatus.php. Ярлык с надписью «Неактуально» у нас выводится в этой строке:
return '<div class="changestatus_off_text" title="'.$value.'">'.($this->options['not_actual'] ? $this->options['not_actual'] : 'Неактуально').'</div>';
Добавим рядом с этим ярлыком кнопку для восстановления, которую будут видеть только админы и автор.
Перед этой строкой пишем:
$restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_restore_'.$cl.'" onclick="statusRestore('.$data.')">Восстановить</div>';
Можно добавить опцию с текстом для восстановления:
new fieldString('btn_restore', [ 'title' => 'Текст кнопки восстановления', 'default' => 'Восстановить' ])
Теперь наши опции выглядят так:
public function getOptions(){ return [ new fieldString('btn_off', [ 'title' => 'Текст кнопки отключения', 'default' => 'Неактуально' ]), new fieldString('btn_restore', [ 'title' => 'Текст кнопки восстановления', 'default' => 'Восстановить' ]), new fieldText('reason', [ 'title' => 'Причины', 'hint' => 'Оставьте поле пустым, если указывать причину не нужно. По одной причине в каждой строке, например:<br><b>Продал на этом сайте<br>Продал на другом сайте<br>Передумал продавать</b>' ]), new fieldString('not_actual', [ 'title' => 'Текст, если запись неактуальная', 'default' => 'Не актуально' ]) ]; }
И вот так выглядит форма опций в админке:
Добавим код, который выведет текст на кнопке:
$restore_btn_text = $this->options['btn_restore'] ? $this->options['btn_restore'] : 'Восстановить';
И изменим код, выводящий эту кнопку, где вместо текста «Восстановить», вставим переменную $restore_btn_text:
$restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_restore_'.$cl.'" onclick="statusRestore('.$data.')">'.$restore_btn_text.'</div>';
Но в таком виде кнопка будет выводиться всем, а надо ее показывать только автору или админам. Изменим немного:
$restore_btn = ''; if ($user->is_admin || $user->id == $this->item['user_id']) { $restore_btn_text = $this->options['btn_restore'] ? $this->options['btn_restore'] : 'Восстановить'; $restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_restore_'.$cl.'" onclick="statusRestore('.$data.')">'.$restore_btn_text.'</div>'; }
Ну и теперь нужно эту кнопку вывести где-нибудь. Добавим ее после ярлыка «Неактуально»:
return '<div class="changestatus_off_text changestatus_off_text_'.$cl.'" title="'.$value.'">'.($this->options['not_actual'] ? $this->options['not_actual'] : 'Неактуально').'</div>'.$restore_btn;
Сделаем кнопку с классом changestatus_restore такой же, как и changestatus_off в файле templates/default/css/field-changestatus.css:
.changestatus_off, .changestatus_restore{ background: #d00000; } .changestatus_off:hover, .changestatus_restore:hover{ background: #e50000; } И добавим кнопкам и ярлыкам отступы: .changestatus_btn, .changestatus_off_text{ margin:0 5px 5px 0; }
Получается вот так:
Теперь нам нужно написать функцию statusRestore(). Открываем файл templates/default/js/field-changestatus.js и добавляем эту функцию:
function statusRestore(ctype_id, item_id, field_id) { /* Отправляем данные в экшен restore */ $.post('/changestatus/restore/' + ctype_id + '/' + item_id + '/' + field_id).done(function(response) { result = $.parseJSON(response); /* Если получаем ошибку */ if (result.action === 'error') { /* Удаляем у кнопки атрибут onclick и добавляем класс ошибки, выводим текст ошибки */ $('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').addClass('error_btn').html(result.text); } else { /* Если всё хорошо */ /* Удаляем метку, что запись неактуальная */ $('.changestatus_off_text_' + ctype_id + '_' + item_id + '_' + field_id).remove(); /* Меняем у кнопки текст и событие */ $('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).attr('onclick', 'statusOff(' + ctype_id + ',' + item_id + ',' + field_id + ')').text(result.text); } }); }
Так как у нас теперь в поле участвует не одна кнопка, а две (будут еще), то теперь надо было бы класс у кнопок сделать другой, одинаковый. В файле поля system/fields/changestatus.php меняем
$btn_off = '<div class="changestatus_btn changestatus_off changestatus_off_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>';
на
$btn_off = '<div class="changestatus_btn changestatus_off changestatus_btn_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>';
Недавний наш код
$restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_restore_'.$cl.'" onclick="statusRestore('.$data.')">'.$restore_btn_text.'</div>';
меняем на
$restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_btn_'.$cl.'" onclick="statusRestore('.$data.')">'.$restore_btn_text.'</div>';
В js-файле в двух местах (в функции statusOff() и statusOffContinue())
$('.changestatus_off_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').addClass('error_btn').html(result.text);
меняем на
$('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').addClass('error_btn').html(result.text);
Теперь создадим новый экшен restore.php в папке system/controllers/changestatus/actions:
<?php class actionChangestatusRestore extends cmsAction { public function run($ctype_id, $item_id, $field_id) { $result = []; $result['action'] = 'error'; $check = $this->model->check($ctype_id, $item_id, $field_id); if ($check['action'] == 'error') { $result['text'] = $check['text']; return $this->cms_template->renderJSON($result); } $result['action'] = 'ok'; $field_options = $check['field']['options']; $result['text'] = $field_options['btn_off'] ? $field_options['btn_off'] : 'Больше неактуально?';; // Мы опять обращаемся к методу updField, но вместо причины передаем метку "actual" $this->model->updField($check['ctype']['name'], $item_id, $check['field']['name'], 'actual'); return $this->cms_template->renderJSON($result); } }
Работает это так:
Давайте добавим кнопку восстановления не только при загрузке страницы, но и после того, как мы сделали запись неактуальной, чтобы вместе с ярлыком появлялась и кнопка. Для этого в экшене off_end.php изменим этот код:
$result['text'] = '<div class="changestatus_off_text changestatus_not_reason">'.($field_options['not_actual'] ? $field_options['not_actual'] : 'Неактуально').'</div>';
на этот:
$cl = $ctype_id.'_'.$item_id.'_'.$field_id; $data = $ctype_id.', '.$item_id.', '.$field_id; $restore_btn = ''; if ($this->cms_user->is_admin || $this->cms_user->id == $check['item']['user_id']) { $restore_btn_text = $field_options['btn_restore'] ? $field_options['btn_restore'] : 'Восстановить'; $restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_btn_'.$cl.'" onclick="statusRestore('.$data.')">'.$restore_btn_text.'</div>'; } $result['text'] = '<div class="changestatus_off_text changestatus_off_text_'.$cl.' changestatus_not_reason">'.($field_options['not_actual'] ? $field_options['not_actual'] : 'Неактуально').'</div>'.$restore_btn;
Принцип точно такой, как и в файле поля.
Еще заметил одну неточность. Когда мы делали запись неактуальной, у нас при успешном завершении кнопка вставлялась внутрь кнопки. Поэтому я немного изменим функцию statusOffContinue():
function statusOffContinue(ctype_id, item_id, field_id) { var reasons = $('.changestatus_modal_' + ctype_id + '_' + item_id + '_' + field_id + ' select'); if (reasons.length) { var reason = reasons.val(); } else { var reason = 'x'; } $.post('/changestatus/off_end/' + ctype_id + '/' + item_id + '/' + field_id + '/' + reason).done(function(response) { result = $.parseJSON(response); statusCancel(ctype_id, item_id, field_id); if (result.action === 'error') { $('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').addClass('error_btn').html(result.text); } else { $('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').replaceWith(result.text); } }); }
Вот так теперь всё работает:
Ну вот теперь можно начать что-то делать. Да, вы не ошиблись)) Мы еще даже не начинали, а всего лишь исправили то, что было.
Давайте создадим админку компонента и форму опций. Но сначала придумаем, какие опции нам нужны и для чего. Представим ситуацию, когда пользователь на сайте добавляет запись в каком-то типе контента. Для примера возьмем тип контента «Справочник организаций». Так вот, пользователь добавил компанию, всё классно. Проходит 2-3 месяца, а компания уже переехала в другое место или сменила номер телефона. Или вообще закрылась. А что, если мы предложим пользователю подтвердить актуальность через какое-то время? Если он проигнорирует, то сделаем компанию неактуальной. Конечно, если у вас огромный справочник, который вы где-то спарсили, то вам вряд ли это нужно. Но если пользователь сам добавлялся, то, возможно, ему будет не сложно зайти на сайт и нажать на кнопку, чтобы его компания продолжала публиковаться. В крайнем случае, админ может сам всё перепроверить и подтвердить. Вот такие мысли. Давайте начнем это делать.
Чтобы у компонента была админка, нужно о нем сделать запись в БД. Вот ее мы сейчас и добавим. Открываем phpMyAdmin, переходим в нужную базу, находим и открываем таблицу cms_controllers. В ней мы увидим список установленных компонентов. Нажимаем возле любого из них на ссылку «Копировать»:
Откроется форма, в которой мы удаляем лишние данные и добавляем свои, как на скриншоте:
Нажимаем на кнопку «Вперед» и на этом пока всё. Теперь в админке у нас появился наш компонент:
Но при переходе в его настройки мы получим примерно такую ошибку:
Давайте добавим настройки. Сначала в папке system/controllers/changestatus создадим файл backend.php с таким содержимым:
<?php class backendChangestatus extends cmsBackend { public $useDefaultOptionsAction = true; protected $useOptions = true; public function actionIndex(){ $this->redirectToAction('options'); } }
Здесь мы написали, что главной страницей в админке компонента у нас будут опции. Давайте создадим форму опций. В папке system/controllers/changestatus создаем папку backend, внутри которой создаем папку forms, а в ней создаем файл form_options.php с таким содержимым:
<?php class formChangestatusOptions extends cmsForm{ public function init(){ return [ 'basic' => [ 'title' => '', 'type' => 'fieldset', 'childs' => [ new fieldCheckbox('option', [ 'title' => 'Опция' ]) ] ] ]; } }
Здесь мы добавили одну единственную опцию — это поле флаг. Проверим, что у нас получилось: переходим в настройки компонента в админке и видим нашу форму:
Чтобы опции работали, как надо, добавим пару строк в файле frontend.php:
public $useDefaultOptionsAction = true; protected $useOptions = true;
Возвращаемся к файлу опций. Давайте напишем нормальную форму. Наша форма должна быть динамическая, ведь типы контента могут появляться или удаляться, а для каждого типа контента у нас будут свои настройки. Заменим содержимое файла form_options.php на это:
<?php class formChangestatusOptions extends cmsForm{ public function init(){ // Получаем модель контента $model_content = cmsCore::getModel('content'); // Получаем типы контента $ctypes = $model_content->getContentTypes(); // Пустой массив опций $options = []; // Если нет типов контента // Хотя так не бывает, но пусть будет, мало ли if (!$ctypes) { // Возвращаем пустой массив return $options; } // Перебираем типы контента. // Каждое поле пишем отдельно с добавлением в ключ элемента массива опций имени типа контента. foreach ($ctypes as $ctype) { $options[$ctype['name']] = new fieldCheckbox($ctype['name'], [ 'title' => $ctype['title'] ]); $options[$ctype['name'].'_exclude_groups'] =new fieldListmultiple($ctype['name'].'_exclude_groups', [ 'title' => 'Группы пользователей, записи которых всегда акутальные', 'generator' => function() use ($model_content){ $groups = $model_content->get('users_groups'); $list = []; if (!$groups) { return $list; } foreach ($groups as $group) { $list[$group['id']] = $group['title']; } return $list; }, 'visible_depend' => [$ctype['name'] => ['show' => ['1']]] ]); $options[$ctype['name'].'_days'] = new fieldNumber($ctype['name'].'_days', [ 'title' => 'Интервал между подтверждениями уникальности', 'units' => 'дней', 'default' => 90, 'visible_depend' => [$ctype['name'] => ['show' => ['1']]], 'options' => ['is_abs' => true, 'is_ceil' => true, 'save_zero' => true], 'hint' => '0 - отключить' ]); $options[$ctype['name'].'_notify'] = new fieldNumber($ctype['name'].'_notify', [ 'title' => 'За сколько дней уведомлять автора до даты окончания актуальности', 'units' => 'дней', 'default' => 3, 'visible_depend' => [$ctype['name'] => ['show' => ['1']]], 'options' => ['is_abs' => true, 'is_ceil' => true, 'save_zero' => true], 'hint' => '0 - не уведомлять' ]); $options[$ctype['name'].'_notify_html'] = new fieldHtml($ctype['name'].'_notify_html', [ 'title' => 'Шаблон письма-предупреждения, что запись будет помечена как неактуальная', 'default' => '<p>Здравствуйте, {nickname}.</p> <p>Проверьте, пожалуйста, и подтвердите актуальность данных вашей записи <a href="{href}">"{title}"</a>, иначе она будет помечена, как неактуальная, {date} в {time}.</p>', 'options' => ['editor' => 'ace', 'editor_options' => ['mode' => 'ace/mode/javascript']], 'visible_depend' => [$ctype['name'] => ['show' => ['1']]], 'hint' => 'Можно использовать специальные выражения {nickname}, {title}, {href}, {date}, {time}' ]); $options[$ctype['name'].'_end_notify'] = new fieldCheckbox($ctype['name'].'_end_notify', [ 'title' => 'Уведомлять автора, когда запись стала неактуальной', 'default' => 1, 'visible_depend' => [$ctype['name'] => ['show' => ['1']]] ]); $options[$ctype['name'].'_end_notify_html'] = new fieldHtml($ctype['name'].'_end_notify_html', [ 'title' => 'Шаблон письма-уведомления, что запись стала неактуальной', 'default' => '<p>Здравствуйте, {nickname}.</p> <p>Ваша запись "{title}" помечена, как неактуальная. Подтвердить актуальность записи вы можете <a href="{href}">по ссылке</a>.</p>', 'options' => ['editor' => 'ace', 'editor_options' => ['mode' => 'ace/mode/javascript']], 'visible_depend' => [$ctype['name'] => ['show' => ['1']]], 'hint' => 'Можно использовать специальные выражения {nickname}, {title}, {href}' ]); } // Выводим нашу форму return [ 'title' => 'Настройка типов контента', 'type' => 'fieldset', 'childs' => $options ) ]; } }
Вот такая получается форма:
Теперь эти опции нужно где-то применить, чтобы что-то как-то сработало. Создадим в БД таблицу, где будем хранить наши данные о всех записях, а добавляться данные туда будут сразу же после добавления или обновления записей выбранных типов контента или при нажатии на кнопки.
Таблица будет состоять из следующих полей:
- id;
- ctype_name — имя типа контента;
- item_id — id записи;
- date_end — дата, когда запись станет неактуальной;
- is_notify — если мы отправили уведомление, что запись станет неактуальной, то запишем сюда что-нибудь, чтобы больше не отправлять;
- is_actual — единица в этом поле говорит о том, что запись акутальная.
Итого 6 полей. Давайте создавать таблицу. Открываем phpMyAdmin, переходим в нашу БД, внизу есть такой блок:
Пишем здесь название нашей новой таблицы cms_changestatus (вместо cms_ напишите нужный префикс) и выбираем 7 полей:
Нажимаем кнопку «Вперед». Откроется таблица, которую нужно заполнить вот так:
Нажимаем сохранить. Ну а теперь давайте начнем что-то в нее писать. Каждый раз при добавлении записи в выбранном в опциях компонента типа контента, нужно записать строку со значениями. Если запись редактируется, то нужно проверить, есть ли строка, соответствующая этой записи. Если удаляем, то нужно удалить строку. Если удаляем в корзину, то нужно снять метку is_actual. Если восстанавливается, то снова поставить метку. И т.д. Это всё должно делаться автоматически, поэтому сейчас начнем писать хуки.
В папке system/controllers/changestatus создаем папку hooks. В ней будем создавать файлы. Начнем с добавления записи. Создаем файл content_after_add_approve.php с таким содержимым:
<?php class onChangestatusContentAfterAddApprove extends cmsAction { public function run($data){ // Если тип контента не включен в опциях // или если пользователь в исключенной группе - ничего не делаем if (empty($this->options[$data['ctype_name']]) || $this->cms_user->isInGroups($this->options[$data['ctype_name'].'_exclude_groups'])) { return $data; } // Если интервал равен нулю, тоже ничего не делаем if ($this->options[$data['ctype_name'].'_days'] == 0) { return $data; } // Прибавляем к текущей дате нужное количество дней с помощью метода dateEnd() // Записываем данные в таблицу $this->model->insertItem($data['ctype_name'], $data['item']['id'], $date_end); return $data; } }
Здесь мы выполнили два обращения к методам модели. Но их еще нет. Добавим. Открываем файл model.php и добавляем метод dateEnd():
public function dateEnd($date, $days) { // Прибавляем к дате количество дней из опций $date_time = new DateTime($date); $date_time->modify('+'.$days.' day'); $date_end = $date_time->format('Y-m-d H:i:s'); // Возвращаем дату окончания return $date_end; }
И метод insertItem():
public function insertItem($ctype_name, $item_id, $date_end) { $this->insert('changestatus',[ 'ctype_name' => $ctype_name, 'item_id' => $item_id, 'date_end' => $date_end, 'is_actual' => 1 ]); return true; }
При записи в БД мы не указываем id, так как он добавится сам, и is_notify, т. к. мы установили значение по-умолчанию 0 — уведомление не отправлено, ведь это новая запись.
Этот хук готов. Еще, согласно документации, раньше хуки нужно было прописывать в файл манифеста, но если версия InstantCMS выше 2.7.2, то этого делать не нужно. Но после создания хука, чтобы он работал, нужно обновить события в БД в разделе Компоненты -> Управление событиями.
Напишем еще один хук, который будет срабатывать после редактирования записи. Создаем в папке hooks файл content_after_update_approve.php с таким содержимым:
<?php class onChangestatusContentAfterUpdateApprove extends cmsAction { public function run($data){ // Проверяем, есть ли в таблице changestatus строка с этой записью типа контента $item = $this->model->filterEqual('ctype_name', $data['ctype_name'])->getItemByField('changestatus', 'item_id', $data['item']['id']); // Получаем автора записи $author = $this->model_users->getUser($data['item']['user_id']); // Получаем группы автора $author_groups = $author['groups']; // Объявляем переменную $exclude - это массив $exclude = []; // Перебираем массив с группами foreach ($author_groups as $author_group) { // Если группы нет в массиве исключенных групп // Пропускаем continue; } // или записываем в массив $exclude $exclude[] = $author_group; } // Если тип контента не выбран в опциях, интервал равен нулю или в массиве $exclude что-то есть if (empty($this->options[$data['ctype_name']]) || $this->options[$data['ctype_name'].'_days'] == 0 || $exclude) { // Если есть строка в таблице changestatus if ($item) { // Удаляем эту строку $this->model->delete('changestatus', $item['id']); // Обновляем поля типа changestatus $this->model->updFields($data['ctype_name'], $item['id'], 'actual'); } return $data; } // Обращаемся к методу dateEnd() для получения даты окончания $date_end = $this->model->dateEnd($this->options[$data['ctype_name'].'_days']); // Если нет строки в таблице changestatus if (!$item) { // Создаем строку $this->model->insertItem($data['ctype_name'], $data['item']['id'], $date_end); return $data; } // Если есть строка, то обновляем ее // Записываем в нее новую дату, указываем, что запись актуальная и уведомление не отправлено $this->model->updateItem($item['id'], $date_end, 1, 0); // Обновляем поля типа changestatus $this->model->updFields($data['ctype_name'], $item['id'], 'actual'); return $data; } }
Давайте проверку, находится ли автор в исключенной группе, перенесем в модель. Нам это пригодится еще в некоторых местах. Добавляем в файле модели метод authorGroups():
public function authorGroups($data) { $author = $this->model_users->getUser($data['item']['user_id']); $exclude = []; foreach ($author['groups'] as $author_group) { continue; } $exclude[] = $author_group; } return $exclude; }
А в хуке этот код
// Получаем автора записи $author = $this->model_users->getUser($data['item']['user_id']); // Получаем группы автора $author_groups = $author['groups']; // Объявляем переменную $exclude - это массив $exclude = []; // Перебираем массив с группами foreach ($author_groups as $author_group) { // Если группы нет в массиве исключенных групп // Пропускаем continue; } // или записываем в массив $exclude $exclude[] = $author_group; }
заменим на такую строчку:
$exclude = $this->model->authorGroups($data);
Еще здесь мы обращаемся к новому методу в модели, которого еще нет — updateItem(). Добавим его в модель:
public function updateItem($id, $date_end, $is_actual, $is_notify) { // Обновляем запись в таблице changestatus // Записываем новую дату, указываем, что запись актуальная и что уведомление не отправлено $this->update('changestatus', $id, [ 'date_end' => $date_end, 'is_actual' => $is_actual, 'is_notify' => $is_notify ]); return true; }
Может появиться вопрос, почему мы не обновляем само поле, делая запись актуальной. Метод store() поля при сохранении делает запись актуальной. А зачем нам делать два раза одно и то же?
Добавление и редактирование добавили, осталось совсем немного — удаление, удаление в корзину и восстановление из корзины. Давайте напишем хук, срабатывающий после удаления записи. В папке hooks добавляем файл content_after_delete.php с таким содержимым:
<?php class onChangestatusContentAfterDelete extends cmsAction { public function run($data) { $item = $this->model->filterEqual('ctype_name', $data['ctype_name'])->getItemByField('changestatus', 'item_id', $data['item']['id']); if ($item) { $this->model->delete('changestatus', $item['id']); } return $data; } }
Здесь всё просто — получили запись в таблице changestatus. Если она есть, то удалили. Не забываем обновлять события при добавления хуков в разделе Компоненты -> Управление событиями.
Теперь напишем хук, срабатывающий после удаления записи в корзину. Он будет называться content_after_trash_put.php, а содержимое файла будет почти такое же, как и в предыдущем, отличаться будет только название класса:
<?php class onChangestatusContentAfterTrashPut extends cmsAction { public function run($data) { $item = $this->model->filterEqual('ctype_name', $data['ctype_name'])->getItemByField('changestatus', 'item_id', $data['item']['id']); if ($item) { $this->model->delete('changestatus', $item['id']); } return $data; } }
И осталось написать последний хук, срабатывающий после восстановления записи из корзины. Название файла content_after_restore.php, а содержимое такое же, как и в хуке, срабатывающем после обновления записи, только класс другой и кое-что еще:
<?php class onChangestatusContentAfterRestore extends cmsAction { public function run($data){ $item = $this->model->filterEqual('ctype_name', $data['ctype_name'])->getItemByField('changestatus', 'item_id', $data['item']['id']); $exclude = $this->model->authorGroups($data); if (empty($this->options[$data['ctype_name']]) || $this->options[$data['ctype_name'].'_days'] == 0 || $exclude) { if ($item) { $this->model->delete('changestatus', $item['id']); } return $data; } $date_end = $this->model->dateEnd($this->options[$data['ctype_name'].'_days']); if (!$item) { $this->model->insertItem($data['ctype_name'], $data['item']['id'], $date_end); return $data; } $this->model->updateItem($item['id'], $date_end, 1, 0); return $data; } }
Обновляем события в админке. Теперь у нас автоматически будет происходить запись в БД строк обо всех записях типа контента, когда мы их создаем, редактируем или удаляем. Но это еще не всё.
Дальше мы добавим еще один хук, который выполняется по расписанию планировщика. Он будет отслеживать дату окончания актуальности, за несколько дней, указанных в опция компонента, уведомлять авторов, что пора подтвердить актуальность. Если автор проигнорирует и актуальность не будет подтверждена, хук будет делать записи неактуальными и уведомлять автора об этом.
А еще нам надо будет добавить одну кнопку в поле, чтобы автор или админ могли подтвердить актуальность до окончания срока. А также в экшены, делающие записи неактуальными или актуальными, добавить запись, обновление и удаление строк в таблице changestatus, чтобы всё это было единым целым.
Но сначала все-таки напишем задание для планировщика. Для этого в папке hooks создаем файл cron_changestatus.php с таким сожержимым:
<?php class onChangestatusCronChangestatus extends cmsAction { public $disallow_event_db_register = true; public function run(){ return true; } }
Здесь пока ничего не происходит. Давайте подумает, что же все-таки должно происходить? Мы должны загрузить строки из таблицы changestatus, срок которых подходит к концу, чтобы выслать уведомления авторам. Если срок уже вышел, то сделать запись неактуальной и снова отправить уведомление автору. Приступим:
<?php class onChangestatusCronChangestatus extends cmsAction { // Этот хук выполняется по крону, нам его не нужно записывать в БД public $disallow_event_db_register = true; public function run(){ // Массив с типами контента $ctypes_arr = $this->model_content->getContentTypes(); // Массив с интервалами, сюда мы запишем значения, за сколько предупреждать авторов $intervals = []; // Перебираем типы контента foreach ($ctypes_arr as $ctype) { // Если опции для типа контента не существует, // например, если мы создали тип контента после сохранения опций // Пропускаем continue; } // Добавляем занчения массива, где // ключ - имя типа контента, // значение - за сколько дней предупредить автора $intervals[$ctype['name']] = $this->options[$ctype['name'].'_notify']; } // Берем из полученного массива максимальное значение // Прибавляем к текущей дате количество дней из переменной $interval // Получаем записи в таблице changestatus, // дата окончания которых наступит через $interval дней или уже наступила. // Системного фильтра такого нет, напишем свой dateApproach. // Также выбираем, чтобы у записей было заполнено поле date_end и is_actual чтобы содержало единицу $items = $this->model->dateApproach('date_end', $interval, $date_plus)->get('changestatus'); // Если нет подходящих строк if (!$items) { // Завершаем работу return true; } // Перебираем строки foreach ($items as $item) { // Получаем запись типа контента. // Она должна быть опубликована, одобрена и не удалена в корзину $c_item = $this->model_content->filterEqual('is_pub', 1)->filterEqual('is_approved', 1)->filterIsNull('is_deleted', 1)->getContentItem($item['ctype_name'], $item['item_id']); // Если запись не нашли if (!$c_item) { // Удаляем эту строку $this->model->delete('changestatus', $item['id']); // и переходим к следующей continue; } // Массив, который передадим в метод authorGroups() // для проверки, не исключены ли группы, к которым // принадлежит автор $data = [ 'item' => ['user_id' => $c_item['user_id']], 'ctype_name' => $item['ctype_name'] ]; // Проверяем группы автора $exclude = $this->model->authorGroups($data); // Если автор в группе, каторая исключена // или отслеживаниеи типа контена не включено // или интервал, в течение которого запись считается актуальной, равно нулю if ($exclude || $this->options[$ctype['name']] || $this->options[$ctype['name'].'_days]' == 0) { // Удаляем строку $this->model->delete('changestatus', $item['id']); // и переходим к следующей continue; } // Если дата окончания меньше или равна текущей дате // Если включено уведомление после того, как запсиь стала неактуальной if ($this->options[$ctype['name'].'_end_notify']) { // Отправляем письмо автору методом sendEmail() // Его мы добавим в модель $this->model->sendEmail($item['ctype_name'], $c_item, $this->options[$ctype['name'].'_end_notify_html'], $item['date_end']); } // Обновляем все поля типа changestatus в записи типа контента // Метод updFields() добавим в модель $this->model->updFields($item['ctype_name'], $c_item['id'], 'Актуальность не подтверждена автором записи'); // Обновляем строку в БД $this->model->updateItem($item['id'], null, 0, 1); // Переходим к следующей строке continue; } $c_interval = $intervals[$item['ctype_name']]; // Если разница даты окончания и текущей // больше значения, за сколько дней предупреждать автора, умноженного на количество секунд в сутках // или предупреждения отключены (равно 0) // или предупреждение уже было отправлено // переходим к следующей строке continue; } // Отправляем предупреждение $this->model->sendEmail($item['ctype_name'], $c_item, $this->options[$ctype['name'].'_notify_html'], $item['date_end']); // В строку пишем, что предупреждение отправлено $this->model->updateItem($item['id'], $item['date_end'], 1, 1); } return true; } }
Вот такое получилось задание. Но чтобы оно выполнялось по расписанию, нужно его добавить в планировщике. Переходим по пути Панель управления -> Настройки -> Планировщик, нажимаем «Добавить задание», заполняем поля (компонент — changestatus, хук — changestatus) и сохраняем.
Давайте теперь добавим 3 метода в модель, о которых говорилось в комментариях в коде хука:
public function dateApproach($field, $days){ // Переводим дни в минуты $minutes = $days * 1440; // Используем функцию TIMESTAMPDIFF // Смысл такой: отнимаем от даты из поля текущую дату // и разница должна быть меньше или равна количеству минут $this->filter("TIMESTAMPDIFF(MINUTE, NOW(), {$field}) <= $minutes"); return $this; } public function updFields($ctype_name, $item_id, $reason) { // Модель контента $model_content = cmsCore::getModel('content'); // Поля типа контента типа changestatus $fields = $model_content->filterEqual('type', 'changestatus')->getContentFields($ctype_name); // Если нет подходящих полей if (!$fields) { // возвращаемся return true; } // Перебираем поля foreach ($fields as $field) { // Обновляем каждое поле $this->updField($ctype_name, $item_id, $field['name'], $reason); } return true; } public function sendEmail($ctype_name, $item, $body, $date){ // Модель пользователей $users_model = cmsCore::getModel('users'); // Автор записи $user = $users_model->getUser($item['user_id']); $config = cmsConfig::getInstance(); // Тело письма из опций с заменой выражений в фигурных скобках на значения '{nickname}', '{title}', '{href}', '{date}', '{time}' ],[ $user['nickname'], $item['title'], $config->host.href_to($ctype_name, $item['slug'].'.html'), ], $body); // В конец тела письма добавляем такую вставку $body .= '<br>--<br>C уважением, '.$config->sitename.'<br><small>Письмо отправлено автоматически, пожалуйста, не отвечайте на него.</small>'; // Создаем объект cmsMailer $mailer = new cmsMailer(); // Кому - email автора $mailer->addTo($user['email']); // От кого - от сайта - email и имя отправителя $mailer->setFrom($config->mail_from, $config->mail_from_name); // Тема письма $mailer->setSubject('Подтвердите актуальность страницы "'.$item['title'].'"'); // Тело письма $mailer->setBodyHTML($body); //Отправляем $mailer->send(); // Очищаем $mailer->clearTo(); return true; }
Ну и осталось совсем немного — добавить запись и обновление строк в таблице changestatus при нажатии на кнопки. У нас всего 2 кнопки — одна кнопка внутри модального окна с подтверждением, вторая для восстановления записи. Когда это сделаем, напишем еще одну кнопку, позволяющую подтвердить актуальность, пока запись еще актуальная, но это чуть позже.
Если мы делаем запись неактуальной, нам надо в таблице changestatus, если есть строка этой записи, удалить дату окончания и пометить, что запись неактуальная. Но сначала давайте сделаем кое-что еще — если у нас зачем-то добавлено несколько полей, то мы их все сделаем неакутальными, чтобы не было конфликтов. Работать будем в файле system/controllers/changestatus/actions/off_end.php. Заменим строку
$this->model->updField($check['ctype']['name'], $item_id, $check['field']['name'], $reason);
на это:
$this->model->updFields($check['ctype']['name'], $item_id, $reason);
Теперь добавим обновление строки в таблице changestatus. Сразу после этой строки пишем:
// Получаем строку $item = $this->model->filterEqual('ctype_name', $check['ctype']['name'])->getItemByField('changestatus', 'item_id', $item_id); // Если не нашли if (!$item) { // То и не надо return $this->cms_template->renderJSON($result); } $data = [ 'item' => ['user_id' => $check['item']['user_id']], 'ctype_name' => $check['ctype']['name'] ]; $exclude = $this->model->authorGroups($data); // Если тип контента не отслеживается или пользователь в исключенной группе if (!$this->options[$check['ctype']['name']] || $this->options[$check['ctype']['name'].'_days'] == 0 || $exclude) { // Удаляем строку $this->model->delete('changestatus', $item['id']); return $this->cms_template->renderJSON($result); } // Обновляем строку $this->model->updateItem($item['id'], null, 0, 0);
Давайте некоторые куски кода перенесем в модель. Например, вот этот:
$c_item = $this->model_content->filterEqual('is_pub', 1)->filterEqual('is_approved', 1)->filterIsNull('is_deleted', 1)->getContentItem($item['ctype_name'], $item['item_id']);
Создадим для него отдельный метод в модели:
public function cItem($ctype_name, $item_id) { $model_content = cmsCore::getModel('content'); $item = $model_content->filterEqual('is_pub', 1)->filterEqual('is_approved', 1)->filterIsNull('is_deleted', 1)->getContentItem($ctype_name, $item_id); return $item; }
Теперь в файле cron_changestatus.php найдем
$c_item = $this->model_content->filterEqual('is_pub', 1)->filterEqual('is_approved', 1)->filterIsNull('is_deleted', 1)->getContentItem($item['ctype_name'], $item['item_id']);
и заменим на
$c_item = $this->model->cItem($item['ctype_name'], $item['item_id'])
У нас повторяется вот это:
$data = [ 'item' => ['user_id' => $c_item['user_id']], 'ctype_name' => $item['ctype_name'] ]; $exclude = $this->model->authorGroups($data);
В модели добавим метод:
public function dataExclude($user_id, $ctype_name) { $data = [ 'item' => ['user_id' => $user_id], 'ctype_name' => $ctype_name ]; return $this->authorGroups($data); }
В cron_changestatus.php заменим приведенный выше код на
$exclude = $this->model->dataExclude($c_item['user_id'], $item['ctype_name']);
А в off_end.php похожий код
$data = [ 'item' => ['user_id' => $c_item['user_id']], 'ctype_name' => $check['ctype']['name'] ]; $exclude = $this->model->authorGroups($data);
Заменим на
$exclude = $this->model->dataExclude($c_item['user_id'], $check['ctype']['name']);
Теперь будем работать в файле system/controllers/changestatus/actions/restore.php. Это восстановление. Нам нужно при восстановлении проверить, есть ли запись. Если есть, то обновить ее, а если нет, то создать. И это при условии, что для типа контента включено отслеживание и что автор не в исключенной группе.
Здесь точно так же найдем такую строку:
$this->model->updField($check['ctype']['name'], $item_id, $check['field']['name'], 'actual');
и заменим на
$this->model->updFields($check['ctype']['name'], $item_id, 'actual');
Теперь сразу после нее пишем код:
$item = $this->model->filterEqual('ctype_name', $check['ctype']['name'])->getItemByField('changestatus', 'item_id', $item_id); $exclude = $this->model->dataExclude($check['item']['user_id'], $check['ctype']['name']); $date_end = $this->model->dateEnd($this->options[$check['ctype']['name'].'_days']); // Если для типа контента не включено отслеживание // или если период актуальности равен нулю // или если автор в исключенной группе if (!$this->options[$check['ctype']['name']] || $this->options[$check['ctype']['name'].'_days'] == 0 || $exclude) { // Если есть строка if ($item) { // то удалим $this->model->delete('changestatus', $item['id']); } // Вот и всё, закрыли вопрос return $this->cms_template->renderJSON($result); } // Если есть строка if ($item) { // обновляем $this->model->updateItem($item['id'], $date_end, 0, 0); // или } else { // создаем $this->model->insertItem($check['ctype']['name'], $item_id, $date_end); }
Вот как-то так. Ну и осталось добавить последнее — кнопку подтверждения актуальности, если запись еще актуальная. Во-первых, проверим, нужно ли подтверждать актуальность. Если тип контента не включен или автор в исключенной группе, то эта кнопка не нужна. Во-вторых, выведем эту кнопку только при приближении времени перевода в неактуальные. Думаю, половина срока актуальности будет нормально. А что будет делать кнопка? Ровно то же самое, что и кнопка восстановления.
Работать будем в файле system/fields/changestatus.php. Находим там этот код:
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_btn_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>'; return $btn_off; } return ''; }
Во-первых, давайте его немного изменим:
if ($value == 'actual') { if (!$user->is_admin && $user->id != $this->item['user_id']) { return ''; } $off_text = $this->options['btn_off'] ? $this->options['btn_off'] : 'Больше неактуально?'; $btn_off = '<div class="changestatus_btn changestatus_off changestatus_btn_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>'; return $btn_off; }
Теперь здесь же перед $off_text = ... добавим такой код:
// Кнопки досрочного подтверждения нет $btn_confirm = ''; // Контроллер $controller = cmsCore::getController('changestatus'); // Модель $model = cmsCore::getModel('changestatus'); // Проверяем, нет ли автора в исключенных группах $exclude = $model->dataExclude($this->item['user_id'], $this->item['ctype']['name']); // Если включено отслеживание типа контента // Если период актуальности больше нуля // и автор не в исключенной группе if ($controller->options[$this->item['ctype']['name'].'_days'] > 0 && !$exclude) { // Получаем запись в таблице changestatus $item = $model->filterEqual('ctype_name', $this->item['ctype']['name'])->getItemByField('changestatus', 'item_id', $this->item['id']); // Текст кнопки $btn_confirm_text = $this->options['btn_confirm'] ? $this->options['btn_confirm'] : 'Подтвердить актуальность'; // Сама кнопка $btn_confirm = '<div class="changestatus_btn changestatus_confirm changestatus_btn_'.$cl.'" onclick="statusRestore('.$data.', \'confirm\')">'.$btn_confirm_text.'</div>'; // Если есть запись, но разница в днях между временем окончания и текущим временем // больше половины периода актуальности // то кнопка не нужна $btn_confirm = ''; } } }
Теперь давайте изменим немного весь метод parse(), а то получилось слишком много вложенных условий:
public function parse($value){ return ''; } $user = cmsUser::getInstance(); $cl = $this->ctype_id.'_'.$this->item['id'].'_'.$this->field_id; $data = $this->ctype_id.', '.$this->item['id'].', '.$this->field_id; 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') { $restore_btn = ''; if ($user->is_admin || $user->id == $this->item['user_id']) { $restore_btn_text = $this->options['btn_restore'] ? $this->options['btn_restore'] : 'Восстановить'; $restore_btn = '<div class="changestatus_btn changestatus_restore changestatus_btn_'.$cl.'" onclick="statusRestore('.$data.')">'.$restore_btn_text.'</div>'; } return '<div class="changestatus_off_text changestatus_off_text_'.$cl.'" title="'.$value.'">'.($this->options['not_actual'] ? $this->options['not_actual'] : 'Неактуально').'</div>'.$restore_btn; } if (!$user->is_admin && $user->id != $this->item['user_id']) { return ''; } $btn_confirm = ''; $controller = cmsCore::getController('changestatus'); $model = cmsCore::getModel('changestatus'); $exclude = $model->dataExclude($this->item['user_id'], $this->item['ctype']['name']); if ($controller->options[$this->item['ctype']['name'].'_days'] > 0 && !$exclude) { $item = $model->filterEqual('ctype_name', $this->item['ctype']['name'])->getItemByField('changestatus', 'item_id', $this->item['id']); $btn_confirm_text = $this->options['btn_confirm'] ? $this->options['btn_confirm'] : 'Подтвердить актуальность'; $btn_confirm = '<div class="changestatus_btn changestatus_confirm changestatus_btn_'.$cl.'" onclick="statusRestore('.$data.', \'confirm\')">'.$btn_confirm_text.'</div>'; $btn_confirm = ''; } } } $off_text = $this->options['btn_off'] ? $this->options['btn_off'] : 'Больше неактуально?'; $btn_off = '<div class="changestatus_btn changestatus_off changestatus_btn_'.$cl.'" onclick="statusOff('.$data.')">'.$off_text.'</div>'; return $btn_off.$btn_confirm; }
Еще у нас появилась новая опция — текст кнопки досрочного подтверждения актуальности. Добавим ее:
new fieldString('btn_confirm', [ 'title' => 'Текст кнопки подтверждения актуальности', 'default' => 'Подтвердить актуальность' ])
Ну и добавим немного стилей. Сделаем эту кнопку другого цвета:
.changestatus_confirm{ background:#ff9900; } .changestatus_confirm:hover{ background:#ff5300; }
И немного подправим функцию statusRestore() в js-файле:
/* Добавили еще один параметр, по-умолчанию пустой */ function statusRestore(ctype_id, item_id, field_id, type = null) { $.post('/changestatus/restore/' + ctype_id + '/' + item_id + '/' + field_id).done(function(response) { result = $.parseJSON(response); if (result.action === 'error') { $('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').addClass('error_btn').html(result.text); } else { $('.changestatus_off_text_' + ctype_id + '_' + item_id + '_' + field_id).remove(); /* Если тип confirm, то удаляем кнопку после выполнения */ if (type === 'confirm') { $('.changestatus_confirm.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).remove(); } else { $('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeClass('error_btn').attr('onclick', 'statusOff(' + ctype_id + ',' + item_id + ',' + field_id + ')').text(result.text); } } }); }
Там же добавим еще одну строку в функцию statusOffContinue(). Находим строку
$('.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).removeAttr('onclick').replaceWith(result.text);
И перед ней добавляем
$('.changestatus_confirm.changestatus_btn_' + ctype_id + '_' + item_id + '_' + field_id).remove();
Вот вроде бы и всё.
Осталось упаковать это всё дело в установщик. Так как это уже не поле, а компонент, то кроме файлов нам еще нужно при установке сделать запрос в БД, чтобы добавить нужные таблицы и строки.
Но сначала соберем в кучу файлы.
Создаем папку с именем changestatus, в ней создаем в ней папки и копируем в них файлы. Структура файлов и папок должна быть такая:
package
--system
----controllers
------changestatus
--------actions
----------off_end.php
----------off_start.php
----------restore.php
--------backend
----------forms
------------form_options.php
--------hooks
----------content_after_add_approve.php
----------content_after_update_approve.php
----------content_after_trash_put.php
----------content_after_restore.php
----------content_after_delete.php
----------cron_changestatus.php
--------backend.php
--------frontend.php
--------model.php
----fields
------changestatus.php
--templates
----default
------assets
--------fields
----------changestatus.tpl.php
------css
--------field-changestatus.css
------js
--------field-changestatus.js
manifest.ru.ini
install.php
install.sql
В файл install.php пишем:
<?php function install_package() { return true; }
В install.sql пишем:
INSERT INTO `cms_controllers` (`title`, `name`, `is_enabled`, `author`, `url`, `version`, `is_backend`) SELECT 'Смена статуса записей', 'changestatus', 1, 'Нифигассе о-го-гошеньки', 'https://nifigasse.ru', '', 1 FROM DUAL WHERE NOT EXISTS( SELECT 1 FROM `cms_controllers` WHERE `name` = 'changestatus' ) LIMIT 1; UPDATE`cms_controllers` SET `version` = '1.0.0' WHERE `name`= 'changestatus'; INSERT INTO `cms_scheduler_tasks` (`title`, `controller`, `hook`, `period`, `date_last_run`, `is_active`, `is_new`) SELECT 'Смена статуса записей', 'changestatus', 'changestatus', 30, CURRENT_TIMESTAMP(), 1, 0 FROM DUAL WHERE NOT EXISTS( SELECT 1 FROM `cms_scheduler_tasks` WHERE `controller` = 'changestatus' AND `hook` = 'changestatus' ) LIMIT 1; CREATE TABLE IF NOT EXISTS `cms_changestatus` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `ctype_name` VARCHAR(128) NOT NULL, `item_id` INT(11) NOT NULL, `date_end` TIMESTAMP NULL DEFAULT NULL, `is_notify` INT(1) NOT NULL DEFAULT 0, `is_actual` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
В файл manifest.ru.ini пишем:
[info] title = "Смена статуса записей" [version] major = "2" minor = "0" build = "0" date = "20220811" [depends] core = "2.12.2" [author] name = "Нифигиссе о-го-го-шеньки" url = "https://nifigasse.ru" [description] text[] = "Компонент отслеживания актуальности записей типов контента." text[] = "" text[] = "Распространяется бесплатно. Поддержка не оказывается."
Теперь выделяем все файлы и папки и добавляем их в архив zip.
Готово.
Я не тестировал компонент, но по идее должно работать. Все ошибки или неточности можно исправлять прямо здесь в комментариях. Ну а кому лень это всё читать, можете просто скачать компонент в каталоге дополнений:
Ну и конечно буду рад любой конструктивной критике и замечаниям.
Спасибо за внимание))
Реклама #
Vlad 2 года назад #
Спасибо! Реально, интересный и пошаговый мануал получился.
Читая такие блоги, можно потихоньку научится кодить.🧠
Happy 2 года назад #
Спасибо
Loadырь 2 года назад #
Зачёт!!! 👍.
Прошлый раз удержался от комментария кода, в этот раз тоже промолчу. Скажу немного про MVC:
1. Модель. Файл модели используется для работы с базой данных — получение (запись, удаление, обновление и т.п.) данных и представление их в нужном виде. Логику по проверкам и прочие методы, не используемые в файле модели, лучше выносить в controller (файл frontend или backend). У вас это public function check($ctype_id, $item_id, $field_id, $reason_id = null) и public function dataExclude($user_id, $ctype_name).
Для работы с датами и временем в базе данных есть такие методы. Отсюда и ниже можно использовать их
2. Вид. Такие конструкции желательно выносить в файл шаблона
Или хотя бы использовать стандартные функции шаблона github.com/instantsoft/icms2/blob/master/system/libs/template.helper.php#L333 Так ваши кнопки, селекторы и прочее сразу будут нужного цвета и размера для выбранного шаблона на сайте у пользователей данного компонента.
3. Контроллер. Для отправки писем «счастья» есть нормальные методы и им тоже не место в файле модели github.com/instantsoft/icms2/blob/master/system/controllers/comments/frontend.php#L204
&$!#% 2 года назад #
А вот это зря))
Там нет нужного, поэтому пришлось делать так. Кстати, за основу я как раз-таки взял filterTimestampYounger() и немного изменил.
Да, согласен, я не знаю всех методов. Я это и не скрываю. Но ничего страшного в этом не вижу. Иногда использование системных методов вызывает ошибки на старых версиях движка, потому что тогда их еще не было. Честное слово, не все обновляются)) Но вот с опытом начинаю больше использовать системных всё-таки.
Ну а модель-контроллер — изучу подробнее этот вопрос, спасибо. И за весь коммент в целом тоже спасибо.
Fuze 2 года назад #
Тоже удержался от комментирования, но Loadырь написал основное :) Писать слишком много, могу только голосом онлайн объяснить что-либо.
Добавлю лишь вот это, рекомендую погуглить с примерами в контексте php.
Так файлы открыть и посмотреть что мешает?)
99% методов filter* живут с первой версии движка.
IamB 2 года назад #
Раз уж зашла об этом речь, то основная идея заключается в том, чтобы разделить задачи. И вынести однородные задачи в отдельный слой, а не валить все в кучу. Будет это MVC или что-то еще модное не суть важно.
Напишите это на каком-нибудь форуме PHP-программистов и вас обольют помоями, ибо это не свойственные контроллерам действия. Уже давненько и вполне обоснованно «толстые» контроллеры подвергаются обструкции, но не здесь.
&$!#% 2 года назад #
В общем, загуглил, почитал, что за зверь такой MVC. Ну и из того, что я увидел, делаю вывод, что у меня всё правильно в этом плане. Вкратце — в модели выполняются все процессы, включая проверки, запросы и т.д., контроллер — это только посредник между моделью и представлением. Но может в инстанте своя концепция? Спорить не буду.
Про фильтр. Нужного фильтра в системе НЕТ, поэтому пришлось писать свой, но опять же за основу взят системный.
Код можно было бы сделать легче и читабельнее, согласен. Хоть все и воздержались от комментирования кода, но я и сам это вижу.
На досуге перепишу немного.
IamB 2 года назад #
Да, здесь принято все писать в контроллер. А для того чтобы контроллеры были не такими большими по объему, можно экшены выносить в отдельные классы расширяя cmsAction. Но тут исторически так уже сложилось и споры на тему: как правильно, деструктивны. Но это не значит, что в своем компоненте вы не можете все сделать «правильно».
Evg 2 года назад #
В сети есть места, где «евангилисты» устраивают битвы по поводу своего понимания различных вещей: MVC, толщины контроллеров, моделей, количество слоёв в луковице и т.д. Есть те, кто категорично выступает против ООП, другие против ещё чего-то. Занятно читать такие вещи, где действительно есть фанаты, которые «прям уписывают» по разным вещам. Да, кстати, они все убедительны и все правы! Серьезно.
ИМХО, достаточно смотреть на принятую, уже существующую манеру внутри продукта. Это и будет, опять же ИМХО, самым «правильным» подходом.
Fuze 2 года назад #
Именно. Опять же не забывая про базовые, не зависящие от языка, принципы программирования.
IamB 2 года назад #
Да это так, но это в случае, когда манера продукта соответствует вашему пониманию «как надо» или вам по барабану. И именно поэтому у меня есть сомнения, что программисты потянуться косяками, какой бы мощной ни была раскрутка.
&$!#% 2 года назад #
Так я и не спорю. Всего-лишь хочу понять. Многое понял, многое ещё нет. Да и не претендую я, чтобы мои разработки оказались в коробке. Fuze не даст соврать, я сам попросил его исключить меня из группы «Разработчики». Ни на что не претендую и не спорю, просто вникаю. Ну а как правильно — когда-то Fuze на форуме сам и говорил, что если работает, значит правильно))
А ещё иногда, чтобы получить обратную связь, нужно написать какую-нибудь провокационную хрень. Ну не могу я онлайн голосом поговорить, нет возможности))
Loadырь 2 года назад #
Да, всё нормально. Сам часто пользуюсь этим мемасиком
Можно писать всю логику и в модели. Просто здесь принято это делать немного иначе.
Да, движок развивается и что-то новое появляется. Поэтому разработчики пишут, что дополнение будет работать начиная с некоторой версии. У меня самого есть компоненты, которые полноценно будут работать только в следующем релизе. Но использование стандартных методов поможет избежать ошибок в будущих версиях. Например может обновиться либа по работе с почтой или выйдет новая крутая либа и заменит старую совсем. Тогда ваш код по отправке писем перестанет работать. А при использовании штатных методов всё будет работать, так как при обновлении либы обновятся и методы работы с ней. От того и процесс обновления движка сайта со сторонними дополнениями будет не таким «ужасающим».
И конечно, если не нашли методы в движке, как в случае с фильтром, то придется писать свои, это факт. Но когда есть в движке и не используется, то тогда это выглядит примерно так
Fuze 2 года назад #
Методам типа «check» (реализации внутри) не место в модели. Модель не должна работать ни с cmsTemplate ни даже с cmsUser и cmsCore. Читаем про SOLID
И да, понятие модели в InstantCMS несколько иное docs.instantcms.ru/dev/models/overview
Vlad 2 года назад #
Добрый вечер. Спасибо за компонент.
В новом шаблоне туллтипы можно вызывать немного по другому.
Так в файле system/fields/changestatus.php можно добавить на 68 строчке
атрибуты data-toggle=«tooltip» data-placement=«top», тогда туллтипы будут системные. Сейчас они «ломают» другие туллтипы со страници.
Немного подправить css (и js наверно… я в js не шарю😔)
&$!#% 2 года назад #
Посмотрите пост, где я рассказывал, как писал поле (пока ещё не компонент). Там в конце где-то я добавлял скрипт для тултипа. Если есть конфликт, то просто удалите этот кусок скрипта.
Def 2 года назад #
Странно, у меня почему-то не дает модальное окно с выбором варианта перевода записи в неактуальное. Компонент установлен, события обновлены.
Def 2 года назад #
Возможно ли еще сделать зависимость поля «Вывода в ТОП» с этим полем? Т.е если статус «неактивен», то и кнопку «Продвинуть» не показывать? Или показывать, но при клике сначала уведомлять о том, что необходимо сменить статус записи.
Или в идеале вообще в этом поле дать возможность выбирать поля для скрытия, если статус данного поля «Неактивен». Позволит скрывать например поле с ценой или оплатой, чтобы не вводить посетителя в заблуждение.
Вообще зависимости полей друг от друга в системе пока сильно не хватает.
Def 2 года назад #
в остальном с точки зрения функциональности все очень хорошо визуально.
Def 2 года назад #
Еще пробовал настроить зависимость полей через системный вариант заполненности поля, но почему-то поле UP JUMP основа показывается если значение поля «Статус изменен решением администрации сайта»
&$!#% 2 года назад #
Читайте внимательно: «Зависимость показа поля в форме».
&$!#% 2 года назад #
Нужно добавить одну опцию и два хука.
Def 2 года назад #
Круть-круть!)
&$!#% 2 года назад #
Обновление.
Def 2 года назад #
очень круто! Спасибо, то что доктор прописал) Как вы на отдыхе это делаете)
&$!#% 2 года назад #
Я не совсем на отдыхе)) А на второй вечер мне стало копец как скучно, пришлось за ноутом домой съездить.
Def 2 года назад #
еще такой вопрос — а можно еще добавить настройку — показывать поля, если статус записи «не актуальная»?)
&$!#% 2 года назад #
Можно, конечно. Добавляйте.
Def 2 года назад #
спасибо) я думаю что лучше в чужую разработку лишний раз не лазить, чтобы не поломать что-то) Я лучше донатом помогать буду:)
&$!#% 2 года назад #
Жмете на кнопку и ничего происходит?
Def 2 года назад #
происходит, но без выбора варианта причины.
&$!#% 2 года назад #
Так и должно быть, если статус записи меняет админ.
Def 2 года назад #
а, сори, протупил
Def 2 года назад #