Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Этот учебник является частью серии статей Пишем свой стартап на PHP на Envato Tuts +. В этой серии я направляю вас через запуск приложения от концепции до готового продукта, используя приложение Планировщик встреч в качестве реального приложения. На каждом шаге я делаю релиз кода планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете узнать что-то новое. Я также рассмотрю связанные с запуском бизнес-вопросы по мере их возникновения.
В этой серии из двух частей я опишу, как мы построили инфраструктуру уведомлений и их доставку. Сегодня я собираюсь сосредоточиться на MeetingLog для отслеживания изменений, которые помогают нам определить, когда отправлять обновления.
Если вы еще не опробовали Планировщик собраний, то обязательно запланируйте свою первую встречу. Я участвую в комментариях ниже, так что скажите мне, что вы думаете! Меня особенно интересует, если вы хотите предложить новые функции или темы для будущих учебников.
Напомним, что весь код Планировщика собраний написан в Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с нашей параллельной серией Программирование с Yii2.
Мое видение уведомлений
Вот что обычно видит участник собрания, когда они получают приглашение. Они начнут делиться своей доступностью на различные места и время, а иногда и делать окончательный выбор:

Когда участник собрания отвечает на приглашение, подобное приведенному выше, он может внести несколько изменений. Например:
- Эти дата и время подходят, а эти - нет.
- Это место подходит, а другое - нет, давайте также рассмотрим это дополнительно.
- Я выбираю это место и это время.
- Добавьте примечание: «Я рад, что мы наконец собрались вместе».
После внесения изменений нам необходимо сообщить организатору.
Наша цель - упростить планирование встреч и сократить количество связанных с ними электронных писем. Если мы подождем несколько минут после того, как человек внесет изменения и консолидирует изменения в одно обновление, то, скорее всего, это будет подходящее время для доставки уведомления. Но как мы напишем код, который это делает? Как мы будем отслеживать, какие изменения были внесены, и покажем их организатору?
Еще одна идея, о которой я думал, - избавиться от необходимости нажимать кнопку «Отправить», когда вы закончите внесение изменений. Во-первых, для этого потребуется некоторое обучение пользователей, чтобы заверить их, что мы отправим их изменения, когда они будут сделаны. Но нам также нужен был способ узнать, когда можно было бы отправить изменения организатору.
Как будут работать уведомления
Пересказывая и обобщая требования, вот как будут работать уведомления:
Все действия, связанные с встречами, хранятся в нашей модели MeetingLog. Но нам понадобится способ создания текстовой сводки журнала для обмена с участниками. Мы можем просто показать изменения, произошедшие со времени последнего обновления. Поэтому нам придется их отслеживать.
Поскольку человек вносит изменения в собрание, мы будем наглядно указывать, что нет необходимости нажимать кнопку несуществующей отправки или сохранения изменений.
Мы хотим отслеживать, когда с момент изменений прошло уже несколько минут, и объединим их в одно обновление для другого участника или организатора.
И, наконец, нам нужно будет доставить обновление электронной почты только другим участникам, а не тем, кто внес изменения. Или, если оба участника внесли изменения в течение этого времени, нам нужно будет уведомить о каждом из действий другого.
Создание журнала собрания
По ряду причин нам с самого начала нужен полный журнал действий, выполняемых при планировании совещаний. Возможно, люди захотят увидеть историю планирования или вернуться к определенному моменту, но также, это полезно для отладки. Также оказалось необходимым знать, когда следует доставлять обновления об изменениях в собрании.
Давайте рассмотрим структуру модели MeetingLog. Вот миграция, создающая таблицу:
class m141025_220133_create_meeting_log_table extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%meeting_log}}', [ 'id' => Schema::TYPE_PK, 'meeting_id' => Schema::TYPE_INTEGER.' NOT NULL', 'action' => Schema::TYPE_INTEGER.' NOT NULL', 'actor_id' => Schema::TYPE_BIGINT.' NOT NULL', 'item_id' => Schema::TYPE_INTEGER.' NOT NULL', 'extra_id' => Schema::TYPE_INTEGER.' NOT NULL', 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', ], $tableOptions); $this->addForeignKey('fk_meeting_log_meeting', '{{%meeting_log}}', 'meeting_id', '{{%meeting}}', 'id', 'CASCADE', 'CASCADE'); $this->addForeignKey('fk_meeting_log_actor', '{{%meeting_log}}', 'actor_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); }
Основные элементы, которые мы записываем:
-
meeting_id
сообщает нам, какую встречу мы отслеживаем. -
action
говорит нам, что было сделано. -
actor_id
сообщает нам, кто выполнил действие (например,user_id
). - Перегруженный
item_id
может представлять собой время, место или примечание. -
extra_id
предназначен для записи другой информации в зависимости от действия. -
created_at
сообщает нам, когда действие было выполнено.
Затем я определил константы для всех действий, таких как ACTION_SUGGEST_PLACE
или ACTION_ADD_NOTE
и т.д.
class MeetingLog extends \yii\db\ActiveRecord { const ACTION_CREATE_MEETING = 0; const ACTION_EDIT_MEETING = 3; const ACTION_CANCEL_MEETING = 7; const ACTION_DELETE_MEETING = 8; const ACTION_DECLINE_MEETING = 9; const ACTION_SUGGEST_PLACE = 10; const ACTION_ACCEPT_ALL_PLACES = 11; const ACTION_ACCEPT_PLACE = 12; const ACTION_REJECT_PLACE = 15; const ACTION_SUGGEST_TIME = 20; const ACTION_ACCEPT_ALL_TIMES = 21; const ACTION_ACCEPT_TIME = 22; const ACTION_REJECT_TIME = 25; const ACTION_INVITE_PARTICIPANT = 30; const ACTION_ADD_NOTE = 40; const ACTION_SEND_INVITE = 50; const ACTION_FINALIZE_INVITE = 60; const ACTION_COMPLETE_MEETING = 100; const ACTION_CHOOSE_PLACE = 110; const ACTION_CHOOSE_TIME = 120; const ACTION_SENT_CONTACT_REQUEST = 150; const TIMELAPSE = 300; // five minutes
Метод add упрощает функциональность внутри приложения для записи активности в MeetingLog:
// add to log public static function add($meeting_id,$action,$actor_id=0,$item_id=0,$extra_id=0) { $log = new MeetingLog; $log->meeting_id=$meeting_id; $log->action =$action; $log->actor_id =$actor_id; $log->item_id =$item_id; $log->extra_id =$extra_id; $log->save(); // sets the touched_at field for the Meeting Meeting::touchLog($meeting_id); }
Я также добавил два новых поля в таблицу Meeting: logged_at
и cleared_at
. Когда добавляются записи журнала, собрание обновляет отметку logged_at
, показывающую момент последнего изменения собрания:
public static function touchLog($id) { $mtg = Meeting::findOne($id); $mtg->logged_at = time(); $mtg->update(); }
Например, всякий раз, когда кто-то добавляет новый параметр MeetingPlace, в журнал добавляется событие afterSave
, а затем, конечно, также обновляется Meeting-> logged_at
:
public function afterSave($insert,$changedAttributes) { parent::afterSave($insert,$changedAttributes); if ($insert) { // if MeetingPlace is added // add MeetingPlaceChoice for owner and participants $mpc = new MeetingPlaceChoice; $mpc->addForNewMeetingPlace($this->meeting_id,$this->suggested_by,$this->id); MeetingLog::add($this->meeting_id,MeetingLog::ACTION_SUGGEST_PLACE,$this->suggested_by,$this->place_id); } }
Время logged_at
указывает нам, когда произошло последнее изменение. Время Meeting->cleared_at
сообщит нам время, когда мы в последний раз делились обновлениями с участниками. Итак, если logged_at
> cleared_at
, мы знаем, что участники не полностью обновлены.
Создание краткого описания на английском языке

Затем я создал методы, помогающие перевести журнал в историю изменений встречи на английском языке.
Во-первых, я создал getMeetingLogCommand(), чтобы получить текстовое описание действия:
public function getMeetingLogCommand() { switch ($this->action) { case MeetingLog::ACTION_CREATE_MEETING: $label = Yii::t('frontend','create meeting'); break; case MeetingLog::ACTION_EDIT_MEETING: $label = Yii::t('frontend','edit meeting'); break; case MeetingLog::ACTION_CANCEL_MEETING: $label = Yii::t('frontend','cancel meeting'); break; case MeetingLog::ACTION_DELETE_MEETING: $label = Yii::t('frontend','cancel meeting'); break; case MeetingLog::ACTION_DELETE_MEETING: $label = Yii::t('frontend','deleted meeting'); break; case MeetingLog::ACTION_SUGGEST_PLACE: $label = Yii::t('frontend','add place'); break; case MeetingLog::ACTION_SUGGEST_TIME: $label = Yii::t('frontend','add time'); break; case MeetingLog::ACTION_ADD_NOTE: $label = Yii::t('frontend','add note'); break; case MeetingLog::ACTION_INVITE_PARTICIPANT: $label = Yii::t('frontend','Invite participant'); break; case MeetingLog::ACTION_ACCEPT_ALL_PLACES: $label = Yii::t('frontend','accept all places'); break; case MeetingLog::ACTION_ACCEPT_PLACE: $label = Yii::t('frontend','accept place'); break; case MeetingLog::ACTION_REJECT_PLACE: $label = Yii::t('frontend','reject place'); break; case MeetingLog::ACTION_ACCEPT_ALL_TIMES: $label = Yii::t('frontend','accept all times'); break; case MeetingLog::ACTION_ACCEPT_TIME: $label = Yii::t('frontend','accept time'); break; case MeetingLog::ACTION_REJECT_TIME: $label = Yii::t('frontend','reject time'); break; case MeetingLog::ACTION_CHOOSE_PLACE: $label = Yii::t('frontend','choose place'); break; case MeetingLog::ACTION_CHOOSE_TIME: $label = Yii::t('frontend','choose time'); break; case MeetingLog::ACTION_SEND_INVITE: $label = Yii::t('frontend','Send'); break; case MeetingLog::ACTION_FINALIZE_INVITE: $label = Yii::t('frontend','Finalize'); break; case MeetingLog::ACTION_COMPLETE_MEETING: $label = Yii::t('frontend','Complete meeting'); break; case MeetingLog::ACTION_SENT_CONTACT_REQUEST: $label = Yii::t('frontend','Send request for contact information'); default: $label = Yii::t('frontend','Unknown'); break; } return $label; }
Затем я создал getMeetingLogItem(), который контекстно находит соответствующую метку объекта, на основе которой было выполнено действие. Здесь есть некоторые отладочные заметки:
public function getMeetingLogItem() { $label=''; switch ($this->action) { case MeetingLog::ACTION_CREATE_MEETING: case MeetingLog::ACTION_EDIT_MEETING: case MeetingLog::ACTION_CANCEL_MEETING: case MeetingLog::ACTION_DECLINE_MEETING: $label = Yii::t('frontend','-'); break; case MeetingLog::ACTION_INVITE_PARTICIPANT: $label = MiscHelpers::getDisplayName($this->item_id); if (is_null($label)) { $label = 'Error - unknown user'; } break; case MeetingLog::ACTION_SUGGEST_PLACE: $label = Place::find()->where(['id'=>$this->item_id])->one(); if (is_null($label)) { $label = 'Error - suggested unknown place'; } else { $label = $label->name; if (is_null($label)) { $label = 'Error - suggested place has unknown name'; } } break; case MeetingLog::ACTION_ACCEPT_PLACE: case MeetingLog::ACTION_REJECT_PLACE: $label = MeetingPlace::find()->where(['id'=>$this->item_id])->one(); if (is_null($label)) { $label = 'Error - Accept or reject unknown place x1'; } else { if (is_null($label->place)) $label = 'Error Accept or reject unknown place x2'; else { $label = $label->place->name; if (is_null($label)) { $label = 'Error accept or reject unknown place name x3'; } } } break; case MeetingLog::ACTION_CHOOSE_PLACE: $label = MeetingPlace::find()->where(['id'=>$this->item_id])->one(); if (is_null($label)) { $label = 'Error - chose unknown place x1'; } else { if (is_null($label->place)) $label = 'Error chose unknown place x2'; else { $label = $label->place->name; if (is_null($label)) { $label = 'Error - choose unknown place name x3'; } } } break; case MeetingLog::ACTION_CHOOSE_TIME: case MeetingLog::ACTION_SUGGEST_TIME: case MeetingLog::ACTION_ACCEPT_TIME: case MeetingLog::ACTION_REJECT_TIME: // get the start time $mt = MeetingTime::find()->where(['id'=>$this->item_id])->one(); if (is_null($mt)) { $label = 'Error meeting time unknown'; } else { $label = Meeting::friendlyDateFromTimestamp($mt->start); } break; case MeetingLog::ACTION_ADD_NOTE: if ($this->item_id ==0) { $label = 'note not logged'; } else { $label = MeetingNote::find()->where(['id'=>$this->item_id])->one()->note; } break; case MeetingLog::ACTION_ACCEPT_ALL_PLACES: case MeetingLog::ACTION_ACCEPT_ALL_TIMES: case MeetingLog::ACTION_SEND_INVITE: case MeetingLog::ACTION_FINALIZE_INVITE: case MeetingLog::ACTION_COMPLETE_MEETING: case MeetingLog::ACTION_SENT_CONTACT_REQUEST: $label = Yii::t('frontend','-'); break; default: $label = Yii::t('frontend','n/a'); break; } return $label; }
Например, создание, редактирование, отмена, удаление собрания не требует информации о позиции, тогда как для принятия времени требуется определенный item_id
, соответствующий дате и времени MeetingTimeChoice
.
Предоставление визуальных подсказок
Когда участник меняет ползунки на местах и времени, чтобы указать их предпочтения или, если разрешено, делает окончательный выбор, я хотел указать им, что их организатор собрания будет уведомлен. Я также хотел, чтобы это выглядело ненавязчивым, например, всплывающее предупреждение.
Поскольку многие из этих изменений связаны с AJAX, сообщение должно появляться рядом с действиями мыши. В случае отправки изменения (например, новая заметка) уведомление может появляться в верхней части страницы.
Yii Flash отлично здесь подходят, но они отображаются только в верхней части страниц. Например, в настоящее время добавление заметки требует обновления страницы. Люди будут видеть подсказку уведомления в верхней части страницы после публикации своего примечания:

Итак, пока наша дизайн еще в процессе, я только что создал скрытые элементы флэш-уведомлений над областями, показывающими места и над областью, показывающей время. Всякий раз, когда люди вносят изменения, они будут видеть уведомление рядом с действием мыши.

В будущем мы не будем показывать эти уведомления опытным пользователям.
Итак, если человек вносит изменения в какое-то время или место, мы вызываем displayNotifier():
// users can say if a time is an option for them $('input[name="meeting-time-choice"]').on('switchChange.bootstrapSwitch', function(e, s) { // set intval to pass via AJAX from boolean state if (s) state = 1; else state =0; $.ajax({ url: '$urlPrefix/meeting-time-choice/set', data: {id: e.target.id, 'state': state}, success: function(data) { displayNotifier('time'); refreshSend(); refreshFinalize(); return true; } }); }); JS;
Этот код в /frontend/views/meeting/view.php гарантирует, что они не будут видеть оповещения повторно через сеанс. Он использует переменные сеанса для отслеживания того, была ли подсказка уже показана:
$session = Yii::$app->session; if ($session['displayHint']=='on' || $model->status == $model::STATUS_PLANNING ) { $notifierOkay='off'; $session->remove('displayHint'); } else { $notifierOkay='on'; } ?> <input id="notifierOkay" value="<?= $notifierOkay ?>" type="hidden"> <?php $script = <<< JS var notifierOkay; // meeting sent already and no page change session flash if ($('#notifierOkay').val() == 'on') { notifierOkay = true; } else { notifierOkay = false; } function displayNotifier(mode) { if (notifierOkay) { if (mode == 'time') { $('#notifierTime').show(); } else if (mode == 'place') { $('#notifierPlace').show(); } else { alert("We\'ll automatically notify the organizer when you're done making changes."); } notifierOkay=false; } }
Что дальше?

Я описал мое видение, которое я пытаюсь создать при планировании действий между участниками. В следующем уроке я покажу вам, как мы отслеживаем время, чтобы узнать, когда и как доставлять обновления. И я покажу вам, как мы доставляем обновления по электронной почте. Я также покажу вам, как мы создаем текстовое резюме последних изменений, которые были сделаны.
Пока вы ждете, попробуйте функцию уведомлений и запланируйте первую встречу. Кроме того, я был бы признателен, если вы поделитесь своим опытом ниже в комментариях, меня всегда интересуют ваши предложения. Вы также можете связаться со мной в Twitter @reifman.
Следите за предстоящими учебными пособиями в разделе серии Пишем свой стартап на PHP. Есть еще несколько больших функций.
Ссылки по теме
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post