Repository navigation
Add json download button #756
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
Changes from all commits
d3ef35c
6c3f13d
643bd14
6e43ec3
1ccfae5
29b6b95
0c796d3
cc3824c
ac3b908
ff557fb
6e89962
531c29a
ff58c7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| """Init in views.""" | ||
|
|
||
| from .account_data_views import * # noqa: F403 | ||
| from .account_views import * # noqa: F403 | ||
| from .data_views import * # noqa: F403 | ||
| from .page_views import * # noqa: F403 |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this file. You should be able to import the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I partly agree except for two points:
I wasn't sure about having the dataview endpoints redirect to the login page which is what the |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| """Views for getting account-related data from the framework database.""" | ||
|
|
||
| import csv | ||
|
|
||
| from django.http import ( | ||
| HttpResponse, | ||
| JsonResponse, | ||
| ) | ||
| from django.views import View | ||
|
|
||
| from ..models import User, UserSkill | ||
| from .account_views import AuthenticatedHttpRequest, TermsAcceptedMixin | ||
|
|
||
|
|
||
| def format_user_skill_data_for_download(user: User) -> list[dict[str, str | int]]: | ||
| """Format the user skill data for download.""" | ||
| user_skills = UserSkill.objects.filter(user=user.pk) | ||
| return [ | ||
| { | ||
| "skill": user_skill.skill.name, | ||
| "competency_domain": user_skill.skill.competency.competency_domain.name, | ||
| "competency": user_skill.skill.competency.name, | ||
| "skill_level": user_skill.skill_level.level, | ||
| } | ||
| for user_skill in user_skills | ||
| ] | ||
|
|
||
|
|
||
| class DownloadUserSkillDataCsvView(TermsAcceptedMixin, View): | ||
| """A view that returns the user skill data as a CSV file.""" | ||
|
|
||
| def get(self, request: AuthenticatedHttpRequest) -> HttpResponse: | ||
| """Handle GET request to download user skill data as CSV. | ||
|
|
||
| Args: | ||
| request: The HTTP request object. | ||
|
|
||
| Returns: | ||
| An HttpResponse containing the user skill data as a CSV file | ||
| """ | ||
| user_skills_data = format_user_skill_data_for_download(request.user) | ||
| response = HttpResponse( | ||
| content_type="text/csv", | ||
| headers={"Content-Disposition": 'attachment; filename="skill_profile.csv"'}, | ||
| ) | ||
| writer = csv.writer(response) | ||
| writer.writerow(["skill", "competency_domain", "competency", "skill_level"]) | ||
| for user_skill in user_skills_data: | ||
| writer.writerow( | ||
| [ | ||
| user_skill["skill"], | ||
| user_skill["competency_domain"], | ||
| user_skill["competency"], | ||
| user_skill["skill_level"], | ||
| ] | ||
| ) | ||
| return response | ||
|
|
||
|
|
||
| class DownloadUserSkillDataJSONView(TermsAcceptedMixin, View): | ||
| """A view that returns the user skill data as a JSON file.""" | ||
|
|
||
| def get(self, request: AuthenticatedHttpRequest) -> JsonResponse: | ||
| """Define the GET response. | ||
|
|
||
| Args: | ||
| request: The HTTP request object. | ||
|
|
||
| Returns: | ||
| A JsonResponse containing the user skill data in JSON format. | ||
| """ | ||
| user_skills_data = format_user_skill_data_for_download(request.user) | ||
| get_anonymous = request.GET.get("anonymous", "false").lower() == "true" | ||
| returned_data = { | ||
| "username": "anonymous" if get_anonymous else request.user.username, | ||
| "user_data": user_skills_data, | ||
| } | ||
| return JsonResponse( | ||
| returned_data, | ||
| json_dumps_params=dict(indent=2), | ||
| headers={ | ||
| "Content-Disposition": 'attachment; filename="skill_profile.json"' | ||
| }, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,3 +119,10 @@ def event() -> Event: | |
| blog="https://example.com/blog", | ||
| contributors="Test Contributor", | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def client_logged_in(client, user): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't realise we didn't have one of these!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know if you want me to pull out to another PR or just slide in with this one.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it's fine to be in this PR. Adding these kinds of thing as and when they're needed makes the most sense to me |
||
| """Fixture for a client that is logged in as a user.""" | ||
| client.force_login(user) | ||
| return client | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """Test suite for the account-related data views. | ||
|
|
||
| This test module includes tests for data views of the app. | ||
| """ | ||
|
|
||
| from http import HTTPStatus | ||
|
|
||
| from django.urls import reverse | ||
|
|
||
| from .test_main_views import LoginRequiredMixin | ||
|
|
||
|
|
||
| class DownloadUserSkillDataMixin(LoginRequiredMixin): | ||
| """Test suite for the GetUserSkillsDataMixin.""" | ||
|
|
||
| _url: str | ||
| _content_type: str | ||
|
|
||
| def test_authenticated_user_can_access(self, client_logged_in): | ||
| """Test that an authenticated user can access the view.""" | ||
| response = client_logged_in.get(self._get_url()) | ||
| assert response.status_code == HTTPStatus.OK | ||
|
|
||
| def test_returns_correct_content_type(self, client_logged_in): | ||
| """Test that the response returns the correct content type.""" | ||
| response = client_logged_in.get(self._get_url()) | ||
| assert response.status_code == HTTPStatus.OK | ||
| assert response["Content-Type"] == self._content_type | ||
|
|
||
| def test_return_type_should_be_file_attachement(self, client_logged_in): | ||
| """Test that the response has the correct file attachment header.""" | ||
| response = client_logged_in.get(self._get_url()) | ||
| assert response.status_code == HTTPStatus.OK | ||
| content_disposition = response["Content-Disposition"] | ||
| assert content_disposition.startswith("attachment;") | ||
|
|
||
| def _get_url(self): | ||
| return reverse(self._url) | ||
|
|
||
|
|
||
| class TestDownloadUserSkillDataCsvView(DownloadUserSkillDataMixin): | ||
| """Test suite for the DownloadCSVDataLinkView.""" | ||
|
|
||
| _url = "download-csv-data-link" | ||
| _content_type = "text/csv" | ||
|
|
||
| def test_download_csv_data_link_view_csv_content( | ||
| self, client_logged_in, user_skill | ||
| ): | ||
| """Test that the CSV content is correct.""" | ||
| response = client_logged_in.get(self._get_url()) | ||
| content = response.content.decode("utf-8") | ||
| assert content.startswith("skill") | ||
| assert "competency_domain" in content | ||
| assert "competency" in content | ||
| assert "skill_level" in content | ||
| assert user_skill.skill.name in content | ||
| assert ( | ||
| content.count("\n") > 1 | ||
| ) # Ensure there is at least one row of data besides the header | ||
|
|
||
|
|
||
| class TestDownloadUserSkillDataJSONView(DownloadUserSkillDataMixin): | ||
| """Test suite for the DownloadUserSkillDataJSONView.""" | ||
|
|
||
| _url = "download-json-data-link" | ||
| _content_type = "application/json" | ||
|
|
||
| def test_download_json_data_link_view_json_content(self, client_logged_in): | ||
| """Test that the JSON content is correct.""" | ||
| response = client_logged_in.get(self._get_url()) | ||
| assert response.status_code == HTTPStatus.OK | ||
| content = response.json() | ||
| assert isinstance(content, dict) | ||
| assert "username" in content | ||
| assert "user_data" in content | ||
| assert isinstance(content["user_data"], list) | ||
| if content["user_data"]: | ||
| assert "skill" in content["user_data"][0] | ||
| assert "competency_domain" in content["user_data"][0] | ||
| assert "competency" in content["user_data"][0] | ||
| assert "skill_level" in content["user_data"][0] | ||
|
|
||
| def test_returns_anonymous_username_when_anonymous_param_is_true( | ||
| self, client_logged_in | ||
| ): | ||
| """Assert username is 'anonymous' when anonymous URL para is set to true.""" | ||
| response = client_logged_in.get(self._get_url() + "?anonymous=true") | ||
| assert response.status_code == HTTPStatus.OK | ||
| content = response.json() | ||
| assert content["username"] == "anonymous" | ||
| # Also check that the user_data is still present | ||
| assert "user_data" in content |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Not for this PR) I wonder if these snippets of JS scripts should be in a separate folder, or sub folder? They seem like they should be considered a separate category from the HTML snippets that are intended to be in this folder. Perhaps in
main/templates/main/snippets/scriptsormain/templates/main/scripts?