├── .gitignore ├── License.md ├── Procfile ├── Readme.md ├── circle.yml ├── requirements.txt ├── run_debug_server.py ├── runtime.txt └── todo ├── __init__.py ├── database.py ├── error_handlers.py ├── models.py ├── test_base.py ├── test_models.py ├── test_views.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Virtualenv 60 | virtualenv/ 61 | 62 | # debug database 63 | debug.db 64 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Faerbit 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn todo:app --log-file - 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | #todo-backend-flask 2 | [![Circle CI](https://circleci.com/gh/Faerbit/todo-backend-flask.svg?style=shield)](https://circleci.com/gh/Faerbit/todo-backend-flask) 3 | [![Requirements Status](https://requires.io/github/Faerbit/todo-backend-flask/requirements.svg?branch=master)](https://requires.io/github/Faerbit/todo-backend-flask/requirements/?branch=master) 4 | 5 | A [todo backend](http://todobackend.com) written in Python with Flask. 6 | 7 | ##Tests 8 | You can run the unit tests with `python -m unittest discover`. 9 | 10 | ##Server 11 | You can start a debug server by executing `run_debug_server.py` 12 | 13 | ## License 14 | Licensed under the MIT License. 15 | See License.md for further details. 16 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | python: 3 | version: 3.4.2 4 | 5 | test: 6 | override: 7 | - python -m unittest discover 8 | 9 | deployment: 10 | production: 11 | branch: master 12 | commands: 13 | - heroku maintenance:on --app todo-backend-flask 14 | - git fetch --all --unshallow 15 | - git push git@heroku.com:todo-backend-flask.git $CIRCLE_SHA1:refs/heads/master 16 | - heroku run 'python -c "from todo.database import drop_tables, init_db; drop_tables(); init_db()"' --app todo-backend-flask 17 | - heroku maintenance:off --app todo-backend-flask 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | decorator==4.0.10 2 | Flask==0.12 3 | Flask-Cors==3.0.2 4 | Flask-SQLAlchemy==2.1 5 | gunicorn==19.6.0 6 | itsdangerous==0.24 7 | Jinja2==2.8.1 8 | MarkupSafe==0.23 9 | pbr==1.10.0 10 | psycopg2==2.6.2 11 | six==1.10.0 12 | SQLAlchemy==1.1.4 13 | Werkzeug==0.15.3 14 | wheel==0.29.0 15 | -------------------------------------------------------------------------------- /run_debug_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from todo import app 3 | from todo.database import init_db 4 | 5 | if __name__ == "__main__": 6 | init_db() 7 | app.run(debug=True, threaded=True) 8 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.4.3 2 | -------------------------------------------------------------------------------- /todo/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask.ext.cors import CORS 4 | 5 | DATABASE = os.getenv("DATABASE_URL", "sqlite:///debug.db") 6 | DATABASE = DATABASE.strip() 7 | 8 | app = Flask(__name__) 9 | app.config.from_object(__name__) 10 | CORS(app, resources=r'/*', allow_headers="Content-Type") 11 | 12 | import todo.views 13 | -------------------------------------------------------------------------------- /todo/database.py: -------------------------------------------------------------------------------- 1 | from todo import app 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import scoped_session, sessionmaker 5 | from sqlalchemy.ext.declarative import declarative_base 6 | 7 | engine = create_engine(app.config["DATABASE"], convert_unicode=True) 8 | db_session = scoped_session(sessionmaker(autocommit=False, 9 | autoflush=False, 10 | bind=engine)) 11 | 12 | Base = declarative_base() 13 | Base.query = db_session.query_property() 14 | 15 | def init_db(): 16 | import todo.models 17 | Base.metadata.create_all(bind=engine) 18 | 19 | def drop_tables(): 20 | Base.metadata.drop_all(bind=engine) 21 | -------------------------------------------------------------------------------- /todo/error_handlers.py: -------------------------------------------------------------------------------- 1 | class InvalidUsage(Exception): 2 | status_code = 400 3 | 4 | def __init__(self, message, status_code=None): 5 | Exception.__init__(self) 6 | self.message = message 7 | if status_code is not None: 8 | self.status_code = status_code 9 | 10 | def to_dict(self): 11 | return dict(message=self.message) 12 | -------------------------------------------------------------------------------- /todo/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Boolean 2 | from todo.database import Base 3 | 4 | class Entry(Base): 5 | __tablename__ = "entries" 6 | id = Column(Integer, primary_key=True) 7 | title = Column(String) 8 | order = Column(Integer) 9 | completed = Column(Boolean) 10 | 11 | def __init__(self, title=None, order=None): 12 | self.title = title 13 | self.order = order 14 | self.completed = False 15 | 16 | def __repr__(self): 17 | return "".format(self.title) 18 | -------------------------------------------------------------------------------- /todo/test_base.py: -------------------------------------------------------------------------------- 1 | from todo import app 2 | from todo.database import init_db, Base, engine, drop_tables 3 | 4 | import unittest 5 | import os 6 | 7 | class BaseTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | app.config["TESTING"] = True 11 | if os.environ.get("CI"): 12 | app.config["DATABASE"] = "postgresql://ubuntu:@localhost/circle_test" 13 | else: 14 | app.config["DATABASE"] = "sqlite://" 15 | self.app = app.test_client() 16 | init_db() 17 | self.context = app.test_request_context() 18 | self.context.push() 19 | 20 | def tearDown(self): 21 | self.context.pop() 22 | drop_tables() 23 | -------------------------------------------------------------------------------- /todo/test_models.py: -------------------------------------------------------------------------------- 1 | from todo.test_base import BaseTestCase 2 | 3 | from todo.models import Entry 4 | from todo.database import db_session 5 | 6 | class EntryTestCase(BaseTestCase): 7 | 8 | def test_string_representation(self): 9 | text = "some text" 10 | entry = Entry(text) 11 | db_session.add(entry) 12 | db_session.commit() 13 | query = Entry.query.all()[0] 14 | self.assertEqual(str(query), "") 15 | 16 | def test_entries_get_created_unfinished(self): 17 | entry = Entry("some text") 18 | db_session.add(entry) 19 | db_session.commit() 20 | query = Entry.query.filter(Entry.completed == True).first() 21 | self.assertEqual(None, query) 22 | query = Entry.query.filter(Entry.completed == False).first() 23 | self.assertEqual(entry, query) 24 | 25 | def test_entries_are_ordered(self): 26 | text1 = "some text" 27 | text2 = "more text" 28 | text3 = "additional text" 29 | entry1 = Entry(text1) 30 | db_session.add(entry1) 31 | entry2 = Entry(text2) 32 | db_session.add(entry2) 33 | entry3 = Entry(text3) 34 | db_session.add(entry3) 35 | db_session.commit() 36 | query = Entry.query.all() 37 | self.assertEqual([entry1, entry2, entry3], query) 38 | self.assertNotEqual([entry2, entry1, entry3], query) 39 | 40 | def test_entries_can_be_created_with_order(self): 41 | text = "text" 42 | order = 10 43 | entry = Entry(text, order) 44 | db_session.add(entry) 45 | db_session.commit() 46 | query = Entry.query.all() 47 | self.assertEqual([entry], query) 48 | -------------------------------------------------------------------------------- /todo/test_views.py: -------------------------------------------------------------------------------- 1 | from todo.test_base import BaseTestCase 2 | 3 | import unittest 4 | from flask import json, url_for 5 | 6 | class IndexTestCase(BaseTestCase): 7 | 8 | def test_index(self): 9 | response = self.app.get(url_for("index")) 10 | self.assertEqual(response.status_code, 200) 11 | 12 | def test_cors_headers(self): 13 | response = self.app.get(url_for("index"), headers={"Origin": "www.example.com"}) 14 | self.assertEqual(response.headers["Access-Control-Allow-Origin"], "www.example.com") 15 | 16 | def test_index_allows_posts(self): 17 | data = dict(title="some text") 18 | response = self.app.post(url_for("index"), 19 | data=json.dumps(data), content_type="application/json") 20 | self.assertEqual(response.status_code, 200) 21 | 22 | def test_index_returns_lists(self): 23 | response = self.app.get(url_for("index") ) 24 | self.assertIsInstance(json.loads(response.data.decode("utf-8")), list) 25 | 26 | def test_index_returns_entry(self): 27 | data = dict(title="some other text") 28 | response = self.app.post(url_for("index"), 29 | data=json.dumps(data), content_type="application/json") 30 | self.assertEqual(data["title"], json.loads(response.data)["title"]) 31 | 32 | def test_index_allows_delete(self): 33 | response = self.app.delete(url_for("index")) 34 | self.assertEqual(response.status_code, 200) 35 | 36 | def test_index_responds_with_empty_array_after_delete(self): 37 | response = self.app.delete(url_for("index")) 38 | self.assertEqual(response.data.decode("utf-8"), "[]") 39 | 40 | def test_index_saves_posted_data(self): 41 | data = dict(title="different text") 42 | self.app.post(url_for("index"), data=json.dumps(data), content_type="application/json") 43 | response = self.app.get(url_for("index")) 44 | response_data = json.loads(response.data.decode("utf-8")) 45 | self.assertEqual(response_data[0]["title"], data["title"]) 46 | 47 | def test_index_deletes_all_entries_after_delete(self): 48 | data1 = dict(title="different text") 49 | self.app.post(url_for("index"), data=json.dumps(data1), content_type="application/json") 50 | data2 = dict(title="some different text") 51 | self.app.post(url_for("index"), data=json.dumps(data2), content_type="application/json") 52 | data3 = dict(title="more different text") 53 | self.app.post(url_for("index"), data=json.dumps(data3), content_type="application/json") 54 | self.app.delete(url_for("index")) 55 | response = self.app.get(url_for("index")) 56 | self.assertEqual(response.data.decode("utf-8"), "[]") 57 | 58 | def test_index_returns_multiple_entries_properly_formatted(self): 59 | data1 = dict(title="different text") 60 | self.app.post(url_for("index"), data=json.dumps(data1), content_type="application/json") 61 | data2 = dict(title="some different text") 62 | self.app.post(url_for("index"), data=json.dumps(data2), content_type="application/json") 63 | data3 = dict(title="more different text") 64 | self.app.post(url_for("index"), data=json.dumps(data3), content_type="application/json") 65 | response = self.app.get(url_for("index")) 66 | response_data = json.loads(response.data.decode("utf-8")) 67 | self.assertEqual(response_data[0]["title"], data1["title"]) 68 | self.assertEqual(response_data[1]["title"], data2["title"]) 69 | self.assertEqual(response_data[2]["title"], data3["title"]) 70 | 71 | def test_index_returns_no_comma_at_the_end_of_the_list(self): 72 | data = dict(title="different text") 73 | self.app.post(url_for("index"), data=json.dumps(data), content_type="application/json") 74 | response = self.app.get(url_for("index")) 75 | self.assertEqual(response.data.decode("utf-8")[-2:], "}]") 76 | 77 | def test_entries_contain_completed_property(self): 78 | data = dict(title="different text") 79 | self.app.post(url_for("index"), data=json.dumps(data), content_type="application/json") 80 | response = self.app.get(url_for("index")) 81 | response_data = json.loads(response.data.decode("utf-8")) 82 | self.assertIn("completed", response_data[0]) 83 | 84 | def test_new_entries_have_completed_property(self): 85 | data = dict(title="different text") 86 | response = self.app.post(url_for("index"), 87 | data=json.dumps(data), content_type="application/json") 88 | response_data = json.loads(response.data.decode("utf-8")) 89 | self.assertIn("completed", response_data) 90 | 91 | def test_new_entries_are_not_completed_post(self): 92 | data = dict(title="different text") 93 | response = self.app.post(url_for("index"), 94 | data=json.dumps(data), content_type="application/json") 95 | response_data = json.loads(response.data.decode("utf-8")) 96 | self.assertEqual(response_data["completed"], False) 97 | 98 | def test_new_entries_are_not_completed_get(self): 99 | data = dict(title="different text") 100 | self.app.post(url_for("index"), data=json.dumps(data), content_type="application/json") 101 | response = self.app.get(url_for("index")) 102 | response_data = json.loads(response.data.decode("utf-8")) 103 | self.assertEqual(response_data[0]["completed"], False) 104 | 105 | def test_new_entries_have_url_property(self): 106 | data = dict(title="different text") 107 | response = self.app.post(url_for("index"), 108 | data=json.dumps(data), content_type="application/json") 109 | response_data = json.loads(response.data.decode("utf-8")) 110 | self.assertIn("url", response_data) 111 | 112 | def test_entries_have_url_property(self): 113 | data = dict(title="different text") 114 | self.app.post(url_for("index"), data=json.dumps(data), content_type="application/json") 115 | response = self.app.get(url_for("index")) 116 | response_data = json.loads(response.data.decode("utf-8")) 117 | self.assertIn("url", response_data[0]) 118 | 119 | def test_entries_have_proper_url(self): 120 | data = dict(title="different text") 121 | self.app.post(url_for("index"), data=json.dumps(data), content_type="application/json") 122 | response = self.app.get(url_for("index")) 123 | response_data = json.loads(response.data.decode("utf-8")) 124 | self.assertEqual(url_for("entry", entry_id=1, _external=True), response_data[0]["url"]) 125 | 126 | def test_new_entries_have_proper_url(self): 127 | data = dict(title="different text") 128 | response = self.app.post(url_for("index"), 129 | data=json.dumps(data), content_type="application/json") 130 | response_data = json.loads(response.data.decode("utf-8")) 131 | self.assertEqual(url_for("entry", entry_id=1, _external=True), response_data["url"]) 132 | 133 | def test_can_create_new_entry_with_order(self): 134 | data = dict(title="different text", order=10) 135 | response = self.app.post(url_for("index"), 136 | data=json.dumps(data), content_type="application/json") 137 | self.assertEqual(response.status_code, 200) 138 | 139 | def test_new_entries_with_order_have_correct_order_property(self): 140 | data = dict(title="different text", order=10) 141 | self.app.post(url_for("index"), 142 | data=json.dumps(data), content_type="application/json") 143 | response = self.app.get(url_for("entry", entry_id=1)) 144 | response_data = json.loads(response.data.decode("utf-8")) 145 | self.assertEqual(data["order"], response_data["order"]) 146 | 147 | def test_new_entries_order_input_validation_string(self): 148 | data = dict(title="different text", order="not a number") 149 | response = self.app.post(url_for("index"), 150 | data=json.dumps(data), content_type="application/json") 151 | response_data = json.loads(response.data.decode("utf-8")) 152 | self.assertEqual(response.status_code, 400) 153 | self.assertEqual(data["order"] + " is not an integer.", response_data["message"]) 154 | 155 | def test_new_entries_order_input_validation_float(self): 156 | data = dict(title="different text", order=23.3) 157 | response = self.app.post(url_for("index"), 158 | data=json.dumps(data), content_type="application/json") 159 | response_data = json.loads(response.data.decode("utf-8")) 160 | self.assertEqual(response.status_code, 400) 161 | self.assertEqual(str(data["order"]) + " is not an integer.", response_data["message"]) 162 | 163 | 164 | class EntryTestCase(BaseTestCase): 165 | 166 | def setUp(self): 167 | BaseTestCase.setUp(self) 168 | self.data = dict(title="text", order=10) 169 | self.app.post(url_for("index"), 170 | data=json.dumps(self.data), content_type="application/json") 171 | 172 | def test_entry_returns_entry(self): 173 | response = self.app.get(url_for("entry", entry_id=1)) 174 | self.assertEqual(response.status_code, 200) 175 | 176 | def test_entry_returns_correct_entry(self): 177 | response = self.app.get(url_for("entry", entry_id=1)) 178 | response_data = json.loads(response.data.decode("utf-8")) 179 | self.assertEqual(self.data["title"], response_data["title"]) 180 | 181 | def test_entry_allows_patching_title(self): 182 | data = dict(title="different text") 183 | response = self.app.patch(url_for("entry", entry_id=1), 184 | data=json.dumps(data), content_type="application/json") 185 | self.assertEqual(response.status_code, 200) 186 | 187 | def test_patching_entry_changes_title(self): 188 | data = dict(title="different text") 189 | self.app.patch(url_for("entry", entry_id=1), 190 | data=json.dumps(data), content_type="application/json") 191 | response = self.app.get(url_for("entry", entry_id=1)) 192 | response_data = json.loads(response.data.decode("utf-8")) 193 | self.assertEqual(data["title"], response_data["title"]) 194 | 195 | def test_patching_entrys_completedness(self): 196 | data = dict(completed=True) 197 | self.app.patch(url_for("entry", entry_id=1), 198 | data=json.dumps(data), content_type="application/json") 199 | response = self.app.get(url_for("entry", entry_id=1)) 200 | response_data = json.loads(response.data.decode("utf-8")) 201 | self.assertEqual(data["completed"], response_data["completed"]) 202 | 203 | def test_entry_allows_delete(self): 204 | response = self.app.delete(url_for("entry", entry_id=1)) 205 | self.assertEqual(response.status_code, 200) 206 | 207 | def test_entry_delete_returns_empty_json(self): 208 | response = self.app.delete(url_for("entry", entry_id=1)) 209 | response_data = json.loads(response.data.decode("utf-8")) 210 | self.assertEqual(response_data, dict()) 211 | 212 | def test_entry_delete_deletes_entry(self): 213 | self.app.delete(url_for("entry", entry_id=1)) 214 | response = self.app.get(url_for("entry", entry_id=1)) 215 | response_data = json.loads(response.data.decode("utf-8")) 216 | self.assertEqual(response_data, dict()) 217 | 218 | def test_entry_delete_only_deletes_referenced_entry(self): 219 | data = dict(title="other") 220 | self.app.post(url_for("index"), 221 | data=json.dumps(data), content_type="application/json") 222 | self.app.delete(url_for("entry", entry_id=1)) 223 | response = self.app.get(url_for("entry", entry_id=2)) 224 | response_data = json.loads(response.data.decode("utf-8")) 225 | self.assertEqual(response_data["title"], data["title"]) 226 | 227 | def test_can_patch_order(self): 228 | data = dict(order=3) 229 | response = self.app.patch(url_for("entry", entry_id=1), 230 | data=json.dumps(data), content_type="application/json") 231 | self.assertEqual(response.status_code, 200) 232 | 233 | def test_patching_order_changes_order(self): 234 | data = dict(order=3) 235 | self.app.patch(url_for("entry", entry_id=1), 236 | data=json.dumps(data), content_type="application/json") 237 | response = self.app.get(url_for("entry", entry_id=1)) 238 | response_data = json.loads(response.data.decode("utf-8")) 239 | self.assertEqual(data["order"], response_data["order"]) 240 | 241 | def test_patching_completed_input_validation_string(self): 242 | data = dict(completed="not a bool") 243 | response = self.app.patch(url_for("entry", entry_id=1), 244 | data=json.dumps(data), content_type="application/json") 245 | response_data = json.loads(response.data.decode("utf-8")) 246 | self.assertEqual(response.status_code, 400) 247 | self.assertEqual(data["completed"] + " is not a boolean.", response_data["message"]) 248 | 249 | def test_patching_completed_input_validation_float(self): 250 | data = dict(completed=23.5) 251 | response = self.app.patch(url_for("entry", entry_id=1), 252 | data=json.dumps(data), content_type="application/json") 253 | response_data = json.loads(response.data.decode("utf-8")) 254 | self.assertEqual(response.status_code, 400) 255 | self.assertEqual(str(data["completed"]) + " is not a boolean.", response_data["message"]) 256 | 257 | def test_patching_order_input_validation_string(self): 258 | data = dict(order="not a number") 259 | response = self.app.patch(url_for("entry", entry_id=1), 260 | data=json.dumps(data), content_type="application/json") 261 | response_data = json.loads(response.data.decode("utf-8")) 262 | self.assertEqual(response.status_code, 400) 263 | self.assertEqual(data["order"] + " is not an integer.", response_data["message"]) 264 | 265 | def test_patching_order_input_validation_float(self): 266 | data = dict(order=23.5) 267 | response = self.app.patch(url_for("entry", entry_id=1), 268 | data=json.dumps(data), content_type="application/json") 269 | response_data = json.loads(response.data.decode("utf-8")) 270 | self.assertEqual(response.status_code, 400) 271 | self.assertEqual(str(data["order"]) + " is not an integer.", response_data["message"]) 272 | 273 | if __name__ == "__main__": 274 | unittest.main() 275 | -------------------------------------------------------------------------------- /todo/views.py: -------------------------------------------------------------------------------- 1 | from todo import app 2 | 3 | from flask import jsonify, request, url_for 4 | from flask import json 5 | 6 | from todo.database import db_session 7 | from todo.models import Entry 8 | from todo.error_handlers import InvalidUsage 9 | 10 | @app.route("/", methods=["GET", "POST", "DELETE"]) 11 | def index(): 12 | if request.method == "POST": 13 | request_json = request.get_json() 14 | if "order" in request_json: 15 | if type(request_json["order"]) is int: 16 | entry = Entry(request_json["title"], request_json["order"]) 17 | else: 18 | raise InvalidUsage(str(request_json["order"]) + " is not an integer.") 19 | else: 20 | entry = Entry(request_json["title"]) 21 | db_session.add(entry) 22 | db_session.commit() 23 | return jsonify(construct_dict(entry)) 24 | else: 25 | if request.method == "DELETE": 26 | Entry.query.delete() 27 | db_session.commit() 28 | response = [] 29 | for entry in Entry.query.all(): 30 | response.append(construct_dict(entry)) 31 | return json.dumps(response) 32 | 33 | @app.route("/", methods=["GET", "PATCH", "DELETE"]) 34 | def entry(entry_id): 35 | entry = Entry.query.filter(Entry.id == entry_id).first() 36 | if request.method == "PATCH": 37 | request_json = request.get_json() 38 | if "title" in request_json: 39 | entry.title = request_json["title"] 40 | if "completed" in request_json: 41 | if type(request_json["completed"]) is bool: 42 | entry.completed = request_json["completed"] 43 | else: 44 | raise InvalidUsage(str(request_json["completed"]) + " is not a boolean.") 45 | if "order" in request_json: 46 | if type(request_json["order"]) is int: 47 | entry.order = request_json["order"] 48 | else: 49 | raise InvalidUsage(str(request_json["order"]) + " is not an integer.") 50 | db_session.commit() 51 | elif request.method == "DELETE": 52 | db_session.delete(entry) 53 | db_session.commit() 54 | return jsonify(dict()) 55 | if entry: 56 | return jsonify(construct_dict(entry)) 57 | else: 58 | return jsonify(dict()) 59 | 60 | @app.errorhandler(InvalidUsage) 61 | def handle_invalid_usage(error): 62 | response = jsonify(error.to_dict()) 63 | response.status_code = error.status_code 64 | return response 65 | 66 | def construct_dict(entry): 67 | if entry.order: 68 | return dict(title=entry.title, completed=entry.completed, 69 | url=url_for("entry", entry_id=entry.id, _external=True), 70 | order=entry.order) 71 | else: 72 | return dict(title=entry.title, completed=entry.completed, 73 | url=url_for("entry", entry_id=entry.id, _external=True)) 74 | 75 | 76 | @app.teardown_appcontext 77 | def shutdown_session(exception=None): 78 | db_session.remove() 79 | --------------------------------------------------------------------------------