From 0154bf11216b773c8904d1bfe72ba9e175cf4eb0 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Wed, 6 May 2020 18:59:22 -0300 Subject: [PATCH 1/8] WIP: issue 213. Implements state spreadsheet import --- covid19/tests/test_spreadsheet_api.py | 80 +++++++++++++++++++++++++++ covid19/urls.py | 21 ++++++- covid19/views.py | 34 ++++++++++++ requirements.txt | 1 + 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 covid19/tests/test_spreadsheet_api.py diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py new file mode 100644 index 00000000..ae734f52 --- /dev/null +++ b/covid19/tests/test_spreadsheet_api.py @@ -0,0 +1,80 @@ +import shutil +import io +from unittest.mock import patch +from datetime import date, timedelta +from pathlib import Path +from model_bakery import baker + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth.models import Permission, Group +from django.core.files.uploadedfile import SimpleUploadedFile + +from covid19.exceptions import SpreadsheetValidationErrors +from covid19.forms import StateSpreadsheetForm +from covid19.tests.utils import Covid19DatasetTestCase + +SAMPLE_SPREADSHEETS_DATA_DIR = Path(settings.BASE_DIR).joinpath("covid19", "tests", "data") + + +class ImportSpreadsheetByDateAPIViewTests(Covid19DatasetTestCase): + def setUp(self): + valid_csv = SAMPLE_SPREADSHEETS_DATA_DIR / "sample-PR.csv" + assert valid_csv.exists() + + self.data = { + "date": date.today(), + "state": "PR", + "boletim_urls": "http://google.com\r\n\r http://brasil.io", + "boletim_notes": "notes", + } + self.file_data = {"file": self.gen_file(f"sample.csv", valid_csv.read_bytes())} + self.user = baker.make(settings.AUTH_USER_MODEL) + self.user.groups.add(Group.objects.get(name__endswith="Rio de Janeiro")) + self.user.groups.add(Group.objects.get(name__endswith="Paraná")) + + def gen_file(self, name, content): + if isinstance(content, str): + content = str.encode(content) + return SimpleUploadedFile(name, content) + + def tearDown(self): + if Path(settings.MEDIA_ROOT).exists(): + shutil.rmtree(settings.MEDIA_ROOT) + + @patch("covid19.views.StateSpreadsheetViewList.post", autospec=True) + def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): + exception = SpreadsheetValidationErrors() + exception.new_error("error 1") + exception.new_error("error 2") + mock_merge.side_effect = exception + + reverse_name = "covid19:statespreadsheet-list" + + tomorrow = date.today() + timedelta(days=1) + tomorrow = tomorrow.isoformat() + self.url = reverse(reverse_name, args=["RJ", f"{tomorrow}"]) + + response = self.client.post(self.url, data=self.data) + + assert len(exception.errors) == 2 + assert 400 == response.status_code + assert { + 'success': False, + 'errors': ["error 1", "error 2"] + } == response.json() + + def test_import_data_from_a_valid_state_spreadsheet_request(self): + reverse_name = "covid19:statespreadsheet-list" + + self.url = reverse(reverse_name, args=["RJ", "2020-05-04"]) + + response = self.client.post(self.url, data=self.data) + + assert len(response.json()['errors']) == 0 + assert 200 == response.status_code + assert { + 'success': True, + 'errors': [] + } == response.json() diff --git a/covid19/urls.py b/covid19/urls.py index d4c85594..e17bd76f 100644 --- a/covid19/urls.py +++ b/covid19/urls.py @@ -1,9 +1,24 @@ -from django.urls import path +from django.urls import include, path, register_converter +from datetime import datetime from . import views -app_name = "covid19" +# From: https://stackoverflow.com/questions/41212865/django-url-that-captures-yyyy-mm-dd-date +class DateConverter: + regex = '\d{4}-\d{2}-\d{2}' + + def to_python(self, value): + return datetime.strptime(value, '%Y-%m-%d') + + def to_url(self, value): + return value + + +register_converter(DateConverter, 'ymd') + +app_name = 'covid19' + urlpatterns = [ path("", views.dashboard, name="dashboard"), path("status/", views.status, name="status"), @@ -12,5 +27,7 @@ path("cities/geo/", views.cities_geojson, name="cities-geo"), path("states/geo/", views.states_geojson, name="states-geo"), path("import-data//", views.import_spreadsheet_proxy, name="spreadsheet_proxy"), + path('api/import-data///', views.StateSpreadsheetViewList.as_view(), name='statespreadsheet-list'), + path('api/import-data////', views.StateSpreadsheetViewDetail.as_view(), name='statespreadsheet-detail'), path("/", views.dashboard, name="dashboard"), ] diff --git a/covid19/views.py b/covid19/views.py index 07594e5e..951de64d 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -3,11 +3,16 @@ from django.http import JsonResponse, HttpResponse, Http404 from django.shortcuts import render +from rest_framework import views +from rest_framework.response import Response +from rest_framework import permissions + from brazil_data.cities import get_state_info from brazil_data.states import STATES, STATE_BY_ACRONYM from core.middlewares import disable_non_logged_user_cache from core.util import cached_http_get_json from covid19.exceptions import SpreadsheetValidationErrors +from covid19.forms import StateSpreadsheetForm from covid19.geo import city_geojson, state_geojson from covid19.spreadsheet import create_merged_state_spreadsheet from covid19.stats import Covid19Stats, max_values @@ -230,3 +235,32 @@ def status(request): data.append(table_entry) return render(request, "covid-status.html", {"import_data": data}) + + +class StateSpreadsheetViewList(views.APIView): + def get(self, request, *args, **kwargs): + return Response({'message': 'ok'}) + + def post(self, request, *args, **kwargs): + data = { + 'state': kwargs.get('state'), + 'date': kwargs.get('date'), + 'boletim_urls': kwargs.get('boletim_urls'), + 'boletim_notes': kwargs.get('boletim_notes'), + } + file_data = kwargs.get('file_data') + + form = StateSpreadsheetForm(data, file_data, user=request.user) + if form.is_valid(): + spreadsheet = form.save() + spreadsheet.refresh_from_db() + transaction.on_commit( + lambda: new_spreadsheet_imported_signal.send( + sender=self, + spreadsheet=spreadsheet + ) + ) + + state = data['state'] + return Response({'success': True, 'errors': []}) + return Response({'success': False, 'errors': form.errors}, status=400) diff --git a/requirements.txt b/requirements.txt index 666b00fc..9d7cf63e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ ipython jinja2 lxml==4.5.0 markdown +model-bakery==1.1.0 networkx==2.1 openpyxl psycopg2-binary From 5c6964e500d740983e6781dbf8657a14445d9961 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Sun, 10 May 2020 19:46:14 -0300 Subject: [PATCH 2/8] API tests with errors. --- brasilio/settings.py | 3 +- covid19/tests/test_spreadsheet_api.py | 59 ++++++++++++++------- covid19/tests/test_spreadsheet_validator.py | 1 + covid19/urls.py | 17 +----- covid19/views.py | 38 +++++++------ requirements.txt | 1 - 6 files changed, 66 insertions(+), 53 deletions(-) diff --git a/brasilio/settings.py b/brasilio/settings.py index 0855c1a1..0f168276 100644 --- a/brasilio/settings.py +++ b/brasilio/settings.py @@ -1,4 +1,5 @@ from urllib.parse import urlparse +from pathlib import Path import environ import sentry_sdk @@ -211,7 +212,6 @@ def get_neo4j_config_dict(neo4j_uri): } } - # django-rq config RQ_QUEUES = {"default": {"URL": REDIS_URL, "DEFAULT_TIMEOUT": 500,}} RQ = { @@ -224,6 +224,7 @@ def get_neo4j_config_dict(neo4j_uri): # Covid19 import settings COVID_IMPORT_PERMISSION_PREFIX = "can_import_covid_state_" +SAMPLE_SPREADSHEETS_DATA_DIR = Path(BASE_DIR).joinpath("covid19", "tests", "data") # RockecChat config ROCKETCHAT_BASE_URL = env("ROCKETCHAT_BASE_URL") diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index ae734f52..c035b73e 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -1,6 +1,6 @@ import shutil import io -from unittest.mock import patch +from unittest.mock import patch, Mock from datetime import date, timedelta from pathlib import Path from model_bakery import baker @@ -10,30 +10,35 @@ from django.urls import reverse from django.contrib.auth.models import Permission, Group from django.core.files.uploadedfile import SimpleUploadedFile +from django.contrib.auth import login +from rest_framework.test import APITestCase from covid19.exceptions import SpreadsheetValidationErrors from covid19.forms import StateSpreadsheetForm -from covid19.tests.utils import Covid19DatasetTestCase -SAMPLE_SPREADSHEETS_DATA_DIR = Path(settings.BASE_DIR).joinpath("covid19", "tests", "data") - -class ImportSpreadsheetByDateAPIViewTests(Covid19DatasetTestCase): +class ImportSpreadsheetByDateAPIViewTests(APITestCase): def setUp(self): - valid_csv = SAMPLE_SPREADSHEETS_DATA_DIR / "sample-PR.csv" + valid_csv = settings.SAMPLE_SPREADSHEETS_DATA_DIR / "sample-PR.csv" assert valid_csv.exists() self.data = { "date": date.today(), - "state": "PR", - "boletim_urls": "http://google.com\r\n\r http://brasil.io", + "boletim_urls": ["http://google.com", "http://brasil.io"], "boletim_notes": "notes", } - self.file_data = {"file": self.gen_file(f"sample.csv", valid_csv.read_bytes())} + + self.filename = f'sample.csv' + + self.file_data = {"file": self.gen_file(self.filename, valid_csv.read_bytes())} + self.data['file'] = self.file_data + self.user = baker.make(settings.AUTH_USER_MODEL) self.user.groups.add(Group.objects.get(name__endswith="Rio de Janeiro")) self.user.groups.add(Group.objects.get(name__endswith="Paraná")) + self.client.force_login(user=self.user) + def gen_file(self, name, content): if isinstance(content, str): content = str.encode(content) @@ -43,38 +48,52 @@ def tearDown(self): if Path(settings.MEDIA_ROOT).exists(): shutil.rmtree(settings.MEDIA_ROOT) - @patch("covid19.views.StateSpreadsheetViewList.post", autospec=True) + @patch("covid19.forms.format_spreadsheet_rows_as_dict", autospec=True) def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): exception = SpreadsheetValidationErrors() exception.new_error("error 1") exception.new_error("error 2") mock_merge.side_effect = exception - reverse_name = "covid19:statespreadsheet-list" - tomorrow = date.today() + timedelta(days=1) tomorrow = tomorrow.isoformat() - self.url = reverse(reverse_name, args=["RJ", f"{tomorrow}"]) + self.data['date'] = tomorrow + + reverse_name = "covid19:statespreadsheet-list" + + self.url = reverse(reverse_name, args=["RJ"]) - response = self.client.post(self.url, data=self.data) + response = self.client.post(self.url, data=self.data, format='json') assert len(exception.errors) == 2 assert 400 == response.status_code assert { - 'success': False, 'errors': ["error 1", "error 2"] } == response.json() + @patch("covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning"])) def test_import_data_from_a_valid_state_spreadsheet_request(self): reverse_name = "covid19:statespreadsheet-list" - self.url = reverse(reverse_name, args=["RJ", "2020-05-04"]) + self.url = reverse(reverse_name, args=["RJ"]) - response = self.client.post(self.url, data=self.data) + response = self.client.get(reverse('covid19:dashboard')) + assert response.status_code == 200 + csrftoken = response.cookies['csrftoken'] + + response = self.client.post(self.url, data=self.data, format='json', headers={'X-CSRFToken': csrftoken}) - assert len(response.json()['errors']) == 0 assert 200 == response.status_code assert { - 'success': True, - 'errors': [] + "warnings": ["warning 1", "warning 2"], + "detail_url": "https://brasil.io/covid19/dataset/RJ" } == response.json() + + def test_login_required(self): + reverse_name = "covid19:statespreadsheet-list" + + self.url = reverse(reverse_name, args=["RJ"]) + + self.client.logout() + response = self.client.post(self.url, self.data, format='json') + assert 403 == response.status_code diff --git a/covid19/tests/test_spreadsheet_validator.py b/covid19/tests/test_spreadsheet_validator.py index b13a441c..c07bc597 100644 --- a/covid19/tests/test_spreadsheet_validator.py +++ b/covid19/tests/test_spreadsheet_validator.py @@ -4,6 +4,7 @@ from io import BytesIO from model_bakery import baker from unittest.mock import patch, Mock +from pathlib import Path from django.conf import settings from django.test import TestCase diff --git a/covid19/urls.py b/covid19/urls.py index e17bd76f..0b303579 100644 --- a/covid19/urls.py +++ b/covid19/urls.py @@ -3,20 +3,6 @@ from . import views - -# From: https://stackoverflow.com/questions/41212865/django-url-that-captures-yyyy-mm-dd-date -class DateConverter: - regex = '\d{4}-\d{2}-\d{2}' - - def to_python(self, value): - return datetime.strptime(value, '%Y-%m-%d') - - def to_url(self, value): - return value - - -register_converter(DateConverter, 'ymd') - app_name = 'covid19' urlpatterns = [ @@ -27,7 +13,6 @@ def to_url(self, value): path("cities/geo/", views.cities_geojson, name="cities-geo"), path("states/geo/", views.states_geojson, name="states-geo"), path("import-data//", views.import_spreadsheet_proxy, name="spreadsheet_proxy"), - path('api/import-data///', views.StateSpreadsheetViewList.as_view(), name='statespreadsheet-list'), - path('api/import-data////', views.StateSpreadsheetViewDetail.as_view(), name='statespreadsheet-detail'), + path('api/import-data//', views.StateSpreadsheetViewList.as_view(), name='statespreadsheet-list'), path("/", views.dashboard, name="dashboard"), ] diff --git a/covid19/views.py b/covid19/views.py index 46b23d28..3b8ffa21 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -6,6 +6,7 @@ from rest_framework import views from rest_framework.response import Response from rest_framework import permissions +from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser, JSONParser from brazil_data.cities import get_state_info from brazil_data.states import STATES, STATE_BY_ACRONYM @@ -17,6 +18,7 @@ from covid19.spreadsheet import create_merged_state_spreadsheet from covid19.stats import Covid19Stats, max_values from covid19.models import StateSpreadsheet +from covid19.serializers import StateSpreadsheetSerializer stats = Covid19Stats() @@ -238,29 +240,35 @@ def status(request): class StateSpreadsheetViewList(views.APIView): - def get(self, request, *args, **kwargs): - return Response({'message': 'ok'}) + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser, FormParser, MultiPartParser] def post(self, request, *args, **kwargs): data = { - 'state': kwargs.get('state'), - 'date': kwargs.get('date'), - 'boletim_urls': kwargs.get('boletim_urls'), - 'boletim_notes': kwargs.get('boletim_notes'), - } - file_data = kwargs.get('file_data') - - form = StateSpreadsheetForm(data, file_data, user=request.user) + "state": self.kwargs['state'], # sempre terá um state dado que ele está na URL + "date": request.data.get('date'), + "boletim_urls": '\n'.join(request.data.get('boletim_urls', [])), + "boletim_notes": request.data.get('boletim_notes'), + } + + files = { + 'file': request.data.get('file'), + } + + form = StateSpreadsheetForm(data, files, user=request.user) + if form.is_valid(): - spreadsheet = form.save() - spreadsheet.refresh_from_db() transaction.on_commit( lambda: new_spreadsheet_imported_signal.send( sender=self, spreadsheet=spreadsheet ) ) - + + spreadsheet = form.save() + spreadsheet.refresh_from_db() state = data['state'] - return Response({'success': True, 'errors': []}) - return Response({'success': False, 'errors': form.errors}, status=400) + + return Response({"warnings": spreadsheet.warnings, "detail_url": spreadsheet.admin_url}) + + return Response({'errors': form.errors}, status=400) diff --git a/requirements.txt b/requirements.txt index 9d7cf63e..666b00fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,6 @@ ipython jinja2 lxml==4.5.0 markdown -model-bakery==1.1.0 networkx==2.1 openpyxl psycopg2-binary From fafc489853b644115aa1fe919a476a7897b97284 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Wed, 20 May 2020 18:35:43 -0300 Subject: [PATCH 3/8] Starting the test - code - refactor proccess. --- brasilio/settings.py | 1 + covid19/tests/test_spreadsheet_api.py | 66 ++++++++++++++++----------- covid19/urls.py | 2 +- covid19/views.py | 51 +++++++++++---------- 4 files changed, 69 insertions(+), 51 deletions(-) diff --git a/brasilio/settings.py b/brasilio/settings.py index 06fb37c5..f37282e4 100644 --- a/brasilio/settings.py +++ b/brasilio/settings.py @@ -36,6 +36,7 @@ "corsheaders", "django_extensions", "rest_framework", + "rest_framework.authtoken", "markdownx", "django_rq", # Project apps diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index c035b73e..f52d07fa 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -1,20 +1,23 @@ import shutil -import io from unittest.mock import patch, Mock from datetime import date, timedelta from pathlib import Path from model_bakery import baker +import requests +from urllib import request # to build absolute url. from django.conf import settings -from django.test import TestCase from django.urls import reverse -from django.contrib.auth.models import Permission, Group +from django.contrib.auth import get_user_model +from django.contrib.auth.models import User, Group from django.core.files.uploadedfile import SimpleUploadedFile -from django.contrib.auth import login +from django.views.decorators.csrf import ensure_csrf_cookie +from django.test import Client from rest_framework.test import APITestCase +from rest_framework.test import force_authenticate, RequestsClient +from rest_framework.authtoken.models import Token from covid19.exceptions import SpreadsheetValidationErrors -from covid19.forms import StateSpreadsheetForm class ImportSpreadsheetByDateAPIViewTests(APITestCase): @@ -29,14 +32,19 @@ def setUp(self): } self.filename = f'sample.csv' - - self.file_data = {"file": self.gen_file(self.filename, valid_csv.read_bytes())} + + self.file_data = self.gen_file(self.filename, valid_csv.read_bytes()) self.data['file'] = self.file_data + self.setUp_user_credentials() - self.user = baker.make(settings.AUTH_USER_MODEL) + def setUp_user_credentials(self): + self.user = baker.make(get_user_model()) self.user.groups.add(Group.objects.get(name__endswith="Rio de Janeiro")) self.user.groups.add(Group.objects.get(name__endswith="Paraná")) + self.token = baker.make(Token, user=self.user) + self.headers = {'Authorization': f'Token {self.token.key}'} + self.client.credentials(HTTP_AUTHORIZATION=self.headers['Authorization']) self.client.force_login(user=self.user) def gen_file(self, name, content): @@ -50,6 +58,11 @@ def tearDown(self): @patch("covid19.forms.format_spreadsheet_rows_as_dict", autospec=True) def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): + expected_response = { + 'errors': ["error 1", "error 2"] + } + expected_status = 400 + exception = SpreadsheetValidationErrors() exception.new_error("error 1") exception.new_error("error 2") @@ -63,37 +76,36 @@ def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): self.url = reverse(reverse_name, args=["RJ"]) - response = self.client.post(self.url, data=self.data, format='json') + breakpoint() + response = self.client.post(self.url, data=self.data) assert len(exception.errors) == 2 - assert 400 == response.status_code - assert { - 'errors': ["error 1", "error 2"] - } == response.json() + assert expected_status == response.status_code + assert expected_response == response.json() @patch("covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning"])) def test_import_data_from_a_valid_state_spreadsheet_request(self): - reverse_name = "covid19:statespreadsheet-list" + expected_response = { + "warnings": ["warning 1", "warning 2"], + "detail_url": "https://brasil.io/covid19/dataset/RJ" + } - self.url = reverse(reverse_name, args=["RJ"]) + expected_status = 200 - response = self.client.get(reverse('covid19:dashboard')) - assert response.status_code == 200 - csrftoken = response.cookies['csrftoken'] + reverse_name = "covid19:statespreadsheet-list" + self.url = reverse(reverse_name, args=["RJ"]) - response = self.client.post(self.url, data=self.data, format='json', headers={'X-CSRFToken': csrftoken}) + response = self.client.post(self.url, data=self.data) - assert 200 == response.status_code - assert { - "warnings": ["warning 1", "warning 2"], - "detail_url": "https://brasil.io/covid19/dataset/RJ" - } == response.json() + assert expected_status == response.status_code + assert expected_response == response.json() def test_login_required(self): + expected_status = 403 reverse_name = "covid19:statespreadsheet-list" self.url = reverse(reverse_name, args=["RJ"]) - self.client.logout() - response = self.client.post(self.url, self.data, format='json') - assert 403 == response.status_code + self.client.credentials() + response = self.client.post(self.url, data=self.data) + assert expected_status == response.status_code diff --git a/covid19/urls.py b/covid19/urls.py index af9c2634..8e57c397 100644 --- a/covid19/urls.py +++ b/covid19/urls.py @@ -14,6 +14,6 @@ path("cities/geo/", views.cities_geojson, name="cities-geo"), path("states/geo/", views.states_geojson, name="states-geo"), path("import-data//", views.import_spreadsheet_proxy, name="spreadsheet_proxy"), - path('api/import-data//', views.StateSpreadsheetViewList.as_view(), name='statespreadsheet-list'), + path('api/import-data//', views.state_spreadsheet_view_list, name='statespreadsheet-list'), path("/", views.dashboard, name="dashboard"), ] diff --git a/covid19/views.py b/covid19/views.py index 0becdd23..2c2a2e39 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -1,13 +1,15 @@ import datetime import random +import json from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import render +from django.db import transaction from rest_framework import views from rest_framework.response import Response from rest_framework import permissions -from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser, JSONParser +from rest_framework.parsers import FormParser, MultiPartParser, JSONParser from brazil_data.cities import get_state_info from brazil_data.states import STATE_BY_ACRONYM, STATES @@ -22,7 +24,7 @@ from covid19.util import row_to_column from covid19.epiweek import get_epiweek from covid19.models import StateSpreadsheet -from covid19.serializers import StateSpreadsheetSerializer +from covid19.signals import new_spreadsheet_imported_signal stats = Covid19Stats() @@ -310,36 +312,39 @@ def status(request): return render(request, "covid-status.html", {"import_data": data}) -class StateSpreadsheetViewList(views.APIView): - permission_classes = [permissions.IsAuthenticated] - parser_classes = [JSONParser, FormParser, MultiPartParser] - - def post(self, request, *args, **kwargs): +def state_spreadsheet_view_list(request, *args, **kwargs): + if request.method == 'POST': data = { - "state": self.kwargs['state'], # sempre terá um state dado que ele está na URL - "date": request.data.get('date'), - "boletim_urls": '\n'.join(request.data.get('boletim_urls', [])), - "boletim_notes": request.data.get('boletim_notes'), + "state": kwargs['state'], # sempre terá um state dado que ele está na URL + "date": request.POST.get('date'), + "boletim_urls": '\n'.join(request.POST.get('boletim_urls', [])), + "boletim_notes": request.POST.get('boletim_notes'), } - + + breakpoint() + # XXX Remove + boletim_urls = request.POST.get('boletim_urls', []) + # XXX + + data["state"] = kwargs['state'] # sempre terá um state dado que ele está na URL + files = { - 'file': request.data.get('file'), + 'file': data.get('file'), } - + form = StateSpreadsheetForm(data, files, user=request.user) if form.is_valid(): transaction.on_commit( - lambda: new_spreadsheet_imported_signal.send( - sender=self, - spreadsheet=spreadsheet - ) - ) - + lambda: new_spreadsheet_imported_signal.send( + sender=self, + spreadsheet=spreadsheet + ) + ) + spreadsheet = form.save() spreadsheet.refresh_from_db() - state = data['state'] - return Response({"warnings": spreadsheet.warnings, "detail_url": spreadsheet.admin_url}) + return JsonResponse({"warnings": spreadsheet.warnings, "detail_url": spreadsheet.admin_url}) - return Response({'errors': form.errors}, status=400) + return JsonResponse({'errors': form.errors}, status=400) From ff4cb4e4b6ce998937d2afb966e7accf165db2f3 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Wed, 20 May 2020 20:57:30 -0300 Subject: [PATCH 4/8] Unauthorized access and simple response. --- covid19/tests/test_spreadsheet_api.py | 53 +++++++++++---------------- covid19/views.py | 21 ++++------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index f52d07fa..cbe6f13b 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -56,35 +56,9 @@ def tearDown(self): if Path(settings.MEDIA_ROOT).exists(): shutil.rmtree(settings.MEDIA_ROOT) - @patch("covid19.forms.format_spreadsheet_rows_as_dict", autospec=True) - def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): - expected_response = { - 'errors': ["error 1", "error 2"] - } - expected_status = 400 - - exception = SpreadsheetValidationErrors() - exception.new_error("error 1") - exception.new_error("error 2") - mock_merge.side_effect = exception - - tomorrow = date.today() + timedelta(days=1) - tomorrow = tomorrow.isoformat() - self.data['date'] = tomorrow - - reverse_name = "covid19:statespreadsheet-list" - - self.url = reverse(reverse_name, args=["RJ"]) - - breakpoint() - response = self.client.post(self.url, data=self.data) - - assert len(exception.errors) == 2 - assert expected_status == response.status_code - assert expected_response == response.json() - @patch("covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning"])) def test_import_data_from_a_valid_state_spreadsheet_request(self): + # XXX: not sure if it is working. expected_response = { "warnings": ["warning 1", "warning 2"], "detail_url": "https://brasil.io/covid19/dataset/RJ" @@ -95,10 +69,26 @@ def test_import_data_from_a_valid_state_spreadsheet_request(self): reverse_name = "covid19:statespreadsheet-list" self.url = reverse(reverse_name, args=["RJ"]) - response = self.client.post(self.url, data=self.data) + response = self.client.post(self.url, data=self.data, format='json') assert expected_status == response.status_code - assert expected_response == response.json() + #assert expected_response == response.json() + assert { + 'date': '2020-05-20', + 'boletim_urls': ['http://google.com', 'http://brasil.io'], + 'boletim_notes': 'notes', + 'file': [ + 'municipio,confirmados,mortes\r\n', + 'TOTAL NO ESTADO,102,32\r\n', + 'Importados/Indefinidos,2,2\r\n', + 'Abatiá,9,1\r\n', + 'Adrianópolis,11,2\r\n', + 'Agudos do Sul,12,3\r\n', + 'Almirante Tamandaré,8,4\r\n', + 'Altamira do Paraná,13,5\r\n', + 'Alto Paraíso,47,15\r\n' + ] + } == response.json() def test_login_required(self): expected_status = 403 @@ -106,6 +96,7 @@ def test_login_required(self): self.url = reverse(reverse_name, args=["RJ"]) - self.client.credentials() - response = self.client.post(self.url, data=self.data) + self.client.force_authenticate(user=None) + + response = self.client.post(self.url, data=self.data, format='json') assert expected_status == response.status_code diff --git a/covid19/views.py b/covid19/views.py index 2c2a2e39..0d2b0b48 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -7,9 +7,10 @@ from django.db import transaction from rest_framework import views -from rest_framework.response import Response from rest_framework import permissions +from rest_framework.decorators import api_view, permission_classes from rest_framework.parsers import FormParser, MultiPartParser, JSONParser +from rest_framework.response import Response from brazil_data.cities import get_state_info from brazil_data.states import STATE_BY_ACRONYM, STATES @@ -311,26 +312,18 @@ def status(request): return render(request, "covid-status.html", {"import_data": data}) - +@api_view(['POST']) +@permission_classes([permissions.IsAuthenticated]) def state_spreadsheet_view_list(request, *args, **kwargs): if request.method == 'POST': data = { "state": kwargs['state'], # sempre terá um state dado que ele está na URL - "date": request.POST.get('date'), - "boletim_urls": '\n'.join(request.POST.get('boletim_urls', [])), - "boletim_notes": request.POST.get('boletim_notes'), } - breakpoint() - # XXX Remove - boletim_urls = request.POST.get('boletim_urls', []) - # XXX + data = json.loads(request.body) - data["state"] = kwargs['state'] # sempre terá um state dado que ele está na URL - - files = { - 'file': data.get('file'), - } + return JsonResponse(data) + files = {'file': data['file']} form = StateSpreadsheetForm(data, files, user=request.user) From c64b9b8c7db3733652334bec88f231240e927c47 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Thu, 21 May 2020 16:55:29 -0300 Subject: [PATCH 5/8] WIP. issue #213. Implemets the regular upload and anonymous tests. --- covid19/tests/test_spreadsheet_api.py | 39 +++++++++------------------ covid19/views.py | 22 ++++++++++----- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index cbe6f13b..9e115f58 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -1,5 +1,5 @@ import shutil -from unittest.mock import patch, Mock +from unittest.mock import patch, Mock, PropertyMock from datetime import date, timedelta from pathlib import Path from model_bakery import baker @@ -27,7 +27,7 @@ def setUp(self): self.data = { "date": date.today(), - "boletim_urls": ["http://google.com", "http://brasil.io"], + "boletim_urls": "http://google.com\r\n\r http://brasil.io", "boletim_notes": "notes", } @@ -56,45 +56,30 @@ def tearDown(self): if Path(settings.MEDIA_ROOT).exists(): shutil.rmtree(settings.MEDIA_ROOT) - @patch("covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning"])) - def test_import_data_from_a_valid_state_spreadsheet_request(self): - # XXX: not sure if it is working. + @patch("covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning 1", "warning 2"])) + @patch("covid19.models.StateSpreadsheet.admin_url", new_callable=PropertyMock) + def test_import_data_from_a_valid_state_spreadsheet_request(self, mock_admin_url): + mock_admin_url.return_value = "https://brasil.io/covid19/dataset/PR" + + expected_status = 200 expected_response = { "warnings": ["warning 1", "warning 2"], - "detail_url": "https://brasil.io/covid19/dataset/RJ" + "detail_url": "https://brasil.io/covid19/dataset/PR" } - expected_status = 200 - reverse_name = "covid19:statespreadsheet-list" - self.url = reverse(reverse_name, args=["RJ"]) + self.url = reverse(reverse_name, args=["PR"]) response = self.client.post(self.url, data=self.data, format='json') assert expected_status == response.status_code - #assert expected_response == response.json() - assert { - 'date': '2020-05-20', - 'boletim_urls': ['http://google.com', 'http://brasil.io'], - 'boletim_notes': 'notes', - 'file': [ - 'municipio,confirmados,mortes\r\n', - 'TOTAL NO ESTADO,102,32\r\n', - 'Importados/Indefinidos,2,2\r\n', - 'Abatiá,9,1\r\n', - 'Adrianópolis,11,2\r\n', - 'Agudos do Sul,12,3\r\n', - 'Almirante Tamandaré,8,4\r\n', - 'Altamira do Paraná,13,5\r\n', - 'Alto Paraíso,47,15\r\n' - ] - } == response.json() + assert expected_response == response.json() def test_login_required(self): expected_status = 403 reverse_name = "covid19:statespreadsheet-list" - self.url = reverse(reverse_name, args=["RJ"]) + self.url = reverse(reverse_name, args=["PR"]) self.client.force_authenticate(user=None) diff --git a/covid19/views.py b/covid19/views.py index 0d2b0b48..56152bd9 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -2,6 +2,7 @@ import random import json +from django.core.files.uploadedfile import SimpleUploadedFile from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import render from django.db import transaction @@ -315,17 +316,24 @@ def status(request): @api_view(['POST']) @permission_classes([permissions.IsAuthenticated]) def state_spreadsheet_view_list(request, *args, **kwargs): - if request.method == 'POST': - data = { - "state": kwargs['state'], # sempre terá um state dado que ele está na URL - } + def gen_file(name, content): + if isinstance(content, str): + content = str.encode(content) + return SimpleUploadedFile(name, content) + + if request.method == 'POST': data = json.loads(request.body) - return JsonResponse(data) - files = {'file': data['file']} + data["state"] = kwargs['state'] # sempre terá um state dado que ele está na URL + + state = data['state'] + date = data['date'] + filename = f"{state}-{date}-import.csv" + + file_data = {"file": gen_file(filename, ''.join(data['file']))} - form = StateSpreadsheetForm(data, files, user=request.user) + form = StateSpreadsheetForm(data, file_data, user=request.user) if form.is_valid(): transaction.on_commit( From 40209509c3b5226b88b3630405da6a2832908074 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Thu, 21 May 2020 17:36:37 -0300 Subject: [PATCH 6/8] Implements invalid date validation test. fix #213. --- covid19/tests/test_spreadsheet_api.py | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index 9e115f58..506edee4 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -75,7 +75,7 @@ def test_import_data_from_a_valid_state_spreadsheet_request(self, mock_admin_url assert expected_status == response.status_code assert expected_response == response.json() - def test_login_required(self): + def test_403_login_required(self): expected_status = 403 reverse_name = "covid19:statespreadsheet-list" @@ -85,3 +85,29 @@ def test_login_required(self): response = self.client.post(self.url, data=self.data, format='json') assert expected_status == response.status_code + + @patch("covid19.spreadsheet_validator.validate_historical_data", autospec=True) + def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): + exception = SpreadsheetValidationErrors() + exception.new_error("error 1") + exception.new_error("error 2") + mock_merge.side_effect = exception + + expected_status = 400 + expected_exception_messages = ["error 1", "error 2"] + expected_response = {'errors': {'date': ['Campo não aceita datas futuras.']}} + + tomorrow = date.today() + timedelta(days=1) + tomorrow = tomorrow.isoformat() + self.data['date'] = tomorrow + + reverse_name = "covid19:statespreadsheet-list" + + self.url = reverse(reverse_name, args=["RJ"]) + + response = self.client.post(self.url, data=self.data, format='json') + + assert len(exception.error_messages) == 2 + assert expected_exception_messages == sorted(exception.error_messages) + assert expected_status == response.status_code + assert expected_response == response.json() From 897ba903e4b7773ce2e71d89cd79151cfd62af97 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Thu, 21 May 2020 18:01:43 -0300 Subject: [PATCH 7/8] Fix some flake8 checks, just one message left. --- brazil_data/cities.py | 1 - covid19/stats.py | 3 ++- covid19/tests/test_spreadsheet_api.py | 7 +------ covid19/urls.py | 3 +-- covid19/views.py | 5 +---- 5 files changed, 5 insertions(+), 14 deletions(-) diff --git a/brazil_data/cities.py b/brazil_data/cities.py index f400811a..d20d260a 100644 --- a/brazil_data/cities.py +++ b/brazil_data/cities.py @@ -1,7 +1,6 @@ from collections import namedtuple from functools import lru_cache from itertools import groupby -from pathlib import Path import rows import rows.utils diff --git a/covid19/stats.py b/covid19/stats.py index b0be014d..528699b5 100644 --- a/covid19/stats.py +++ b/covid19/stats.py @@ -337,7 +337,8 @@ def aggregate_state_data(self, select_columns, groupby_columns, state=None): return list(qs.order_by(*groupby_columns).values(*groupby_columns).annotate(**annotate_dict)) def aggregate_epiweek(self, data, group_key="epidemiological_week"): - row_key = lambda row: row[group_key] + def row_key(row): + return row[group_key] result = [] data.sort(key=row_key) for epiweek, group in groupby(data, key=row_key): diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index 506edee4..c3cbda98 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -3,18 +3,13 @@ from datetime import date, timedelta from pathlib import Path from model_bakery import baker -import requests -from urllib import request # to build absolute url. from django.conf import settings from django.urls import reverse from django.contrib.auth import get_user_model -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from django.core.files.uploadedfile import SimpleUploadedFile -from django.views.decorators.csrf import ensure_csrf_cookie -from django.test import Client from rest_framework.test import APITestCase -from rest_framework.test import force_authenticate, RequestsClient from rest_framework.authtoken.models import Token from covid19.exceptions import SpreadsheetValidationErrors diff --git a/covid19/urls.py b/covid19/urls.py index 8e57c397..714f8dbc 100644 --- a/covid19/urls.py +++ b/covid19/urls.py @@ -1,5 +1,4 @@ -from django.urls import include, path, register_converter -from datetime import datetime +from django.urls import path from . import views diff --git a/covid19/views.py b/covid19/views.py index 56152bd9..ec6adcf9 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -7,11 +7,8 @@ from django.shortcuts import render from django.db import transaction -from rest_framework import views from rest_framework import permissions from rest_framework.decorators import api_view, permission_classes -from rest_framework.parsers import FormParser, MultiPartParser, JSONParser -from rest_framework.response import Response from brazil_data.cities import get_state_info from brazil_data.states import STATE_BY_ACRONYM, STATES @@ -25,7 +22,6 @@ from covid19.stats import Covid19Stats, max_values from covid19.util import row_to_column from covid19.epiweek import get_epiweek -from covid19.models import StateSpreadsheet from covid19.signals import new_spreadsheet_imported_signal stats = Covid19Stats() @@ -313,6 +309,7 @@ def status(request): return render(request, "covid-status.html", {"import_data": data}) + @api_view(['POST']) @permission_classes([permissions.IsAuthenticated]) def state_spreadsheet_view_list(request, *args, **kwargs): From 42b73901843f708e7bcae4756863b7af52dc7e43 Mon Sep 17 00:00:00 2001 From: Ramiro Batista da Luz Date: Thu, 21 May 2020 18:10:30 -0300 Subject: [PATCH 8/8] Fix black ckecks. --- covid19/stats.py | 7 ++++--- covid19/tests/test_google_data.py | 1 - covid19/tests/test_spreadsheet_api.py | 24 ++++++++++++----------- covid19/urls.py | 4 ++-- covid19/views.py | 28 +++++++++------------------ 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/covid19/stats.py b/covid19/stats.py index 528699b5..5bc6085a 100644 --- a/covid19/stats.py +++ b/covid19/stats.py @@ -85,7 +85,7 @@ class Covid19Stats: "new_deaths_indeterminate_2019": (Sum, "new_deaths_indeterminate_2019"), "new_deaths_others_2019": (Sum, "new_deaths_others_2019"), "new_deaths_pneumonia_2019": (Sum, "new_deaths_pneumonia_2019"), - "new_deaths_respiratory_failure_2019": (Sum, "new_deaths_respiratory_failure_2019"), + "new_deaths_respiratory_failure_2019": (Sum, "new_deaths_respiratory_failure_2019",), "new_deaths_sars_2019": (Sum, "new_deaths_sars_2019"), "new_deaths_septicemia_2019": (Sum, "new_deaths_septicemia_2019"), "new_deaths_total_2019": (Sum, "new_deaths_total_2019"), @@ -339,6 +339,7 @@ def aggregate_state_data(self, select_columns, groupby_columns, state=None): def aggregate_epiweek(self, data, group_key="epidemiological_week"): def row_key(row): return row[group_key] + result = [] data.sort(key=row_key) for epiweek, group in groupby(data, key=row_key): @@ -353,7 +354,7 @@ def row_key(row): def historical_case_data_for_state_per_day(self, state): return self.aggregate_state_data( - groupby_columns=["date"], select_columns=self.graph_daily_cases_columns, state=state + groupby_columns=["date"], select_columns=self.graph_daily_cases_columns, state=state, ) def historical_case_data_for_state_per_epiweek(self, state): @@ -375,7 +376,7 @@ def aggregate_registry_data(self, select_columns, groupby_columns, state=None): def historical_registry_data_for_state_per_day(self, state=None): # If state = None, return data for Brazil return self.aggregate_registry_data( - groupby_columns=["date"], select_columns=self.graph_daily_registry_deaths_columns, state=state + groupby_columns=["date"], select_columns=self.graph_daily_registry_deaths_columns, state=state, ) def excess_deaths_registry_data_for_state_per_day(self, state=None): diff --git a/covid19/tests/test_google_data.py b/covid19/tests/test_google_data.py index 09e0f40e..d73d3965 100644 --- a/covid19/tests/test_google_data.py +++ b/covid19/tests/test_google_data.py @@ -9,7 +9,6 @@ class TestGoogleDataIntegration(TestCase): - @skip("This test won't work with Django's DummyCache, which is enabled for development") def test_cache_general_spreadsheet(self): cache.clear() diff --git a/covid19/tests/test_spreadsheet_api.py b/covid19/tests/test_spreadsheet_api.py index c3cbda98..f657c2f2 100644 --- a/covid19/tests/test_spreadsheet_api.py +++ b/covid19/tests/test_spreadsheet_api.py @@ -26,10 +26,10 @@ def setUp(self): "boletim_notes": "notes", } - self.filename = f'sample.csv' + self.filename = f"sample.csv" self.file_data = self.gen_file(self.filename, valid_csv.read_bytes()) - self.data['file'] = self.file_data + self.data["file"] = self.file_data self.setUp_user_credentials() def setUp_user_credentials(self): @@ -38,8 +38,8 @@ def setUp_user_credentials(self): self.user.groups.add(Group.objects.get(name__endswith="Paraná")) self.token = baker.make(Token, user=self.user) - self.headers = {'Authorization': f'Token {self.token.key}'} - self.client.credentials(HTTP_AUTHORIZATION=self.headers['Authorization']) + self.headers = {"Authorization": f"Token {self.token.key}"} + self.client.credentials(HTTP_AUTHORIZATION=self.headers["Authorization"]) self.client.force_login(user=self.user) def gen_file(self, name, content): @@ -51,7 +51,9 @@ def tearDown(self): if Path(settings.MEDIA_ROOT).exists(): shutil.rmtree(settings.MEDIA_ROOT) - @patch("covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning 1", "warning 2"])) + @patch( + "covid19.spreadsheet_validator.validate_historical_data", Mock(return_value=["warning 1", "warning 2"]), + ) @patch("covid19.models.StateSpreadsheet.admin_url", new_callable=PropertyMock) def test_import_data_from_a_valid_state_spreadsheet_request(self, mock_admin_url): mock_admin_url.return_value = "https://brasil.io/covid19/dataset/PR" @@ -59,13 +61,13 @@ def test_import_data_from_a_valid_state_spreadsheet_request(self, mock_admin_url expected_status = 200 expected_response = { "warnings": ["warning 1", "warning 2"], - "detail_url": "https://brasil.io/covid19/dataset/PR" + "detail_url": "https://brasil.io/covid19/dataset/PR", } reverse_name = "covid19:statespreadsheet-list" self.url = reverse(reverse_name, args=["PR"]) - response = self.client.post(self.url, data=self.data, format='json') + response = self.client.post(self.url, data=self.data, format="json") assert expected_status == response.status_code assert expected_response == response.json() @@ -78,7 +80,7 @@ def test_403_login_required(self): self.client.force_authenticate(user=None) - response = self.client.post(self.url, data=self.data, format='json') + response = self.client.post(self.url, data=self.data, format="json") assert expected_status == response.status_code @patch("covid19.spreadsheet_validator.validate_historical_data", autospec=True) @@ -90,17 +92,17 @@ def test_400_if_spreadsheet_error_on_import_data(self, mock_merge): expected_status = 400 expected_exception_messages = ["error 1", "error 2"] - expected_response = {'errors': {'date': ['Campo não aceita datas futuras.']}} + expected_response = {"errors": {"date": ["Campo não aceita datas futuras."]}} tomorrow = date.today() + timedelta(days=1) tomorrow = tomorrow.isoformat() - self.data['date'] = tomorrow + self.data["date"] = tomorrow reverse_name = "covid19:statespreadsheet-list" self.url = reverse(reverse_name, args=["RJ"]) - response = self.client.post(self.url, data=self.data, format='json') + response = self.client.post(self.url, data=self.data, format="json") assert len(exception.error_messages) == 2 assert expected_exception_messages == sorted(exception.error_messages) diff --git a/covid19/urls.py b/covid19/urls.py index 714f8dbc..961c8851 100644 --- a/covid19/urls.py +++ b/covid19/urls.py @@ -12,7 +12,7 @@ path("historical/weekly/", views.historical_weekly, name="historical-weekly"), path("cities/geo/", views.cities_geojson, name="cities-geo"), path("states/geo/", views.states_geojson, name="states-geo"), - path("import-data//", views.import_spreadsheet_proxy, name="spreadsheet_proxy"), - path('api/import-data//', views.state_spreadsheet_view_list, name='statespreadsheet-list'), + path("import-data//", views.import_spreadsheet_proxy, name="spreadsheet_proxy",), + path("api/import-data//", views.state_spreadsheet_view_list, name="statespreadsheet-list",), path("/", views.dashboard, name="dashboard"), ] diff --git a/covid19/views.py b/covid19/views.py index ec6adcf9..47d46de4 100644 --- a/covid19/views.py +++ b/covid19/views.py @@ -69,11 +69,7 @@ def clean_weekly_data(data, skip=0, diff_days=-14): now = datetime.datetime.now() today = datetime.date(now.year, now.month, now.day) _, until_epiweek = get_epiweek(today + datetime.timedelta(days=diff_days)) - return [ - row - for index, row in enumerate(data) - if index >= skip and row["epidemiological_week"] < until_epiweek - ] + return [row for index, row in enumerate(data) if index >= skip and row["epidemiological_week"] < until_epiweek] def historical_data(request, period): @@ -310,39 +306,33 @@ def status(request): return render(request, "covid-status.html", {"import_data": data}) -@api_view(['POST']) +@api_view(["POST"]) @permission_classes([permissions.IsAuthenticated]) def state_spreadsheet_view_list(request, *args, **kwargs): - def gen_file(name, content): if isinstance(content, str): content = str.encode(content) return SimpleUploadedFile(name, content) - if request.method == 'POST': + if request.method == "POST": data = json.loads(request.body) - data["state"] = kwargs['state'] # sempre terá um state dado que ele está na URL + data["state"] = kwargs["state"] # sempre terá um state dado que ele está na URL - state = data['state'] - date = data['date'] + state = data["state"] + date = data["date"] filename = f"{state}-{date}-import.csv" - file_data = {"file": gen_file(filename, ''.join(data['file']))} + file_data = {"file": gen_file(filename, "".join(data["file"]))} form = StateSpreadsheetForm(data, file_data, user=request.user) if form.is_valid(): - transaction.on_commit( - lambda: new_spreadsheet_imported_signal.send( - sender=self, - spreadsheet=spreadsheet - ) - ) + transaction.on_commit(lambda: new_spreadsheet_imported_signal.send(sender=self, spreadsheet=spreadsheet)) spreadsheet = form.save() spreadsheet.refresh_from_db() return JsonResponse({"warnings": spreadsheet.warnings, "detail_url": spreadsheet.admin_url}) - return JsonResponse({'errors': form.errors}, status=400) + return JsonResponse({"errors": form.errors}, status=400)