├── .env.example ├── .gitignore ├── README.md ├── app.py ├── requirements.txt ├── start.sh ├── static └── assets │ └── main.css └── templates └── index.html /.env.example: -------------------------------------------------------------------------------- 1 | TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID 2 | TWILIO_AUTH_TOKEN=YOUR_AUTH_TOKEN 3 | TWILIO_PHONE_NUMBER=YOUR_TWILIO_NUMBER 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/flask,python 3 | # Edit at https://www.gitignore.io/?templates=flask,python 4 | 5 | ### Flask ### 6 | instance/* 7 | !instance/.gitignore 8 | .webassets-cache 9 | 10 | ### Flask.Python Stack ### 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # pipenv 80 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 81 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 82 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 83 | # install all needed dependencies. 84 | #Pipfile.lock 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # Mr Developer 100 | .mr.developer.cfg 101 | .project 102 | .pydevproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | .dmypy.json 110 | dmypy.json 111 | 112 | # Pyre type checker 113 | .pyre/ 114 | 115 | ### Python ### 116 | # Byte-compiled / optimized / DLL files 117 | 118 | # C extensions 119 | 120 | # Distribution / packaging 121 | 122 | # PyInstaller 123 | # Usually these files are written by a python script from a template 124 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 125 | 126 | # Installer logs 127 | 128 | # Unit test / coverage reports 129 | 130 | # Translations 131 | 132 | # Scrapy stuff: 133 | 134 | # Sphinx documentation 135 | 136 | # PyBuilder 137 | 138 | # pyenv 139 | 140 | # pipenv 141 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 142 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 143 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 144 | # install all needed dependencies. 145 | 146 | # celery beat schedule file 147 | 148 | # SageMath parsed files 149 | 150 | # Spyder project settings 151 | 152 | # Rope project settings 153 | 154 | # Mr Developer 155 | 156 | # mkdocs documentation 157 | 158 | # mypy 159 | 160 | # Pyre type checker 161 | 162 | # End of https://www.gitignore.io/api/flask,python 163 | 164 | .env 165 | .venv 166 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Application - Complimentr 2 | 3 | This application is meant to be used with the [Introduction to APIs course](https://github.com/craigsdennis/intro-to-apis-course). 4 | 5 | ## Use this on Glitch 6 | 7 | [Remix on Glitch](https://glitch.com/edit/#!/import/git?url=https://github.com/craigsdennis/intro-to-apis-flask) 8 | 9 | ⚠️ Several students have reported that cloning erroneously sets up a default Glitch application. If this happens to you, in the Glitch app that is created choose **Tools** >> **Extras** >> **Git Import and Export** >> **Import from GitHub** when prompted enter `craigsdennis/intro-to-apis-flask` 10 | 11 | ## Local Installation 12 | 13 | Copy `.env.example` to `.env` and update it with your [Twilio](https://twilio.com) credentials. 14 | 15 | ### Running the application 16 | 17 | * `python -m venv .venv` 18 | * `source ./.venv/bin/activate` 19 | * `pip install -r requirements.txt` 20 | * `FLASK_ENV=development flask run` 21 | 22 | #### In Development mode 23 | 24 | * Run [ngrok](https://ngrok.com/) on port 5000 25 | * Visit your ngrok url! 26 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from flask import ( 5 | Flask, 6 | flash, 7 | render_template, 8 | redirect, 9 | request, 10 | url_for, 11 | ) 12 | 13 | load_dotenv() 14 | app = Flask(__name__) 15 | app.secret_key = "ssssh don't tell anyone" 16 | 17 | TWILIO_PHONE_NUMBER = os.getenv('TWILIO_PHONE_NUMBER') 18 | 19 | def get_sent_messages(): 20 | # TODO: Make this return a collection of messages that were sent from the number 21 | messages = [] 22 | return messages 23 | 24 | def send_message(to, body): 25 | # TODO: Send the text message 26 | pass 27 | 28 | @app.route("/", methods=["GET"]) 29 | def index(): 30 | messages = get_sent_messages() 31 | return render_template("index.html", messages=messages) 32 | 33 | @app.route("/add-compliment", methods=["POST"]) 34 | def add_compliment(): 35 | sender = request.values.get('sender', 'Someone') 36 | receiver = request.values.get('receiver', 'Someone') 37 | compliment = request.values.get('compliment', 'wonderful') 38 | to = request.values.get('to') 39 | body = f'{sender} says: {receiver} is {compliment}. See more compliments at {request.url_root}' 40 | send_message(to, body) 41 | flash('Your message was successfully sent') 42 | return redirect(url_for('index')) 43 | 44 | if __name__ == '__main__': 45 | app.run() 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | Flask==1.1.1 3 | itsdangerous==1.1.0 4 | Jinja2==2.10.3 5 | MarkupSafe==1.1.1 6 | python-dotenv==0.10.3 7 | Werkzeug==0.16.0 8 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | # This is a Glitch startup script 2 | ROOT_ENV=.rootenv 3 | VIRTUALENV=.venv 4 | if [ ! -d $ROOT_ENV ]; then 5 | python3 -m venv $ROOT_ENV 6 | fi 7 | if [ ! -f $ROOT_ENV/bin/pip ]; then 8 | curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | $ROOT_ENV/bin/python 9 | $ROOT_ENV/bin/pip install virtualenv 10 | fi 11 | if [ ! -d $VIRTUALENV ]; then 12 | $ROOT_ENV/bin/virtualenv $VIRTUALENV 13 | fi 14 | if [ ! -f .env ]; then 15 | cp .env.example .env 16 | refresh 17 | fi 18 | source $VIRTUALENV/bin/activate 19 | echo "Activated virtualenv Python is `which python`" 20 | pip install -r requirements.txt 21 | python app.py -------------------------------------------------------------------------------- /static/assets/main.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | /*********************************** 51 | MAIN CSS 52 | ************************************/ 53 | body { 54 | font-family: 'Rubik', sans-serif; 55 | background-color: #3498db; 56 | color: rgba(0, 0, 0, .88); 57 | line-height: 1.5; 58 | display: flex; 59 | align-items: center; 60 | height: 100vh; 61 | letter-spacing: .25px; 62 | } 63 | 64 | h1, h2, h3, h4, h5, h6 { 65 | text-transform: uppercase; 66 | color: #20325C; 67 | } 68 | 69 | h1 { 70 | font-family: 'Rubik', sans-serif; 71 | font-weight: 900; 72 | font-size: 2em; 73 | color: #3498db; 74 | margin-top: .25em; 75 | } 76 | 77 | h2 { 78 | text-transform: uppercase; 79 | text-align: center; 80 | letter-spacing: 2.5px; 81 | font-size: 1em; 82 | margin-top: .5em; 83 | } 84 | 85 | .container { 86 | display: flex; 87 | flex-flow: column; 88 | align-items: center; 89 | justify-content: space-between; 90 | width: 100vw; 91 | height: 100vh; 92 | background-color: #fff; 93 | } 94 | 95 | .content { 96 | flex-grow: 2; 97 | margin: .5em 2em 0 2em; 98 | border-bottom: #bdc3c7 solid 1px; 99 | overflow-y: scroll; 100 | } 101 | 102 | .content li { 103 | margin: .38em 0; 104 | } 105 | 106 | .main-form { 107 | display: flex; 108 | flex-flow: column; 109 | width: 90%; 110 | margin: .5em 2em 0 2em; 111 | } 112 | 113 | .form-row { 114 | display: flex; 115 | flex-flow: column; 116 | justify-content: space-between; 117 | align-items: bottom; 118 | margin: .75em 0; 119 | } 120 | 121 | .form-row label { 122 | font-size: .75em; 123 | margin-top: .75em; 124 | } 125 | 126 | .main-form input { 127 | display: flex; 128 | flex-flow: column; 129 | width: 100%; 130 | background-color: transparent; 131 | padding-top: .5em; 132 | border-style: none none solid none; 133 | border-color: #555; 134 | outline: none; 135 | font-size: 1.25em; 136 | line-height: 2em; 137 | border-radius: 0; 138 | } 139 | 140 | .float-text { 141 | display: flex; 142 | margin: .5em 0 2em 0; 143 | line-height: 2em; 144 | } 145 | 146 | .compliment-button { 147 | height: 4em; 148 | width: 100%; 149 | margin: .5em 0 -.1em 0; 150 | font-size: 1.25em; 151 | border-style: none; 152 | background-color: #F25749; 153 | color: #fff; 154 | } 155 | 156 | .compliment-button:hover { 157 | background-color: #c0392b; 158 | } 159 | 160 | .compliment-button:active { 161 | padding: 8px 13px 6px; 162 | } 163 | 164 | @media(min-width: 48rem) { 165 | h1 { 166 | font-size: 3em; 167 | } 168 | 169 | .container { 170 | width: 35vw; 171 | height: 90vh; 172 | box-shadow: 5px 5px 4px 1px rgba(52,73,94,0.75); 173 | border-radius: .5em; 174 | margin: 0 auto; 175 | } 176 | 177 | .form-row { 178 | flex-flow: row; 179 | } 180 | 181 | .form-row label { 182 | width: 45%; 183 | font-size: .75em; 184 | } 185 | 186 | .compliment-button { 187 | height: 2.5em; 188 | border-radius: 0 0 .5em .5em; 189 | } 190 | 191 | .float-text { 192 | margin: 0; 193 | } 194 | 195 | .form-row label { 196 | margin-top: 0; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Complimentr 4 | 5 | 6 | 7 |
8 | 9 |

Complimentr

10 | {% for flash_message in get_flashed_messages() %} 11 |
{{ flash_message }}
12 | {% endfor %} 13 | 14 |

Recent Compliments

15 |
16 | {% if messages %} 17 | 22 | {% endif %} 23 |
24 | 25 |

Send a compliment

26 |
27 |
28 | 32 | 36 |
37 |
38 | 42 | is 43 | 47 |
48 |
49 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | --------------------------------------------------------------------------------