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

feat: Create new API for uploading papers #1932

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/paper/serializers/paper_upload_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from rest_framework import serializers


class PaperUploadSerializer(serializers.Serializer):
"""
Serializer for uploading a paper.
Used to validate request data.
"""

filename = serializers.CharField(required=True)
47 changes: 47 additions & 0 deletions src/paper/services/storage_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import uuid

from boto3 import session

from researchhub import settings


class StorageService:
"""
Service for interacting with S3 storage.
"""

def create_presigned_url(
self,
filename: str,
user_id: str,
content_type: str = "application/pdf",
valid_for: int = 2,
) -> str:
"""
Create a presigned URL for uploading a file to S3 that is time-limited.
"""

s3_filename = f"/uploads/{user_id}/{uuid.uuid4()}/{filename}"

boto3_session = session.Session()
s3_client = boto3_session.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)

url = s3_client.generate_presigned_url(
"put_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": s3_filename,
"ContentType": content_type,
"Metadata": {
"created-by-id": f"{user_id}",
"file-name": filename,
},
},
ExpiresIn=60 * valid_for,
)

return url
56 changes: 56 additions & 0 deletions src/paper/tests/test_paper_upload_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from unittest.mock import Mock

from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate

from paper.views.paper_upload_views import PaperUploadView
from user.tests.helpers import create_random_default_user


class PaperUploadViewTest(APITestCase):

def setUp(self):
self.factory = APIRequestFactory()
self.view = PaperUploadView.as_view()
self.mock_storage_service = Mock()
self.user = create_random_default_user("user1")

def test_post(self):
# Arrange
request = self.factory.post(
"/paper/upload/",
{
"filename": "test.pdf",
},
)

force_authenticate(request, self.user)

# Act
response = self.view(request, storage_service=self.mock_storage_service)

# Assert
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
{
"presigned_url": self.mock_storage_service.create_presigned_url.return_value,
},
)
self.mock_storage_service.create_presigned_url.assert_called_once_with(
"test.pdf",
request.user.id,
)

def test_post_fails_with_validation_error(self):
# Arrange
request = self.factory.post("/paper/upload/", {})

force_authenticate(request, self.user)

# Act
response = self.view(request, storage_service=self.mock_storage_service)

# Assert
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data, {"filename": ["This field is required."]})
self.mock_storage_service.create_presigned_url.assert_not_called()
43 changes: 43 additions & 0 deletions src/paper/tests/test_storage_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import uuid
from unittest import TestCase
from unittest.mock import Mock, patch

from paper.services.storage_service import StorageService
from researchhub import settings


class StorageServiceTest(TestCase):

@patch("paper.services.storage_service.session.Session")
@patch("paper.services.storage_service.uuid.uuid4")
def test_create_presigned_url(self, mock_uuid, mock_session):
# Arrange
uuid1 = uuid.uuid4()
mock_uuid.return_value = uuid1

mock_s3_client = Mock()
mock_session.return_value.client.return_value = mock_s3_client

mock_s3_client.generate_presigned_url.return_value = "https://presignedUrl1"

service = StorageService()

# Act
url = service.create_presigned_url("file1.pdf", "userId1", valid_for=2)

# Assert
mock_s3_client.generate_presigned_url.assert_called_once_with(
"put_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": f"/uploads/userId1/{uuid1}/file1.pdf",
"ContentType": "application/pdf",
"Metadata": {
"created-by-id": "userId1",
"file-name": "file1.pdf",
},
},
ExpiresIn=60 * 2,
)

self.assertEqual(url, "https://presignedUrl1")
40 changes: 40 additions & 0 deletions src/paper/views/paper_upload_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from paper.serializers.paper_upload_serializer import PaperUploadSerializer
from paper.services.storage_service import StorageService


class PaperUploadView(APIView):
"""
View for uploading papers.
"""

permission_classes = [IsAuthenticated]

def dispatch(self, request, *args, **kwargs):
self.storage_service = kwargs.pop("storage_service", StorageService())
return super().dispatch(request, *args, **kwargs)

def post(self, request: Request, *args, **kwargs) -> Response:
"""
Creates a presigned URL for uploading a paper and returns it.
"""
user = request.user
data = request.data

# Validate request data
serializer = PaperUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

filename = data.get("filename")

presigned_url = self.storage_service.create_presigned_url(filename, user.id)

return Response(
{
"presigned_url": presigned_url,
}
)
6 changes: 6 additions & 0 deletions src/researchhub/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import search.urls
import user.views
from citation.views import CitationEntryViewSet, CitationProjectViewSet
from paper.views import paper_upload_views
from researchhub.settings import INSTALLED_APPS, USE_DEBUG_TOOLBAR
from researchhub_comment.views.rh_comment_view import RhCommentViewSet
from review.views.review_view import ReviewViewSet
Expand Down Expand Up @@ -252,6 +253,11 @@
path("email_notifications/", mailing_list.views.email_notifications),
path("health/", researchhub.views.healthcheck),
path("", researchhub.views.index, name="index"),
path(
"paper/upload/",
paper_upload_views.PaperUploadView.as_view(),
name="paper_upload",
),
path("robots.txt", researchhub.views.robots_txt, name="robots_txt"),
path(
"webhooks/persona/",
Expand Down