Skip to content

Commit 332bc41

Browse files
committed
Merge branch 'release/v0.3'
2 parents 2f8a305 + 73761b0 commit 332bc41

File tree

10 files changed

+103
-34
lines changed

10 files changed

+103
-34
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [0.3.0] - 2020-11-23
5+
6+
- Ad Sanity checks against incorrect entries in columns or date_field
7+
- Add support to create ReportField on the fly in all report types
8+
- Enhance exception verbosity.
9+
- Removed `doc_date` field reference .
10+
411
## [0.2.9] - 2020-10-22
512
### Updated
613
- Fixed an issue getting a db field verbose column name

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ You can use ``SampleReportView`` *which is an enhanced subclass of ``django.view
7474
# columns = ['title', '__total_quantity__', '__total__']
7575
7676
# in your urls.py
77-
path('url-to-report', TotalProductSales.as_view())
77+
path('path-to-report', TotalProductSales.as_view())
7878
7979
This will return a page, with a table looking like
8080

@@ -213,4 +213,4 @@ If you like this package, chances are you may like those packages too!
213213

214214
`Django Ra ERP Framework <https://github.com/ra-systems/RA>`_ A framework to build business solutions with ease.
215215

216-
If you find this project useful or proimosing , You can support us by a github ⭐
216+
If you find this project useful or promising , You can support us by a github ⭐

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
master_doc = 'index'
2525

2626
# The full version, including alpha/beta/rc tags
27-
release = '0.2.7'
27+
release = '0.3.0'
2828

2929
# -- General configuration ---------------------------------------------------
3030

slick_reporting/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
default_app_config = 'slick_reporting.apps.ReportAppConfig'
33

4-
VERSION = (0, 2, 9)
4+
VERSION = (0, 3, 0)
55

6-
__version__ = '0.2.9'
6+
__version__ = '0.3.0'

slick_reporting/fields.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ class BaseReportField(object):
4747
_debit_and_credit = True
4848

4949
@classmethod
50-
def create(cls, method, field, name=None, verbose_name=None):
50+
def create(cls, method, field, name=None, verbose_name=None, is_summable=True):
5151
"""
5252
Creates a ReportField class on the fly
53-
:param method:
54-
:param field:
55-
:param name:
56-
:param verbose_name:
53+
:param method: The computation Method to be used
54+
:param field: The field on which the computation would occur
55+
:param name: a name to refer to this field else where
56+
:param verbose_name: Verbose name
57+
:param is_summable:
5758
:return:
5859
"""
5960
if not name:
@@ -66,10 +67,10 @@ def create(cls, method, field, name=None, verbose_name=None):
6667
'name': name,
6768
'verbose_name': verbose_name,
6869
'calculation_field': field,
69-
'calculation_method': method
70+
'calculation_method': method,
71+
'is_summable': is_summable,
7072
})
71-
cls._field_registry.register(report_klass)
72-
return name
73+
return report_klass
7374

7475
def __init__(self, plus_side_q=None, minus_side_q=None,
7576
report_model=None,

slick_reporting/generator.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.core.exceptions import ImproperlyConfigured, FieldDoesNotExist
88
from django.db.models import Q
99

10+
from .fields import BaseReportField
1011
from .helpers import get_field_from_query_text
1112
from .registry import field_registry
1213

@@ -367,17 +368,32 @@ def get_report_data(self):
367368
data = [get_record_data(obj, all_columns) for obj in main_queryset]
368369
return data
369370

370-
def _parse(self):
371+
@classmethod
372+
def check_columns(cls, columns, group_by, report_model, ):
373+
"""
374+
Check and parse the columns, throw errors in case an item in the columns cant not identified
375+
:param columns: List of columns
376+
:param group_by: group by field if any
377+
:param report_model: the report model
378+
:return: List of dict, each dict contains relevant data to the respective field in `columns`
379+
"""
380+
group_by_model = None
381+
if group_by:
382+
group_by_field = [x for x in report_model._meta.fields if x.name == group_by][0]
383+
group_by_model = group_by_field.related_model
371384

372-
if self.group_by:
373-
self.group_by_field = [x for x in self.report_model._meta.fields if x.name == self.group_by][0]
374-
self.group_by_model = self.group_by_field.related_model
385+
parsed_columns = []
386+
for col in columns:
387+
magic_field_class = None
388+
attr = None
389+
390+
if type(col) is str:
391+
attr = getattr(cls, col, None)
392+
elif issubclass(col, BaseReportField):
393+
magic_field_class = col
375394

376-
self.parsed_columns = []
377-
for col in self.columns:
378-
attr = getattr(self, col, None)
379395
try:
380-
magic_field_class = field_registry.get_field_by_name(col)
396+
magic_field_class = magic_field_class or field_registry.get_field_by_name(col)
381397
except KeyError:
382398
magic_field_class = None
383399

@@ -395,7 +411,7 @@ def _parse(self):
395411
# These are placeholder not real computation field
396412
continue
397413

398-
col_data = {'name': col,
414+
col_data = {'name': magic_field_class.name,
399415
'verbose_name': magic_field_class.verbose_name,
400416
'source': 'magic_field',
401417
'ref': magic_field_class,
@@ -404,7 +420,7 @@ def _parse(self):
404420
}
405421
else:
406422
# A database field
407-
model_to_use = self.group_by_model if self.group_by else self.report_model
423+
model_to_use = group_by_model if group_by else report_model
408424
try:
409425
if '__' in col:
410426
# A traversing link order__client__email
@@ -413,19 +429,23 @@ def _parse(self):
413429
field = model_to_use._meta.get_field(col)
414430
except FieldDoesNotExist:
415431
raise FieldDoesNotExist(
416-
f'Field "{col}" not found as an attribute to the generator class, nor as computation field, nor as a database column for the model "{model_to_use._meta.model_name}"')
432+
f'Field "{col}" not found either as an attribute to the generator class {cls}, '
433+
f'or a computation field, or a database column for the model "{model_to_use}"')
417434

418435
col_data = {'name': col,
419436
'verbose_name': getattr(field, 'verbose_name', col),
420437
'source': 'database',
421438
'ref': field,
422439
'type': field.get_internal_type()
423440
}
424-
self.parsed_columns.append(col_data)
441+
parsed_columns.append(col_data)
442+
return parsed_columns
425443

426-
self._parsed_columns = list(self.parsed_columns)
427-
self._time_series_parsed_columns = self.get_time_series_parsed_columns()
428-
self._crosstab_parsed_columns = self.get_crosstab_parsed_columns()
444+
def _parse(self):
445+
self.parsed_columns = self.check_columns(self.columns, self.group_by, self.report_model)
446+
self._parsed_columns = list(self.parsed_columns)
447+
self._time_series_parsed_columns = self.get_time_series_parsed_columns()
448+
self._crosstab_parsed_columns = self.get_crosstab_parsed_columns()
429449

430450
def get_database_columns(self):
431451
return [col['name'] for col in self.parsed_columns if col['source'] == 'database']
@@ -467,7 +487,13 @@ def get_time_series_parsed_columns(self):
467487

468488
for dt in series:
469489
for col in cols:
470-
magic_field_class = field_registry.get_field_by_name(col)
490+
magic_field_class = None
491+
492+
if type(col) is str:
493+
magic_field_class = field_registry.get_field_by_name(col)
494+
elif issubclass(col, BaseReportField):
495+
magic_field_class = col
496+
471497
_values.append({
472498
'name': col + 'TS' + dt[1].strftime('%Y%m%d'),
473499
'original_name': col,
@@ -547,7 +573,12 @@ def get_crosstab_parsed_columns(self):
547573
ids_length = len(ids) - 1
548574
for counter, id in enumerate(ids):
549575
for col in report_columns:
550-
magic_field_class = field_registry.get_field_by_name(col)
576+
magic_field_class = None
577+
if type(col) is str:
578+
magic_field_class = field_registry.get_field_by_name(col)
579+
elif issubclass(col, BaseReportField):
580+
magic_field_class = col
581+
551582
output_cols.append({
552583
'name': f'{col}CT{id}',
553584
'original_name': col,

slick_reporting/helpers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ def get_foreign_keys(model):
2929

3030

3131
def get_field_from_query_text(path, model):
32+
"""
33+
return the field of a query text
34+
`modelA__modelB__foo_field` would return foo_field on modelsB
35+
:param path:
36+
:param model:
37+
:return:
38+
"""
3239
relations = path.split('__')
3340
_rel = model
3441
field = None

slick_reporting/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def register(self, report_field, override=False):
1616
:return: report_field passed
1717
"""
1818
if report_field.name in self._registry and not override:
19-
raise AlreadyRegistered('This field is already registered')
19+
raise AlreadyRegistered(f'The field name {report_field.name} is used before and `override` is False')
2020

2121
self._registry[report_field.name] = report_field
2222
return report_field

slick_reporting/views.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class SampleReportView(FormView):
1919
time_series_pattern = ''
2020
time_series_columns = None
2121

22-
date_field = 'doc_date'
22+
date_field = None
2323

2424
swap_sign = False
2525

@@ -222,3 +222,14 @@ def get_initial(self):
222222
'start_date': SLICK_REPORTING_DEFAULT_START_DATE,
223223
'end_date': SLICK_REPORTING_DEFAULT_END_DATE
224224
}
225+
226+
def __init_subclass__(cls) -> None:
227+
date_field = getattr(cls, 'date_field', '')
228+
if not date_field:
229+
raise TypeError(f'`date_field` is not set on {cls}')
230+
231+
# sanity check, raises error if the columns or date fields is not mapped
232+
cls.report_generator_class.check_columns([cls.date_field], False, cls.report_model)
233+
cls.report_generator_class.check_columns(cls.columns, cls.group_by, cls.report_model)
234+
235+
super().__init_subclass__()

tests/tests.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from tests.report_generators import ClientTotalBalance
1313
from .models import Client, Product, SimpleSales, OrderLine
1414
from slick_reporting.registry import field_registry
15+
from .views import SampleReportView
1516

1617
User = get_user_model()
1718
SUPER_LOGIN = dict(username='superlogin', password='password')
@@ -253,6 +254,17 @@ def test_chart_settings(self):
253254
self.assertTrue('pie' in data['chart_settings'][0]['id'])
254255
self.assertTrue(data['chart_settings'][0]['title'], 'awesome report title')
255256

257+
def _test_column_names_are_always_strings(self):
258+
# todo
259+
pass
260+
261+
def test_error_on_missing_date_field(self):
262+
def test_function():
263+
class TotalClientSales(SampleReportView):
264+
report_model = SimpleSales
265+
266+
self.assertRaises(TypeError, test_function)
267+
256268

257269
class TestReportFieldRegistry(TestCase):
258270
def test_unregister(self):
@@ -302,9 +314,9 @@ def register():
302314
def test_creating_a_report_field_on_the_fly(self):
303315
from django.db.models import Sum
304316
name = BaseReportField.create(Sum, 'value', '__sum_of_value__')
305-
self.assertIn(name, field_registry.get_all_report_fields_names())
317+
self.assertNotIn(name, field_registry.get_all_report_fields_names())
306318

307319
def test_creating_a_report_field_on_the_fly_wo_name(self):
308320
from django.db.models import Sum
309321
name = BaseReportField.create(Sum, 'value')
310-
self.assertIn(name, field_registry.get_all_report_fields_names())
322+
self.assertNotIn(name, field_registry.get_all_report_fields_names())

0 commit comments

Comments
 (0)