
Compartir:
Tolulope es un ingeniero de software afincado en Nigeria. Le encanta crear soluciones multiplataforma y herramientas de comunicación para personas y empresas. Cuando no está trabajando, le encanta leer libros sobre diseño de productos, humanidad y emprendimiento.
Creación de un CRM social con Django y la API Messages API de Vonage
Tiempo de lectura: 11 minutos
En este artículo, aprenderás a crear la función principal de un CRM social utilizando Django y Vonage Messages API. Nuestro CRM social ayudará a los agentes de ventas y al equipo de atención al cliente a comunicarse con clientes potenciales directamente en Facebook en tiempo real. Llamémoslo Sales Fox.
Requisitos previos
Crea una aplicación de mensajes desde tu panel de Vonage. Sigue los pasos descritos aquí.
Autoriza a Vonage a acceder a tu página de empresa de Facebook y vincula tu aplicación a tu página de Facebook. Sigue los pasos descritos aquí.
Instale Redis - Si utiliza Linux o Mac, siga las instrucciones aquí. Si utiliza Windows, siga las instrucciones aquí.
Instala Ngrok. Ir a la Página de descarga de Ngrok y sigue las instrucciones para instalar Ngrok en tu ordenador.
Ahora que ha completado los requisitos previos. Es necesario configurar el entorno de desarrollo para el tutorial.
Puesta en marcha del proyecto
Cree y active su entorno virtual Crea un directorio para tu proyecto y cambia tu directorio de trabajo al directorio que acabas de crear. A continuación, ejecuta los siguientes comandos para crear y activar un entorno virtual para tu proyecto.
python3 -m venv sales-envsource sales-env/bin/activateInstalar los paquetes necesarios Para instalar todos los paquetes necesarios a la vez, cree un archivo
requirements.txten el directorio creado en el paso 1. Copie y pegue el siguiente fragmento de código en el archivorequirements.txtarchivo.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.0CopiaAhora, instala todos los paquetes en
requirements.txtejecutando el siguiente comando en tu terminal.pip install -r requirements.txtCree su proyecto Django
Ejecute
django-admin startproject sales_foxlo siguiente para crear el proyecto Django llamado "sales_fox".Crearemos dos aplicaciones en sales_fox: La aplicación
lead_managerpara gestionar clientes potenciales y la aplicaciónconversationpara que los agentes de ventas se comuniquen con clientes potenciales (conocidos como leads). Ahora, vamos a crear nuestras dos aplicaciones mediante la ejecución de estos comandos.python manage.py startapp lead_manager python manage.py startapp conversationCopia
Tenga en cuenta que en este tutorial,
Utilizaré las palabras "clientes potenciales" y "clientes" indistintamente. Los leads son clientes potenciales, así que no está de más considerarlos clientes cuando sea conveniente.
Utilizaré el término
Project Directorypara referirme al directorio donde tienessettings.py. Este directorio se creó cuando ejecutódjango-admin startproject sales_fox.Utilizaré el término
Overall Directorypara referirme al directorio que creaste al principio del tutorial. Contiene la carpeta de tu entorno virtual, los directorios de las aplicaciones y el directorio de tu proyecto.
Cree un
.envarchivo en su directorio general. DefinaFACEBOOK_ID,VONAGE_API_KEYyVONAGE_API_SECRET. Su archivo .env debería tener el siguiente aspecto:FACEBOOK_ID=YOUR-LINKED-FACEBOOK-ID VONAGE_API_KEY=YOUR-VONAGE-API-KEY VONAGE_API_SECRET=YOUR-VONAGE-API-SECRETCopiaPuedes encontrar tu clave y secreto de API de Vonage en tu página de configuración de Vonage. Y puedes encontrar tu ID de Facebook en la
Link social channelsen la página de tu aplicación.En el directorio de su proyecto, vaya a
settings.pycargue las variables en su archivo .env utilizandopython-dotenvinstalado desderequirements.txt. Añada el siguiente fragmento ensettings.pypara cargar el archivo .env:from dotenv import load_dotenv import os load_dotenv()Copia
load_dotenv carga todas las variables en nuestro archivo .env como variable de entorno. Ahora, define FACEBOOK_ID, VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_MESSAGES_ENDPOINT en tu archivo settings.py archivo. Sólo tiene que copiar y pegar el siguiente fragmento.
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"En settings.pyencuentra STATIC_URL y suma las variables STATICFILES_DIRS y STATIC_FILES debajo de STATIC_URLDebería tener algo como
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'Vaya a su directorio general y cree una carpeta llamada static. Aquí es donde guardará todos sus archivos estáticos. Tenga en cuenta que sólo debe hacer esto para un entorno de desarrollo. En un entorno de producción, debe configurar un almacén externo como un cubo de AWS S3 para servir sus archivos estáticos.
Tenemos que añadir channels y las aplicaciones que hemos creado (lead_manager y conversation) a INSTALLED_APPS en el directorio settings.py. Su INSTALLED_APPS en el archivo settings.py debe tener este aspecto:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'lead_manager',
'conversation',
]Los canales Django nos ayudan a incluir soporte WebSocket a Sales Fox. Una capa de canales introduce el uso de canales y grupos en SalesFox. Nos ayuda a construir características distribuidas en nuestra aplicación. Puedes leer más sobre capas de canales aquí. Para este proyecto, usaré Redis como nuestra capa de canal. Hemos instalado channels-redis desde requirements.txt. Ahora, vamos a añadir CHANNEL_LAYER a settings.py. Copia y pega el siguiente fragmento de código:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', '6379')],
},
},
} Las tachuelas
Ahora, vayamos a lo importante.
Cree modelos para la lead_manager aplicación. Aquí, añadiremos modelos para Lead y Agent. El modelo Lead representará a los clientes y posibles clientes. El modelo Agent representará a los vendedores de SalesFox que estarán en contacto con los clientes. Copie y pegue el siguiente fragmento de código en 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.usernameCreamos un modelo de Usuario para representar a cada usuario en SalesFox - Esto podría ser community managers, representantes de región, soporte al cliente, etc. Sin embargo, para mantener SalesFox lo más ligero posible, el único tipo de usuarios que tenemos son los agentes.
El modelo Lead representa a los clientes potenciales que contactan desde su cuenta de Facebook. El campo facebook_id representa el ID de la cuenta de Facebook de un cliente. Es el campo que necesitamos para que los agentes envíen un mensaje directo a los clientes en Facebook. El modelo Lead también tiene un campo preferred_medium. En él se indica el medio de comunicación preferido por el cliente. Nos centraremos únicamente en la comunicación a través de Facebook.
`AUTH_USER_MODEL = 'lead_manager.User'`Ahora, vamos a crear un Message modelo en la aplicación de conversación. El modelo Message representa un único mensaje enviado desde/a SalesFox. Copia y Pega el siguiente fragmento de código en models.py de la conversation aplicación.
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)En nuestro Message tenemos dos relaciones genéricas para identificar al remitente y al destinatario del mensaje. El remitente y el destinatario pueden ser un lead o un agente. Esto significa que sólo los agentes o los clientes potenciales pueden enviar o recibir mensajes. Visite aquí para aprender más sobre las relaciones genéricas en Django.
Crear un método de propiedad messages para el modelo Lead en lead_manager/models.py. Este método devuelve todos los mensajes entrantes y salientes de un lead.
En lead_manager/models.py, pega las siguientes sentencias import.
En el modelo Lead cree el método de propiedad "mensajes" como en el siguiente fragmento:
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 messagesEmpecemos con las vistas.
En lead_manager, crearemos vistas para realizar operaciones CRUD en el modelo Lead. Ve a la carpeta de la app lead_manager, luego copia y pega el siguiente código en views.py para crear las vistas:
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')En las vistas anteriores, sobrescribimos el método dispatch para gestionar los permisos de cada vista.
Cree forms.py dentro del directorio de la aplicación lead_manager. En forms.pydefine 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'
]Cree views.py dentro de una subcarpeta en lead_manager llamada agent. Y defina sus AgentLoginView y AgentDashboardView vistas.
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)Creemos lead_manager/urls.py y lead_manager/agent/urls.py.
Vaya al directorio lead_manager y cree un archivo urls.py archivo. Ahora, defina patrones de URL para las vistas 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'),
]
Desde los dos urls.py en la aplicación lead_manager, puedes confirmar que todas las vistas que hemos creado en la aplicación lead_manager tienen sus correspondientes configuraciones de URL.
Ahora, informemos a Django de la URL de inicio de sesión, la URL de redirección de inicio de sesión y la URL de redirección de cierre de sesión. Añade lo siguiente a settings.py
LOGIN_URL = 'agent:agent_login'
LOGIN_REDIRECT_URL = 'agent:agent_dashboard'
LOGOUT_REDIRECT_URL = 'home'Ahora, pasemos a la aplicación de conversación.
Además de las vistas y la configuración de URL, también configurarás un consumidor web-socket en la app de conversación. Permitirá la comunicación entre los agentes de SalesFox y los leads en tiempo real.
Vamos a crear la lead_conversation_room para la sala de conversación. Ve a views.py en la carpeta de conversación y pega el siguiente fragmento de código
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 vista lead_conversation_room gestiona las solicitudes realizadas por los agentes para abrir una sala de conversación con un cliente.
Ahora, cree send_outbound la función. send_outbound La función es responsable de enviar mensajes desde SalesFox a los clientes en Facebook Messenger. Se necesita el mensaje que se enviará y el ID de Facebook de plomo como argumentos.
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 responseComo queremos comunicación en tiempo real entre clientes potenciales y agentes en la sala de conversación, necesitamos crear un WebSocket en el lado del cliente y configurar un consumidor WebSocket en el backend.
En la carpeta de la aplicación de conversación, crea una carpeta consumers.py carpeta. En la carpeta consumers.pycrea una clase de consumidor 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'],
}
)
)Para explicar los métodos - Por cada agente que abre la página de conversación, hay una llamada a ConversationConsumer. El resultado es un nuevo canal para el agente.
connect()es llamado cuando se recibe una conexión WebSocket. Aquí, añadimos el canal del agente a una conversación y luego aceptamos la conexión.disconnect(): Aquí, eliminamos el canal del agente de la conversación.receive(): Aquí, recibimos un nuevo mensaje del cliente. Después llamamos asave_messageque guarda el mensaje en nuestra base de datos. A continuación, enviamos el mensaje como un mensaje directo de Facebook para el plomo llamando asend_outbound. A continuación, el mensaje se envía a la sala de conversación. Al final del métodoreceiveel mensaje se enviará a todos los agentes de la sala de conversación.save_message(): Aquí guardamos el mensaje del agente en la base de datos. Esto se llama enreceivesend_to_conversation(): Se utiliza para transmitir el mensaje del agente a la sala de conversación, de forma que todos los agentes de la sala puedan ver el mensaje.
Ahora, vamos a configurar el enrutamiento para nuestro . ConversationConsumer.
Crea routing.py en el directorio de aplicaciones de conversación y pega lo siguiente:
from django.urls import re_path
from .consumers import ConversationConsumer
websocket_urlpatterns = [
re_path(r'ws/conversation/(?P<lead_id>\d+)/$', ConversationConsumer),
]Cree un archivo routing.py en el directorio del proyecto. Este archivo contiene la configuración de enrutamiento global para el proyecto.
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from conversation import routing
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(URLRouter(
routing.websocket_urlpatterns
)),
})Ahora, la referencia application en settings.py como aplicación ASGI que se ejecutará cuando Sales-Fox se sirva a través de la interfaz de pasarela de servidor asíncrono:
ASGI_APPLICATION = 'sales_fox.routing.application'Vamos a crear una inbound vista. La vista inbound recibe el mensaje de un cliente de Vonage, guarda el mensaje y lo envía a los agentes de la sala de conversación.
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 envía actualizaciones de estado de mensajes a través del punto final de estado.
Como no vamos a utilizar la información de estado en este tutorial, vamos a crear una vista de estado simple para escribir el cuerpo de la petición en un archivo status.txt archivo.
En el archivo views.py de la aplicación de conversación, copia lo siguiente para crear la vista de estado.
@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)Vamos a crear configuraciones de URL para la app de conversación. Ve al directorio de la aplicación de conversación y crea el archivo urls.py el archivo A continuación, copia y pega el siguiente fragmento de código:
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"),
]Vaya al directorio del proyecto y busque el archivo urls.py. Este archivo está en el mismo directorio que settings.py. Ahora, copia y pega el siguiente código:
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"),
]Ahora que hemos terminado con el backend de nuestro proyecto. Vamos a crear los archivos frontend.
Vaya a su carpeta estática en el directorio general y cree una carpeta llamada css. En css cree dos archivos style.css y chat.css.
En styles.csscopie y pegue los siguientes estilos
.container {
margin: 30px;
}
.link-group {
display: inline-flex;
column-gap: 20px;
}
a {
text-decoration: none;
}
.list {
margin-bottom: 20px;
}En chat.css, copia y pega los siguientes estilos:
.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;
}Usaremos el archivo chat.css para la conversación room.html mientras que usaremos styles.css para otras páginas. Ahora, en tu directorio general, crea una carpeta llamada templates y crea dos archivos HTML - base.html y index.html. Extenderás base.html en todos los demás archivos HTML excepto en la conversación room.html.
En base.html, copia y pega lo siguiente
{% 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>
En index.html (página de inicio), copie y pegue lo siguiente
{% extends 'base.html' %}
{% load static %}
{% block content %}Ahora, vaya al directorio de la aplicación lead_manager. Cree una carpeta templates y en templatescree otra carpeta lead_manager. En lead_manager/templates/lead_manager, cree cinco archivos 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 %}En el directorio de la aplicación de conversación, cree una carpeta templates y en la carpeta templates cree una subcarpeta conversation.
Dentro de la carpeta "conversation/templates/conversation", crea un archivo room.html archivo. Copie y pegue lo siguiente:
{% 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>
</htmlAntes de la etiqueta de cierre para el elemento body en room.html, tenemos un script que maneja la operación WebSocket y la renderización del mensaje en la sala de conversación.
Ponga en marcha SalesFox
Ya hemos completado el desarrollo de SalesFox.
Siga estos pasos para que SalesFox funcione localmente.
Ejecute
redis-serverpara iniciar Redis. Puede detener el servidor Redis de forma segura ejecutandoredis-cli shutdownCrea un túnel HTTP con Ngrok que reenvíe las peticiones al puerto desde el que estás ejecutando SalesFox. Esto le proporciona una URL pública disponible para su SalesFox
localhost:port. Aprende más sobre esto aquí.Vaya al archivo .env en su directorio general. Define una nueva variable env llamada HOST con la URL de tu túnel Ngrok.
HOST=4339-197-210-53-35.ngrok.ioAñada
HOSTdel archivo .env aALLOWED_HOSTensettings.py.ALLOWED_HOSTdefinición ensettings.pydebería tener este aspecto:ALLOWED_HOSTS = [os.getenv('HOST'), "localhost", "127.0.0.1"].Recuerda que completamos URL falsas como URL de entrada y de estado en nuestra página de la aplicación de Vonage. Ahora, reemplazaremos estas URL con los valores correctos. Como mi host de túnel es
http://4339-197-210-53-35.ngrok.iomi URL de entrada seráhttp://4339-197-210-53-35.ngrok.io/conversation/inboundy mi URL de estado seráhttp://4339-197-210-53-35.ngrok.io/conversation/status.Ve a la página de tu aplicación de Vonage y actualiza los campos
Inbound URLyStatus URLyAhora, ve a tu terminal (asegúrate de que estás en el directorio general). Luego, ejecute
python manage.py runserverpara servir a SalesFox en el puerto 8000.python manage.py runserver 9000.
Conclusión
Si has llegado hasta aquí, gracias por construir este proyecto conmigo. En el curso de la construcción de SalesFox, nos hemos ceñido al mínimo posible de características y diseño. Sin embargo, Usted puede hacer mucho más mediante la creación de más características sobre SalesFox.
Puedes agregar más opciones de preferred_medium para clientes potenciales. Vonage ofrece diversas API de comunicación, algunas de las cuales puedes desarrollar para que SalesFox las admita. Vale la pena revisarlas aquí.
¡Salud!
Compartir:
Tolulope es un ingeniero de software afincado en Nigeria. Le encanta crear soluciones multiplataforma y herramientas de comunicación para personas y empresas. Cuando no está trabajando, le encanta leer libros sobre diseño de productos, humanidad y emprendimiento.