├── .dockerignore
├── .env.example
├── .github
├── dependabot.yml
└── workflows
│ └── flask.yml
├── .gitignore
├── .mergify.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── lead_alerts
├── __init__.py
├── config.py
├── services
│ ├── __init__.py
│ └── twilio_service.py
├── static
│ ├── images
│ │ └── house.jpg
│ └── stylesheets
│ │ └── main.css
├── templates
│ └── index.html
└── views.py
├── manage.py
├── requirements.txt
└── tests
├── __init__.py
├── base.py
└── views_tests.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | venv/
2 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 |
2 | TWILIO_ACCOUNT_SID=AC2XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3 | TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
4 | TWILIO_PHONE_NUMBER=+15551230987
5 | AGENT_PHONE_NUMBER=+15551230989
6 | SECRET_KEY=YourSecretKey
7 |
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: python-dotenv
10 | versions:
11 | - 0.17.0
12 | - dependency-name: twilio
13 | versions:
14 | - 6.51.1
15 |
--------------------------------------------------------------------------------
/.github/workflows/flask.yml:
--------------------------------------------------------------------------------
1 | name: Flask
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ${{ matrix.platform }}
13 | strategy:
14 | max-parallel: 4
15 | matrix:
16 | python-version: [3.6, 3.7, 3.8]
17 | platform: [windows-latest, macos-latest, ubuntu-latest]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v1
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install Dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install -r requirements.txt
29 | - name: Run Tests
30 | run: |
31 | coverage run manage.py test
32 | env:
33 | TWILIO_ACCOUNT_SID: ACXXXXX
34 | TWILIO_AUTH_TOKEN: xxxxxxx
35 | TWILIO_PHONE_NUMBER: twilio_number
36 | AGENT_PHONE_NUMBER: agent_number
37 | SECRET_KEY: YourSecretKey
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | build
3 | dist
4 | include
5 | lib
6 | .tox
7 | venv
8 | .coverage
9 | .Python
10 | *.pyc
11 | *.project
12 | *.pydevproject
13 | *.pyo
14 | *.swp
15 | *.coverage
16 | .env
17 | .vscode
18 | .tool-versions
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatic merge for Dependabot pull requests
3 | conditions:
4 | - author=dependabot-preview[bot]
5 | - status-success=build (3.6, macos-latest)
6 | - status-success=build (3.7, macos-latest)
7 | - status-success=build (3.8, macos-latest)
8 | - status-success=build (3.6, windows-latest)
9 | - status-success=build (3.7, windows-latest)
10 | - status-success=build (3.8, windows-latest)
11 | - status-success=build (3.6, ubuntu-latest)
12 | - status-success=build (3.7, ubuntu-latest)
13 | - status-success=build (3.8, ubuntu-latest)
14 | actions:
15 | merge:
16 | method: squash
17 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at open-source@twilio.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Twilio
2 |
3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY requirements.txt ./
6 |
7 | COPY Makefile ./
8 |
9 | RUN make install
10 |
11 | COPY . .
12 |
13 | EXPOSE 5000
14 |
15 | CMD ["sh", "-c", ". /usr/src/app/venv/bin/activate && make serve"]
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Twilio Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: venv install serve
2 | UNAME := $(shell uname)
3 | venv:
4 | ifeq ($(UNAME), Windows)
5 | py -3 -m venv venv;
6 | else
7 | python3 -m venv venv
8 | endif
9 | install: venv
10 | ifeq ($(UNAME), Windows)
11 | venv\Scripts\activate.bat; \
12 | pip3 install -r requirements.txt;
13 | else
14 | . venv/bin/activate; \
15 | pip3 install -r requirements.txt;
16 | endif
17 |
18 | serve:
19 | python3 manage.py runserver -h 0.0.0.0
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Instant Lead Alerts implemented with Python and Flask
6 |
7 | 
8 |
9 | > This template is part of Twilio CodeExchange. If you encounter any issues with this code, please open an issue at [github.com/twilio-labs/code-exchange/issues](https://github.com/twilio-labs/code-exchange/issues).
10 |
11 | ## About
12 |
13 | This demo application shows how to implement instant lead alerts using Python and Flask. Notify sales reps or agents right away when a new lead comes in for a real estate listing or other high value channel.
14 |
15 | [Read the full tutorial here](https://www.twilio.com/docs/tutorials/walkthrough/lead-alerts/python/flask)!
16 |
17 | Implementations in other languages:
18 |
19 | | .NET | Java | Node | PHP | Ruby |
20 | | :--- | :--- | :----- | :-- | :--- |
21 | | [Done](https://github.com/TwilioDevEd/lead-alerts-csharp) | [Done](https://github.com/TwilioDevEd/lead-alerts-servlets) | [Done](https://github.com/TwilioDevEd/lead-alerts-node) | [Done](https://github.com/TwilioDevEd/lead-alerts-laravel) | [Done](https://github.com/TwilioDevEd/lead-alerts-rails) |
22 |
23 | ## Set up
24 |
25 | ### Requirements
26 |
27 | - [Python](https://www.python.org/) **3.6**, **3.7** or **3.8** version.
28 |
29 | In some environments when both version 2
30 | and 3 are installed, you may substitute the Python executables below with
31 | `python3` and `pip3` unless you use a version manager such as
32 | [pyenv](https://github.com/pyenv/pyenv).
33 |
34 | ### Twilio Account Settings
35 |
36 | This application should give you a ready-made starting point for writing your own application.
37 | Before we begin, we need to collect all the config values we need to run the application:
38 |
39 | | Config Value | Description |
40 | | :---------- | :---------- |
41 | | TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN | You could find them in your [Twilio Account Settings](https://www.twilio.com/console/account/settings)|
42 | | TWILIO_NUMBER | You may find it [here](https://www.twilio.com/console/phone-numbers/incoming) |
43 | | AGENT_NUMBER | This variable represents the number alerts will be sent to. Please make sure you have allowed SMS to be sent to the Country this number belongs to on the [Global SMS Permissions page](https://www.twilio.com/console/sms/settings/geo-permissions). Also, if you are on a trial account, make sure you have verified this number on the [Verified Callers IDs page](https://www.twilio.com/console/phone-numbers/verified) |
44 |
45 | ### Local development
46 |
47 | 1. First clone this repository and `cd` into it.
48 |
49 | ```bash
50 | git clone git@github.com:TwilioDevEd/lead-alerts-flask.git
51 | cd lead-alerts-flask
52 | ```
53 |
54 | 2. Create the virtual environment, load it and install the dependencies.
55 |
56 | ```bash
57 | make install
58 | ```
59 |
60 | 3. Copy the sample configuration file and edit it to match your configuration.
61 |
62 | ```bash
63 | cp .env.example .env
64 | ```
65 |
66 | See [Twilio Account Settings](#twilio-account-settings) to locate the necessary environment variables.
67 |
68 | 4. Start the server.
69 |
70 | ```bash
71 | make serve
72 | ```
73 |
74 | 5. Check it out at: [http://localhost:5000/](http://localhost:5000/).
75 |
76 | That's it!
77 |
78 | ### Docker
79 |
80 | If you have [Docker](https://www.docker.com/) already installed on your machine, you can use our `docker-compose.yml` to setup your project.
81 |
82 | 1. Make sure you have the project cloned.
83 | 2. Setup the `.env` file as outlined in the [Local Development](#local-development) steps.
84 | 3. Run `docker-compose up`.
85 |
86 | ### Tests
87 |
88 | To execute tests, run the following command in the project directory:
89 |
90 | ```bash
91 | python3 manage.py test
92 | ```
93 |
94 | ### Cloud deployment
95 |
96 | Additionally to trying out this application locally, you can deploy it to a variety of host services. Here is a small selection of them.
97 |
98 | Please be aware that some of these might charge you for the usage or might make the source code for this application visible to the public. When in doubt research the respective hosting service first.
99 |
100 | | Service | |
101 | | :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
102 | | [Heroku](https://www.heroku.com/) | [](https://heroku.com/deploy) |
103 |
104 | ## Resources
105 |
106 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/).
107 |
108 | ## Contributing
109 |
110 | This template is open source and welcomes contributions. All contributions are subject to our [Code of Conduct](https://github.com/twilio-labs/.github/blob/master/CODE_OF_CONDUCT.md).
111 |
112 | ## License
113 |
114 | [MIT](http://www.opensource.org/licenses/mit-license.html)
115 |
116 | ## Disclaimer
117 |
118 | No warranty expressed or implied. Software is as is.
119 |
120 | [twilio]: https://www.twilio.com
121 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | app:
4 | container_name: app
5 | restart: always
6 | build: .
7 | ports:
8 | - "5000:5000"
9 |
--------------------------------------------------------------------------------
/lead_alerts/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | import os
3 | from dotenv import load_dotenv
4 |
5 | load_dotenv()
6 |
7 | # Declare and configure application
8 | app = Flask(__name__, static_url_path='/static')
9 | app.config.from_pyfile('config.py')
10 | app.secret_key = os.environ.get('SECRET_KEY', None)
11 |
12 | import lead_alerts.views
13 |
--------------------------------------------------------------------------------
/lead_alerts/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', None)
4 | TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN', None)
5 | TWILIO_PHONE_NUMBER = os.environ.get('TWILIO_PHONE_NUMBER', None)
6 | AGENT_PHONE_NUMBER = os.environ.get('AGENT_PHONE_NUMBER', None)
7 |
--------------------------------------------------------------------------------
/lead_alerts/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/lead-alerts-flask/01501c42eff683501c34e792c89da4efa0fc67ba/lead_alerts/services/__init__.py
--------------------------------------------------------------------------------
/lead_alerts/services/twilio_service.py:
--------------------------------------------------------------------------------
1 | from lead_alerts import app
2 | from twilio.rest import Client
3 |
4 | class TwilioService:
5 | client = None
6 |
7 | def __init__(self):
8 | # Find these values at https://twilio.com/user/account
9 | account_sid = app.config['TWILIO_ACCOUNT_SID']
10 | auth_token = app.config['TWILIO_AUTH_TOKEN']
11 | self.client = Client(account_sid, auth_token)
12 |
13 | def send_message(self, message):
14 | agent_phone_number = app.config['AGENT_PHONE_NUMBER']
15 | twilio_phone_number = app.config['TWILIO_PHONE_NUMBER']
16 | self.client.messages.create(to=agent_phone_number,
17 | from_=twilio_phone_number,
18 | body=message)
19 |
--------------------------------------------------------------------------------
/lead_alerts/static/images/house.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/lead-alerts-flask/01501c42eff683501c34e792c89da4efa0fc67ba/lead_alerts/static/images/house.jpg
--------------------------------------------------------------------------------
/lead_alerts/static/stylesheets/main.css:
--------------------------------------------------------------------------------
1 | #main {
2 | padding-top:10px;
3 | }
4 |
5 | .demo {
6 | background-color:#eee;
7 | border:1px solid #ccc;
8 | padding:10px;
9 | }
10 |
11 | footer {
12 | border-top:1px solid #eee;
13 | padding:20px;
14 | margin:20px;
15 | text-align:center;
16 | }
17 |
18 | footer i {
19 | color:red;
20 | }
21 |
--------------------------------------------------------------------------------
/lead_alerts/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 | A trained real estate professional is standing by to answer any
37 | questions you might have about this property. Fill out the form below
38 | with your contact information, and an agent will reach out soon.
39 |
40 |
56 |
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/lead_alerts/views.py:
--------------------------------------------------------------------------------
1 | from lead_alerts import app
2 | from flask import flash, redirect, render_template, request
3 | from twilio.base.exceptions import TwilioRestException
4 |
5 |
6 | from .services.twilio_service import TwilioService
7 |
8 | @app.route('/')
9 | def index():
10 | house = {
11 | 'title': '555 Sunnybrook Lane',
12 | 'price': '$349,999',
13 | 'description':
14 | 'You and your family will love this charming home. ' +
15 | 'Featuring granite appliances, stainless steel windows, and ' +
16 | 'high efficiency dual mud rooms, this joint is loaded to the max. ' +
17 | 'Motivated sellers have priced for a quick sale, act now!'
18 | }
19 | return render_template('index.html', house=house)
20 |
21 | @app.route('/notifications', methods=['POST'])
22 | def create():
23 | house_title = request.form["house_title"]
24 | name = request.form["name"]
25 | phone = request.form["phone"]
26 | message = request.form["message"]
27 |
28 | twilio_service = TwilioService()
29 |
30 | formatted_message = build_message(house_title, name, phone, message)
31 | try:
32 | twilio_service.send_message(formatted_message)
33 | flash('Thanks! An agent will be contacting you shortly', 'success')
34 | except TwilioRestException as e:
35 | print(e)
36 | flash('Oops! There was an error. Please try again.', 'danger')
37 |
38 | return redirect('/')
39 |
40 | def build_message(house_title, name, phone, message):
41 | template = 'New lead received for {}. Call {} at {}. Message {}'
42 | return template.format(house_title, name, phone, message)
43 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | """
2 | This script holds the commands nedeed for runnin the migrations and the tests
3 | """
4 |
5 | from flask import Flask
6 | from flask_script import Manager
7 | from lead_alerts import app
8 |
9 | manager = Manager(app)
10 |
11 | @manager.command
12 | def test():
13 | """Run the unit tests."""
14 | import sys, unittest
15 | tests = unittest.TestLoader().discover('.', pattern="*_tests.py")
16 | result = unittest.TextTestRunner(verbosity=2).run(tests)
17 |
18 | if not result.wasSuccessful():
19 | sys.exit(1)
20 |
21 | if __name__ == "__main__":
22 | manager.run()
23 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | blinker==1.4
2 | coverage==5.5
3 | Flask==1.1.2
4 | Flask-Script==2.0.6
5 | Flask-Testing==0.8.1
6 | Flask-WTF==0.14.3
7 | funcsigs==1.0.2
8 | httplib2==0.19.1
9 | itsdangerous==1.1.0
10 | Jinja2==2.11.3
11 | linecache2==1.0.0
12 | MarkupSafe==1.1.1
13 | mock==4.0.3
14 | pbr==5.6.0
15 | pytz==2021.1
16 | python-dotenv==0.17.0
17 | six==1.15.0
18 | traceback2==1.4.0
19 | twilio==6.57.0
20 | unittest2==1.1.0
21 | Werkzeug==1.0.1
22 | wheel==0.36.2
23 | WTForms==2.3.3
24 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/lead-alerts-flask/01501c42eff683501c34e792c89da4efa0fc67ba/tests/__init__.py
--------------------------------------------------------------------------------
/tests/base.py:
--------------------------------------------------------------------------------
1 | from lead_alerts import app
2 | from flask_testing import TestCase
3 |
4 |
5 | class BaseTestCase(TestCase):
6 | render_templates = False
7 |
8 | def create_app(self):
9 | return app
10 |
--------------------------------------------------------------------------------
/tests/views_tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from mock import patch
3 |
4 | from .base import BaseTestCase
5 |
6 | class ViewsTests(BaseTestCase):
7 | def test_get_to_root_should_render_default_view(self):
8 | self.client.get('/')
9 | self.assert_template_used('index.html')
10 |
11 | @patch('lead_alerts.services.twilio_service.TwilioService.send_message')
12 | def test_post_to_notifications_should_send_a_message(self, mock_service):
13 | response = self.client.post('/notifications',
14 | data=dict(
15 | house_title='house title',
16 | name='name',
17 | phone='phone',
18 | message='message'
19 | ),
20 | follow_redirects=True)
21 |
22 | mock_service.assert_called_with(
23 | 'New lead received for house title. Call name at phone. Message message')
24 |
25 | if __name__ == '__main__':
26 | unittest.main()
27 |
--------------------------------------------------------------------------------