Skip to content
This repository has been archived by the owner on Jul 25, 2024. It is now read-only.

Commit

Permalink
Merge pull request #127 from communitiesuk/FPASF-153-download-report
Browse files Browse the repository at this point in the history
Send user to find service download page to download the file
  • Loading branch information
israr-ulhaq authored Jul 9, 2024
2 parents 8ccab57 + 4dae423 commit e4f5b5f
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 15 deletions.
76 changes: 76 additions & 0 deletions app/main/download_data.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import json
from collections import namedtuple
from datetime import datetime
from enum import StrEnum
from typing import Any
Expand Down Expand Up @@ -238,3 +239,78 @@ def process_api_response(query_params: dict) -> tuple:
return abort(500), f"Unknown content type: {content_type}"

return content_type, file_content


def get_presigned_url(filename: str):
"""Get the presigned link for the short time to retrieve the file from s3 bucket.
:param filename (str): object name which needs to be retrieved from s3 if exists
Raises:ValueError: If object doest not exists in S3, it will raise an error.
Returns:Returns the response the API.
"""
if not filename:
raise ValueError("filename is required")

response = get_response(Config.DATA_STORE_API_HOST, f"/get-presigned-url/{filename}")
return response.json()["presigned_url"]


FileMetadata = namedtuple("FileMetadata", ["response_status_code", "formated_date", "file_format", "file_size_str"])


def get_find_download_file_metadata(filename: str) -> FileMetadata:
"""To get the object metadata from S3 using the ovject Key
:param filename (str): object name to get the metadata
Raises:
ValueError: If object doest not exists in S3, it will raise an error.
Returns: FileMetadata:
- Returns the last modified date,
-file format, and human-readable file size.
"""
response = get_response(Config.DATA_STORE_API_HOST, f"/get-find-download-metadata/{filename}")
response_status_code = response.status_code

if response_status_code == 200:
metadata = response.json()
file_size = metadata["content_length"]
file_size_str = get_human_readable_file_size(file_size)
last_modified_date = metadata["last_modified"]
content_type = metadata["content_type"]

date = datetime.fromisoformat(last_modified_date)
formated_date = date.strftime("%d %B %Y")
file_format = get_file_format_from_content_type(content_type)

return FileMetadata(response_status_code, formated_date, file_format, file_size_str)
else:
return FileMetadata(response_status_code, None, None, None)


def get_file_format_from_content_type(file_extension: str) -> str:
"""Return nice file format name based on the file extension.
:param file_extension: file extension,
:return: nice file format name,
"""

file_format = "Unknown file"
if file_extension == MIMETYPE.XLSX:
file_format = "Microsoft Excel spreadsheet"
elif file_extension == MIMETYPE.JSON:
file_format = "JSON file"
return file_format


def get_human_readable_file_size(file_size_bytes: int) -> str:
"""Return a human-readable file size string.
:param file_size_bytes: file size in bytes,
:return: human-readable file size,
"""

file_size_kb = round(file_size_bytes / 1024, 1)
if file_size_kb < 1024:
return f"{round(file_size_kb, 1)} KB"
elif file_size_kb < 1024 * 1024:
return f"{round(file_size_kb / 1024, 1)} MB"
else:
return f"{round(file_size_kb / (1024 * 1024), 1)} GB"
4 changes: 4 additions & 0 deletions app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ class DownloadForm(FlaskForm):
default=None,
)
download = SubmitField("Download", widget=GovSubmitInput())


class RetrieveForm(FlaskForm):
download = SubmitField("Download your data", widget=GovSubmitInput())
37 changes: 33 additions & 4 deletions app/main/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
render_template,
request,
url_for,
send_file,
abort,
current_app,
g,
send_file,
)

# isort: on
Expand All @@ -24,14 +24,16 @@
FormNames,
financial_quarter_from_mapping,
financial_quarter_to_mapping,
get_find_download_file_metadata,
get_fund_checkboxes,
get_org_checkboxes,
get_outcome_checkboxes,
get_presigned_url,
get_region_checkboxes,
get_returns,
process_api_response,
)
from app.main.forms import DownloadForm
from app.main.forms import DownloadForm, RetrieveForm


@bp.route("/", methods=["GET"])
Expand Down Expand Up @@ -121,6 +123,7 @@ def download():
"request_type": "download",
},
)

return send_file(
file_content,
download_name=f"download-{current_datetime}.{file_format}",
Expand All @@ -132,10 +135,36 @@ def download():
@bp.route("/request-received", methods=["GET", "POST"])
@login_required(return_app=SupportedApp.POST_AWARD_FRONTEND)
def request_received():
return render_template("request-received.html", user_email=g.user.email)


@bp.route("/retrieve-download/<filename>", methods=["GET", "POST"])
@login_required(return_app=SupportedApp.POST_AWARD_FRONTEND)
def retrieve_download(filename: str):
"""Get file from S3, send back to user with presigned link
and file metadata, if file is not exist
return file not found page
:param: filename (str):filename of the file which needs to be retrieved with metadata
Returns: redirect to presigned url
"""
file_metadata = get_find_download_file_metadata(filename)
if file_metadata.response_status_code == 404:
if request.method == "POST":
return redirect(url_for(".retrieve_download", filename=filename))
return render_template("file-not-found.html")
form = RetrieveForm()
context = {
"user_email": g.user.email,
"filename": filename,
"file_size": file_metadata.file_size_str,
"file_format": file_metadata.file_format,
"date": file_metadata.formated_date,
}
return render_template("request-received.html", context=context)
if form.validate_on_submit():
presigned_url = get_presigned_url(filename)
return redirect(presigned_url)

else:
return render_template("retrieve-download.html", context=context, form=form)


@bp.route("/accessibility", methods=["GET"])
Expand Down
10 changes: 0 additions & 10 deletions app/static/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@
}


.govuk-button {
background-color: #1d70b8;
}


.govuk-button:hover {
background-color: #12066d;
}


.govuk-footer__meta {
display: flex;
margin-right: -15px;
Expand Down
19 changes: 19 additions & 0 deletions app/templates/main/file-not-found.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "base.html" %}

{%- from 'govuk_frontend_jinja/components/back-link/macro.html' import govukBackLink -%}

{% block pageTitle %}
Your link to download data has expired – {{ config['SERVICE_NAME'] }} – GOV.UK
{% endblock pageTitle %}

{% set mainClasses = "govuk-main-wrapper--l" %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl">Your link to download data has expired</h1>
<p class="govuk-body">Your data download link has expired.</p>
<p class="govuk-body">You can <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.download') }}">create a new data download using new filter options</a>.</p>
</div>
</div>
{% endblock content %}
2 changes: 1 addition & 1 deletion app/templates/main/request-received.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h2 class="govuk-heading-m govuk-!-margin-top-7">
What happens next
</h2>
<p class="govuk-body">
We will email a link to <span class="govuk-!-font-weight-bold">{{ context["user_email"] }}</span>.
We will email a link to <span class="govuk-!-font-weight-bold">{{ user_email }}</span>.
</p>
<p class="govuk-body">
This may take up to 5 minutes to be delivered to your inbox.</p>
Expand Down
17 changes: 17 additions & 0 deletions app/templates/main/retrieve-download.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "base.html" %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl">Your data is ready to be downloaded</h1>
<div class="govuk-inset-text">
<p>You requested a data download on <span class="govuk-!-font-weight-bold">{{ context.date }}</span>.
<br>File format: <span class="govuk-!-font-weight-bold">{{ context.file_format }}, {{ context.file_size }}</span></p>
</div>
<form id="download-form" method="post" action="{{ url_for('.retrieve_download', filename=context.filename) }}">
{{ form.csrf_token }}
{{ form.download }}
</form>
</div>
</div>
{% endblock content %}
33 changes: 33 additions & 0 deletions tests/test_download_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest

from app.main.download_data import get_file_format_from_content_type, get_human_readable_file_size


@pytest.mark.parametrize(
"file_extension, expected_file_format",
[
("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Microsoft Excel spreadsheet"),
("application/json", "JSON file"),
("plain/text", "Unknown file"),
("", "Unknown file"),
],
)
def test_get_file_format_from_content_type(file_extension, expected_file_format):
"""Test get_file_format_from_content_type() function with various file extensions."""
assert get_file_format_from_content_type(file_extension) == expected_file_format


@pytest.mark.parametrize(
"file_size_bytes, expected_file_size_str",
[
(1024, "1.0 KB"),
(1024 * 20 + 512, "20.5 KB"),
(1024 * 1024, "1.0 MB"),
(1024 * 1024 * 10.67, "10.7 MB"),
(1024 * 1024 * 1024, "1.0 GB"),
(1024 * 1024 * 1024 * 2.58, "2.6 GB"),
],
)
def test_get_human_readable_file_size(file_size_bytes, expected_file_size_str):
"""Test get_human_readable_file_size() function with various file sizes."""
assert get_human_readable_file_size(file_size_bytes) == expected_file_size_str
62 changes: 62 additions & 0 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import pytest
from bs4 import BeautifulSoup

from app.main.download_data import FileMetadata


def test_index_page_redirect(flask_test_client):
response = flask_test_client.get("/")
Expand Down Expand Up @@ -152,3 +154,63 @@ def test_user_not_signed(unauthenticated_flask_test_client):
response = unauthenticated_flask_test_client.get("/request-received")
assert response.status_code == 302
assert response.location == "authenticator/sessions/sign-out?return_app=post-award-frontend"


def test_download_file_exist(flask_test_client):
file_metadata = FileMetadata(200, "06 July 2024", "Microsoft Excel spreadsheet", "1 MB")

with patch("app.main.routes.get_find_download_file_metadata", return_value=file_metadata):
response = flask_test_client.get(
"/retrieve-download/fund-monitoring-data-2024-07-05-11:18:45-e4c77136-18ca-4ba3-9896-0ce572984e72.json"
)

assert response.status_code == 200
page = BeautifulSoup(response.text)
download_button = page.select_one("button#download")
assert download_button is not None


def test_file_not_found(flask_test_client):
file_metadata = FileMetadata(404, None, None, None)

with patch("app.main.routes.get_find_download_file_metadata", return_value=file_metadata):
response = flask_test_client.get(
"/retrieve-download/fund-monitoring-data-2024-07-05-11:18:45-e4c77136-18ca-4ba3-9896-0ce572984e72.json"
)

assert response.status_code == 200
page = BeautifulSoup(response.text)
download_button = page.select_one("button#download")
assert download_button is None
assert b"Your link to download data has expired" in response.data


def test_presigned_url(
flask_test_client,
):
presigned_url = "https://example/presigned-url"
file_metadata = FileMetadata(200, "06 July 2024", "Microsoft Excel spreadsheet", "1 MB")
with (
patch("app.main.routes.get_find_download_file_metadata", return_value=file_metadata),
patch("app.main.routes.get_presigned_url", return_value=presigned_url),
):
response = flask_test_client.post(
"/retrieve-download/fund-monitoring-data-2024-07-05-11:18:45-e4c77136-18ca-4ba3-9896-0ce572984e72.json"
)

assert response.status_code == 302
assert response.location == presigned_url


def test_file_not_exist(flask_test_client):
file_metadata = FileMetadata(404, None, None, None)
with patch("app.main.routes.get_find_download_file_metadata", return_value=file_metadata):
response = flask_test_client.post(
"/retrieve-download/fund-monitoring-data-2024-07-05-11:18:45-e4c77136-18ca-4ba3-9896-0ce572984e72.json"
)

assert response.status_code == 302
assert (
response.location
== "/retrieve-download/fund-monitoring-data-2024-07-05-11:18:45-e4c77136-18ca-4ba3-9896-0ce572984e72.json"
)

0 comments on commit e4f5b5f

Please sign in to comment.