├── .coveragerc ├── .dockerignore ├── .env.example ├── .github ├── dependabot.yml └── workflows │ └── django.yml ├── .gitignore ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── browser_calls ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_create_sample_data.py │ └── __init__.py ├── models.py ├── static │ ├── css │ │ └── bicycle-polo.css │ ├── img │ │ └── bicycle-polo.jpg │ └── js │ │ └── browser-calls.js ├── tests.py ├── urls.py └── views.py ├── docker-compose.yml ├── manage.py ├── package-lock.json ├── package.json ├── requirements.txt ├── templates ├── _messages.html ├── base.html ├── browser_calls │ └── support_dashboard.html └── index.html └── twilio_sample_project ├── __init__.py ├── settings ├── __init__.py ├── common.py ├── local.py ├── production.py └── test.py ├── urls.py └── wsgi.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # Coverage configuration file 2 | 3 | [run] 4 | source = . 5 | 6 | omit = 7 | twilio_sample_project/settings/* 8 | twilio_sample_project/wsgi.py 9 | */migrations/* 10 | */tests.py 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables for browser-calls-django 2 | 3 | # Django settings to use in this environment 4 | DJANGO_SETTINGS_MODULE=twilio_sample_project.settings.local 5 | 6 | # Twilio API credentials 7 | # Found at https://www.twilio.com/user/account/voice 8 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXX 9 | 10 | # Twilio phone number 11 | # Purchase one at https://www.twilio.com/user/account/phone-numbers 12 | TWILIO_NUMBER=+15555555555 13 | 14 | # You need to create a TwiML app to use this project. 15 | # Create one at https://www.twilio.com/user/account/apps/add, 16 | # then use its "Sid" value here 17 | TWIML_APPLICATION_SID=APZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 18 | 19 | # Your REST API Key information 20 | # https://www.twilio.com/console/project/api-keys 21 | # OR 22 | # twilio api:core:keys:create --friendly-name=voice-client-javascript -o json 23 | # NOTE: Make sure to copy the secret, it'll will only be displayed once 24 | API_KEY=SKXXXXXXXXXXXXXXXXXXXXXX 25 | API_SECRET=XXXXXXXXXXXXXXXXXXXXX 26 | -------------------------------------------------------------------------------- /.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: flake8 10 | versions: 11 | - 3.8.4 12 | - 3.9.0 13 | - dependency-name: twilio 14 | versions: 15 | - 6.51.1 16 | - 6.52.0 17 | - 6.53.0 18 | - 6.54.0 19 | - 6.55.0 20 | - 6.56.0 21 | - dependency-name: python-dotenv 22 | versions: 23 | - 0.16.0 24 | - dependency-name: pyflakes 25 | versions: 26 | - 2.2.0 27 | - 2.3.0 28 | - dependency-name: pycodestyle 29 | versions: 30 | - 2.6.0 31 | - dependency-name: phonenumbers 32 | versions: 33 | - 8.12.17 34 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django 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 --settings=twilio_sample_project.settings.test 32 | env: 33 | TWILIO_ACCOUNT_SID: ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 34 | TWILIO_NUMBER: +1XXXYYYZZZZ 35 | TWIML_APPLICATION_SID: APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 36 | API_KEY: SKXXXXXXXXXXXX 37 | API_SECRET: XXXXXXXXXXXXXX 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # virtualenv 5 | venv 6 | 7 | # Staticfiles 8 | twilio_sample_project/staticfiles 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # PyCharm 69 | .idea 70 | 71 | # NPM stuff: 72 | node_modules 73 | 74 | # SQLite database 75 | db.sqlite3 76 | 77 | .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 nikolaik/python-nodejs:python3.8-nodejs10 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | WORKDIR /usr/src/app 6 | 7 | COPY requirements.txt ./ 8 | 9 | COPY Makefile ./ 10 | 11 | COPY package.json ./ 12 | 13 | RUN make install 14 | 15 | RUN npm install 16 | 17 | COPY . . 18 | 19 | RUN make serve-setup 20 | 21 | EXPOSE 8000 22 | 23 | CMD ["sh", "-c", ". /usr/src/app/venv/bin/activate && make serve"] 24 | -------------------------------------------------------------------------------- /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-setup serve 2 | UNAME := $(shell uname) 3 | 4 | venv: 5 | ifeq ($(UNAME), Windows) 6 | py -3 -m venv venv; 7 | else 8 | python3 -m venv venv 9 | endif 10 | 11 | install: venv 12 | ifeq ($(UNAME), Windows) 13 | venv\Scripts\activate.bat; \ 14 | pip3 install -r requirements.txt; 15 | else 16 | . venv/bin/activate; \ 17 | pip3 install -r requirements.txt; 18 | endif 19 | 20 | serve-setup: 21 | . venv/bin/activate; \ 22 | python3 manage.py migrate; 23 | 24 | serve: 25 | . venv/bin/activate; \ 26 | python3 manage.py runserver 0.0.0.0:8000; 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Browser Calls (Django) 6 | 7 | > This repository is now archived and is no longer being maintained. 8 | > Check out the [JavaScript SDK Quickstarts](https://www.twilio.com/docs/voice/sdks/javascript/get-started) to get started with browser-based calling. 9 | 10 | ## About 11 | 12 | Learn how to use [Twilio's JavaScript SDK](https://www.twilio.com/docs/voice/sdks/javascript) to make browser-to-phone and browser-to-browser calls with ease. The unsatisfied customers of the Birchwood Bicycle Polo Co. need your help! 13 | 14 | ## Set up 15 | 16 | ### Requirements 17 | 18 | - [Python](https://www.python.org/) **3.6**, **3.7** or **3.8** version 19 | - [Sqlite3](https://www.sqlite.org/) 20 | - [Nodejs](https://nodejs.org/) v10 or v12 21 | 22 | ### Twilio Account Settings 23 | 24 | This application should give you a ready-made starting point for writing your own application. 25 | Before we begin, we need to collect all the config values we need to run the application: 26 | 27 | | Config Value | Description | 28 | | :----------- | :----------------------| 29 | | TWILIO_ACCOUNT_SID | Your primary Twilio account identifier - find this [in the Console](https://www.twilio.com/console).| 30 | | TWILIO_NUMBER | A Twilio phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) - you can [get one here](https://www.twilio.com/console/phone-numbers/incoming) | 31 | | TWIML_APPLICATION_SID | The TwiML application with a voice URL configured to access your server running this app - create one [in the console here](https://www.twilio.com/console/voice/twiml/apps). Also, you will need to configure the Voice "REQUEST URL" on the TwiML app once you've got your server up and running. | 32 | | API_KEY / API_SECRET | Your REST API Key information needed to create an [Access Token](https://www.twilio.com/docs/iam/access-tokens) - create [one here](https://www.twilio.com/console/project/api-keys). | 33 | 34 | ### Create a TwiML App 35 | 36 | This project is configured to use a **TwiML App**, which allows us to easily set the voice URLs for all Twilio phone numbers we purchase in this app. 37 | 38 | Create a new TwiML app at https://www.twilio.com/console/voice/twiml/apps and use its `Sid` as the `TWIML_APPLICATION_SID` environment variable wherever you run this app. 39 | 40 | Once you have created your TwiML app, configure your Twilio phone number to use it ([instructions here](https://support.twilio.com/hc/en-us/articles/223180928-How-Do-I-Create-a-TwiML-App-)). If you don't have a Twilio phone number yet, you can purchase a new number in your [Twilio Account Dashboard](https://www.twilio.com/console/phone-numbers/incoming). 41 | 42 | ### Local development 43 | 44 | 1. Clone this repo and `cd` into it. 45 | 46 | ```bash 47 | git clone https://github.com/TwilioDevEd/browser-calls-django.git 48 | cd browser-calls-django 49 | ``` 50 | 51 | 2. Create a new virtual environment, load it and install dependencies. 52 | 53 | ```bash 54 | make install 55 | ``` 56 | 57 | 58 | 3. Install the twilio-client js library. 59 | 60 | ```bash 61 | npm install 62 | ``` 63 | 64 | 4. Set your environment variables. Copy the env.example file and edit it. 65 | 66 | ```bash 67 | cp .env.example .env 68 | ``` 69 | 70 | See [Twilio Account Settings](#twilio-account-settings) to locate the necessary environment variables. 71 | 72 | 5. Run the migrations. 73 | 74 | ```bash 75 | make serve-setup 76 | ``` 77 | 78 | 6. Start the development server (will run on port 8000). Before running the following command, make sure the virtual environment is activated. 79 | 80 | ```bash 81 | make serve 82 | ``` 83 | 84 | 7. Expose your application to the wider internet using [ngrok](http://ngrok.com). This step 85 | **is important** because the application won't work as expected if you run it through 86 | localhost. 87 | 88 | ```bash 89 | $ ngrok http 8000 90 | ``` 91 | 92 | 8. Once you have started ngrok, update your [TwiML app's](#create-a-twiml-app) voice URL setting to use 93 | your ngrok hostname, so it will look something like this: 94 | 95 | ```bash 96 | http://.ngrok.io/support/call 97 | ``` 98 | 99 | 9. Everything is setup, now you can open two tabs: 100 | - The support agent: http://localhost:8000/support/dashboard 101 | - The customer: http://localhost:8000 102 | 103 | When the customer click on the "Call Support" button, the support agent will see the call immediatly and be able to pick up the call with the "Answer Call" button. 104 | 105 | Another scenario is the customer fill out the form to open a ticket, the support agent can refresh the dashboard and we'll be able to click the "Call customer" button which will start a call to the phone number listed in the ticket. 106 | 107 | That's it! 108 | 109 | ### Tests 110 | 111 | You can run the tests locally through [coverage](http://coverage.readthedocs.org/), before running the following command, make sure the virtual environment is activated. 112 | 113 | ``` 114 | $ coverage run manage.py test --settings=twilio_sample_project.settings.test 115 | ``` 116 | 117 | You can then view the results with `coverage report` or build an HTML report with `coverage html`. | 118 | 119 | ## Resources 120 | 121 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/). 122 | 123 | ## License 124 | 125 | [MIT](http://www.opensource.org/licenses/mit-license.html) 126 | 127 | ## Disclaimer 128 | 129 | No warranty expressed or implied. Software is as is. 130 | 131 | [twilio]: https://www.twilio.com 132 | -------------------------------------------------------------------------------- /browser_calls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-django/9e76672d22cf622cc22bb3e9c1e86b53753b57d8/browser_calls/__init__.py -------------------------------------------------------------------------------- /browser_calls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import SupportTicket 4 | 5 | 6 | class SupportTicketAdmin(admin.ModelAdmin): 7 | list_display = ('id', 'name', 'phone_number') 8 | 9 | 10 | admin.site.register(SupportTicket, SupportTicketAdmin) 11 | -------------------------------------------------------------------------------- /browser_calls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import phonenumber_field.modelfields 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='SupportTicket', 15 | fields=[ 16 | ( 17 | 'id', 18 | models.AutoField( 19 | serialize=False, 20 | auto_created=True, 21 | primary_key=True, 22 | verbose_name='ID', 23 | ), 24 | ), 25 | ('name', models.CharField(max_length=100)), 26 | ( 27 | 'phone_number', 28 | phonenumber_field.modelfields.PhoneNumberField( 29 | max_length=128 30 | ), 31 | ), 32 | ('description', models.TextField()), 33 | ('timestamp', models.DateTimeField(auto_now_add=True)), 34 | ], 35 | ) 36 | ] 37 | -------------------------------------------------------------------------------- /browser_calls/migrations/0002_create_sample_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | def create_sample_data(apps, schema_editor): 8 | """Prepopulate the app with some sample data""" 9 | SupportTicket = apps.get_model('browser_calls', 'SupportTicket') 10 | 11 | SupportTicket.objects.create( 12 | name='Charles Holdsworth', 13 | phone_number='+14153674129', 14 | description="I played for Middlesex in the championships and my \ 15 | mallet squeaked the whole time! I demand a refund!", 16 | ) 17 | 18 | SupportTicket.objects.create( 19 | name='John Woodger', 20 | phone_number='+15712812415', 21 | description='The mallet you sold me broke! Call me immediately!', 22 | ) 23 | 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [('browser_calls', '0001_initial')] 28 | 29 | operations = [migrations.RunPython(create_sample_data)] 30 | -------------------------------------------------------------------------------- /browser_calls/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-django/9e76672d22cf622cc22bb3e9c1e86b53753b57d8/browser_calls/migrations/__init__.py -------------------------------------------------------------------------------- /browser_calls/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from phonenumber_field.modelfields import PhoneNumberField 3 | 4 | 5 | class SupportTicket(models.Model): 6 | """A support ticket submitted by an unsatisfied customer :(""" 7 | 8 | name = models.CharField(max_length=100) 9 | phone_number = PhoneNumberField( 10 | help_text='Must include international prefix - e.g. +1 555 555 55555' 11 | ) 12 | description = models.TextField(help_text='A description of your problem') 13 | 14 | timestamp = models.DateTimeField(auto_now_add=True) 15 | 16 | def __str__(self): 17 | return '#{0} - {1}'.format(self.id, self.name) 18 | -------------------------------------------------------------------------------- /browser_calls/static/css/bicycle-polo.css: -------------------------------------------------------------------------------- 1 | /* Custom CSS for Bicycle Polo example */ 2 | .bicycle-polo-background { 3 | background-image: url(../img/bicycle-polo.jpg); 4 | background-size: cover; 5 | background-position: 75% 25%; 6 | } 7 | 8 | .bicycle-polo-text { 9 | background-color:rgba(255,255,255,0.75); 10 | } 11 | 12 | @media (min-width: 768px) { 13 | .bicycle-polo-text { 14 | margin-right: 5%; 15 | width: 40%; 16 | } 17 | } 18 | 19 | .answer-button, .call-support-button { 20 | width: 48%; 21 | float: left; 22 | } 23 | 24 | .hangup-button { 25 | width: 48%; 26 | float: right; 27 | } 28 | -------------------------------------------------------------------------------- /browser_calls/static/img/bicycle-polo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-django/9e76672d22cf622cc22bb3e9c1e86b53753b57d8/browser_calls/static/img/bicycle-polo.jpg -------------------------------------------------------------------------------- /browser_calls/static/js/browser-calls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Twilio Client configuration for the browser-calls-django 3 | * example application. 4 | */ 5 | 6 | // Store some selectors for elements we'll reuse 7 | var callStatus = $("#call-status"); 8 | var answerButton = $(".answer-button"); 9 | var callSupportButton = $(".call-support-button"); 10 | var hangUpButton = $(".hangup-button"); 11 | var callCustomerButtons = $(".call-customer-button"); 12 | 13 | var device; 14 | 15 | /* Helper function to update the call status bar */ 16 | function updateCallStatus(status) { 17 | callStatus.attr('placeholder', status); 18 | } 19 | 20 | console.log("Requesting Access Token..."); 21 | 22 | $(document).ready(function() { 23 | $.get("/support/token", {forPage: window.location.pathname}) 24 | .then(function(data) { 25 | // Setup Twilio.Device 26 | device = new Twilio.Device(data.token, { 27 | // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and 28 | // providing better audio quality in restrained network conditions. Opus will be default in 2.0. 29 | codecPreferences: ["opus", "pcmu"], 30 | // Use fake DTMF tones client-side. Real tones are still sent to the other end of the call, 31 | // but the client-side DTMF tones are fake. This prevents the local mic capturing the DTMF tone 32 | // a second time and sending the tone twice. This will be default in 2.0. 33 | fakeLocalDTMF: true, 34 | // Use `enableRingingState` to enable the device to emit the `ringing` 35 | // state. The TwiML backend also needs to have the attribute 36 | // `answerOnBridge` also set to true in the `Dial` verb. This option 37 | // changes the behavior of the SDK to consider a call `ringing` starting 38 | // from the connection to the TwiML backend to when the recipient of 39 | // the `Dial` verb answers. 40 | enableRingingState: true 41 | }); 42 | 43 | device.on("ready", function(device) { 44 | console.log("Twilio.Ready"); 45 | updateCallStatus("Ready"); 46 | }); 47 | 48 | device.on("error", function(error) { 49 | console.log("Twilio.Device Error: " + error.message); 50 | updateCallStatus("ERROR: " + error.message); 51 | }); 52 | 53 | device.on("connect", function(conn) { 54 | // Enable the hang up button and disable the call buttons 55 | hangUpButton.prop("disabled", false); 56 | callCustomerButtons.prop("disabled", true); 57 | callSupportButton.prop("disabled", true); 58 | answerButton.prop("disabled", true); 59 | 60 | // If phoneNumber is part of the connection, this is a call from a 61 | // support agent to a customer's phone 62 | if ("phoneNumber" in conn.message) { 63 | updateCallStatus("In call with " + conn.message.phoneNumber); 64 | } else { 65 | // This is a call from a website user to a support agent 66 | updateCallStatus("In call with support"); 67 | } 68 | }); 69 | 70 | device.on("disconnect", function(conn) { 71 | // Disable the hangup button and enable the call buttons 72 | hangUpButton.prop("disabled", true); 73 | callCustomerButtons.prop("disabled", false); 74 | callSupportButton.prop("disabled", false); 75 | 76 | updateCallStatus("Ready"); 77 | }); 78 | 79 | device.on("incoming", function(conn) { 80 | updateCallStatus("Incoming support call"); 81 | 82 | // Set a callback to be executed when the connection is accepted 83 | conn.accept(function() { 84 | updateCallStatus("In call with customer"); 85 | }); 86 | 87 | // Set a callback on the answer button and enable it 88 | answerButton.click(function() { 89 | conn.accept(); 90 | }); 91 | answerButton.prop("disabled", false); 92 | }); 93 | 94 | }) 95 | .catch(function(err){ 96 | console.log(err); 97 | console.log("Could not get a token from server!"); 98 | }) 99 | }); 100 | 101 | /* Call a customer from a support ticket */ 102 | function callCustomer(phoneNumber) { 103 | updateCallStatus("Calling " + phoneNumber + "..."); 104 | 105 | var params = {"phoneNumber": phoneNumber}; 106 | device.connect(params); 107 | } 108 | 109 | /* Call the support_agent from the home page */ 110 | function callSupport() { 111 | updateCallStatus("Calling support..."); 112 | 113 | // Our backend will assume that no params means a call to support_agent 114 | device.connect(); 115 | } 116 | 117 | /* End a call */ 118 | function hangUp() { 119 | device.disconnectAll(); 120 | } 121 | -------------------------------------------------------------------------------- /browser_calls/tests.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | from django.test import Client, TestCase 4 | from model_mommy import mommy 5 | 6 | from .models import SupportTicket 7 | 8 | 9 | class SupportTicketTest(TestCase): 10 | def test_str(self): 11 | # Arrange 12 | support_ticket = mommy.make( 13 | SupportTicket, 14 | name='Charles Holdsworth', 15 | phone_number='+12027621401', 16 | description='I have a problem!', 17 | ) 18 | 19 | # Assert 20 | self.assertEqual( 21 | str(support_ticket), 22 | '#{0} - {1}'.format(support_ticket.id, support_ticket.name), 23 | ) 24 | 25 | 26 | class HomePageTest(TestCase): 27 | def setUp(self): 28 | self.client = Client() 29 | 30 | def test_home_page(self): 31 | # Act 32 | response = self.client.get('/') 33 | 34 | # Assert 35 | # This is a class-based view, so we can mostly rely on Django's own 36 | # tests to make sure it works. We'll check for a bit of copy, though 37 | self.assertIn('sport of kings', str(response.content)) 38 | 39 | 40 | class SupportDashboardTest(TestCase): 41 | def setUp(self): 42 | self.client = Client() 43 | 44 | def test_support_dashboard(self): 45 | # Arrange 46 | support_ticket = mommy.make( 47 | SupportTicket, 48 | name='Charles Holdsworth', 49 | phone_number='+12027621401', 50 | description='I have a problem!', 51 | ) 52 | 53 | # Act 54 | response = self.client.get('/support/dashboard') 55 | 56 | # Assert 57 | self.assertEqual( 58 | len(response.context['support_tickets']), 59 | SupportTicket.objects.count(), 60 | ) 61 | 62 | 63 | class GetTokenTest(TestCase): 64 | def setUp(self): 65 | self.client = Client() 66 | 67 | def test_get_token_unauthenticated(self): 68 | mock_access_token = MagicMock() 69 | mock_access_token.to_jwt.return_value = b'abc123' 70 | 71 | with patch( 72 | 'browser_calls.views.AccessToken', 73 | return_value=mock_access_token 74 | ): 75 | response = self.client.get('/support/token', {'forPage': '/'}) 76 | 77 | self.assertTrue(mock_access_token.add_grant.called) 78 | 79 | self.assertTrue(mock_access_token.to_jwt.called) 80 | 81 | self.assertEqual(response.content, b'{"token": "abc123"}') 82 | 83 | def test_get_token_authenticated(self): 84 | mock_access_token = MagicMock() 85 | mock_access_token.to_jwt.return_value = b'foo123' 86 | 87 | with patch( 88 | 'browser_calls.views.AccessToken', 89 | return_value=mock_access_token 90 | ): 91 | response = self.client.get('/support/token', {'forPage': 'support/dashboard'}) 92 | 93 | self.assertTrue(mock_access_token.add_grant.called) 94 | 95 | self.assertTrue(mock_access_token.to_jwt.called) 96 | 97 | self.assertEqual(response.content, b'{"token": "foo123"}') 98 | 99 | class CallTest(TestCase): 100 | def setUp(self): 101 | self.client = Client() 102 | 103 | def test_call_phone_number(self): 104 | # Act 105 | response = self.client.post( 106 | '/support/call', {'phoneNumber': '+15555555555'} 107 | ) 108 | 109 | # Assert 110 | self.assertIn('+15555555555', str(response.content)) 111 | 112 | def test_call_support(self): 113 | # Act 114 | response = self.client.post('/support/call') 115 | 116 | # Assert 117 | self.assertIn('support_agent', str(response.content)) 118 | -------------------------------------------------------------------------------- /browser_calls/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import support_dashboard, get_token, call 4 | 5 | urlpatterns = [ 6 | # URLs for searching for and purchasing a new Twilio number 7 | path('dashboard', support_dashboard, name='dashboard'), 8 | path('token', get_token, name='token'), 9 | path('call', call, name='call'), 10 | ] 11 | -------------------------------------------------------------------------------- /browser_calls/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.messages.views import SuccessMessageMixin 3 | from django.http import HttpResponse, JsonResponse 4 | from django.shortcuts import render 5 | from django.urls import reverse, reverse_lazy 6 | from django.views.decorators.csrf import csrf_exempt 7 | from django.views.generic import CreateView 8 | from twilio.jwt.access_token import AccessToken 9 | from twilio.jwt.access_token.grants import VoiceGrant 10 | from twilio.twiml.voice_response import VoiceResponse 11 | 12 | from .models import SupportTicket 13 | 14 | 15 | class SupportTicketCreate(SuccessMessageMixin, CreateView): 16 | """Renders the home page and the support ticket form""" 17 | 18 | model = SupportTicket 19 | fields = ['name', 'phone_number', 'description'] 20 | template_name = 'index.html' 21 | success_url = reverse_lazy('home') 22 | success_message = "Your ticket was submitted! An agent will call you soon." 23 | 24 | 25 | def support_dashboard(request): 26 | """Shows the list of support tickets to a support agent""" 27 | context = {} 28 | 29 | context['support_tickets'] = SupportTicket.objects.order_by('-timestamp') 30 | 31 | return render(request, 'browser_calls/support_dashboard.html', context) 32 | 33 | 34 | def get_token(request): 35 | identity = 'support_agent' if 'dashboard' in request.GET['forPage'] else 'customer' 36 | 37 | # Create access token with credentials 38 | access_token = AccessToken(settings.TWILIO_ACCOUNT_SID, settings.API_KEY, settings.API_SECRET, identity=identity) 39 | 40 | # Create a Voice grant and add to token 41 | voice_grant = VoiceGrant( 42 | outgoing_application_sid=settings.TWIML_APPLICATION_SID, 43 | incoming_allow=True, # Optional: add to allow incoming calls 44 | ) 45 | access_token.add_grant(voice_grant) 46 | 47 | token = access_token.to_jwt() 48 | 49 | return JsonResponse({'token': token.decode('utf-8')}) 50 | 51 | @csrf_exempt 52 | def call(request): 53 | """Returns TwiML instructions to Twilio's POST requests""" 54 | response = VoiceResponse() 55 | dial = response.dial(caller_id=settings.TWILIO_NUMBER) 56 | 57 | # If the browser sent a phoneNumber param, we know this request 58 | # is a support agent trying to call a customer's phone 59 | if 'phoneNumber' in request.POST: 60 | dial.number(request.POST['phoneNumber']) 61 | else: 62 | # Otherwise we assume this request is a customer trying 63 | # to contact support from the home page 64 | dial.client('support_agent') 65 | 66 | return HttpResponse( 67 | str(response), content_type='application/xml; charset=utf-8' 68 | ) 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | container_name: app 5 | restart: always 6 | build: . 7 | ports: 8 | - "8000:8000" 9 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault( 7 | "DJANGO_SETTINGS_MODULE", 8 | "twilio_sample_project.settings.local") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-calls-django", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@twilio/audioplayer": { 8 | "version": "1.0.6", 9 | "resolved": "https://registry.npmjs.org/@twilio/audioplayer/-/audioplayer-1.0.6.tgz", 10 | "integrity": "sha512-c9cjX/ifICgXqShtyAQdVMqfe7odnxougiuRMXBJtn3dZ320mFdt7kmuKedpNnc3ZJ6irOZ9M9MZi9/vuEqHiw==", 11 | "requires": { 12 | "babel-runtime": "^6.26.0" 13 | } 14 | }, 15 | "@twilio/voice-errors": { 16 | "version": "1.0.1", 17 | "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.0.1.tgz", 18 | "integrity": "sha512-iXzCuiOhNMhrr8DVjRRzI14YwGUIBM83kWSWcDktxmXim0Tz9xoCth4QFAQcMkNL2h9DlfXlob6noH+3h2iA4A==" 19 | }, 20 | "async-limiter": { 21 | "version": "1.0.1", 22 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 23 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 24 | }, 25 | "babel-runtime": { 26 | "version": "6.26.0", 27 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 28 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 29 | "requires": { 30 | "core-js": "^2.4.0", 31 | "regenerator-runtime": "^0.11.0" 32 | } 33 | }, 34 | "backoff": { 35 | "version": "2.5.0", 36 | "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", 37 | "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", 38 | "requires": { 39 | "precond": "0.2" 40 | } 41 | }, 42 | "core-js": { 43 | "version": "2.6.11", 44 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", 45 | "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" 46 | }, 47 | "jquery": { 48 | "version": "3.5.1", 49 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", 50 | "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" 51 | }, 52 | "loglevel": { 53 | "version": "1.6.7", 54 | "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", 55 | "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==" 56 | }, 57 | "precond": { 58 | "version": "0.2.3", 59 | "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", 60 | "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" 61 | }, 62 | "regenerator-runtime": { 63 | "version": "0.11.1", 64 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 65 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 66 | }, 67 | "rtcpeerconnection-shim": { 68 | "version": "1.2.8", 69 | "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.8.tgz", 70 | "integrity": "sha512-5Sx90FGru1sQw9aGOM+kHU4i6mbP8eJPgxliu2X3Syhg8qgDybx8dpDTxUwfJvPnubXFnZeRNl59DWr4AttJKQ==", 71 | "requires": { 72 | "sdp": "^2.6.0" 73 | } 74 | }, 75 | "sdp": { 76 | "version": "2.12.0", 77 | "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", 78 | "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" 79 | }, 80 | "twilio-client": { 81 | "version": "1.10.2", 82 | "resolved": "https://registry.npmjs.org/twilio-client/-/twilio-client-1.10.2.tgz", 83 | "integrity": "sha512-EQM8Szg+QAoNL45ne+rxhPad4BEIiR88n7EqZXV9V2LnF/kiwe2LrvPbyPDGf+0J6vIrLa3f4yujsttu4h0wfQ==", 84 | "requires": { 85 | "@twilio/audioplayer": "1.0.6", 86 | "@twilio/voice-errors": "1.0.1", 87 | "backoff": "2.5.0", 88 | "loglevel": "1.6.7", 89 | "rtcpeerconnection-shim": "1.2.8", 90 | "ws": "6.1.3", 91 | "xmlhttprequest": "1.8.0" 92 | } 93 | }, 94 | "ws": { 95 | "version": "6.1.3", 96 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", 97 | "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", 98 | "requires": { 99 | "async-limiter": "~1.0.0" 100 | } 101 | }, 102 | "xmlhttprequest": { 103 | "version": "1.8.0", 104 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", 105 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-calls-django", 3 | "version": "1.0.0", 4 | "description": "A sample application which shows you how to make and receive phone calls with a browser and Twilio Client", 5 | "dependencies": { 6 | "jquery": "^3.5.1", 7 | "twilio-client": "^1.10.2" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/TwilioDevEd/browser-calls-django.git" 16 | }, 17 | "keywords": [ 18 | "twilio" 19 | ], 20 | "author": "Twilio Developer Education", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/TwilioDevEd/browser-calls-django/issues" 24 | }, 25 | "homepage": "https://github.com/TwilioDevEd/browser-calls-django#readme" 26 | } 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ########################### 2 | # Direct Dependencies 3 | ########################### 4 | # black 5 | # coverage 6 | # django 7 | # django-bootstrap4 8 | # django-phonenumber-field 9 | # flake8 10 | # model-mommy 11 | # phonenumbers 12 | # psycopg2 13 | # python-dotenv 14 | # twilio 15 | # whitenoise 16 | 17 | appdirs==1.4.4 18 | attrs==20.3.0 19 | Babel==2.9.1 20 | beautifulsoup4==4.9.3 21 | black==21.4b2 22 | certifi==2020.12.5 23 | chardet==4.0.0 24 | Click==7.1.2 25 | coverage==5.5 26 | Django==3.2 27 | django-bootstrap4==3.0.0 28 | django-phonenumber-field==5.1.0 29 | entrypoints==0.3 30 | flake8==3.7.9 31 | idna==2.10 32 | mccabe==0.6.1 33 | model-mommy==2.0.0 34 | phonenumbers==8.12.21 35 | pycodestyle==2.5.0 36 | pyflakes==2.1.1 37 | PyJWT==2.1.0 38 | PySocks==1.7.1 39 | python-dotenv==0.17.0 40 | pytz==2021.1 41 | requests==2.25.1 42 | six==1.15.0 43 | soupsieve==2.2.1 44 | sqlparse==0.4.1 45 | toml==0.10.2 46 | twilio==6.50.1 47 | urllib3==1.26.4 48 | whitenoise==5.2.0 49 | -------------------------------------------------------------------------------- /templates/_messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 | {% for message in messages %} 3 | 7 | {% endfor %} 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 %} 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}{% endblock title %} - Browser Calls 10 | 11 | 12 | 13 | 16 | 17 | 18 | {% bootstrap_css %} 19 | 20 | 21 | {% block page_css %}{% endblock %} 22 | 23 | 24 | 25 | 45 | 46 | {% block content %}{% endblock content %} 47 | 48 | 56 | 57 | 58 | {% bootstrap_javascript jquery=True %} 59 | 60 | 61 | 62 | {% block page_js %}{% endblock %} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /templates/browser_calls/support_dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load bootstrap4 %} 4 | 5 | {% block title %}Support Dashboard{% endblock title %} 6 | 7 | {% block content %} 8 |
9 | 10 |
11 |
12 |

Support Tickets

13 | 14 |

15 | This is the list of most recent support tickets. Click the "Call customer" button to start a phone call from your browser. 16 |

17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 | Make a call 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | 38 | 39 | 42 |
43 |
44 |
45 | 46 | 47 |
48 | {% for ticket in support_tickets %} 49 |
50 |
51 | Ticket #{{ ticket.id }} 52 | 53 | {{ ticket.timestamp }} 54 |
55 | 56 |
57 |
58 |
59 |

Name: {{ ticket.name }}

60 |

Phone number: {{ ticket.phone_number.as_international }}

61 |

Description:

62 | {{ ticket.description|linebreaks }} 63 |
64 | 65 |
66 | 69 |
70 |
71 |
72 |
73 | {% endfor %} 74 |
75 | 76 |
77 |
78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load bootstrap4 %} 4 | 5 | {% block title %}Home{% endblock title %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |

Birchwood Bicycle Polo Co.

12 |

13 | We sell only the finest bicycle polo supplies for those seeking fame and glory in this sport of kings. 14 |

15 |
16 |

17 | Talk to one of our support agents now — or fill out the support form below and someone will call you later. 18 |

19 |
20 |
21 | Get help 22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 | {% include '_messages.html' %} 30 | 31 |

Contact support

32 | 33 |

34 | Talk with one of our support agents right now by clicking the "Call Support" button on the right. If you can't talk now, fill out a support ticket and an agent will call you later. 35 |

36 | 37 |
38 | 39 |
40 |
41 |
42 | Talk to support now 43 |
44 | 45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 | 56 | 57 | 60 |
61 |
62 |
63 | 64 |
65 |
66 | {% csrf_token %} 67 | {% bootstrap_form form %} 68 | {% buttons %} 69 | 70 | {% endbuttons %} 71 |
72 |
73 | 74 |
75 |
76 | {% endblock %} 77 | 78 | 79 | {% block footer %} 80 | 81 | Header photo courtesy David Sachs via Flickr 82 | 83 | {% endblock %} -------------------------------------------------------------------------------- /twilio_sample_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-django/9e76672d22cf622cc22bb3e9c1e86b53753b57d8/twilio_sample_project/__init__.py -------------------------------------------------------------------------------- /twilio_sample_project/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-django/9e76672d22cf622cc22bb3e9c1e86b53753b57d8/twilio_sample_project/settings/__init__.py -------------------------------------------------------------------------------- /twilio_sample_project/settings/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common Django settings for the project. 3 | 4 | See the local, test, and production settings modules for the values used 5 | in each environment. 6 | 7 | For more information on this file, see 8 | https://docs.djangoproject.com/en/1.8/topics/settings/ 9 | 10 | For the full list of settings and their values, see 11 | https://docs.djangoproject.com/en/1.8/ref/settings/ 12 | """ 13 | from django.core.exceptions import ImproperlyConfigured 14 | 15 | import os 16 | 17 | from dotenv import load_dotenv 18 | 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 20 | 21 | # SECURITY WARNING: don't run with debug turned on in production! 22 | DEBUG = False 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = 'not-so-secret' 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Loading .env file automatically 30 | load_dotenv(dotenv_path=os.path.join(BASE_DIR, '.env')) 31 | 32 | # Twilio API credentials 33 | TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID') 34 | 35 | # Twilio number 36 | TWILIO_NUMBER = os.environ.get('TWILIO_NUMBER') 37 | 38 | # TwiML Application SID 39 | TWIML_APPLICATION_SID = os.environ.get('TWIML_APPLICATION_SID') 40 | 41 | API_KEY = os.environ.get('API_KEY') 42 | API_SECRET = os.environ.get('API_SECRET') 43 | 44 | if not (TWILIO_ACCOUNT_SID and TWILIO_NUMBER and TWIML_APPLICATION_SID and API_KEY and API_SECRET): 45 | missing_config_values = \ 46 | """ 47 | You must set the TWILIO_ACCOUNT_SID, TWILIO_NUMBER, TWIML_APPLICATION_SID, API_KEY and API_SECRET environment variables to run this app. 48 | Consult the README for instructions on how to find them. 49 | """ 50 | raise ImproperlyConfigured(missing_config_values) 51 | 52 | # Application definition 53 | 54 | DJANGO_APPS = ( 55 | 'django.contrib.admin', 56 | 'django.contrib.auth', 57 | 'django.contrib.contenttypes', 58 | 'django.contrib.sessions', 59 | 'django.contrib.messages', 60 | 'django.contrib.staticfiles', 61 | 'django.contrib.humanize' 62 | ) 63 | 64 | THIRD_PARTY_APPS = ( 65 | 'bootstrap4', 66 | ) 67 | 68 | LOCAL_APPS = ( 69 | 'browser_calls', 70 | ) 71 | 72 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 73 | 74 | MIDDLEWARE = ( 75 | 'django.middleware.security.SecurityMiddleware', 76 | 'whitenoise.middleware.WhiteNoiseMiddleware', 77 | 'django.contrib.sessions.middleware.SessionMiddleware', 78 | 'django.middleware.common.CommonMiddleware', 79 | 'django.middleware.csrf.CsrfViewMiddleware', 80 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 81 | 'django.contrib.messages.middleware.MessageMiddleware', 82 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 83 | ) 84 | 85 | ROOT_URLCONF = 'twilio_sample_project.urls' 86 | 87 | TEMPLATES = [ 88 | { 89 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 90 | 'DIRS': ['templates/'], 91 | 'APP_DIRS': True, 92 | 'OPTIONS': { 93 | 'context_processors': [ 94 | 'django.template.context_processors.debug', 95 | 'django.template.context_processors.request', 96 | 'django.contrib.auth.context_processors.auth', 97 | 'django.contrib.messages.context_processors.messages', 98 | ], 99 | }, 100 | }, 101 | ] 102 | 103 | WSGI_APPLICATION = 'twilio_sample_project.wsgi.application' 104 | 105 | 106 | # Database 107 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 108 | 109 | DATABASES = { 110 | 'default': { 111 | 'ENGINE': 'django.db.backends.sqlite3', 112 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 113 | } 114 | } 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'en-us' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 133 | STATICFILES_DIRS = ( 134 | os.path.join(BASE_DIR, 'node_modules', 'twilio-client', 'dist'), 135 | os.path.join(BASE_DIR, 'node_modules', 'jquery', 'dist') 136 | ) 137 | 138 | # STATIC_ROOT = BASE_DIR + '/staticfiles' 139 | 140 | STATIC_URL = '/static/' 141 | 142 | # Messages settings for Bootstrap 3 143 | 144 | from django.contrib.messages import constants as messages 145 | MESSAGE_TAGS = { 146 | messages.ERROR: 'danger' 147 | } 148 | 149 | # Redirect login to /support/dashboard 150 | LOGIN_REDIRECT_URL = '/support/dashbaord' 151 | -------------------------------------------------------------------------------- /twilio_sample_project/settings/local.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Local settings 3 | 4 | - Run in Debug mode 5 | ''' 6 | 7 | from .common import * # noqa 8 | 9 | # Use DEBUG for local development 10 | DEBUG = True 11 | 12 | ALLOWED_HOSTS = ['*'] 13 | -------------------------------------------------------------------------------- /twilio_sample_project/settings/production.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Production settings 3 | 4 | - Set secret key from environment variable 5 | ''' 6 | 7 | from .common import * 8 | 9 | SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') 10 | 11 | # Allow all hosts, so we can run on PaaS's like Heroku 12 | ALLOWED_HOSTS = ['*'] 13 | -------------------------------------------------------------------------------- /twilio_sample_project/settings/test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Test settings 3 | 4 | - Run in Debug mode 5 | ''' 6 | 7 | from .common import * # noqa 8 | 9 | # Turn on DEBUG for tests 10 | DEBUG = True 11 | -------------------------------------------------------------------------------- /twilio_sample_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.contrib import admin 3 | from django.contrib.auth import views as auth_views 4 | 5 | from browser_calls.views import SupportTicketCreate 6 | 7 | urlpatterns = [ 8 | # Your URLs go here 9 | path('', SupportTicketCreate.as_view(), name='home'), 10 | path('support/', include('browser_calls.urls')), 11 | 12 | # Include the Django admin 13 | path('admin/', admin.site.urls), 14 | ] 15 | -------------------------------------------------------------------------------- /twilio_sample_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for the project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | # Use our production settings as our default settings, which is most secure 15 | os.environ.setdefault( 16 | "DJANGO_SETTINGS_MODULE", 17 | "twilio_sample_project.settings.production") 18 | 19 | # Get a WSGI application for our project 20 | application = get_wsgi_application() 21 | --------------------------------------------------------------------------------