├── .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 | Twilio 3 | 4 | 5 | # Instant Lead Alerts implemented with Python and Flask 6 | 7 | ![](https://github.com/TwilioDevEd/lead-alerts-flask/workflows/Flask/badge.svg) 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/) | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](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 | 4 | 5 | 6 | Lead Alerts 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | {% with messages = get_flashed_messages(with_categories=true) %} 15 | {% if messages %} 16 | 24 | {% endif %} 25 | {% endwith %} 26 |
27 |
28 |

{{house.title}}

29 |

{{house.price}}

30 | House 31 |

{{house.description}}

32 |
33 |
34 |

Talk To An Agent

35 |

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 |
41 | 42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 | 55 |
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 | --------------------------------------------------------------------------------