Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure internal posts representation #42

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
11 changes: 1 addition & 10 deletions drp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,12 @@ def init_cli(app):
def init_api(app):
api = Api(app)

api.add_resource(res.PostResource, "/api/posts/<int:id>")
api.add_resource(res.PostListResource, "/api/posts")

api.add_resource(res.RevisionResource, "/api/revisions/<int:id>")

api.add_resource(res.PostFetchResource, "/api/fetch/posts/")

api.add_resource(res.PostSearchResource,
"/api/search/posts/<string:searched>")

api.add_resource(res.TagResource, "/api/tags/<int:id>")
api.add_resource(res.TagListResource, "/api/tags")

api.add_resource(res.FileResource, '/api/files/<int:id>')
api.add_resource(res.FileListResource, "/api/files")

api.add_resource(res.RawFileViewResource, '/api/rawfiles/view/<int:id>')
api.add_resource(res.RawFileDownloadResource,
'/api/rawfiles/download/<int:id>')
Expand All @@ -47,6 +37,7 @@ def init_api(app):
api.add_resource(res.SiteResource, "/api/sites/<int:id>")
api.add_resource(res.SiteListResource, "/api/sites")

app.register_blueprint(res.posts, url_prefix="/api/posts")
app.register_blueprint(res.questions, url_prefix="/api/questions")
app.register_blueprint(res.notifications, url_prefix="/api/notifications")
app.register_blueprint(res.users, url_prefix="/api/users")
Expand Down
11 changes: 3 additions & 8 deletions drp/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
from .auth import auth
from .posts import (PostResource, PostListResource, RevisionResource,
PostFetchResource)
from .posts import posts
from .search import PostSearchResource
from .tags import TagListResource, TagResource
from .files import (FileResource, FileListResource, RawFileViewResource,
RawFileDownloadResource)
from .files import RawFileViewResource, RawFileDownloadResource
from .questions import QuestionResource, QuestionListResource, questions
from .site import SiteResource, SiteListResource
from .subject import SubjectResource, SubjectListResource
from .notifications import notifications
from .users import users

__all__ = ["PostResource", "PostListResource",
"RevisionResource", "PostFetchResource",
"PostSearchResource",
__all__ = ["posts", "PostSearchResource",
"TagResource", "TagListResource",
"QuestionResource", "QuestionListResource",
"FileResource", "FileListResource",
"RawFileViewResource", "RawFileDownloadResource",
"SiteResource", "SiteListResource",
"SubjectResource", "SubjectListResource",
Expand Down
24 changes: 11 additions & 13 deletions drp/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ..swag import swag

from .users import serialize_role
from .utils import error
from .utils import abort

auth = Blueprint("auth", __name__)

Expand Down Expand Up @@ -75,29 +75,27 @@ def authenticate():
password = body.get("password")

if email is None or password is None:
return error(400,
message="`email` and `password` fields"
"are required.")
abort(400, message="`email` and `password` fields are required.")

user = User.query.filter(User.email == email).one_or_none()

if user is None:
return error(401, type="InvalidCredentials")
abort(401, type="InvalidCredentials")

hasher = PasswordHasher()

try:
hasher.verify(user.password_hash, password)
except VerifyMismatchError:
return error(401, type="InvalidCredentials")
abort(401, type="InvalidCredentials")

if hasher.check_needs_rehash(user.password_hash):
hash = hasher.hash(password)
user.password_hash = hash
db.session.commit()

if not user.confirmed:
return error(401, type="Unconfirmed")
abort(401, type="Unconfirmed")

now = time.time()
expiration_time = now + 2 * 60 * 60
Expand Down Expand Up @@ -125,27 +123,27 @@ def register():

email = body.get("email")
if not email:
return error(400, message="`email` is required")
abort(400, message="`email` is required")

password = body.get("password")
if not password:
return error(400, message="`password` is required")
abort(400, message="`password` is required")

email = email.lower()
parts = email.split("@")
if len(parts) != 2:
return error(400, type="InvalidEmail")
abort(400, type="InvalidEmail")

if len(password) < 8:
return error(400, type="ShortPassword")
abort(400, type="ShortPassword")

domain = parts[1]
if domain != "nhs.net" and domain != "ic.ac.uk" \
and domain != "imperial.ac.uk":
return error(400, type="UnauthorisedDomain")
abort(400, type="UnauthorisedDomain")

if User.query.filter(User.email == email).one_or_none() is not None:
return error(400, type="Registered")
abort(400, type="Registered")

hasher = PasswordHasher()
hash = hasher.hash(password)
Expand Down
166 changes: 2 additions & 164 deletions drp/api/files.py
Original file line number Diff line number Diff line change
@@ -1,176 +1,14 @@
import os
import werkzeug
from datetime import datetime
from flask import current_app, request, send_from_directory
from flask import current_app, send_from_directory
from flask_restful import Resource, abort

from ..db import db
from ..models import File, Post
from ..swag import swag
from ..models import File


def allowed_file(filename, allowed):
return '.' in filename \
and filename.rsplit('.', 1)[1].lower() in allowed


@swag.definition("File")
def serialize_file(file):
"""
Represents an uploaded file.
---
properties:
id:
type: integer
name:
type: string
post:
type: integer
"""
return {
"id": file.id,
"name": file.name,
"post": file.post_id
}


class FileResource(Resource):

def get(self, id):
"""
Gets a single file by id.
---
parameters:
- name: id
in: path
type: integer
required: true
responses:
200:
schema:
$ref: "#/definitions/File"
404:
description: Not found
"""
file = File.query.filter(File.id == id).one_or_none()
return serialize_file(file) if file is not None else abort(404)

def delete(self, id):
"""
Deletes a single file by id.
---
parameters:
- name: id
in: path
type: integer
required: true
responses:
204:
description: Success
404:
description: Not found
"""
file = File.query.filter(File.id == id).one_or_none()

if file is None:
return abort(404)

try:
os.remove(os.path.join(
current_app.config['UPLOAD_FOLDER'], file.filename))
except OSError as e:
print("Could not delete file, " + repr(e))

db.session.delete(file)
db.session.commit()

return '', 204


class FileListResource(Resource):

def get(self):
"""
Gets a list of all files.
---
responses:
200:
schema:
type: array
items:
$ref: "#/definitions/File"
"""
return [serialize_file(file) for file in File.query.all()]

def post(self):
"""
Uploads a new file.
---
parameters:
- in: formData
name: file
type: file
required: true
description: The file to upload.
- in: formData
name: name
type: string
required: true
description: The logical name of the file
- in: formData
name: post
type: string
required: true
description: The associated post

responses:
200:
schema:
$ref: "#/definitions/File"
"""
file_content = request.files.get('file')
name = request.form.get('name')
post_id = request.form.get('post')

if name is None:
return abort(400, message="`name` field is required.")

if file_content is None:
return abort(400, message="`file` filed is required.")

if file_content == "":
return abort(400, message="A valid file is required.")

if not allowed_file(name,
current_app.config['ALLOWED_FILE_EXTENSIONS']):
return abort(400, message=f"The file extension of {name} is "
"not allowed for security reasons. If "
"you believe that this file type is safe "
"to upload, contact the developer.")

post = Post.query.filter(Post.id == post_id).one_or_none()
if post is None:
return abort(400, message="Invalid post ID, associated post must "
"already exist.")

# Prefix file name with current time to allow mutliple files with the
# same name
filename = datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f_') + \
werkzeug.utils.secure_filename(name)
path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
if (os.path.isfile(path)):
return abort(422, message="a file upload collision occured, "
"please try again later")
file_content.save(path)

file = File(name=name, filename=filename, post=post)

db.session.add(file)
db.session.commit()

return serialize_file(file)


class RawFileViewResource(Resource):

def get(self, id):
Expand Down
6 changes: 3 additions & 3 deletions drp/api/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..db import db
from ..models import Device, User

from .utils import error
from .utils import abort

notifications = Blueprint("notifications", __name__)

Expand All @@ -15,11 +15,11 @@ def register():
user = request.args.get("user")

if not token:
return error(400, "Missing `token` query parameter")
abort(400, "Missing `token` query parameter")

user = User.query.filter(User.id == user).one_or_none()
if not user:
return error(400, "Missing `user` query parameter")
abort(400, "Missing `user` query parameter")

device = Device(expo_push_token=token, user=user)

Expand Down
Loading