Рубрики
Без рубрики

Использование сигналов Django для упрощения и разделения кода

Автор оригинала: Robley Gori.

Использование сигналов Django для упрощения и разделения кода

Вступление

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

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

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

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

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

Сигналы с первого взгляда

Сигналы Django-это реализация паттерна Observer . В таком шаблоне проектирования реализуется механизм подписки, в котором несколько объектов подписываются или “наблюдают” за конкретным объектом и любыми событиями, которые могут с ним произойти. Хорошая аналогия заключается в том, как все подписчики канала YouTube получают уведомление, когда создатель контента загружает новый контент.

С помощью “диспетчера сигналов” Django может распределять сигналы в несвязанном наборе на зарегистрированные “приемники” в различных компонентах системы. Сигналы регистрируются и срабатывают всякий раз, когда происходят определенные события, и любой слушатель этого события получит уведомление о том, что событие произошло, наряду с получением некоторых контекстуальных данных в полезной нагрузке, которые могут иметь отношение к функциональности приемника. Приемником может быть любая функция или метод Python. Подробнее об этом позже.

Помимо диспетчера сигналов, Django также поставляется с некоторыми полезными сигналами, которые мы можем слушать. Они включают в себя:

  • post_save , который отправляется всякий раз, когда новая модель Django была создана и сохранена. Например, когда пользователь подписывается или загружает новый пост,
  • pre_delete , который отправляется непосредственно перед удалением модели Django. Хорошим сценарием будет то, когда пользователь удаляет сообщение или свою учетную запись,
  • request_finished , который запускается всякий раз, когда Django завершает обслуживание HTTP-запроса. Это может варьироваться от открытия веб-сайта или доступа к определенному ресурсу.

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

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

from django.dispatch import Signal, receiver

# Create a custom signal
ping_signal = Signal(providing_args=["context"])

class SignalDemo(object):
    # function to send the signal
    def ping(self):
        print('PING')
        ping_signal.send(sender=self.__class__, PING=True)

# Function to receive the signal
@receiver(ping_signal)
def pong(**kwargs):
    if kwargs['PING']:
        print('PONG')

demo = SignalDemo()
demo.ping()

В этом простом скрипте мы создали класс с методом отправки сигнала и отдельной функцией вне класса, которая будет принимать и отвечать. В нашем случае отправитель сигнала отправит команду PING вместе с сигналом, а функция приемника проверит, присутствует ли команда PING , и распечатает PONG в ответ. Сигнал создается с помощью класса Signal Django и принимается любой функцией, имеющей @receiver | decorator .

Вывод скрипта:

$ python signal_demo.py

PING
PONG

Обычно мы должны были бы вызвать функцию ping() из функции ping () , но с сигналами мы можем получить аналогичное, но несвязанное решение. Функция ping() теперь может находиться в другом файловом проекте и по-прежнему реагировать на наш сигнал PING .

Когда использовать сигналы

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

  • Когда у нас есть много отдельных фрагментов кода, заинтересованных в одних и тех же событиях, сигнал поможет распределить уведомление о событии, а не вызывать все различные фрагменты кода в одной и той же точке, что может привести к беспорядку и появлению ошибок
  • Мы также можем использовать сигналы Django для обработки взаимодействий между компонентами в развязанной системе в качестве альтернативы взаимодействию с помощью механизмов RESTful communication
  • Сигналы также полезны при расширении сторонних библиотек, где мы хотим избежать их изменения, но должны добавить дополнительную функциональность

Преимущества сигналов

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

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

Демонстрационный проект

В нашем демо-проекте мы создадим простую доску вакансий, где пользователи будут заходить на сайт, просматривать доступные вакансии и выбирать вакансию для подписки. Пользователи подпишутся, просто отправив свой адрес электронной почты, и будут уведомлены о любых изменениях в задании. Например, если требования изменяются, открытие вакансии закрывается или если проводка вакансии снимается. Все эти изменения будут выполняться администратором, у которого будет панель мониторинга для создания, обновления и даже удаления объявлений о вакансиях.

В духе разделения нашего приложения мы создадим основное приложение Jobs Board и отдельное приложение уведомлений, которому будет поручено уведомлять пользователей по мере необходимости. Затем мы будем использовать сигналы для вызова функций в приложении уведомлений из основного приложения Jobs Board.

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

Настройка проекта

Рекомендуется создавать проекты Python в виртуальной среде, чтобы мы работали в изолированной среде, которая не влияет на настройку Python системы, поэтому мы будем использовать Pipenv.

Давайте сначала установим наше окружение:

# Set up the environment
$ pipenv install --three

# Activate the virtual environment
$ pipenv shell

# Install Django
$ pipenv install django

Django поставляется с некоторыми командами, которые помогают нам выполнять различные задачи, такие как создание проекта, создание приложений, миграция данных и тестирование кода. Чтобы создать наш проект:

# Create the project
$ django-admin startproject jobs_board && cd jobs_board

# Create the decoupled applications
$ django-admin startapp jobs_board_main
$ django-admin startapp jobs_board_notifications

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

$ python manage.py migrate
$ python manage.py runserver

Когда мы получаем доступ к локальному запущенному экземпляру нашего проекта Django, мы должны увидеть следующее:

джанго настроился

Это означает, что мы успешно настроили ваш проект Django и теперь можем приступить к реализации нашей логики.

Реализация

Django основан на шаблоне архитектуры model-view-template, и этот шаблон также будет направлять нашу реализацию. Мы создадим модели для определения наших данных, затем реализуем представления для обработки доступа к данным и манипулирования ими, и, наконец, шаблоны для отображения наших данных конечному пользователю в браузере.

Чтобы наши приложения были интегрированы в основное приложение Django, мы должны добавить их в jobs_board/settings.py в разделе INSTALLED_APPS , следующим образом:

INSTALLED_APPS = [
    # Existing apps remain...

    # jobs_board apps
    'jobs_board_main',
    'jobs_board_notifications',
]

Часть 1: Основное приложение Jobs Board

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

Давайте начнем с создания наших моделей в jobs_board_main/models.py :

# jobs_board_main/models.py

class Job(models.Model):
    company = models.CharField(max_length=255, blank=False)
    company_email = models.CharField(max_length=255, blank=False)
    title = models.CharField(max_length=255, blank=False)
    details = models.CharField(max_length=255, blank=True)
    status = models.BooleanField(default=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

class Subscriber(models.Model):
    email = models.CharField(max_length=255, blank=False, unique=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

class Subscription(models.Model):
    email = models.CharField(max_length=255, blank=False, unique=True)
    user = models.ForeignKey(Subscriber, related_name="subscriptions", on_delete=models.CASCADE)
    job = models.ForeignKey(Job, related_name="jobs", on_delete=models.CASCADE)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

Мы создаем модель для определения нашей Вакансии проводки, которая будет иметь только название компании и сведения о вакансии наряду со статусом открытия вакансии. У нас также будет модель хранения наших подписчиков, взяв только их адреса электронной почты. Подписчики и Задания объединяются с помощью модели Subscription , в которой мы будем хранить сведения о подписках на публикации заданий.

С нашими моделями на месте нам нужно сделать миграции и перенести их, чтобы иметь таблицы, созданные в базе данных:

$ python manage.py makemigrations
$ python manage.py migrate

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

Мы начнем с создания представления, которое будет обрабатывать отображение всех наших заданий:

# jobs_board_main/views.py

from .models import Job

def get_jobs(request):
    # get all jobs from the DB
    jobs = Job.objects.all()
    return render(request, 'jobs.html', {'jobs': jobs})

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

Django поставляется с шаблонизатором Jinja templating engine , который мы будем использовать для создания HTML-файлов, которые будут отображаться конечному пользователю. В нашем приложении jobs_board_main мы создадим папку templates , в которой будут размещены все HTML-файлы, которые мы будем визуализировать конечным пользователям.

Шаблон для визуализации всех заданий будет отображать все задания со ссылками на отдельные проводки заданий следующим образом:





  
    Jobs Board Homepage
  
  
    

Welcome to the Jobs board

{% for job in jobs %} {% endfor %}

Мы создали модель Job , представление get_jobs для получения и отображения всех представлений и шаблон для отображения списка заданий. Чтобы объединить всю эту работу, мы должны создать конечную точку, из которой будут доступны задания, и мы делаем это, создавая urls.py файл в нашем jobs_board_main_application :

# jobs_board_main/urls.py

from django.urls import path
from .views import get_jobs

urlpatterns = [
    # All jobs
    path('jobs/', get_jobs, name="jobs_view"),
]

В этом файле мы импортируем наше представление, создаем путь и прикрепляем к нему наше представление. Теперь мы зарегистрируем URL-адреса вашего приложения в главном urls.py файл в папке проекта jobs_board :

# jobs_board/urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('jobs_board_main.urls')), # <--- Add this line
]

Наш проект уже готов к тестированию. Это то, что мы получаем, когда запускаем приложение и переходим к localhost:8000/jobs :

работа доска посадка 1

В настоящее время у нас нет рабочих мест. Django поставляется с приложением администрирования, которое мы можем использовать для ввода данных. Во-первых, мы начинаем с создания суперпользователя:

создать суперпользователя

После создания суперпользователя нам нужно зарегистрировать наши модели в admin.py файл в нашем jobs_board_main приложении:

# jobs_board_main/admin.py
from django.contrib import admin
from .models import Job

# Register your models here.
admin.site.register(Job)

Мы перезапускаем наше приложение и переходим к localhost:8000/admin и входим в систему с учетными данными, которые мы только что установили. Таков результат:

jobs board admin 1

Когда мы нажимаем на знак плюс в строке “Вакансии”, мы получаем форму, в которой заполняем подробную информацию о нашей вакансии:

ввод данных о заданиях

Когда мы сохраняем задание и возвращаемся к конечной точке jobs , нас встречает только что созданная публикация задания:

работа доска посадка 2

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

Наш jobs_board_main/views.py будет расширен следующим образом:

# jobs_board_main/views.py
# previous code remains
def get_job(request, id):
    job = Job.objects.get(pk=id)
    return render(request, 'job.html', {'job': job})

def subscribe(request, id):
    job = Job.objects.get(pk=id)
    sub = Subscriber(email=request.POST['email'])
    sub.save()

    subscription = Subscription(user=sub, job=job)
    subscription.save()

    payload = {
      'job': job,
      'email': request.POST['email']
    }
    return render(request, 'subscribed.html', {'payload': payload})

Нам также нужно будет создать шаблон для единого представления вакансии в templates/job.html , который включает в себя форму, которая будет принимать электронную почту пользователя и подписывать его на публикацию вакансии:



  
    Jobs Board - {{ job.title }}
  
  
      

{{ job.title }} at {{ job.company }}

{{ job.details }}


Subscribe to this job posting by submitting your email

{% csrf_token %}

Как только пользователь подпишется на задание, нам нужно будет перенаправить его на страницу подтверждения. subscribed.html шаблон будет выглядеть следующим образом:





  
    Jobs Board - Subscribed
  
  
      

Subscription confirmed!

Dear {{ payload.email }}, thank you for subscribing to {{ payload.job.title }}

Наконец, наша новая функциональность должна быть открыта через конечные точки, которые мы добавим к нашим существующим jobs_board_main/urls.py следующим образом:

# jobs_board_main/urls.py
from .views import get_jobs, get_job, subscribe

urlpatterns = [
    # All jobs
    path('jobs/', get_jobs, name="jobs_view"),
    path('jobs/', get_job, name="job_view"),
    path('jobs//subscribe', subscribe, name="subscribe_view"),
]

Теперь мы можем протестировать наше основное приложение Jobs Board, просмотрев списки вакансий, нажав на одну из них и отправив адрес электронной почты, который будет получать обновления.

Теперь, когда у нас есть работающее приложение, пришло время вводить сигналы Django и уведомлять пользователей/подписчиков о том, когда происходят определенные события. Объявления о вакансиях привязаны к определенной компании, чью электронную почту мы записываем, и мы хотим уведомить их, когда новый пользователь подпишется на их объявление о вакансиях. Мы также хотим уведомить подписанных пользователей, когда объявление о вакансии будет удалено.

Чтобы уведомить пользователей о снятии или удалении публикации вакансии, мы будем использовать встроенный в Django сигнал post_delete . Мы также создадим наш сигнал под названием new_subscriber , который мы будем использовать для уведомления компаний, когда пользователи подписываются на их вакансии.

Мы создаем наши пользовательские сигналы, создавая signals.py файл в нашем jobs_board_main приложении:

# jobs_board_main/signals.py
from django.dispatch import Signal

new_subscriber = Signal(providing_args=["job", "subscriber"])

Вот оно! Наш пользовательский сигнал готов к вызову после того, как пользователь успешно подписался на публикацию вакансии следующим образом в нашем jobs_board_main/views.py файл:

# jobs_board_main/views.py

# Existing imports and code are maintained and truncated for brevity
from .signals import new_subscriber

def subscribe(request, id):
    job = Job.objects.get(pk=id)
    subscriber = Subscriber(email=request.POST['email'])
    subscriber.save()

    subscription = Subscription(user=subscriber, job=job, email=subscriber.email)
    subscription.save()

    # Add this line that sends our custom signal
    new_subscriber.send(sender=subscription, job=job, subscriber=subscriber)

    payload = {
      'job': job,
      'email': request.POST['email']
    }
    return render(request, 'subscribed.html', {'payload': payload})

Нам не нужно беспокоиться о сигнале pre_delete , так как Django отправит его нам автоматически непосредственно перед удалением записи о задании. Причина, по которой мы используем pre_delete , а не post_delete signal, заключается в том, что при удалении Задания все связанные подписки также удаляются в процессе, и нам нужны эти данные, прежде чем они также будут удалены.

Давайте теперь потребим сигналы, которые мы только что отправили в отдельном приложении jobs_board_notifications .

Часть 2: Приложение Уведомления о Работе Совета директоров

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

Наше приложение jobs_board_notifications не нуждается во взаимодействии с пользователем, поэтому нам не нужно создавать какие-либо представления или шаблоны для этой цели. Единственная цель для наших jobs_board_notifications состоит в том, чтобы получать сигналы и отправлять уведомления. Мы реализуем эту функциональность в нашем models.py так как он импортируется рано, когда приложение запускается.

Давайте принимать наши сигналы в нашем jobs_board_notifications/models.py :

# jobs_board_notifications/models.py.
from django.db.models.signals import pre_delete
from django.dispatch import receiver

from jobs_board_main.signals import new_subscriber
from jobs_board_main.models import Job, Subscriber, Subscription

@receiver(new_subscriber, sender=Subscription)
def handle_new_subscription(sender, **kwargs):
    subscriber = kwargs['subscriber']
    job = kwargs['job']

    message = """User {} has just subscribed to the Job {}.
    """.format(subscriber.email, job.title)

    print(message)

@receiver(pre_delete, sender=Job)
def handle_deleted_job_posting(**kwargs):
    job = kwargs['instance']

    # Find the subscribers list
    subscribers = Subscription.objects.filter(job=job)

    for subscriber in subscribers:
        message = """Dear {}, the job posting {} by {} has been taken down.
        """.format(subscriber.email, job.title, job.company)

        print(message)

В наших jobs_board_notifications мы импортируем наш пользовательский сигнал, сигнал pre_save и наши модели. Используя @receiver |/decorator , мы фиксируем сигналы и контекстные данные, передаваемые вместе с ними, в качестве аргументов ключевых слов.

Получив контекстные данные, мы используем их для отправки “электронных писем” (напомним, что мы просто печатаем на консоли для простоты) подписчикам и компаниям, когда пользователь подписывается и сообщение о работе удаляется, отвечая на сигналы, которые мы посылали.

Тестирование

После того, как мы создали задание в нашей панели администратора, оно доступно для подписки пользователей. Когда пользователи подписываются, из приложения jobs_board_notifications в компанию, владеющую публикацией, отправляется следующее электронное письмо:

сообщение о подписке на задание

Это доказательство того, что наш сигнал new_subscriber был отправлен из приложения jobs_board_main и получен приложением jobs_board_notifications .

При удалении записи о задании все пользователи, подписавшиеся на нее, получают уведомление по электронной почте следующим образом:

работа удаленная электронная почта

Сигнал Django pre_delete оказался очень кстати, и наш обработчик разослал подписанным пользователям уведомления о том, что конкретная вакансия была удалена.

Резюме

В этой статье мы построили проект Django с двумя приложениями, которые общаются через сигналы Django в ответ на определенные события. Наши два приложения разделены, и сложность связи между нашими приложениями значительно уменьшилась. Когда пользователь подписывается на вакансию, мы уведомляем об этом компанию. В свою очередь, когда объявление о вакансии было удалено, мы уведомляем всех подписавшихся клиентов о том, что объявление о вакансии было удалено.

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

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

Исходный код этого проекта доступен здесь, на Github .