Skip to content
This repository has been archived by the owner on May 13, 2018. It is now read-only.

Commit

Permalink
Merge 901b836 into d4cd2a1
Browse files Browse the repository at this point in the history
  • Loading branch information
kawa-kokosowa committed Jul 28, 2016
2 parents d4cd2a1 + 901b836 commit 56fa456
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 39 deletions.
7 changes: 7 additions & 0 deletions msgboard/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
user information.
"""

LIMITS_USER_POST = "2 per minute"
"""str: flask_limiter limit.
Limit the number of users an IP
may create per second.
"""

LIMITS_MESSAGES_GET = "10 per minute"
"""str: flask_limiter limit.
Expand Down
104 changes: 67 additions & 37 deletions msgboard/msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import flask_limiter
from flask_sse import sse
import jsonschema

import os
import json
Expand All @@ -26,6 +27,7 @@
import sqlalchemy
import flask_sqlalchemy

# Flask setup
app = flask.Flask(__name__)

config_path = os.path.dirname(os.path.abspath(__file__))
Expand All @@ -48,6 +50,16 @@ class User(flask_restful.Resource):
"""

SCHEMA_POST = {
"type": "object",
"properties": {
"bio": {"type": "string"},
"username": {"type": "string"},
"password": {"type": "string"},
},
"required": ["username", "password"],
}

@limiter.limit(config.LIMITS_USER_GET)
def get(self, user_id=None, username=None):
"""Get a specific user's info by user ID
Expand All @@ -62,6 +74,10 @@ def get(self, user_id=None, username=None):
user_id (int|None): --
username (str|None): --
Warning:
If both `user_id` and `username` are specified,
user_id will be used.
Returns:
None: If aborted.
dict: If such a user is found, the return value
Expand Down Expand Up @@ -90,7 +106,7 @@ def get(self, user_id=None, username=None):

return user.to_dict()

@limiter.limit(config.LIMITS_USER_GET)
@limiter.limit(config.LIMITS_USER_POST)
def post(self):
"""Create a new user.
Expand All @@ -105,16 +121,10 @@ def post(self):
"""

json_data = flask.request.get_json(force=True)

try:
username = json_data['username']
password = json_data['password']
except KeyError:
message = "Must specify username and password."
flask_restful.abort(400, message=message)

json_data = get_valid_json(self.SCHEMA_POST)
bio = json_data.get('bio')
username = json_data['username']
password = json_data['password']
new_user = models.User(username, password, bio=bio)
db.session.add(new_user)

Expand All @@ -124,6 +134,8 @@ def post(self):
message = "A user already exists with username: %s" % username
flask_restful.abort(400, message=message)
else:
# will happen only if successful/no
# error raised
return new_user.to_dict()


Expand All @@ -132,10 +144,15 @@ class Messages(flask_restful.Resource):
"""

# TODO: Make hard limits on this. Force people
# to always provide limit and offset (config)
# and implement abort when both not provided,
# as well as if the scope is too large.
SCHEMA_GET = {
"type": "object",
"properties": {
"offset": {"type": "integer"},
"limit": {"type": "integer"},
},
"required": ["offset", "limit"],
}

@limiter.limit(config.LIMITS_MESSAGES_GET)
def get(self):
"""Get a range of messages using a "limit"
Expand All @@ -148,22 +165,21 @@ def get(self):
"""

json_data = flask.request.get_json(force=True)
# NOTE: the logic this implies is that if neither
# limit nor offset is specified, all messages will
# be returned (not the best behavior imo).
query = db.session.query(models.Message)

if 'limit' in json_data:
limit = int(json_data['limit'])
assert limit <= config.LIMITS_MESSAGES_GET_LIMIT
query = query.limit(limit)
json_data = get_valid_json(self.SCHEMA_GET)
offset = int(json_data['offset'])
limit = int(json_data['limit'])

if 'offset' in json_data:
offset = int(json_data['offset'])
query = query.offset(offset)
# Just make sure not requesting too many at once!
if limit > config.LIMITS_MESSAGES_GET_LIMIT:
message = ("You may only request %d messages at once."
% config.LIMITS_MESSAGES_GET_LIMIT)
flask_restful.abort(400, message=message)

results = query.all()
# Now we're sure we have the right data to
# make a query, actually do that query and
# return said data or 404 if nothing matches.
query = db.session.query(models.Message)
results = query.limit(limit).offset(offset).all()

if results == []:
message = ("No messages found at offset %d limit %d"
Expand All @@ -178,6 +194,14 @@ class Message(flask_restful.Resource):
"""

SCHEMA = {
"type": "object",
"properties": {
"text": {"type": "string"},
},
"required": ["text"],
}

@auth.login_required
@limiter.limit(config.LIMITS_MESSAGE_PUT)
def put(self, message_id):
Expand All @@ -188,10 +212,11 @@ def put(self, message_id):
"""

json_data = flask.request.get_json(force=True)
text = json_data['text']
text = get_valid_json(self.SCHEMA)['text']
result = db.session.query(models.Message).get(message_id)

# we don't want people editing posts which don't
# belong to them...
if result.user.username == auth.username():
result.text = text
db.session.commit()
Expand All @@ -208,13 +233,7 @@ def post(self):
"""

json_data = flask.request.get_json(force=True)

try:
text = json_data['text']
except KeyError:
flask_restful.abort(400, message="You must specify text field.")

text = get_valid_json(self.SCHEMA)['text']
user_id = (db.session.query(models.User)
.filter(models.User.username == auth.username())
.first().id)
Expand Down Expand Up @@ -296,6 +315,17 @@ def get_password(username, password):
return result.check_password(password)


def get_valid_json(schema):
json_data = flask.request.get_json(force=True)

try:
jsonschema.validate(json_data, schema)
except jsonschema.ValidationError as e:
flask_restful.abort(400, message=e.message)
else:
return json_data


def init_db():
models.Base.metadata.drop_all(bind=db.engine)
models.Base.metadata.create_all(bind=db.engine)
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ requests
pytest
flask_sse
gunicorn
jsonschema
4 changes: 2 additions & 2 deletions tests/msgboard_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def test_create_user_without_username_password(self):
# specifying username or password
status, response = self.post('/user', data={'lol': 'lol'})
assert status == 400
assert response == {'message': 'Must specify username and password.'}
assert response == {'message': "'username' is a required property"}

def test_create_existing_user(self):
"""Attempt to created a user that is already
Expand Down Expand Up @@ -265,7 +265,7 @@ def test_create_message_without_text(self):
status, response = self.post('/message', headers=headers,
data={'textg': 'whoops'})
assert status == 400
assert response == {'message': 'You must specify text field.'}
assert response == {'message': "'text' is a required property"}

def test_get_post(self):
self.test_post()
Expand Down

0 comments on commit 56fa456

Please sign in to comment.