Skip to content

Commit

Permalink
new xls export type, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
githubering182 committed Jul 16, 2024
1 parent 9acec66 commit e6c63c8
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 33 deletions.
99 changes: 80 additions & 19 deletions backend-app/file/export.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
from abc import ABC, abstractmethod, abstractproperty
from abc import ABC, abstractmethod
from typing import Any, List, Dict
from io import BytesIO
from json import dumps
from xlsxwriter import Workbook
from xlsxwriter.worksheet import Worksheet

ROW = Dict[str, Any]
DATA = List[ROW]
ENCODING = "utf-8"
IMPLEMENTED = {"json", "csv"}
IMPLEMENTED = {"json", "csv", "xlsx"}


class Export(ABC):
@abstractmethod
def __init__(self, data: DATA, *args): ...
ATTRIBUTE_HEADERS = "Attribute,Level,Validation Images,Validation Videos,Accepted Images, Accepted Videos,Declined Images, Declined Videos,Total\n"
USER_HEADERS = "User,Validation Images,Validation Videos,Accepted Images, Accepted Videos,Declined Images, Declined Videos,Total\n"

t_val = lambda _, x: (x.get('image', 0), x.get('video', 0))
sm = lambda _, x, y, z: sum(x + y + z)

def __init__(self, data: DATA, *args):
self._data = data
self._type = args[0]

@abstractmethod
def into_response(self) -> BytesIO: ...
Expand All @@ -24,8 +33,6 @@ def _data(self, data: DATA): self.__data = data


class JSON(Export):
def __init__(self, data: DATA, *args): self._data = data

def into_response(self) -> BytesIO:
file = BytesIO()

Expand All @@ -38,17 +45,6 @@ def into_response(self) -> BytesIO:


class CSV(Export):
ATTRIBUTE_HEADERS = "Attribute,Level,On Validation,Accepted,Declined,Total\n"
USER_HEADERS = "User,On Validation,Accepted,Declined,Total\n"

t_val = lambda _, x: (x.get('image', 0), x.get('video', 0))
t_str = lambda _, x: f"images: {x[0]} videos: {x[1]}"
sm = lambda _, x, y, z: sum(x + y + z)

def __init__(self, data: DATA, *args):
self._data = data
self._type = args[0]

def _write_attribute(self, dest: BytesIO, data: ROW):
name = data.get("name")
level_name = data.get("levelName")
Expand All @@ -59,7 +55,7 @@ def _write_attribute(self, dest: BytesIO, data: ROW):
total = self.sm(val, acc, dec)

dest.write(bytes(
f"{name},{level_name},{self.t_str(val)},{self.t_str(acc)},{self.t_str(dec)},{total}\n",
f"{name},{level_name},{val[0]},{val[1]},{acc[0]},{acc[1]},{dec[0]},{dec[1]},{total}\n",
encoding=ENCODING
))

Expand All @@ -74,7 +70,7 @@ def _write_user(self, dest: BytesIO, data: ROW):
total = self.sm(val, acc, dec)

dest.write(bytes(
f"{name},{self.t_str(val)},{self.t_str(acc)},{self.t_str(dec)},{total}\n",
f"{name},{val[0]},{val[1]},{acc[0]},{acc[1]},{dec[0]},{dec[1]},{total}\n",
encoding=ENCODING
))

Expand All @@ -96,3 +92,68 @@ def into_response(self) -> BytesIO:
file.seek(0)

return file


class XLS(Export):
__row_n = 0

@property
def _row_n(self) -> int: return self.__row_n

@_row_n.setter
def _row_n(self, new: int): self.__row_n = new

def _write_attribute(self, dest: Worksheet, data: ROW):
name = data.get("name")
level_name = data.get("levelName")

val = self.t_val(data.get("v", {}))
acc = self.t_val(data.get("a", {}))
dec = self.t_val(data.get("d", {}))
total = self.sm(val, acc, dec)

row = (name, level_name, val[0], val[1], acc[0], acc[1], dec[0], dec[1], total)

for i, item in enumerate(row): dest.write(self._row_n, i, item)
self._row_n += 1

for child in (children := data.get("children", [])): self._write_attribute(dest, child)

def _write_user(self, dest: Worksheet, data: ROW):
name = data.get("name")

val = self.t_val(data.get("v", {}))
acc = self.t_val(data.get("a", {}))
dec = self.t_val(data.get("d", {}))
total = self.sm(val, acc, dec)

row = (name, val[0], val[1], acc[0], acc[1], dec[0], dec[1], total)

for i, item in enumerate(row): dest.write(self._row_n, i, item)
self._row_n += 1

def into_response(self) -> BytesIO:
file = BytesIO()

match self._type:
case "attribute":
headers = self.ATTRIBUTE_HEADERS
write = self._write_attribute
case "user":
headers = self.USER_HEADERS
write = self._write_user
case _: raise AttributeError

xl = Workbook(file)
sheet = xl.add_worksheet()

headers = headers.split(",")

for i, header in enumerate(headers): sheet.write(self._row_n, i, header)
self._row_n += 1
for row in self._data: write(sheet, row)

xl.close()
file.seek(0)

return file
35 changes: 35 additions & 0 deletions backend-app/file/file_tests/export_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from file.export import XLS, JSON, CSV
from file.services import StatsServices
from django.test import TestCase
from attribute.attribute_tests.mock_attribute import MockCase
from json import loads


class ExportTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.case = MockCase()
cls.attr_stat, _ = StatsServices.from_attribute(cls.case.project.id)
cls.user_stat, _ = StatsServices.from_user(cls.case.project.id)

def test_xls(self):
attr_res = XLS(self.attr_stat, "attribute").into_response()
user_res = XLS(self.user_stat, "user").into_response()
# TODO:

def test_csv(self):
attr_res = CSV(self.attr_stat, "attribute").into_response()
user_res = CSV(self.user_stat, "user").into_response()

attributes = attr_res.read().decode().split("\n")
users = user_res.read().decode().split("\n")

self.assertTrue(len(attributes) == len(users) == 3)
# TODO:

def test_json(self):
attr_res = JSON(self.attr_stat, 0).into_response()
user_res = JSON(self.user_stat, 0).into_response()
self.assertEqual(self.attr_stat, loads(attr_res.read().decode()))
self.assertEqual(self.user_stat, loads(user_res.read().decode()))
40 changes: 38 additions & 2 deletions backend-app/file/file_tests/services_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
FileUploader,
StatsServices,
ViewSetServices,
_annotate_files
_annotate_files,
form_export_file
)
from json import dumps
from attribute.models import AttributeGroup, Attribute
Expand Down Expand Up @@ -360,7 +361,6 @@ class StatsServiceTest(TestCase):
def setUpClass(cls):
super().setUpClass()
cls.case = MockCase()
cls.case = MockCase()
cls.empty_project = Project.objects.create(name="some")

def test_from_attribute(self):
Expand Down Expand Up @@ -479,3 +479,39 @@ def test_assign_attributes(self):
set(groups[0].attribute.values_list("id", flat=True)),
set(meta_groups[0])
)


class ExportServicesTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.case = MockCase()

def test_export(self):
self._assert_query_fail({})
self._assert_query_fail({"type": 1})
self._assert_query_fail({"type": 1, "project_id": 1,})
self._assert_query_fail({"type": "json", "project_id": 1, "choice": "user"}, True)

try:
form_export_file({"type": "asd", "project_id": 1, "choice": "zxc"})
self.assertTrue(False)
except Exception as e: self.assertEqual(str(e), "asd not implemented")

try:
form_export_file({"type": "json", "project_id": 1, "choice": "zxc"})
self.assertTrue(False)
except Exception as e: self.assertEqual(str(e), "export for zxc is not implemented")

def _assert_query_fail(self, data, intential=False):
queries = {"type", "project_id", "choice"}

string = " must be provided"
try:
form_export_file(data)
self.assertTrue(intential, "not suppose to go there")
except Exception as e:
self.assertEqual(
set(str(e)[:-len(string)].split(", ")),
queries - set(data)
)
19 changes: 9 additions & 10 deletions backend-app/file/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from typing import Any
from datetime import datetime as dt
from io import BytesIO
from json import dumps
from .export import IMPLEMENTED, JSON, CSV
from .export import IMPLEMENTED, JSON, CSV, XLS
from .serializers import File, FileSerializer


Expand Down Expand Up @@ -387,7 +386,7 @@ def _user_stat_adapt(cls, stats_data: list[dict[str, Any]]) -> list[dict[str, An
target[status][f_type] = prev_count + count
else: target[status] = {f_type: count}

return prepared_stats.values()
return list(prepared_stats.values())


def _annotate_files(request_data: dict[str, Any]) -> tuple[dict[str, Any], int]:
Expand All @@ -413,6 +412,12 @@ def _annotate_files(request_data: dict[str, Any]) -> tuple[dict[str, Any], int]:

def form_export_file(query: dict[str, Any]) -> BytesIO:
query_set = {"type", "project_id", "choice"}
choice_map = {
"attribute": StatsServices.from_attribute,
"user": StatsServices.from_user
}
export_map = {"json": JSON, "csv": CSV, "xlsx": XLS}

assert not (
no_ps := [p for p in query_set if not query.get(p)]
), f"{', '.join(no_ps)} must be provided"
Expand All @@ -422,13 +427,7 @@ def form_export_file(query: dict[str, Any]) -> BytesIO:
file_type = query["type"]

assert file_type in IMPLEMENTED, f"{file_type} not implemented"

choice_map = {
"attribute": StatsServices.from_attribute,
"user": StatsServices.from_user
}

export_map = {"json": JSON, "csv": CSV}
assert choice in set(choice_map), f"export for {choice} is not implemented"

stats, _ = choice_map[choice](project_id)
file = export_map[file_type](stats, choice)
Expand Down
2 changes: 2 additions & 0 deletions backend-app/file/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from file.file_tests.models_test import FileModelTest
from file.file_tests.permissions_test import FilePermissionTest
from file.file_tests.serializers_test import FileSerializerTest, FilesSerializerTest
from file.file_tests.export_test import ExportTest
from file.file_tests.services_test import (
ViewServicesTest,
AnnotationTest,
StatsServiceTest,
FileUploaderTest,
ExportServicesTest
)
from file.file_tests.views_test import (
FileViewSetTest,
Expand Down
1 change: 1 addition & 0 deletions backend-app/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ pycparser==2.21
python-jose==3.3.0
rsa==4.9
six==1.16.0
XlsxWriter==3.2.0
3 changes: 1 addition & 2 deletions frontend-app/src/components/common/FileStats/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import "./styles.css";
const STAT_TYPES = ["attribute", "user"];

/** @type {string[]} */
const EXPORT_VARIANTS = ["csv", "json", "xls"];
const EXPORT_VARIANTS = ["csv", "json", "xlsx"];

/**
* @param {{ image?: number, video?: number }} [a]
Expand Down Expand Up @@ -141,7 +141,6 @@ export default function FileStats({ pathID }) {
type="button"
key={type}
className="iss__stats__exportButton"
style={type === "xls" ? {opacity: 0.3, pointerEvents: "none"} : undefined}
onClick={() => exportStats(type)}
>{type}</button>
))
Expand Down

0 comments on commit e6c63c8

Please sign in to comment.