Skip to content

Commit

Permalink
Merge branch 'release/v0.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
RamezIssac committed Nov 22, 2020
2 parents 2f8a305 + 73761b0 commit 332bc41
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 34 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Changelog
All notable changes to this project will be documented in this file.

## [0.3.0] - 2020-11-23

- Ad Sanity checks against incorrect entries in columns or date_field
- Add support to create ReportField on the fly in all report types
- Enhance exception verbosity.
- Removed `doc_date` field reference .

## [0.2.9] - 2020-10-22
### Updated
- Fixed an issue getting a db field verbose column name
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ You can use ``SampleReportView`` *which is an enhanced subclass of ``django.view
# columns = ['title', '__total_quantity__', '__total__']
# in your urls.py
path('url-to-report', TotalProductSales.as_view())
path('path-to-report', TotalProductSales.as_view())
This will return a page, with a table looking like

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

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

If you find this project useful or proimosing , You can support us by a github ⭐
If you find this project useful or promising , You can support us by a github ⭐
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
master_doc = 'index'

# The full version, including alpha/beta/rc tags
release = '0.2.7'
release = '0.3.0'

# -- General configuration ---------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions slick_reporting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

default_app_config = 'slick_reporting.apps.ReportAppConfig'

VERSION = (0, 2, 9)
VERSION = (0, 3, 0)

__version__ = '0.2.9'
__version__ = '0.3.0'
17 changes: 9 additions & 8 deletions slick_reporting/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ class BaseReportField(object):
_debit_and_credit = True

@classmethod
def create(cls, method, field, name=None, verbose_name=None):
def create(cls, method, field, name=None, verbose_name=None, is_summable=True):
"""
Creates a ReportField class on the fly
:param method:
:param field:
:param name:
:param verbose_name:
:param method: The computation Method to be used
:param field: The field on which the computation would occur
:param name: a name to refer to this field else where
:param verbose_name: Verbose name
:param is_summable:
:return:
"""
if not name:
Expand All @@ -66,10 +67,10 @@ def create(cls, method, field, name=None, verbose_name=None):
'name': name,
'verbose_name': verbose_name,
'calculation_field': field,
'calculation_method': method
'calculation_method': method,
'is_summable': is_summable,
})
cls._field_registry.register(report_klass)
return name
return report_klass

def __init__(self, plus_side_q=None, minus_side_q=None,
report_model=None,
Expand Down
65 changes: 48 additions & 17 deletions slick_reporting/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.core.exceptions import ImproperlyConfigured, FieldDoesNotExist
from django.db.models import Q

from .fields import BaseReportField
from .helpers import get_field_from_query_text
from .registry import field_registry

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

def _parse(self):
@classmethod
def check_columns(cls, columns, group_by, report_model, ):
"""
Check and parse the columns, throw errors in case an item in the columns cant not identified
:param columns: List of columns
:param group_by: group by field if any
:param report_model: the report model
:return: List of dict, each dict contains relevant data to the respective field in `columns`
"""
group_by_model = None
if group_by:
group_by_field = [x for x in report_model._meta.fields if x.name == group_by][0]
group_by_model = group_by_field.related_model

if self.group_by:
self.group_by_field = [x for x in self.report_model._meta.fields if x.name == self.group_by][0]
self.group_by_model = self.group_by_field.related_model
parsed_columns = []
for col in columns:
magic_field_class = None
attr = None

if type(col) is str:
attr = getattr(cls, col, None)
elif issubclass(col, BaseReportField):
magic_field_class = col

self.parsed_columns = []
for col in self.columns:
attr = getattr(self, col, None)
try:
magic_field_class = field_registry.get_field_by_name(col)
magic_field_class = magic_field_class or field_registry.get_field_by_name(col)
except KeyError:
magic_field_class = None

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

col_data = {'name': col,
col_data = {'name': magic_field_class.name,
'verbose_name': magic_field_class.verbose_name,
'source': 'magic_field',
'ref': magic_field_class,
Expand All @@ -404,7 +420,7 @@ def _parse(self):
}
else:
# A database field
model_to_use = self.group_by_model if self.group_by else self.report_model
model_to_use = group_by_model if group_by else report_model
try:
if '__' in col:
# A traversing link order__client__email
Expand All @@ -413,19 +429,23 @@ def _parse(self):
field = model_to_use._meta.get_field(col)
except FieldDoesNotExist:
raise FieldDoesNotExist(
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}"')
f'Field "{col}" not found either as an attribute to the generator class {cls}, '
f'or a computation field, or a database column for the model "{model_to_use}"')

col_data = {'name': col,
'verbose_name': getattr(field, 'verbose_name', col),
'source': 'database',
'ref': field,
'type': field.get_internal_type()
}
self.parsed_columns.append(col_data)
parsed_columns.append(col_data)
return parsed_columns

self._parsed_columns = list(self.parsed_columns)
self._time_series_parsed_columns = self.get_time_series_parsed_columns()
self._crosstab_parsed_columns = self.get_crosstab_parsed_columns()
def _parse(self):
self.parsed_columns = self.check_columns(self.columns, self.group_by, self.report_model)
self._parsed_columns = list(self.parsed_columns)
self._time_series_parsed_columns = self.get_time_series_parsed_columns()
self._crosstab_parsed_columns = self.get_crosstab_parsed_columns()

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

for dt in series:
for col in cols:
magic_field_class = field_registry.get_field_by_name(col)
magic_field_class = None

if type(col) is str:
magic_field_class = field_registry.get_field_by_name(col)
elif issubclass(col, BaseReportField):
magic_field_class = col

_values.append({
'name': col + 'TS' + dt[1].strftime('%Y%m%d'),
'original_name': col,
Expand Down Expand Up @@ -547,7 +573,12 @@ def get_crosstab_parsed_columns(self):
ids_length = len(ids) - 1
for counter, id in enumerate(ids):
for col in report_columns:
magic_field_class = field_registry.get_field_by_name(col)
magic_field_class = None
if type(col) is str:
magic_field_class = field_registry.get_field_by_name(col)
elif issubclass(col, BaseReportField):
magic_field_class = col

output_cols.append({
'name': f'{col}CT{id}',
'original_name': col,
Expand Down
7 changes: 7 additions & 0 deletions slick_reporting/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def get_foreign_keys(model):


def get_field_from_query_text(path, model):
"""
return the field of a query text
`modelA__modelB__foo_field` would return foo_field on modelsB
:param path:
:param model:
:return:
"""
relations = path.split('__')
_rel = model
field = None
Expand Down
2 changes: 1 addition & 1 deletion slick_reporting/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def register(self, report_field, override=False):
:return: report_field passed
"""
if report_field.name in self._registry and not override:
raise AlreadyRegistered('This field is already registered')
raise AlreadyRegistered(f'The field name {report_field.name} is used before and `override` is False')

self._registry[report_field.name] = report_field
return report_field
Expand Down
13 changes: 12 additions & 1 deletion slick_reporting/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class SampleReportView(FormView):
time_series_pattern = ''
time_series_columns = None

date_field = 'doc_date'
date_field = None

swap_sign = False

Expand Down Expand Up @@ -222,3 +222,14 @@ def get_initial(self):
'start_date': SLICK_REPORTING_DEFAULT_START_DATE,
'end_date': SLICK_REPORTING_DEFAULT_END_DATE
}

def __init_subclass__(cls) -> None:
date_field = getattr(cls, 'date_field', '')
if not date_field:
raise TypeError(f'`date_field` is not set on {cls}')

# sanity check, raises error if the columns or date fields is not mapped
cls.report_generator_class.check_columns([cls.date_field], False, cls.report_model)
cls.report_generator_class.check_columns(cls.columns, cls.group_by, cls.report_model)

super().__init_subclass__()
16 changes: 14 additions & 2 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tests.report_generators import ClientTotalBalance
from .models import Client, Product, SimpleSales, OrderLine
from slick_reporting.registry import field_registry
from .views import SampleReportView

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

def _test_column_names_are_always_strings(self):
# todo
pass

def test_error_on_missing_date_field(self):
def test_function():
class TotalClientSales(SampleReportView):
report_model = SimpleSales

self.assertRaises(TypeError, test_function)


class TestReportFieldRegistry(TestCase):
def test_unregister(self):
Expand Down Expand Up @@ -302,9 +314,9 @@ def register():
def test_creating_a_report_field_on_the_fly(self):
from django.db.models import Sum
name = BaseReportField.create(Sum, 'value', '__sum_of_value__')
self.assertIn(name, field_registry.get_all_report_fields_names())
self.assertNotIn(name, field_registry.get_all_report_fields_names())

def test_creating_a_report_field_on_the_fly_wo_name(self):
from django.db.models import Sum
name = BaseReportField.create(Sum, 'value')
self.assertIn(name, field_registry.get_all_report_fields_names())
self.assertNotIn(name, field_registry.get_all_report_fields_names())

0 comments on commit 332bc41

Please sign in to comment.