Skip to content

Commit

Permalink
Validate XLS rows count before exporting.
Browse files Browse the repository at this point in the history
  • Loading branch information
hsmett committed Jul 28, 2023
1 parent 3086712 commit 1956d32
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# A visitor mode has been added; from any list-view, you can visit all the related detail-views the one after the other.
(button "Enter the exploration mode" in list-views).
# The link in the list-views selectors are now opened in other tabs automatically.
# The .XLS file export now displays an error when its maximum number of rows is exceeded.
# In forms :
- The custom fields with ENUM/MULTI_ENUM type are now using the Select2 combobox with lazy loading & search.
- In the form for entity-filters :
Expand Down
3 changes: 3 additions & 0 deletions creme/creme_core/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ def save(self, filename: str, user):
instance of <django.contrib.auth.get_user_model()>.
"""
raise NotImplementedError

def validate(self, *, total_count=None):
pass
12 changes: 12 additions & 0 deletions creme/creme_core/backends/xls_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
from django.conf import settings
from django.http import HttpResponseRedirect
from django.template.defaultfilters import slugify
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _

from ..core.exceptions import ConflictError
from ..models import FileRef
from ..utils.file_handling import FileCreator
from ..utils.xlwt_utils import XlwtWriter
Expand All @@ -34,6 +36,7 @@ class XLSExportBackend(ExportBackend):
verbose_name = _('XLS File')
help_text = ''
dir_parts = ('xls',) # Sub-directory under settings.MEDIA_ROOT
max_row_index = 65535

def __init__(self, encoding='utf-8'):
super().__init__()
Expand All @@ -56,3 +59,12 @@ def save(self, filename, user):

def writerow(self, row):
self.writer.writerow(row)

def validate(self, *, total_count=None):
if total_count is not None and total_count > self.max_row_index:
raise ConflictError(
gettext(
"The Microsoft Excel 97–2003 Workbook (.xls) format has a limit of"
" {count} rows. Use a CSV format instead.".format(count=self.max_row_index)
)
)
7 changes: 7 additions & 0 deletions creme/creme_core/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ msgstr ""
msgid "XLS File"
msgstr "Fichier XLS"

#, python-brace-format
msgid ""
"The Microsoft Excel 97–2003 Workbook (.xls) format has a limit of {count} "
"rows. Use a CSV format instead."
msgstr "Le format Microsoft Excel 97–2003 Workbook (.xls) est limité à {count} lignes. "
"Utilisez un format CSV à la place."

msgid ""
"XLS is a file extension for a spreadsheet file format created by Microsoft "
"for use with Microsoft Excel (Excel 97-2003 Workbook)."
Expand Down
13 changes: 13 additions & 0 deletions creme/creme_core/tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from creme.creme_core.backends.csv_import import CSVImportBackend
from creme.creme_core.backends.xls_import import XLSImportBackend

from ..backends.xls_export import XLSExportBackend
from ..core.exceptions import ConflictError
from .base import CremeTestCase


Expand Down Expand Up @@ -56,3 +58,14 @@ def test_registration_errors03(self):

with self.assertRaises(registry.InvalidClass):
registry.get_backend_class(CSVImportBackend.id)


class XLSExportBackendTestCase(CremeTestCase):
def test_validate_total_count__lte_max(self):
backend = XLSExportBackend()
backend.validate(total_count=XLSExportBackend.max_row_index)

def test_validate_total_count__gt_max(self):
backend = XLSExportBackend()
with self.assertRaises(ConflictError):
backend.validate(total_count=XLSExportBackend.max_row_index + 1)
33 changes: 33 additions & 0 deletions creme/creme_core/tests/views/test_mass_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from django.utils.translation import gettext as _
from django.utils.translation import pgettext

from creme.creme_core.backends import _BackendRegistry
from creme.creme_core.backends.base import ExportBackend
from creme.creme_core.core.entity_cell import (
EntityCellFunctionField,
EntityCellRegularField,
Expand All @@ -23,6 +25,7 @@
RegularFieldConditionHandler,
)
from creme.creme_core.core.entity_filter.operators import ISTARTSWITH
from creme.creme_core.core.exceptions import ConflictError
from creme.creme_core.gui.history import html_history_registry
from creme.creme_core.models import (
CremeProperty,
Expand All @@ -45,16 +48,41 @@
from creme.creme_core.utils.content_type import as_ctype
from creme.creme_core.utils.queries import QSerializer
from creme.creme_core.utils.xlrd_utils import XlrdReader
from creme.creme_core.views.mass_export import MassExport

from .base import ViewsTestCase


class TestExportBackend(ExportBackend):
id: str = 'test_backend_validator'
verbose_name: str = 'test_backend_validator'
help_text: str = 'test_backend_validator'

def writerow(self, row):
pass

def validate(self, **kwargs):
raise ConflictError("TestExportBackend.validate fail")


class MassExportViewsTestCase(ViewsTestCase):
backup_mass_export_backend_registry = None

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ct = ContentType.objects.get_for_model(FakeContact)

cls.backup_mass_export_backend_registry = MassExport.backend_registry
MassExport.backend_registry = _BackendRegistry(ExportBackend, [
*settings.EXPORT_BACKENDS,
"creme.creme_core.tests.views.test_mass_export.TestExportBackend"
])

@classmethod
def tearDownClass(cls):
MassExport.backend_registry = cls.backup_mass_export_backend_registry

# def _build_hf_n_contacts(self, user=None):
def _build_hf_n_contacts(self, user):
# if user is None:
Expand Down Expand Up @@ -529,6 +557,11 @@ def test_extra_filter(self):
# Error
self.assertGET(400, self._build_contact_dl_url(extra_q='[123]'))

def test_backend_validator(self):
self.login_as_root()
response = self.assertGET409(self._build_contact_dl_url(doc_type=TestExportBackend.id))
self.assertContains(response, 'TestExportBackend.validate fail', status_code=409)

def test_list_view_export_with_filter01(self):
# user = self.login()
user = self.login_as_root_and_get()
Expand Down
6 changes: 5 additions & 1 deletion creme/creme_core/views/mass_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class MassExport(base.EntityCTypeRelatedMixin, base.CheckedView):
search_field_registry = search_field_registry
search_form_class = ListViewSearchForm

backend_registry = export_backend_registry

def check_related_ctype(self, ctype):
super().check_related_ctype(ctype=ctype)

Expand All @@ -69,7 +71,7 @@ def check_related_ctype(self, ctype):
def get_backend_class(self):
doc_type = get_from_GET_or_404(self.request.GET, self.doc_type_arg)

backend_class = export_backend_registry.get_backend_class(doc_type)
backend_class = self.backend_registry.get_backend_class(doc_type)
if backend_class is None:
raise Http404(f'No such exporter for extension "{doc_type}"')

Expand Down Expand Up @@ -227,6 +229,8 @@ def get(self, request, *args, **kwargs):
if use_distinct:
entities_qs = entities_qs.distinct()

writer.validate(total_count=entities_qs.count())

paginator = self.get_paginator(queryset=entities_qs, ordering=ordering)

total_count = 0
Expand Down

0 comments on commit 1956d32

Please sign in to comment.