diff --git a/main/templates/main/snippets/export_user_skill_profile.html b/main/templates/main/snippets/export_user_skill_profile.html index 0665c0e2..d3b724ef 100644 --- a/main/templates/main/snippets/export_user_skill_profile.html +++ b/main/templates/main/snippets/export_user_skill_profile.html @@ -1,14 +1,14 @@ - - + diff --git a/main/templates/main/snippets/share_skill_profile_dropdown.html b/main/templates/main/snippets/share_skill_profile_dropdown.html index 10639308..f08fb608 100644 --- a/main/templates/main/snippets/share_skill_profile_dropdown.html +++ b/main/templates/main/snippets/share_skill_profile_dropdown.html @@ -1,4 +1,9 @@ - + diff --git a/main/urls.py b/main/urls.py index 28b87056..a04cadaa 100644 --- a/main/urls.py +++ b/main/urls.py @@ -36,6 +36,16 @@ path("skills-profile/", views.SkillProfileView.as_view(), name="skills_profile"), path("profile/", views.UserUpdateView.as_view(), name="profile"), path("overview/", views.AccountOverviewView.as_view(), name="account-overview"), + path( + "download-skills-data/csv/", + views.DownloadUserSkillDataCsvView.as_view(), + name="download-csv-data-link", + ), + path( + "download-skills-data/json/", + views.DownloadUserSkillDataJSONView.as_view(), + name="download-json-data-link", + ), ] policies_patterns = [ diff --git a/main/views/account_views.py b/main/views/account_views.py index fa26b89e..706413bc 100644 --- a/main/views/account_views.py +++ b/main/views/account_views.py @@ -104,6 +104,10 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context["skill_levels"] = dumps( list(SkillLevel.objects.values("level", "name")) ) + + context["allow_export"] = ( + True # Allow exporting skill profile data as CSV or JSON + ) return context diff --git a/main/views/data_views.py b/main/views/data_views.py index 85d03ecd..a1d9ea1e 100644 --- a/main/views/data_views.py +++ b/main/views/data_views.py @@ -1,9 +1,12 @@ """Views for getting data from the framework database.""" -from django.http import HttpRequest, JsonResponse +import csv + +from django.http import HttpRequest, HttpResponse, JsonResponse from django.views import View from ..io_resources import export_framework +from ..models import UserSkill class FrameworkView(View): @@ -23,3 +26,82 @@ def get(self, request: HttpRequest) -> JsonResponse: - skill_levels: A list of the Skill Levels """ return JsonResponse(export_framework(), json_dumps_params=dict(indent=2)) + + +class DownloadUserSkillDataCsvView(View): + """A view that returns the user skill data as a CSV file.""" + + def get(self, request: HttpRequest) -> HttpResponse: + """Define the GET response. + + Args: + request: A GET request from the user. + + Returns: + A HttpResponse containing the user skill data in CSV format. + """ + user_skills = UserSkill.objects.filter(user=request.user.pk) + user_skills_data = [ + { + "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 + ] + context = {} + context["chart_data"] = [ + { + "user_id": "root", + "user_data": user_skills_data, + } + ] + + response = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": 'attachment; filename="downloaded_.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(View): + """A view that returns the user skill data as a JSON file.""" + + def get(self, request: HttpRequest) -> JsonResponse: + """Define the GET response. + + Args: + request: A GET request from the user. + + Returns: + A JsonResponse containing the user skill data in JSON format. + """ + user_skills = UserSkill.objects.filter(user=request.user.pk) + # get_anonymous url param + get_anonymous = request.GET.get("anonymous", "false").lower() == "true" + user_skills_data = [ + { + "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 + ] + 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)) diff --git a/tests/main/conftest.py b/tests/main/conftest.py index 393eeac6..44d9a545 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -103,3 +103,10 @@ def skill_level() -> SkillLevel: def user_skill(user, skill: Skill, skill_level: SkillLevel) -> UserSkill: """Fixture for creating a UserSkill instance.""" return UserSkill.objects.create(user=user, skill=skill, skill_level=skill_level) + + +@pytest.fixture +def client_logged_in(client, user): + """Fixture for a client that is logged in as a user.""" + client.force_login(user) + return client diff --git a/tests/main/test_data_views.py b/tests/main/test_data_views.py index d4823396..6f101228 100644 --- a/tests/main/test_data_views.py +++ b/tests/main/test_data_views.py @@ -39,3 +39,81 @@ def test_framework_view_can_only_get(self, client): assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED response = client.trace(self._get_url()) assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED + + +class TestDownloadCSVDataLinkView: + """Test suite for the DownloadCSVDataLinkView.""" + + def _get_url(self): + return reverse("download-csv-data-link") + + def test_download_csv_data_link_view_get(self, client_logged_in): + """Test the GET returns the correct CSV data link.""" + response = client_logged_in.get(self._get_url()) + assert response.status_code == HTTPStatus.OK + + def test_download_csv_data_link_view_returns_a_csv_file(self, client_logged_in): + """Test that the GET request returns a CSV file.""" + response = client_logged_in.get(self._get_url()) + assert response.status_code == HTTPStatus.OK + assert response["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()) + assert response.status_code == HTTPStatus.OK + 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: + """Test suite for the DownloadUserSkillDataJSONView.""" + + def _get_url(self): + return reverse("download-json-data-link") + + def test_download_json_data_link_view_get(self, client_logged_in): + """Test the GET returns the correct JSON data link.""" + response = client_logged_in.get(self._get_url()) + assert response.status_code == HTTPStatus.OK + + def test_download_json_data_link_view_returns_a_json_file(self, client_logged_in): + """Test that the GET request returns a JSON file.""" + response = client_logged_in.get(self._get_url()) + assert response.status_code == HTTPStatus.OK + assert response["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