Автоматически экспортируем Google Forms в Notion с помощью IFTTT и Django

Всем доброго дня! Думаю, статья будет интересна всем, кто пользуется Notion, но по какой-то причине не мог переехать на него полностью.

Предистория


Я разрабатываю свой проект. На лэндинге после ввода емейла выдается ссылка на соцопрос на базе Google Forms. Ответы записываются в табличечку на Google Drive.

Проблема в том, что все свое я ношу с собой сохраняю в Notion. Это банально удобней. Обходился ручным копипастом, пока отзывов было мало. Потом их стало больше — и надо было что-то придумать. Кому интересно, что вышло — добро пожаловать под кат.

Проблема


Google Forms записывают ответы только в табличечку — то есть тут никакого другого рецепта нет. Поэтому у меня родился план: давай через IFTTT слушать апдейты табличечки, пересылать на вебхук новые данные, там их как-то обрабатывать и загружать в Notion.

Для тех, кто не знаком с IFTTT: это сервис, который позволяет делать цепочки из действий. Скажем, «пришел пост в телеграм» — «экспортируем его ВКонтакте».

План начал сбоить: у Notion нет официального API. Но кто-то реверснул его и сделал неофициальное API.

image

Окончательный план был такой:

  • Делаем апплет в IFTTT: «Добавлена строка в табличку — отсылаем ее на сервер
  • Делаем непосредственно сервер, который принимает данные и отправляет их в Notion

Вторая проблема появилась, когда выяснилось, что у IFTTT сломалась интеграция с Google Sheets, и поэтому апплет не работает.

image

Поэтому пришлось изменить план: выкачиваем csv'шку с Google Sheets, парсим ее на сервере и кидаем все новое в Notion. IFTTT же используем как триггер для всего процесса.

Часть 1. CSV с Google Sheets


Эта часть, пожалуй, самая легкая. Открываем таблицу для просмотра (чтобы не пришлось возиться с куками). Далее берем и копируем ссылку на экспорт CSV. Для сего действия легким нажатием по клавиатуре набираем Ctrl + Shift + J (то есть открываем консоль разработчика), идем на вкладку Network. Потом жмем на Файл — Скачать — CSV. Видим запрос и копируем ссылку.

Часть 2. Пишем сервер


Так как библиотека у нас питоновская, будем писать на Django.

Теперь немного про структуру конкретно моей таблицы. Таблица в Notion, в отличие от таблицы в Google Sheets, имеет колонку „Reference“. Это ссылка на другую таблицу (в моем случае — на описание функций, которые понравились пользователям). Остальное в целом понятно: просто столбцы с просто данными.

Идем в Notion, уже привычным Ctrl + Shift + J открываем консоль, идем в Application — Cookies, копируем token_v2 и называем его TOKEN. Потом идем на нужную нам страницу с табличкой и копируем ссылку на нее. Называем NOTION. Если у вас тоже есть Relation, идем на страницу с Relation, копируем ссылку и называем, например, NOTION_FUNCTIONS

Далее пишем следующий код (предварительно импортируем notion):

def index(request):
    if request.method == "POST":
        client = NotionClient(token_v2=TOKEN)

        database = client.get_collection_view(NOTION)
        current_rows = database.default_query().execute()
        database_functions = client.get_collection_view(NOTION_FUNCTIONS)
        current_rows_functions = database_functions.default_query().execute()

В нем мы подключаем NotionClient, говорим „Базы данных? Дайте две!“ и получаем непосредственно данные с этих двух табличек (дефолтным запросом, но можно и с сортировкой, подробнее — в документации к библиотеке).

Потом мы должны сделать следующее: запросить CSV у гугла и распарсить ее. Делать мы это будем pandas'ом.

result = requests.get(SHEET).content
pandas_result = pd.read_csv(io.StringIO(result.decode('utf-8')))

timestamps = pandas_result[["Отметка времени"]].values
ages = pandas_result[["Ваш возраст"]].values
sexes = pandas_result[["Ваш пол"]].values
cities = pandas_result[["Ваш город"]].values
socials = pandas_result[["Ссылка на соцсеть (просто чтобы проанализировать получше)"]].values
agreements = pandas_result[["Можно ли вам написать, если есть какой-то вопрос."]].values
control_usages = pandas_result[["Какие примеры из области управления вас заинтересовали"]].values
health_usages = pandas_result[["Какие примеры использования из области здоровья вас  заинтересовали"]].values
prices = pandas_result[["За какую цену вы бы готовы были купить устройство. Можно с пояснением :)"]].values
mentions = pandas_result[["Предложения, замечания, негативные моменты по лендингу или в целом"]].values

Потом мы должны пройтись по всем данным с этой таблички и проверить, добавлены ли они в Notion или еще нет. Для этого мы и запрашивали данные с табличек.

def checkTimestamp(rows, timestamp):
    for i in range(0, len(rows)):
        row = rows[i]

        if row.name == timestamp:
            return True

    return False

Отдельно стоит сказать про „row.name“, потому что внимательный читатель наверняка спросит: а это вообще что такое-то?

Это название колонки в Notion (где хранятся времена записи). У меня как-то не получилось с русскими названиями добавлять, поэтому я изменил все названия на английские и добавляю по ним.

image

И теперь код на проверку данных и добавление строки в табличку Notion:

for i in range(0, len(timestamps)):
     if not checkTimestamp(current_rows, timestamps[i]):
           row = database.collection.add_row()

           health_usage = health_usages[i][0]
           control_usage = control_usages[i][0]
           ticks = health_usage + "," + control_usage

           row.title = timestamps[i][0]
           row.age = ages[i][0]
           row.sex = sexes[i][0]
           row.social_network = checkEmptiness(socials[i][0])
           row.can_we_write_you = checkEmptiness(agreements[i][0])
           row.city = checkEmptiness(cities[i][0])
           row.controlling_examples = checkEmptiness(control_usages[i][0])
           row.health_examples = checkEmptiness(health_usages[i][0])
           row.cost = checkEmptiness(prices[i][0])
           row.noticements = checkEmptiness(mentions[i][0])
           row.castdev_relation = findIds(current_rows_functions, ticks)

checkEmptiness — это функция, которая проверяет, нулевая ли штука в нее была передана. Notion как-то с неохотой работал, когда я ему кормил нулевые поля, поэтому стоит написать.

Теперь перейдем к разбору Relation'ов, потому что в официальной документации я про это не видел. Чтобы сделать ссылку на строчку с другой базы данных, надо взять ее (этой строки) айдишник и передать. Соответственно, если подразумевается массив ссылок на строки из другой таблички, надо взять массив их айдишников. Я лично добавлял Relation'ы по названиям функций.

def findIds(current_rows, titles):
    print("titles", titles)
    print("current rows", current_rows)
    array = []

    for a in range(0, len(current_rows)):
        if current_rows[a].name in titles:
            array.append(current_rows[a].id)

    print("Ids", array)
    return array

В конце после создания строчек добавляем ответ, чтобы на том конце знали, что запрос дошел.

return HttpResponse("Hello, habr.")

Тащемта с самым главным по серверу закончили, переходим к IFTTT.

Часть 3. IFTTT


Переходим на вкладку создания апплетов. Выбираем триггер (в нашем случае — это Date&time), ставим „каждый час“. Выбираем триггерируемым (то есть „that“) Webhook, указываем наш (пока что) локальный адрес, дабы потестить. Ну и все. Тестим.

Часть 4. Heroku


Вы думали, для чего мы возились с вот с этим триггерением со стороны IFTTT — это для того, чтобы не платить. Heroku предлагает бесплатный тариф для хостинга нашей штучки. Главное — чтобы сервис спал минимум 6 часов. А он точно будет спать, потому что мы его зовем работать каждый час, а не каждую минуту.

image

Далее делаем следующее. Идем в heroku создавать новый проект. Далее устанавливаем на свою операционную систему их клиент. А потом делаем все согласно инструкциям, появившимся после создания приложения.

Загрузив все на heroku, переходим в наш апплет и редактируем урл на новый.

Теперь каждый час список должен обновляться. Гипотетически IFTTT может выдавать ошибку, что какой-то у вас долгий реквест, но это не столь важно.

Апдейт


Это оказалось таки важным. Когда IFTTT ловит постоянные ошибки, он начинает скипать апплеты.
Чтобы решить данную проблему, просто запускаем новый поток для вот этой всей всячины, сразу отдавая ответ.
if request.method == "POST":
   thread = Thread(target=run_notion_import)
   thread.start()

   return HttpResponse("Hello, habr.")


Еще одна идейка, которую я забыл озвучить в статье — это проверять на нулльность стандартным pandas'овским методом.
То есть ваша проверка будет выглядеть примерно вот так:
if not pd.isna(health_usages[i][0]):
   row.health_examples = health_usages[i][0]

Средняя зарплата в IT

111 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 268 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое