Автор материала рассказывает, как ему удалось применить навыки программирования для автоматизации рутинных процессов собственного свадебного мероприятия.

Для большинства людей 3 сентября 2016 было самой обычной субботой, но в моей памяти эта дата останется навсегда, ведь именно в этот день мы с моей супругой сыграли свадьбу.

image

Планируя свадьбу, вам нужно учесть множество моментов: еда, оформление и обстановка, настольные светильники (да, отдельно от оформления), цветы, размещение гостей, транспорт, развлечения и выбор места мероприятия. Вообще, планируя свадьбу, вы сталкиваетесь со множеством неизвестных, однако в одном я был уверен наверняка: ни одна свадьба не обходится без целой кучи списков, вложенных списков, не заканчивающихся до самого конца. Чем больше списков проплывали перед моими глазами, тем чаще я стал задумываться о том, как можно улучшить процесс подготовки. Уж больно неэффективно и вручную выполнялась вся работа. Я был уверен, что технологии наверняка смогут помочь улучшить хотя бы некоторые моменты.

Вы, возможно, удивитесь, узнав, что приглашение людей на свадьбу — дело дорогое (более 380 фунтов на человека). Сначала вам нужно отправить предварительные пригласительные с датой и коротким уведомлением, и только потом — полноценные, с более подробным описанием события. Все это, к тому же отправляется по почте, а значит, происходит медленно. Немало времени отнимают и попытки «поймать» приглашенных и получить от них ответ о том, хотят ли они прийти на праздник с бесплатной едой и выпивкой (хотя, казалось бы, кто не хочет?!). Ну и, наконец, рассылка приглашений не экологична, ведь бумажные открытки — вещь одноразовая, быстро забывается и становится никому не нужной.

Но вернемся к спискам. Гостей мы делим на несколько групп:

  1. Те, кого вы хотели бы видеть на празднике
  2. Те, кто ответил на второе приглашение с просьбой ответа
  3. Те, кто принял приглашение
  4. Те, кто принял приглашение и выбрал еду

Но списки мне нравятся: у них есть некие предопределенные требования, превращающие их в отличный объект для автоматизации.

Послание в бутылке


Я был уверен, что у всех потенциальных гостей, независимо от их возраста, имелся мобильный телефон, а это означало, что настало время Twilio. Приведенный здесь код можно при желании смело пропускать, так как он всегда доступен в соответствующем репозитории GitHub.

SMS как коммуникационный канал прекрасно подходил под мои нужды. Я мог настроить массовую рассылку сообщений, быстро и эффективно обрабатывая ответы. Делая первые рабочие наброски продукта и рассматривая варианты БД, я пытался сделать нечто простое, чем можно было бы легко поделиться и не хотел уделять много времени внешнему виду. В итоге я наткнулся на python-библиотеку gspread, позволявшую читать из таблиц google и писать в них. Это был не самый быстрый, но довольно гибкий вариант, предоставивший возможность легко получать доступ к таблицам и считывать результаты.

Для первого приглашения я создал таблицу с тремя колонками:

  • Name (Имя)
  • Telephone_number (Телефонный номер)
  • Confirmation_status (Статус подтверждения)
  • Contact detail status (Статус контактных данных)
  • Message_count (Количество сообщений, отправленных гостю, пригодилось в будущем)

Завершив ввод основных данных, я прогнал список через gspread, который отправил SMS каждому гостю-обладателю мобильного номера:

import json
import time
import gspread
from oauth2client.client import SignedJwtAssertionCredentials
from twilio.rest import TwilioRestClient
 
# отправка сообщений гостям из таблицы
 
# добавляем имя файла для json, созданного для таблицы
json_key = json.load(open('.json'))
scope = ['https://spreadsheets.google.com/feeds']
 
credentials = SignedJwtAssertionCredentials(json_key['client_email'],
                                            json_key['private_key'].encode(),
                                            scope)
gc = gspread.authorize(credentials)
wks = gc.open("wedding_guests")  # здесь добавляем имя своего workbook-файла
wks_attendees = wks.get_worksheet(0)  # обращаемся к списку посетителей
 
ACCOUNT_SID = 'TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'TWILIO_AUTH_TOKEN'
 
client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
 
# проходимся по списку гостей, значения внутри range заменяем на свои
for num in range(2, 60):
    print "sleeping for 2 seconds"
    time.sleep(2)  # добавляем задержку чтобы мобильный оператор не отфильтровал сообщения
 
    guest_number = wks_attendees.acell('B'+str(num)).value
    guest_name = wks_attendees.acell('A'+str(num)).value
    Message_body = <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-pds">"</span><span class="pl-cce">\n\n</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2709</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span><span class="pl-s"><span class="pl-pds">"</span> Save the date! <span class="pl-pds">"</span></span><span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2709</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span><span class="pl-s"><span class="pl-pds">"</span><span class="pl-cce">\n\n</span>Lauren Pang and Thomas Curtis are delighted to invite you to our wedding.<span class="pl-cce">\n\n</span>Saturday 3rd September 2016. <span class="pl-cce">\n\n</span>Colville Hall,<span class="pl-cce">\n</span>Chelmsford Road,<span class="pl-cce">\n</span>White Roding,<span class="pl-cce">\n</span>CM6 1RQ.<span class="pl-cce">\n\n</span>The Ceremony begins at 2pm.<span class="pl-cce">\n\n</span>More details will follow shortly!<span class="pl-cce">\n\n</span>Please text YES if you are saving the date and can join us or text NO if sadly, you won't be able to be with us.<span class="pl-cce">\n\n</span><span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">\u2764</span><span class="pl-pds">"</span></span>,
    if not guest_number:  # гостей без мобильного номера пропускаем
        print guest_name + ' telephone number empty not messaging'
        wks_attendees.update_acell('E'+str(num), '0')  
 
    else:
        print 'Sending message to ' + guest_name
        client.messages.create(
            to="+" + guest_number,  # Добавляем + для соответствия e.164
            from_="",  # сюда вставляем свой номер Twillio
            body=message_body,
        )
        wks_attendees.update_acell('E'+str(num), int(wks_attendees.acell('E'+str(num)).value) + 1)  # increment the message count row
else:                  # else-часть цикла
    print 'finished'

И поскольку текстовые SMS обычно выглядят скучновато, я добавил немного юникода, чтобы их оживить. Вот как выглядело сообщение для приглашенных счастливчиков:

image

Далее я воспользовался Flask в качестве веб-сервера, сделал так, чтобы мой реквест URL для Twilio Messaging указывал на /messages и добавил простые if-проверки для парсинга ответов:

@app.route("/messages", methods=['GET', 'POST'])
def hello_guest():

    if "yes" in body_strip:
        # Ищем гостя и обновляем его confirmation_status
        wks_attendees.update_acell("F"+str(guest_confirmation_cell.row), 'Accepted')  # обновляем статус на «приглашение принято» для этого гостя
        resp.message(u"\u2665" + "Thanks for confirming, we'll be in touch!" + u"\u2665")  # rотвечаем гостю, подтвердившему участие

    elif "no" in from_body.lower():
        # обновляем статус на «приглашение отклонено» для этого гостя
        wks_attendees.update_acell("F"+str(guest_confirmation_cell.row), 'Declined')
        # отвечаем отказавшему гостю
        resp.message("Sorry to hear that, we still love you though!")

    else:  # отвечаем тем, кто дал некорректный ответ
        resp.message("You sent a different keyword, we need a yes or a no, you sent: "+  
                     from_body)
    return str(resp)

imageimage

Первое сообщение было отправлено в 8:37 утра 19 февраля, а первое подтверждение получено чуть позже в 8:40. К 9:38 я получил уже 23 подтверждения, то есть 32% ответов были у меня в кармане! Через 2 дня после начала массовой рассылки свое участие подтвердили уже 58% гостей. Несмотря на очевидный успех моя будущая жена пока что не была на все 100% впечатлена моим SMS-сервисом свадебных приглашений, и я решил добавить приложению еще немного функциональности.

Статистика! Я мог формировать актуальный список гостей и предоставлять его по первому запросу, давая моей будущей невесте мгновенную обратную связь. Код оказался довольно простым, так как я уже настраивал до этого некоторые простейшие счетчики в таблице и потому все сводилось к получению содержания отдельных ячеек и их добавлению в SMS:

# переменные статуса гостей
guest_confirmed = wks_attendees.acell('C70').value
guest_unconfirmed = wks_attendees.acell('C71').value
guest_no_response = wks_attendees.acell('C72').value
guest_acceptance = wks_attendees.acell('C73').value


elif "numbers" in from_body.lower():
    # возвращаем статистику (общее кол-во гостей, список выбранных блюд)
    resp.message("R.S.V.P update:\n\nTotal Accepted: " + guest_confirmed  
                 "\n\nTotal declined: "   guest_unconfirmed   "\n\nTotal no response: "+  
                 guest_no_response + "\n\nTotal acceptance rate: " + guest_acceptance)

Пример отправляемой этим кодом SMS:

image

Выглядит, быть может, не очень красиво, но зато весьма информативно.

Тот факт, что Лорен теперь могла следить за автоматическими обновлениями списка гостей избавил нас от многих головных болей. В итоге я получил от нее добро на повсеместную интеграцию SMS и вскоре этот инструмент использовался едва ли не во всех процессах, где это только было возможно. Некоторые из применений были очевидны, например, отправка SMS-уведомлений о запуске свадебного сайта (сделанного, к слову, на Heroku), или работа со списками свадебных подарков и многие другие решения, которыми я горжусь и по сей день.

Славный пир


После составления списка приглашенных и сбора ответов, дело дошло до занятия, которое обычно откладывают на потом — выяснение вкусовых предпочтений гостей. Первым шагом была отправка еще одной SMS, сообщающей гостям о необходимости посетить веб-сайт и выбрать категории подаваемой еды с помощью google-формы. Выглядела форма вполне обыкновенно, но заполняла тот же файл, в котором находились данные о посетителях. Таким образом, теперь у нас была как таблица принявших приглашения посетителей, так и таблица заполнивших форму выбора еды. Если бы речь шла о каком-нибудь обычном средстве автоматизации, мне пришлось бы ждать, пока гости будут медленно выбирать свои блюда, однако моя свадьба проходила при поддержке Twillio, и это значит, что я мог добиваться ответа от гостей с минимальными усилиями.

Необходимо было сверять две таблицы по имени гостей и обновлять статус выбранных блюд при поступлении новой информации. Это потребовало некоторых дополнительных наворотов в коде, но как только я закончил с ними, я мог по первой необходимости запускать скрипт, получая на выходе SMS с актуальной информацией:

import json
import time
import gspread
from oauth2client.client import SignedJwtAssertionCredentials
from twilio.rest import TwilioRestClient

# добавляем имя файла для json, созданного для таблицы
json_key = json.load(open(''))
scope = ['https://spreadsheets.google.com/feeds']

credentials = SignedJwtAssertionCredentials(json_key['client_email'],
                                            json_key['private_key'].encode(),
                                            scope)
gc = gspread.authorize(credentials)
wks = gc.open("")  # здесь добавляем имя своей таблицы
wks_attendees = wks.get_worksheet(0)  # список гостей
wks_food = wks.get_worksheet(1)  # список выбора блюд

ACCOUNT_SID = 'TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'TWILIO_AUTH_TOKEN'

client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)

# Обрабатываем список гостей. Кол-во указано вручную чтобы никого не забыть
for num in range(2, 60):
    food_guest_name = wks_food.acell('B'+str(num)).value  # ячейка из колонка с названиями блюд

    if food_guest_name:
        attendees_name = wks_attendees.find(val_food_guest_name).value
        attendees_name_row = wks_attendees.find(val_food_guest_name).row
        menu_status = wks_attendees.acell("G"+str(attendees_name_row)).value

        if food_guest_name == attendees_name:
            print
            if menu_status == 'Y':  # данные уже сравнили, идем дальше
                print('Skipping')

            else:  # пользователь сделал свой выбор, обновляем основную таблицу
                print ('Food sheet name ' + food_guest_name + 'Attendees sheet name ' + attendees_name)
                # обновляем строку выбора меню
                wks_attendees.update_acell("G"+str(attendees_name_row), 'Y')
        else:
            print('nothing found, moving on')
            wks_attendees.update_acell('E'+str(num), int(wks.acell('E'+str(num)).value) + 1)  # инкремент строки подсчета сообщений

    else:
        # отправляем админу сообщение о завершении обновления статистики
        client.messages.create(from_="",  # номер Twillio
                               to="",  # номер админа
                               body="Finished processing current meal listnnGuest meals confirmed" + guest_meals_confirmed + "\n\nGuest meals unconfirmed: " + guest_meals_unconfirmed)

Теперь, когда в моем распоряжении был точный список гостей и постоянно пополняющийся список блюд, имело смысл сделать эту статистику общедоступной с помощью главного приложения. Для этого требовалось лишь добавить содержимое соответствующих ячеек в SMS-ответ:

# отправляем актуальные данные об общем количестве еды и выборе блюд
elif "food" in body_strip.strip():

    resp.message("Guest meals decided:" + guest_meals_confirmed + 
                 "\nGuest meals undecided: " + guest_meals_unconfirmed +
                 "\n\nMenu breakdown:\n\n" + starter_option_1 +": " +
                 starter_option_1_amount + "\n" + starter_option_2 +": " +
                 starter_option_2_amount + "\n" + starter_option_3 +": " +
                 starter_option_3_amount + "\n" + main_option_1 +": " +
                 main_option_1_amount + "\n" + main_option_2 +": " + main_option_2_amount +
                 "\n" + main_option_3 +": " + main_option_3_amount + "\n" +
                 dessert_option_1 + ": " + dessert_option_1_amount + "\n" + dessert_option_2
                 + ": " + dessert_option_2_amount)

image

Эта мера оказалась очень полезной, поскольку позволяла обслуживающей праздник фирме быть в курсе нашего прогресса и предоставляла очень полезную с практической точки зрения информацию о тех, кто еще не сделал свой выбор. Следующим претендентом на автоматизацию был процесс получение ответов от гостей. Для этого нужно было всего лишь проходить по списку, находить в нем «нарушителей», не выбравших себе блюда, и отправлять им сообщения!

for num in range(2, 72):  # и снова вручную чтобы никого не забыть
    print "sleeping for 3 seconds"

    time.sleep(3)  # опять небольшая задержка чтобы оператор не отсеял сообщение как спам
    wedding_guest_number = wks_attendees.acell('B'+str(num)).value  # берем номер гостя
    wedding_guest_name = wks_attendees.acell('A'+str(num)).value  # берем имя гостя
    menu_guest = wks_attendees.acell('G'+str(num)).value

    if not wedding_guest_number:
        print wedding_guest_name+' telephone number empty not messaging'  # выведем на консоль, что у гостя нет тел. Номера и мы не можем связаться с ним
        wks_attendees.update_acell('H'+str(num), '1')  # инкремент строки подсчета сообщений для отдельного пользователя
    else:
        if menu_guest == "N":  # гость не выбрал еду! НЕ ОТСТАВАТЬ ОТ НЕГО ДО ПОСЛЕДНЕГО!
            print 'Sending message to '+wedding_guest_name
            client.messages.create(
                to="+" + wedding_guest_number,
                from_="",  # ваш номер Twillio
                body="If you have received this message, you have not chosen your food options for Tom & Lauren's Wedding!\n\nYou can pick your choices via the website, no paper or postage required!\n\nhttp://www.yourwebsitehere.com/food"
            )
            wks_attendees.update_acell('H'+str(num), int(wks_attendees.acell('H'+str(num)).value) + 1)  # инкремент строки подсчета сообщений для отдельного пользователя
else:                  # else-часть цикла
    print 'finished'

image

Большой день близился быстрее, чем мы могли себе представить. Единственное, что нам оставалось сделать — отправить последнее SMS, напоминающее гостям об основных деталях и необходимости вооружиться зонтом, который поможет защититься от типично дождливого британского лета:

image

В заключение


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

Создание масштабируемых решений для комплексных задач никогда не бывает простым, и даже один из конечных вариантов моего приложения местами еле справлялся с поставленными задачами. Изначально я планировал разработать более комплексное решение, с визуализацией прогресса, интеграцией голоса, менее зависимое от CLI-скриптов, но время одержало в этой гонке верх. В целом я доволен тем, как все получилось. Идеальных коммуникационных систем не бывает. Всегда нужно использовать наиболее подходящий для вашей аудитории канал, будь то SMS, Voice, Chat, Video или семафор.

Если захотите поговорить об автоматизации свадеб, пишите мне в Twitter.

image
Поделиться с друзьями
-->

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


  1. Tirathangil
    26.04.2017 13:45
    +1

    … И тут я вспомнил «Записки невесты программиста». :)


    1. tmin10
      26.04.2017 14:04

      Правда там всё вышло не совсем удачно…


      1. Tirathangil
        26.04.2017 15:25

        В статье о трудностях не рассказали. Прозреваю в междустрочье, что проблемы таки были. И возможно, не менее анекдотичные.


  1. SkyPhantasm
    26.04.2017 14:30
    +3

    -Согласен ли ты…
    -Да да да, далее далее *убирает «установить яндекс бар»* СОГЛАСЕН.


  1. willmore
    27.04.2017 00:22
    +1

    Спасибо!
    Жаль, что это перевод, а то аж захотелось виртуально пожать руку автору. Статьи а-ля «давайте запилим очередной %project_name%» это капец как скучно, а вот автоматизация такого жизненного события, как свадьба, на питоне — это как раз по мне. (И, чую, мои преподы бы кипятком писали :) ) В общем, если я все-таки смогу найти ту самую, я уже знаю, на что потрачу время предсвадебной подготовки :D