https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-a-social-crm-with-django-and-the-vonage-messages-api/social-crm_django.png

Construire un CRM social avec Django et l'API Messages de Vonage

Publié le January 4, 2022

Temps de lecture : 11 minutes

Dans cet article, vous apprendrez à construire la fonctionnalité principale d'un CRM social en utilisant Django et l'API Messages de Vonage. Notre CRM social aidera les agents de vente et l'équipe d'assistance à la clientèle à communiquer avec des clients potentiels directement sur Facebook en temps réel. Appelons-le Sales Fox.

Pré-requis

  1. Créez une application de messages à partir de votre tableau de bord Vonage. Suivez les étapes décrites ici.

  2. Autorisez Vonage à accéder à votre page professionnelle Facebook et reliez votre application à votre page Facebook. Suivez les étapes décrites ici.

  3. Installer Redis - Si vous utilisez Linux ou Mac, suivez les instructions ici. Si vous utilisez Windows, suivez les instructions ici.

  4. Installer Ngrok. Aller à la page de téléchargement de Ngrok Page de téléchargement de Ngrok et suivez les instructions pour installer Ngrok sur votre ordinateur.

Maintenant que vous avez rempli les conditions préalables, vous devez configurer votre environnement de développement pour le tutoriel. Vous devez configurer votre environnement de développement pour le tutoriel.

Mise en place du projet

  1. Créez et activez votre environnement virtuel

    Créez un répertoire pour votre projet et remplacez votre répertoire de travail par le répertoire que vous venez de créer. Ensuite, exécutez les commandes suivantes pour créer et activer un environnement virtuel pour votre projet.

    python3 -m venv sales-env

    source sales-env/bin/activate

  2. Installer les paquets requis

    Pour installer tous les paquets requis en une seule fois, créez un fichier requirements.txt dans le répertoire créé à l'étape 1. Copiez et collez l'extrait de code ci-dessous dans votre fichier requirements.txt fichier.

    aioredis==1.3.1
    asgiref==3.3.4
    async-timeout==3.0.1
    attrs==21.2.0
    autobahn==21.3.1
    Automat==20.2.0
    certifi==2021.10.8
    cffi==1.14.6
    channels==2.4.0
    channels-redis==2.4.2
    charset-normalizer==2.0.7
    constantly==15.1.0
    cryptography==3.4.7
    daphne==2.5.0
    Django==3.2.2
    djangorestframework==3.12.4
    hiredis==2.0.0
    hyperlink==21.0.0
    idna==3.2
    incremental==21.3.0
    msgpack==0.6.2
    Pillow==8.2.0
    pyasn1==0.4.8
    pyasn1-modules==0.2.8
    pycparser==2.20
    pyOpenSSL==20.0.1
    python-dotenv==0.19.2
    pytz==2021.1
    requests==2.26.0
    service-identity==21.1.0
    six==1.16.0
    sqlparse==0.4.1
    Twisted==21.7.0
    txaio==21.2.1
    typing-extensions==3.10.0.0
    urllib3==1.26.7
    zope.interface==5.4.0

    Maintenant, installez tous les paquets dans requirements.txt en exécutant la commande ci-dessous dans votre terminal.

    pip install -r requirements.txt

  3. Créez votre projet Django

  • Exécutez django-admin startproject sales_fox ce qui suit pour créer le projet Django nommé "sales_fox".

  • Nous allons créer deux applications dans sales_fox : L'application lead_manager pour gérer les prospects et l'application conversation pour que les agents commerciaux puissent communiquer avec des clients potentiels (appelés "leads"). Créons maintenant nos deux applications en exécutant les commandes suivantes.

    python manage.py startapp lead_manager
    python manage.py startapp conversation

Notez que dans ce tutoriel,

  • J'utiliserai indifféremment les mots "prospects" et "clients". Les prospects sont des clients potentiels, il n'est donc pas inutile de les considérer comme des clients lorsque c'est possible.

  • J'utiliserai le terme Project Directory pour désigner le répertoire dans lequel vous avez settings.py. Ce répertoire a été créé lorsque vous avez exécuté django-admin startproject sales_fox.

  • J'utiliserai le terme Overall Directory pour désigner le répertoire que vous avez créé au début de ce tutoriel. Il contient votre dossier d'environnement virtuel, les répertoires d'applications et le répertoire de votre projet

  1. Préparons SalesFox à utiliser Vonage.

  • Créez un fichier .env dans votre répertoire général. Définir FACEBOOK_ID, VONAGE_API_KEY, et VONAGE_API_SECRET. Votre fichier .env devrait ressembler à ceci :

    FACEBOOK_ID=YOUR-LINKED-FACEBOOK-ID
    VONAGE_API_KEY=YOUR-VONAGE-API-KEY
    VONAGE_API_SECRET=YOUR-VONAGE-API-SECRET

    Vous pouvez trouver votre clé d'API et votre secret d'API de Vonage dans la page des paramètres de Vonage. page des paramètres de Vonage. Et votre identifiant Facebook se trouve dans l'onglet Link social channels sur votre page d'application.

    Dans le répertoire de votre projet, allez à settings.pyChargez les variables dans votre fichier .env en utilisant la commande python-dotenv installé à partir de requirements.txt. Ajoutez l'extrait suivant dans settings.py pour charger le fichier .env :

    from  dotenv  import  load_dotenv	
    import  os
    load_dotenv()

load_dotenv charge toutes les variables de notre fichier .env en tant que variables d'environnement. Maintenant, définissons FACEBOOK_ID, VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_MESSAGES_ENDPOINT dans votre fichier settings.py fichier. Il suffit de copier et de coller l'extrait ci-dessous.

FACEBOOK_ID = os.getenv("FACEBOOK_ID")
VONAGE_API_KEY = os.getenv("VONAGE_API_KEY")
VONAGE_API_SECRET = os.getenv("VONAGE_API_SECRET")
VONAGE_MESSAGES_ENDPOINT = "https://api.nexmo.com/v0.1/messages"
  1. Mise en place des fichiers statiques

En settings.py, trouver la variable STATIC_URL et additionner les variables STATICFILES_DIRS et STATIC_FILES en dessous de STATIC_URLVous devriez obtenir quelque chose comme :

STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'

Allez dans votre répertoire général et créez un dossier nommé static. C'est dans ce dossier que vous conserverez tous vos fichiers statiques. Notez que vous ne devriez faire cela que pour un environnement de développement. Dans un environnement de production, vous devriez configurer un magasin externe comme un seau AWS S3 pour servir vos fichiers statiques.

  1. Mise à jour des applications installées et définition de la couche de canaux

Nous devons ajouter channels et les applications que nous avons créées (lead_manager et conversation) à INSTALLED_APPS dans le répertoire settings.py. Votre fichier INSTALLED_APPS dans settings.py devrait ressembler à ceci :

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'lead_manager',
    'conversation',
]

Les canaux Django nous aident à inclure le support WebSocket dans Sales Fox. Une couche de canaux introduit l'utilisation de canaux et de groupes dans SalesFox. Elle nous aide à intégrer des fonctionnalités distribuées dans notre application. Vous pouvez en savoir plus sur les couches de canaux ici. Pour ce projet, j'utiliserai Redis comme couche de canaux. Nous avons installé channels-redis à partir du fichier requirements.txt. Maintenant, ajoutons CHANNEL_LAYER à settings.py. Copiez et collez l'extrait de code ci-dessous :

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('127.0.0.1', '6379')],
        },
    },
}

Les petits détails

Passons maintenant aux choses sérieuses.

Créer des modèles pour l lead_manager app. Ici, nous ajouterons des modèles pour les prospects et les agents. Le modèle Lead représentera les clients et les clients potentiels. Le modèle Agent représentera les vendeurs de SalesFox qui seront en contact avec les clients. Copiez et collez l'extrait de code suivant dans lead_manager/models.py:

from django.db import models
from django.contrib.auth.models import AbstractUser


#Users are staffs or partners that use the CRM
class User(AbstractUser):
    country = models.CharField(max_length=100, blank=True)
    address = models.CharField(max_length=200, blank=True)
    phone_number = models.CharField(max_length=15, blank=True)

  def __str__(self):
      return self.username


class Lead(models.Model):
    LEAD_SOURCES = (
        ('organic_search', 'Organic Search'),
        ('google_ad', 'Google Ad'),
        ('youtube', 'YouTube'),
        ('facebook', 'Facebook'),
        ('instagram', 'Instagram'),
        ('twitter', 'Twitter'),
    )

    MEDIA_CHOICES = (
        ('sms', 'SMS'),
        ('facebook', 'Facebook'),
        ('phone_call', 'Phone call')
    )

    first_name = models.CharField(max_length=25, blank=True)
    last_name = models.CharField(max_length=25, blank=True)
    age = models.IntegerField(default=0)

    facebook_id = models.CharField(max_length=100, blank=True)
    phone_number = models.CharField(max_length=15, blank=True)

    source = models.CharField(
        choices=LEAD_SOURCES, 
        max_length=50,
        blank=True,
        help_text="Where Lead found us",
        default=LEAD_SOURCES[3][0]
    )
    preferred_medium = models.CharField(
        choices=MEDIA_CHOICES, 
        max_length=50,
        default=MEDIA_CHOICES[1][0],
        help_text="Lead's preferred social media for communication"
    )
    active = models.BooleanField(default=False)

    profile_picture = models.ImageField(blank=True, null=True)

    date_created = models.DateTimeField(auto_now_add=True)
    date_updated = models.DateTimeField(auto_now=True)

    agent = models.ForeignKey("Agent", on_delete=models.SET_NULL, null=True, blank=True, related_name='leads')

    def __str__(self):
        return self.first_name

    @property
    def has_agent(self):
        return self.agent is not None


class Agent(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='agent')

    def __str__(self):
        return self.user.username

Nous avons créé un modèle d'utilisateur pour représenter chaque utilisateur de SalesFox - il peut s'agir de gestionnaires de communauté, de représentants régionaux, de l'assistance à la clientèle, etc. Cependant, pour que SalesFox soit le plus léger possible, le seul type d'utilisateur que nous avons est celui des agents.

Le modèle Lead représente des clients potentiels contactés à partir de leur compte Facebook. Le champ facebook_id représente l'identifiant du compte Facebook d'un client. C'est le champ dont nous avons besoin pour que les agents puissent envoyer un message direct aux clients sur Facebook. Le modèle Lead comporte également un champ preferred_medium. Il contient le moyen de communication préféré du client. Nous nous concentrerons uniquement sur la communication via Facebook.

`AUTH_USER_MODEL = 'lead_manager.User'`

Maintenant, créons un modèle Message dans l'application de conversation. Le modèle Message représente un seul message envoyé de/vers SalesFox. Copiez et collez l'extrait de code suivant dans la section models.py de l'application conversation app.

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

LEAD_MODEL = models.Q(app_label='lead_manager', model='Lead')
AGENT_MODEL = models.Q(app_label='lead_manager', model='Agent')
communicating_parties = LEAD_MODEL | AGENT_MODEL

class Message(models.Model):
    body = models.TextField()

    sender_type = models.ForeignKey(
        ContentType, 
        limit_choices_to=communicating_parties,
        null=True, blank=True, on_delete=models.SET_NULL, related_name="sent_messages"
    ) 
    sender_id = models.PositiveIntegerField(null=True, blank=True, db_index=True)
    sender = GenericForeignKey(ct_field='sender_type', fk_field='sender_id')

    receiver_type = models.ForeignKey(
        ContentType, 
        limit_choices_to=communicating_parties,
        null=True, blank=True, on_delete=models.SET_NULL, related_name="received_messages"
    )
    receiver_id = models.PositiveIntegerField(null=True, blank=True, db_index=True)
    receiver = GenericForeignKey(ct_field='receiver_type', fk_field='receiver_id')

    date_created = models.DateTimeField(auto_now_add=True)
    message_key = models.CharField(null=True, blank=True, max_length=50)
    is_delivered = models.BooleanField(default=False)

    def __str__(self):
        return "Message (%s) from %s to %s" % (self.id, self.sender, self.receiver)

Dans notre Message nous disposons de deux relations génériques pour identifier l'expéditeur et le destinataire du message. L'expéditeur et le destinataire peuvent être un lead ou un agent. Cela signifie que seuls les agents ou les prospects peuvent envoyer ou recevoir des messages. Visitez le site ici pour en savoir plus sur les relations génériques dans Django.

Créer une méthode de propriété messages pour le Lead dans lead_manager/models.py. Cette méthode renvoie tous les messages entrants et sortants d'un lead.

Dans lead_manager/models.py, collez les instructions d'importation suivantes.

Sous le modèle Lead créez la méthode de propriété "messages" comme dans l'extrait ci-dessous :

from  django.contrib.contenttypes.models  import  ContentType
from  django.db.models  import  Value
from  itertools  import  chain

@property
def messages(self):
  from conversation.models import Message
  message_type = ContentType.objects.get_for_model(self)
  msgFromLead = Message.objects.filter(sender_id=self.id, sender_type=message_type).annotate(
      from_lead=Value(True, models.BooleanField())
  )

  msgToLead =  Message.objects.filter(receiver_id=self.id, receiver_type=message_type).annotate(
      from_lead=Value(False, models.BooleanField())
  )
  messages = sorted(
      chain(msgFromLead, msgToLead), 
      key=lambda instance: instance.date_created
  )

  return messages

Commençons par les vues.

Dans lead_manager, nous allons créer des vues pour effectuer des opérations CRUD sur le modèle Lead. Allez dans le dossier de l'application lead_manager, puis copiez et collez le code suivant dans views.py pour créer les vues :

from django.shortcuts import render, redirect
from django.urls import reverse
from django.views.generic import TemplateView, ListView, UpdateView, CreateView
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from .models import Lead
from .forms import LeadForm	
    
class HomeView(TemplateView):
    template_name = 'index.html'


class LeadListView(ListView, LoginRequiredMixin):
    template_name = 'lead_manager/lead_list.html'
    queryset = Lead.objects.all()
    context_object_name = 'leads'

    def dispatch(self, request, *args, **kwargs):
        if not (request.user.is_superuser and hasattr(request.user, 'agent')):
            return PermissionDenied
        return super().dispatch(request, *args, **kwargs)


class LeadCreateView(CreateView, LoginRequiredMixin):
    template_name = 'lead_manager/lead_create.html'
    form_class = LeadForm

    def dispatch(self, request, *args, **kwargs):
        if not (request.user.is_superuser and hasattr(request.user, 'agent')):
            return PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('lead_manager:lead_list')


class LeadUpdateView(UpdateView, LoginRequiredMixin):
    template_name = 'lead_manager/lead_update.html'
    queryset = Lead.objects.all()
    form_class = LeadForm

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(request.user, 'agent'):
            raise PermissionDenied

        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        messages.success(self.request, "{}'s info is successfully updated".format(self.get_object()))
        return reverse('lead_manager:lead_update', args=[self.get_object().id])


@login_required
def lead_delete(request, pk):
    if not request.user.is_superuser:
        return PermissionDenied

    lead = Lead.objects.only('id').get(id=pk)
    lead.delete()

    return redirect('lead_manager:lead_list')

Dans les vues ci-dessus, nous surchargeons la méthode dispatch pour gérer les permissions de chaque vue.

Créer forms.py dans le répertoire de l'application lead_manager. Dans l'application forms.pydéfinissez LeadForm:

from django import forms
from .models import Lead

class LeadForm(forms.ModelForm):
    class Meta:
        model = Lead
        fields = [
            'first_name', 
            'last_name', 
            'age',
            'facebook_id',
            'phone_number',
            'source', 
            'preferred_medium', 
            'agent'
        ]

Créer views.py dans un sous-dossier de lead_manager nommé agent. Et définissez vos AgentLoginView et AgentDashboardView vues.

from django.contrib.auth.views import LoginView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.views.generic.base import TemplateView

class AgentLoginView(LoginView):
    template_name = 'lead_manager/agent_login.html'


class AgentDashboardView(LoginRequiredMixin, TemplateView):
    template_name = 'lead_manager/agent_dashboard.html'

    def dispatch(self, request, *args, **kwargs):
        if not hasattr(request.user, 'agent'):
            raise PermissionDenied

        return super().dispatch(request, *args, **kwargs)

    def get(self, request):
        assigned_leads = request.user.agent.leads.all()
        context = {
            'assigned_leads': assigned_leads,
        }

        return self.render_to_response(context)

Créons lead_manager/urls.py et lead_manager/agent/urls.py.

Allez dans le répertoire lead_manager et créez un fichier urls.py et créez un fichier Maintenant, définissez des modèles d'URL pour les vues de lead_manager.

from django.urls import path
from . import views

app_name = 'lead_manager'
urlpatterns = [
    path('', views.LeadListView.as_view(), name='lead_list'),
    path('create/', views.LeadCreateView.as_view(), name='lead_create'),
    path('<int:pk>/update/', views.LeadUpdateView.as_view(), name='lead_update'),
    path('<int:pk>/delete/', views.lead_delete, name='lead_delete'),
]
```
In your lead_manager directory, go to `agent` folder and create a file named `urls.py`. Define URL patterns for agent views as in the snippet below:

```
from django.contrib.auth.views import LogoutView
from django.urls import path
from .views import AgentLoginView, AgentDashboardView  

app_name = 'agent'
urlpatterns = [
    path('login/', AgentLoginView.as_view(), name='agent_login'),
    path('logout/', LogoutView.as_view(), name='agent_logout'),
    path('dashboard/', AgentDashboardView.as_view(), name='agent_dashboard'),
]

A partir des deux urls.py dans l'application lead_manager, vous pouvez confirmer que toutes les vues que nous avons créées dans l'application lead_manager ont des configurations d'URL correspondantes.

Maintenant, informons Django de l'URL de connexion, de l'URL de redirection de la connexion et de l'URL de redirection de la déconnexion. Ajoutez ce qui suit à settings.py

LOGIN_URL = 'agent:agent_login'
LOGIN_REDIRECT_URL = 'agent:agent_dashboard'
LOGOUT_REDIRECT_URL = 'home'

Passons maintenant à l'application de conversation.

Outre les vues et la configuration des URL, vous allez également configurer un consommateur web-socket dans l'application de conversation. Il permettra la communication entre les agents SalesFox et les leads en temps réel.

Créons la vue lead_conversation_room pour la salle de conversation. Allez à views.py dans le dossier conversation et collez l'extrait de code ci-dessous

from  django.shortcuts  import  render
from  django.contrib.auth.decorators  import  login_required
from  django.http  import  HttpResponse, HttpResponseForbidden
from django.core.exceptions import PermissionDenied
from lead_manager.models import Lead	
    
@login_required
def lead_conversation_room(request, lead_id):
    if not hasattr(request.user, 'agent'):
        return PermissionDenied

    agent = request.user.agent
    try:
        lead = agent.leads.get(id=lead_id)
    except Lead.DoesNotExist:
        return HttpResponseForbidden()

    context = {"lead": lead}
    return render(request, "conversation/room.html", context)

La vue lead_conversation_room gère les demandes faites par les agents pour ouvrir une salle de conversation avec un client.

Créez maintenant la fonction send_outbound fonction. send_outbound La fonction est responsable de l'envoi de messages de SalesFox aux clients sur Facebook Messenger. Elle prend le message à envoyer et l'ID Facebook du client comme arguments.

from django.conf  import  settings
import  requests
import  json
import  base64
from  requests.exceptions  import  ConnectionError

def send_outbound(message, lead_facebook_id):
    url = settings.VONAGE_MESSAGES_ENDPOINT

    auth_param = settings.VONAGE_API_KEY + ":" + settings.VONAGE_API_SECRET
    auth_code = base64.b64encode(auth_param.encode('utf-8'))

    payload = json.dumps({
    "from": {
        "type": "messenger",
        "id": settings.FACEBOOK_ID
    },
    "to": {
        "type": "messenger",
        "id": lead_facebook_id
    },
    "message": {
        "content": {
        "type": "text",
        "text": message
        }
    }
    })
    headers = {
    'Authorization': 'Basic %s' % auth_code.decode('utf-8'),
    'Accept': 'application/json',
    'Content-Type': 'application/json'
    }
    try:
        response = requests.request("POST", url, headers=headers, data=payload)
    except ConnectionError:
        return
    return response

Comme nous voulons une communication en temps réel entre les clients potentiels et les agents dans la salle de conversation, nous devons créer une WebSocket du côté client et configurer un consommateur WebSocket dans le backend.

Dans le dossier de l'application de conversation, créez un dossier consumers.py dossier. Dans le dossier consumers.pycréez une classe de consommateur WebSocket - ConversationConsumer.

import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from django.contrib.contenttypes.models import ContentType

from .models import Message
from .views import send_outbound
from lead_manager.models import Lead, Agent

def create_conversation_group(convo_id):
    return "conversation_%s" % convo_id

class ConversationConsumer(WebsocketConsumer):
    def connect(self):
        self.lead_id = self.scope['url_route']['kwargs']['lead_id']
        self.conversation = create_conversation_group(self.lead_id)
        self.agent = getattr(self.scope['user'], 'agent', None)
        
        try:
            self.lead = Lead.objects.get(id=self.lead_id)
        except Lead.DoesNotExist:
            self.lead = None

        # join conversation
        async_to_sync(self.channel_layer.group_add)(
            self.conversation,
            self.channel_name
        )

        self.accept()
    
    def disconnect(self, exit_code):
        # leave conversation
        async_to_sync(self.channel_layer.group_discard)(
            self.conversation,
            self.channel_name
        )

    def save_message(self, message_data):
        if self.agent:
            sender_type = ContentType.objects.get_for_model(self.agent)
            message_data['sender_type'] = sender_type
            message_data['sender_id'] = self.agent.id
        
        if self.lead:
            receiver_type = ContentType.objects.get_for_model(self.lead)
            message_data['receiver_type'] = receiver_type
            message_data['receiver_id'] = self.lead.id

        message = Message.objects.create(**message_data)
        return message

    def receive(self, text_data):
        data = json.loads(text_data)
        message = data['message']
        saved_message = self.save_message({'body': message})

        if self.lead:
            # send message to Lead on social media (Facebook)
            response = send_outbound(message, self.lead.facebook_id)
            if response and response.ok:
                response_data = response.json()
                saved_message.is_delivered = True
                saved_message.message_key = response_data["message_uuid"]
                saved_message.save()

        # send message to everyone connected to the conversation
        async_to_sync(self.channel_layer.group_send)(
            self.conversation,
            {
                'type': 'send_to_conversation',
                'message': message,
                'from_agent': True
            }
        )
    
    def send_to_conversation(self, event):
        # send message to Websocket
        self.send(
            json.dumps(
                {
                    "message": event['message'],
                    "from_agent": event['from_agent'],
                }
            )
        )

Pour expliquer les méthodes - Pour chaque agent qui ouvre la page de conversation, il y a un appel à ConversationConsumer. Il en résulte un nouveau canal pour l'agent.

  • connect()est appelé lorsqu'une connexion WebSocket est reçue. Ici, nous ajoutons le canal de l'agent à une conversation, puis nous acceptons la connexion.

  • disconnect(): Ici, nous supprimons le canal de l'agent de la conversation.

  • receive(): Ici, nous recevons un nouveau message du client. Nous appelons ensuite save_message qui enregistre le message dans notre base de données. Nous envoyons ensuite le message en tant que message direct Facebook au prospect en appelant send_outbound. Le message est ensuite renvoyé à la salle de conversation. À la fin de la méthode receive le message sera envoyé à tous les agents de la salle de conversation.

  • save_message(): Nous enregistrons ici le message de l'agent dans la base de données. Ceci est appelé dans receive

  • send_to_conversation(): Nous l'utilisons pour diffuser le message de l'agent dans la salle de conversation afin que chaque agent présent dans la salle puisse voir le message.

Mettons maintenant en place le routage pour notre fichier ConversationConsumer.

Créez routing.py dans le répertoire de l'application de conversation et collez ce qui suit :

from  django.urls  import  re_path
from .consumers  import  ConversationConsumer

websocket_urlpatterns = [
re_path(r'ws/conversation/(?P<lead_id>\d+)/$', ConversationConsumer),
]

Créez un fichier routing.py dans le répertoire de votre projet. Ce fichier contient la configuration globale du routage pour le projet.

from  channels.routing  import  ProtocolTypeRouter, URLRouter
from  channels.auth  import  AuthMiddlewareStack
from  conversation  import  routing

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(URLRouter(
        routing.websocket_urlpatterns
    )),
})

Maintenant, la référence application dans settings.py comme application ASGI à exécuter lorsque Sales-Fox est servi par l'interface de passerelle de serveur asynchrone :

ASGI_APPLICATION = 'sales_fox.routing.application'

Créons une inbound vue. La vue inbound reçoit le message d'un client de Vonage, l'enregistre et l'envoie aux agents de la salle de conversation.

from  channels.layers  import  get_channel_layer
from  asgiref.sync  import  async_to_sync
from  django.views.decorators.http  import  require_POST
from  django.views.decorators.csrf  import  csrf_exempt
from  django.contrib.contenttypes.models  import  ContentType
from  lead_manager.models  import  Lead
from .models  import  Message


@require_POST
@csrf_exempt
def inbound(request):
    from .consumers import create_conversation_group
    body = json.loads(request.body)
    channel_layer = get_channel_layer()

    message = body["message"]["content"].get("text")
    lead_facebook_id = body["from"]["id"]
    lead, _ = Lead.objects.get_or_create(facebook_id=lead_facebook_id)
    if message:
        sender_type = ContentType.objects.get_for_model(lead)
        sender_id = lead.id

        message_data = dict(body=message, sender_type=sender_type, sender_id=sender_id)
        agent = lead.agent
        if agent:
            receiver_type = ContentType.objects.get_for_model(agent)
            receiver_id = agent.id

            message_data["receiver_type"] = receiver_type
            message_data["receiver_id"] = receiver_id

        message_obj = Message.objects.create(**message_data)

        conversation_group = create_conversation_group(lead.id)
        try:
            async_to_sync(channel_layer.group_send)(
                conversation_group,
                {
                    "type": "send_to_conversation",
                    "message": message,
                    "from_agent": False, 
                } 
            )

            message_obj.is_delivered = True
            message_obj.save()
        except Exception as e:
            print("Something went wrong")
            print(e)

    with open('inbound.txt', 'w') as inbound_file:
        json.dump(body, inbound_file, sort_keys=True, indent=2)
    return HttpResponse(status=204)

Vonage envoie des mises à jour de l'état des messages via le point de terminaison de l'état.

Comme nous n'utiliserons pas les informations d'état dans ce tutoriel, créons une simple vue d'état pour écrire le corps de la requête dans un fichier status.txt fichier.

Dans le fichier views.py de l'application de conversation, copiez ce qui suit pour créer la vue d'état.

@require_POST
@csrf_exempt
def  status(request):
	body = json.loads(request.body)
	with  open('status.txt', 'w') as  status_file:
		json.dump(body, status_file)
	return  HttpResponse(status=204)

Créons des configurations d'URL pour l'application de conversation. Allez dans le répertoire de l'application de conversation et créez le fichier urls.py et créez le fichier Copiez et collez ensuite l'extrait de code ci-dessous :

from django.urls import path
from .views import inbound, status, lead_conversation_room

app_name = 'conversation'
urlpatterns = [
    path('inbound/', inbound, name='conversation-inbound'),
    path('status/', status, name='conversation-status'),
    path('lead/<int:lead_id>/', lead_conversation_room, name="lead-conversation-room"),
]

Allez dans le répertoire du projet et trouvez le fichier urls.py. Ce fichier se trouve dans le même répertoire que settings.py. Copiez et collez le code suivant :

from django.urls import path
from .views import inbound, status, lead_conversation_room

app_name = 'conversation'
urlpatterns = [
    path('inbound/', inbound, name='conversation-inbound'),
    path('status/', status, name='conversation-status'),
    path('lead/<int:lead_id>/', lead_conversation_room, name="lead-conversation-room"),
]

Maintenant que nous avons terminé le backend de notre projet. Créons les fichiers frontaux.

Allez dans votre dossier statique dans le répertoire général et créez un dossier nommé css. Dans le dossier css créez deux fichiers style.css et chat.css.

En styles.csscopier et coller les styles suivants

.container {
    margin: 30px;
}

.link-group {
    display: inline-flex; 
    column-gap: 20px;
}

a {
    text-decoration: none;
}

.list {
    margin-bottom: 20px;
}

Dans chat.css, copiez et collez les styles suivants :

.container {
    max-width: 500 !important;
    margin: auto;
    margin-top: 4%;
    letter-spacing: 0.5px;
}

.msg-header {
    border: 1px solid #ccc;
    width: 100%;
    height: 10%;
    border-bottom: none;
    display: inline-block;
    background-color: #007bff;
}

.active {
    width: 120px;
    float: left;
    margin-top: 10px;
}

.active h4 {
    font-size: 20px;
    margin-left: 10px;
    color: #fff;
}

.msg-inbox {
    border: 1px solid #ccc;
    overflow: hidden;
    padding-bottom: 20px;
}

.chats {
    padding: 30px 15px 0 25px;

}

.msg-page {
    height: 400px;
    overflow-y: auto;
}

.received-msg {
    display: inline-block;
    padding: 0 0 0 10px;
    vertical-align: top;
    width: 53%;
}

.received-msg p {
    background: #efefef none repeat scroll;
    border-radius: 10px;
    color: #646464;
    font-size: 14px;
    margin: 0;
    padding: 5px 10px 5px 12px;
    width: 100%;
}

.time {
    color: #777;
    display: block;
    font-size: 12px;
    margin: 8px 0 0;
}
.outgoing-msg {
    float: left;
    width: 46%;
    margin-left: 45%;
}

.outgoing-msg p {
    background: #007bff none repeat scroll 0 0;
    color: #fff;
    border-radius: 10px;
    font-size: 14px;
    margin: 0;
    padding: 5px 10px 5px 12px;
    width: 100%;
}

.msg-bottom {
    position: relative;
    width: 100%;
    height: 20%;
    background: #007bff;
    display: inline-block;
}

.input-group {
    float: right;
    margin: 10px 20px 10px 0;
    outline: none !important;
    border-radius: 20px;
    width: 61% !important;
    background-color: #fff;
}

.form-control {
    border: none !important;
    border-radius:  20px !important;
}

.input-group-text {
    background: transparent !important;
    border: none !important;
    color: #007bff;
    cursor: pointer;
}

.input-group-append {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

.input-group .fa {
    color: #007bff;
    float: right;
}

.bottom-icons {
    float: left;
    margin-top: 17ox;
    width: 30px !important;
    margin-left: 22px;
}

.bottom-icons .fa {
    color: #007bff;
    padding: 5px;
}

.form-control:focus {
    border-color: none !important;
    box-shadow: none !important;
}

Nous utiliserons le fichier chat.css pour la conversation room.html tandis que nous utiliserons styles.css pour les autres pages. Maintenant, dans votre répertoire général, créez un dossier nommé templates et créez deux fichiers HTML - base.html et index.html. Vous étendrez base.html dans tous les autres fichiers HTML, sauf dans la conversation room.html.

Dans base.html, copiez et collez ce qui suit

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{% static 'css/style.css' %}">

    <title>{% block title %} Sales Fox {% endblock title %}</title>
</head>
<body>
    <div class="container">
    {% block content %}
    {% endblock content %}
    </div>
</body>
{% block script %}
{% endblock script %}
</html>

Dans le fichier index.html (page d'accueil), copiez et collez le texte suivant

{% extends 'base.html' %}
{% load static %}
{% block content %}

Allez maintenant dans le répertoire de l'application lead_manager. Créez un dossier templates et dans templatescréez un autre dossier lead_manager. Dans lead_manager/templates/lead_manager, créez cinq fichiers html - lead_list.html, lead_create.html, lead_update.html, agent_login.html, agent_dashboard.html.

lead_list.html,

{% extends 'base.html' %}
  {% load static %}

  {% block content %}
      <a href="{% url 'agent:agent_dashboard' %}">Go to dashboard</a>
      <h4>List of leads</h4>
      <ul>
          {% for lead in leads %}
          <li class="list">
              <div class="link-group">
                  <div style="width: 100px;">
                      {{lead.first_name}} ({{lead.id}})
                  </div>
                  <a href="{% url 'lead_manager:lead_update' lead.id %}">Update</a>
                  <a href="{% url 'lead_manager:lead_delete' lead.id %}"> Delete</a>
                  {% if not lead.has_agent %} | <span style="color: red;">Not Assigned</span> {% endif %}
              </div>
          </li>
          {% empty %}
          <p>Lead list is empty</p>
          {% endfor %}
      </ul> 

  <div class="create_lead_link">
      <a href="{% url 'lead_manager:lead_create' %}">Create new lead</a>
  </div>
{% endblock content %}

lead_create.html,

{% extends 'base.html' %}
{% load static %}

{% block content %}
    <a href="{% url 'lead_manager:lead_list'  %}">Go to lead list</a>
    <h1>Lead Creation Form</h1>

    <form action="." method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Send">
    </form>
{% endblock content %}

lead_update.html,

{% extends 'base.html' %}
{% load static %}	

{% block content %}
    <a href="{% url 'agent:agent_dashboard' %}">Go to dashboard</a>

    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
        <li{% if message.tags %}>{{ message }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <form action="." method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Send">
    </form>
{% endblock content %}
	

agent_login.html

{% extends 'base.html' %}
{% load static %}

{% block content %}
    <h1>Login to your dashboard</h1>

<form action="." method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>
{% endblock content %}

agent_dashboard.html

{% extends 'base.html' %}
{% load static %}

{% block content %}
    <h4>List of leads assigned to you</h4>

    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
        <li{% if="" message.tags="" %}="" class="message-{{ message.tags }}" {%="" endif="">{{ message }}
        {% endfor %}
    </li{%></ul>
    {% endif %}

    <div>
        <ul>
            {% for lead in assigned_leads %}
            <li class="list">
                <div class="link-group">
                    <div style="width: 100px;">
                        {{lead.first_name}} ({{lead.id}})
                    </div>
                    <a href="{% url 'lead_manager:lead_update' lead.id %}">Update</a>
                    <a href="{% url 'conversation:lead-conversation-room' lead.id %}">Go to conversation room</a>
                    </div>
            </li>
            {% empty %}
            <p>No assigned lead</p>
            {% endfor %}
        </ul>
    </div>

    <div class="link-group">
        {% if request.user.is_superuser %}    
        <a href="{% url 'lead_manager:lead_list' %}">View lead list</a>
        <a href="{% url 'lead_manager:lead_create' %}">Create new lead</a>    
        {% endif %}
        <a href="{% url 'agent:agent_logout' %}">Logout</a>
    </div>
{% endblock content %}

Dans le répertoire de l'application de conversation, créez un dossier templates et dans templates créer un sous-dossier conversation.

Dans le dossier "conversation/templates/conversation", créez un fichier room.html fichier. Copiez et collez ce qui suit :

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" href="{% static 'css/chat.css' %}">
    <title>Conversation with {{lead}}</title>
</head>
<body>
    <div class="container">
        <a href="{% url 'agent:agent_dashboard' %}">Go to Dashboard</a>
        <div class="msg-header">
            <div class="active">
                <h4>{{lead.first_name}} {{lead.last_name}}</h4>
            </div>
        </div>

        <div class="conversation">
            <div class="msg-inbox">
                <div class="chats">
                    <div class="msg-page" id="msgPage">
                        {% for message in lead.messages %}
                        {% if message.from_lead %}
                        
                        <div class="received-msg">
                            <div class="received-msg-inbox">
                                <p>{{message.body}} {{message.from_lead}}</p>
                                <span class="time">{{message.date_created}}</span>
                            </div>
                        </div>
                        
                        {% else %}
                        <div class="outgoing-msg">
                            <p>{{message.body}}</p>
                            <span class="time">{{message.date_created}}</span>
                        </div>
                        {% endif %}
                        {% endfor %}
                    </div>
                </div>
            </div>

            <div class="msg-bottom">
                <div class="input-group">
                    <textarea name="message" id="msgWriter" rows="3" class="form-control"></textarea>
                    <div class="input-group-append">
                        <span class="input-group-text" id="send">Send</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        const selectElement = (e) => document.querySelector(e);

        let messagePage = selectElement("#msgPage");
        let msgWriter = selectElement("#msgWriter");
        const msgType = {agent: 'outgoing', lead: 'received'}
        let socket = null

        const keepScrollToEnd = () => {
            messagePage.scrollTop = messagePage.scrollHeight
        }

        const getMessageBox = (text, date, type=msgType.agent) => {
            const parentDiv = document.createElement('div')
            parentDiv.classList.add(`${type}-chats`)

            const childDiv = document.createElement('div')
            childDiv.classList.add(`${type}-msg`)

            const msgParagraph = document.createElement('p')
            msgParagraph.textContent = text

            const dateSpan = document.createElement('span')
            dateSpan.classList.add('time')
            dateSpan.textContent = date

            childDiv.append(msgParagraph, dateSpan)
            parentDiv.append(childDiv)

            return parentDiv
        }

        // Displays new message in messagePage
        const showNewMessage = (val) => {
            let msgElement
            if (val.from_agent){
                msgElement = getMessageBox(text=val.message, date=val.date, type=msgType.agent)
            } else {
                msgElement = getMessageBox(text=val.message, date=val.date, type=msgType.lead)
            }
            messagePage.append(msgElement);
            keepScrollToEnd()
        }

         
        function sendMessage(event) {
            if (!msgWriter.value) return false;
            if (!socket) {
                alert("No socket connection. Reload browser");
                return false
            }

            socket.send(JSON.stringify({"message": msgWriter.value}));
            msgWriter.value = "";
            event.preventDefault();
            return false
        }

        if (!window["WebSocket"]) {
            alert("Your browser does not support web sockets. Change browser");
        } else {
            var conversationURL = "ws://" + window.location.host + "/ws/conversation/" + "{{ lead.id }}/"
            socket = new WebSocket(conversationURL);
            socket.onclose = function(){
                alert("Web socket connection has been closed");
            }

            // calls showNewMessage if socket receives message
            socket.onmessage = function(msg) {
                showNewMessage(JSON.parse(msg.data));
            }
        }
        selectElement("#send").addEventListener("click", sendMessage, false);
    </script>
</body>
</html

Avant la balise de fermeture de l'élément body dans le fichier room.html, nous avons un script qui gère l'opération WebSocket et le rendu des messages dans la salle de conversation.

Faire fonctionner SalesFox

Nous avons maintenant terminé le développement de SalesFox.

Suivez les étapes suivantes pour faire fonctionner SalesFox localement.

  1. Exécuter redis-server pour démarrer Redis. Vous pouvez arrêter le serveur Redis en toute sécurité en exécutant redis-cli shutdown

  2. Créez un tunnel HTTP avec Ngrok qui redirige les demandes vers le port à partir duquel vous exécutez SalesFox. Vous disposez ainsi d'une URL publique pour votre SalesFox localhost:port. Pour en savoir plus ici.

  3. Allez dans le fichier .env dans votre répertoire général. Définissez une nouvelle variable env appelée HOST qui correspond à l'URL de votre tunnel Ngrok.

    HOST=4339-197-210-53-35.ngrok.io
  4. Ajouter HOST du fichier .env à ALLOWED_HOST dans settings.py. ALLOWED_HOST définition dans settings.py devrait ressembler à ceci :

    ALLOWED_HOSTS = [os.getenv('HOST'), "localhost", "127.0.0.1"]
  5. Rappelez-vous que nous avons indiqué des URL factices en tant qu'URL d'entrée et d'état dans notre page d'application Vonage. Nous allons maintenant remplacer ces URL par les valeurs correctes. Comme l'hôte de mon tunnel est http://4339-197-210-53-35.ngrok.iomon URL entrant sera http://4339-197-210-53-35.ngrok.io/conversation/inbound et mon URL d'état sera http://4339-197-210-53-35.ngrok.io/conversation/status.

    Allez sur votre page d'application Vonage et mettez à jour les champs Inbound URL et Status URL et

  6. Maintenant, allez dans votre terminal (assurez-vous que vous êtes dans le répertoire général). Ensuite, exécutez python manage.py runserver pour servir SalesFox sur le port 8000.

    python manage.py runserver 9000.

Conclusion

Si vous êtes arrivé jusqu'ici, je vous remercie d'avoir construit ce projet avec moi. Au cours de la construction de SalesFox, nous nous en sommes tenus aux fonctionnalités et à la conception minimales possibles. Cependant, vous pouvez faire beaucoup plus en créant plus de fonctionnalités sur SalesFox.

Vous pouvez ajouter d'autres options de preferred_medium pour les leads. Vonage fournit des variétés d'API de communication, dont certaines peuvent être développées pour SalesFox. Cela vaut la peine de les vérifier ici.

Santé !

Partager:

https://a.storyblok.com/f/270183/400x400/5625a429b4/tolulope-olanrewaju.png
Tolulope OlanrewajuAuteur invité

Tolulope est un ingénieur logiciel basé au Nigeria. Il aime créer des solutions multiplateformes et des outils de communication pour les personnes et les entreprises. Lorsqu'il ne travaille pas, il aime lire des livres sur la conception de produits, l'humanité et l'esprit d'entreprise.