Skip to content

Commit 9d9c8de

Browse files
authored
Merge pull request #152 from WikiMovimentoBrasil/2025-01-csv-report
feat: basic csv report of finished batches
2 parents 142fc9e + 2d2bbf0 commit 9d9c8de

File tree

6 files changed

+117
-0
lines changed

6 files changed

+117
-0
lines changed

src/core/models.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import csv
23
import logging
34
import jsonpatch
45
from typing import Optional
@@ -201,6 +202,10 @@ def is_initial_or_running(self):
201202
def is_preview_initial_or_running(self):
202203
return self.is_preview or self.is_initial or self.is_running
203204

205+
@property
206+
def is_done(self):
207+
return self.status == Batch.STATUS_DONE
208+
204209
def add_preview_command(self, preview_command: "BatchCommand") -> bool:
205210
if not hasattr(self, "_preview_commands"):
206211
self._preview_commands = []
@@ -234,6 +239,43 @@ def wikibase_url(self):
234239
"""
235240
return settings.BASE_REST_URL.replace("https://", "http://").split("/w/rest.php")[0]
236241

242+
# ------
243+
# REPORT
244+
# ------
245+
246+
def write_report(self, csvfile):
247+
"""
248+
Uses `csvfile` as the csv writer's file to write the batch report.
249+
"""
250+
writer = csv.writer(csvfile)
251+
writer.writerow(
252+
[
253+
"batch_id",
254+
"index",
255+
"operation",
256+
"status",
257+
"error",
258+
"message",
259+
"entity_id",
260+
"raw_input",
261+
"api_response",
262+
]
263+
)
264+
for cmd in self.commands():
265+
called_api = bool(cmd.response_json)
266+
writer.writerow(
267+
[
268+
self.pk,
269+
cmd.index,
270+
cmd.operation,
271+
cmd.get_status_display(),
272+
cmd.error,
273+
cmd.message,
274+
cmd.entity_id(),
275+
cmd.raw.replace("\t", "|"), # tabs are weird in csv
276+
cmd.response_json if called_api else None,
277+
]
278+
)
237279

238280
class BatchCommand(models.Model):
239281
"""

src/core/tests/test_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ def create_item_failed_server(cls, mocker):
180180
status_code=500,
181181
)
182182

183+
@classmethod
184+
def patch_item_successful(cls, mocker, item_id, json_result):
185+
mocker.patch(
186+
cls.wikibase_url(f"/entities/items/{item_id}"),
187+
json=json_result,
188+
status_code=200,
189+
)
190+
183191
@classmethod
184192
def add_statement_successful(cls, mocker, item_id):
185193
mocker.patch(

src/web/templates/batch.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ <h2> {% translate "Batch" %} #{{ batch.pk }} <img id="spinner" class="htmx-indic
3030
{% csrf_token %}
3131
<input type="submit" value="Restart">
3232
</form>
33+
{% elif batch.is_done %}
34+
<form method="GET" action="{% url 'batch_report' pk=batch.pk %}">
35+
<input type="submit" value="Report">
36+
</form>
3337
{% endif %}
3438
{% endif %}
3539
</div>

src/web/tests/test_views.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
class ViewsTest(TestCase):
2020
URL_NAME = "profile"
21+
maxDiff = None
2122

2223
def assertInRes(self, substring, response):
2324
"""Checks if a substring is contained in response content"""
@@ -560,3 +561,44 @@ def test_batch_preview_commands(self, mocker):
560561
self.assertInRes("Save and run batch", res)
561562
res = self.client.get("/batch/new/preview/commands/")
562563
self.assertEqual(res.status_code, 200)
564+
565+
@requests_mock.Mocker()
566+
def test_batch_report(self, mocker):
567+
ApiMocker.is_autoconfirmed(mocker)
568+
ApiMocker.wikidata_property_data_types(mocker)
569+
ApiMocker.property_data_type(mocker, "P2", "wikibase-item")
570+
ApiMocker.item_empty(mocker, "Q1234")
571+
ApiMocker.item_empty(mocker, "Q11")
572+
ApiMocker.patch_item_successful(mocker, "Q1234", {"id": "Q1234$stuff"})
573+
ApiMocker.patch_item_successful(mocker, "Q11", {"id": "Q11", "labels": {"en": "label"}})
574+
user, api_client = self.login_user_and_get_token("wikiuser")
575+
parser = V1CommandParser()
576+
batch = parser.parse("Batch", "wikiuser", """Q1234\tP2\tQ1||Q11|Len|"label" """)
577+
batch.save_batch_and_preview_commands()
578+
pk = batch.pk
579+
580+
response = self.client.get(f"/batch/{pk}/")
581+
self.assertEqual(response.status_code, 200)
582+
self.assertTemplateUsed("batch.html")
583+
self.assertNotInRes(f"""<form method="GET" action="/batch/{pk}/report/">""", response)
584+
585+
batch.run()
586+
587+
response = self.client.get(f"/batch/{pk}/")
588+
self.assertEqual(response.status_code, 200)
589+
self.assertTemplateUsed("batch.html")
590+
self.assertInRes(f"""<form method="GET" action="/batch/{pk}/report/">""", response)
591+
592+
response = self.client.post(f"/batch/{pk}/report/")
593+
self.assertEqual(response.status_code, 405)
594+
595+
response = self.client.get(f"/batch/{pk}/report/")
596+
self.assertEqual(response.status_code, 200)
597+
self.assertEqual(response.headers["Content-Disposition"], f'attachment; filename="batch-{pk}-report.csv"')
598+
result = (
599+
"""b'batch_id,index,operation,status,error,message,entity_id,raw_input,api_response\\r\\n"""
600+
"""1,0,set_statement,Done,,,Q1234,Q1234|P2|Q1,{\\\'id\\\': \\\'Q1234$stuff\\\'}\\r\\n"""
601+
"""1,1,set_label,Done,,,Q11,"Q11|Len|""label"" ","""
602+
""""{\\\'id\\\': \\\'Q11\\\', \\\'labels\\\': {\\\'en\\\': \\\'label\\\'}}"\\r\\n\'"""
603+
)
604+
self.assertEqual(result, str(response.content).strip())

src/web/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .views.batch import batch_commands
1010
from .views.batch import batch_stop
1111
from .views.batch import batch_restart
12+
from .views.batch import batch_report
1213
from .views.batch import batch_summary
1314
from .views.batches import home
1415
from .views.batches import last_batches
@@ -34,6 +35,7 @@
3435
path("batch/preview/", batch, name="batch_preview"),
3536
path("batch/<int:pk>/stop/", batch_stop, name="batch_stop"),
3637
path("batch/<int:pk>/restart/", batch_restart, name="batch_restart"),
38+
path("batch/<int:pk>/report/", batch_report, name="batch_report"),
3739
path("batch/<int:pk>/summary/", batch_summary, name="batch_summary"),
3840
path("batch/<int:pk>/commands/", batch_commands, name="batch_commands"),
3941
path("batch/new/", new_batch, name="new_batch"),

src/web/views/batch.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.shortcuts import render
44
from django.urls import reverse
55
from django.views.decorators.http import require_http_methods
6+
from django.views.decorators.http import require_GET
7+
from django.http import HttpResponse
68

79
from core.client import Client
810
from core.models import Batch
@@ -81,6 +83,23 @@ def batch_restart(request, pk):
8183
except Batch.DoesNotExist:
8284
return render(request, "batch_not_found.html", {"pk": pk}, status=404)
8385

86+
@require_GET
87+
def batch_report(request, pk):
88+
try:
89+
batch = Batch.objects.get(pk=pk)
90+
current_owner = request.user.is_authenticated and request.user.username == batch.user
91+
if current_owner and batch.is_done:
92+
res = HttpResponse(
93+
content_type="text/csv",
94+
headers={"Content-Disposition": f'attachment; filename="batch-{pk}-report.csv"'},
95+
)
96+
batch.write_report(res)
97+
return res
98+
else:
99+
return render(request, "batch_not_found.html", {"pk": pk}, status=404)
100+
except Batch.DoesNotExist:
101+
return render(request, "batch_not_found.html", {"pk": pk}, status=404)
102+
84103

85104
@require_http_methods(
86105
[

0 commit comments

Comments
 (0)