Skip to content

Commit

Permalink
Merge branch 'release/0.6.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
Ramez Ashraf committed Jan 24, 2023
2 parents cb9cca0 + b9e925f commit 64e95ba
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 51 deletions.
8 changes: 3 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
language: python

python:
- "3.7"
- "3.8"
- "3.9"

env:
- DJANGO=django==2.2.20
- DJANGO=django==3.1.8
- DJANGO=django==3.2
- DJANGO=django==4.0
- DJANGO=django==3.2.15
- DJANGO=django==4.0.7
- DJANGO=django==4.1.1

matrix:
exclude:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file.

## [0.6.4]
- Fix highchart cache to target the specific chart
- Added initial and required to report_form_factory
- Added base_q_filters and base_kwargs_filters to SlickReportField to control the base queryset
- Add ability to customize ReportField on the fly
- Adds `prevent_group_by` option to SlickReportField Will prevent group by calculation for this specific field, serves when you want to compute overall results.
- Support reference to SlickReportField class directly in `requires` instead of its "registered" name.
- Adds PercentageToBalance report field

## [0.6.3]

- Change the deprecated in Django 4 `request.is_ajax` .
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
sphinx==4.2.0
sphinx_rtd_theme==1.0.0
readthedocs-sphinx-search==0.1.1
django-slick-reporting==0.6.3
django-slick-reporting==0.6.4
4 changes: 2 additions & 2 deletions docs/source/concept.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
Structure
==========

If you haven't, please check https://django-slick-reporting.com for a quick walk-though with live code examples..
If you haven't yet, please check https://django-slick-reporting.com for a quick walk-though with live code examples..

And now, Let's explore the main components of Django Slick Reporting and what setting you can set on project level.

Components
----------


1. Result Field: represent a number, a calculation unit, for example: a Sum of a certain field.
1. Report Field: represent a number, a calculation unit, for example: a Sum of a certain field.
The report field identifies how the calculation should be done. ResultFields can depend on each other.

2. Generator: Represent a concrete report structure.If it would group by certain field, do a time series or a cross tab, and which columns (Report Field) to be calculated.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
master_doc = 'index'

# The full version, including alpha/beta/rc tags
release = '0.6.3'
release = '0.6.4'

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

Expand Down
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
django>=2.2
python-dateutil>=2.8.1
pytz
simplejson
django-crispy-forms
4 changes: 2 additions & 2 deletions slick_reporting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
default_app_config = 'slick_reporting.apps.ReportAppConfig'

VERSION = (0, 6, 3)
VERSION = (0, 6, 4)

__version__ = '0.6.3'
__version__ = '0.6.4'
35 changes: 30 additions & 5 deletions slick_reporting/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ class SlickReportField(object):
plus_side_q = None
minus_side_q = None

base_kwargs_filters = None
base_q_filters = None

_require_classes = None
_debit_and_credit = True

prevent_group_by = False
"""Will prevent group by calculation for this specific field, serves when you want to compute overall results"""

@classmethod
def create(cls, method, field, name=None, verbose_name=None, is_summable=True):
"""
Expand Down Expand Up @@ -86,15 +92,15 @@ def __init__(self, plus_side_q=None, minus_side_q=None,
self.requires = self.requires or []
self.group_by = self.group_by or group_by
self._cache = None, None, None
self._require_classes = [field_registry.get_field_by_name(x) for x in self.requires]
self._require_classes = self._get_required_classes()

if not self.plus_side_q and not self.minus_side_q:
self._debit_and_credit = False

@classmethod
def _get_required_classes(cls):
requires = cls.requires or []
return [field_registry.get_field_by_name(x) for x in requires]
return [field_registry.get_field_by_name(x) if type(x) is str else x for x in requires]

def apply_q_plus_filter(self, qs):
return qs.filter(*self.plus_side_q)
Expand Down Expand Up @@ -138,14 +144,15 @@ def prepare(self, q_filters=None, kwargs_filters=None, **kwargs):
:return:
"""
queryset = self.get_queryset()
group_by = '' if self.prevent_group_by else self.group_by
if q_filters:
queryset = queryset.filter(*q_filters)
if kwargs_filters:
queryset = queryset.filter(**kwargs_filters)

if self.plus_side_q:
queryset = self.apply_q_plus_filter(queryset)
debit_results = self.apply_aggregation(queryset, self.group_by)
debit_results = self.apply_aggregation(queryset, group_by)

credit_results = None
if self._debit_and_credit:
Expand All @@ -157,12 +164,16 @@ def prepare(self, q_filters=None, kwargs_filters=None, **kwargs):
if self.minus_side_q:
queryset = self.apply_q_minus_filter(queryset)

credit_results = self.apply_aggregation(queryset, self.group_by)
credit_results = self.apply_aggregation(queryset, group_by)

return debit_results, credit_results

def get_queryset(self):
queryset = self.report_model.objects
if self.base_q_filters:
queryset = queryset.filter(*self.base_q_filters)
if self.base_kwargs_filters:
queryset = queryset.filter(**self.base_kwargs_filters)
return queryset.order_by()

def get_annotation_name(self):
Expand Down Expand Up @@ -221,7 +232,7 @@ def _resolve_dependencies(self, current_obj):
return dep_results

def extract_data(self, cached, current_obj):
group_by = self.group_by
group_by = '' if self.prevent_group_by else self.group_by
debit_value = 0
credit_value = 0
annotation = self.get_annotation_name()
Expand Down Expand Up @@ -361,6 +372,19 @@ def final_calculation(self, debit, credit, dep_dict):
field_registry.register(BalanceReportField)


class PercentageToBalance(SlickReportField):
requires = [BalanceReportField]
name = 'PercentageToBalance'
verbose_name = _('%')

prevent_group_by = True

def final_calculation(self, debit, credit, dep_dict):
obj_balance = dep_dict.get('__balance__')
total = debit - credit
return (obj_balance/total) * 100


class CreditReportField(SlickReportField):
name = '__credit__'
verbose_name = _('Credit')
Expand Down Expand Up @@ -408,6 +432,7 @@ class BalanceQTYReportField(SlickReportField):
verbose_name = _('Cumulative QTY')
calculation_field = 'quantity'
requires = ['__fb_quan__']
is_summable = False

def final_calculation(self, debit, credit, dep_dict):
# Use `get` so it fails loud if its not there
Expand Down
18 changes: 13 additions & 5 deletions slick_reporting/form_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _default_foreign_key_widget(f_field):


def report_form_factory(model, crosstab_model=None, display_compute_reminder=True, fkeys_filter_func=None,
foreign_key_widget_func=None, excluded_fields=None):
foreign_key_widget_func=None, excluded_fields=None, initial=None, required=None):
"""
Create a Report Form based on the report_model passed by
1. adding a start_date and end_date fields
Expand All @@ -110,12 +110,16 @@ def report_form_factory(model, crosstab_model=None, display_compute_reminder=Tru
:param fkeys_filter_func: a receives an OrderedDict of Foreign Keys names and their model field instances found on the model, return the OrderedDict that would be used
:param foreign_key_widget_func: receives a Field class return the used widget like this {'form_class': forms.ModelMultipleChoiceField, 'required': False, }
:param excluded_fields: a list of fields to be excluded from the report form
:param initial a dict for fields initial
:param required a list of fields that should be marked as required
:return:
"""
foreign_key_widget_func = foreign_key_widget_func or _default_foreign_key_widget
fkeys_filter_func = fkeys_filter_func or (lambda x: x)

# gather foreign keys
initial = initial or {}
required = required or []
fkeys_map = get_foreign_keys(model)
excluded_fields = excluded_fields or []
for excluded in excluded_fields:
Expand All @@ -127,17 +131,21 @@ def report_form_factory(model, crosstab_model=None, display_compute_reminder=Tru
fields = OrderedDict()

fields['start_date'] = forms.DateTimeField(required=False, label=_('From date'),
initial=app_settings.SLICK_REPORTING_DEFAULT_START_DATE,
initial=initial.get('start_date',
app_settings.SLICK_REPORTING_DEFAULT_START_DATE),
widget=forms.DateTimeInput(attrs={'autocomplete': "off"}))

fields['end_date'] = forms.DateTimeField(required=False, label=_('To date'),
initial=app_settings.SLICK_REPORTING_DEFAULT_END_DATE,
initial=initial.get('end_date',
app_settings.SLICK_REPORTING_DEFAULT_END_DATE),
widget=forms.DateTimeInput(attrs={'autocomplete': "off"}))

for name, f_field in fkeys_map.items():
fkeys_list.append(name)

fields[name] = f_field.formfield(**foreign_key_widget_func(f_field))
field_attrs = foreign_key_widget_func(f_field)
if name in required:
field_attrs['required'] = True
fields[name] = f_field.formfield(**field_attrs)

if crosstab_model and display_compute_reminder:
fields['crosstab_compute_reminder'] = forms.BooleanField(required=False,
Expand Down
33 changes: 22 additions & 11 deletions slick_reporting/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import datetime
import logging
from inspect import isclass

from django.core.exceptions import ImproperlyConfigured, FieldDoesNotExist
from django.db.models import Q, ForeignKey
from inspect import isclass

from .app_settings import SLICK_REPORTING_DEFAULT_CHARTS_ENGINE
from .fields import SlickReportField
Expand Down Expand Up @@ -57,6 +56,11 @@ class ReportGenerator(object):
Example:
columns = ['product_id', '__time_series__', 'col_b']
Same is true with __crosstab__
You can customize aspects of the column by adding it as a tuple like this
('field_name', dict(verbose_name=_('My Enhanced Verbose_name'))
"""

time_series_pattern = ''
Expand Down Expand Up @@ -249,7 +253,8 @@ def __init__(self, report_model=None, main_queryset=None, start_date=None, end_d
concrete_fields = [f.name for f in self.group_by_field.related_model._meta.concrete_fields]
# add database columns that are not already in concrete_fields
final_fields = concrete_fields + list(set(self.get_database_columns()) - set(concrete_fields))
self.main_queryset = self.group_by_field.related_model.objects.filter(pk__in=ids).values(*final_fields)
self.main_queryset = self.group_by_field.related_model.objects.filter(pk__in=ids).values(
*final_fields)
else:
self.main_queryset = self.main_queryset.distinct().values(self.group_by_field_attname)
else:
Expand Down Expand Up @@ -411,23 +416,30 @@ def check_columns(cls, columns, group_by, report_model, ):
"""
group_by_model = None
if group_by:
group_by_field = [x for x in report_model._meta.get_fields() if x.name == group_by.split('__')[0]][0]
try:
group_by_field = [x for x in report_model._meta.get_fields() if x.name == group_by.split('__')[0]][0]
except IndexError:
raise ImproperlyConfigured(f"Could not find {group_by} in {report_model}")
if group_by_field.is_relation:
group_by_model = group_by_field.related_model
else:
group_by_model = report_model

parsed_columns = []
for col in columns:
options = {}
if type(col) is tuple:
col, options = col

if col in ['__time_series__', '__crosstab__']:
# These are placeholder not real computation field
continue

magic_field_class = None
attr = None
attribute_field = None

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

Expand All @@ -436,12 +448,11 @@ def check_columns(cls, columns, group_by, report_model, ):
except KeyError:
magic_field_class = None

if attr:
# todo Add testing here
if attribute_field:
col_data = {'name': col,
'verbose_name': getattr(attr, 'verbose_name', col),
'verbose_name': getattr(attribute_field, 'verbose_name', col),
# 'type': 'method',
'ref': attr,
'ref': attribute_field,
'type': 'text'
}
elif magic_field_class:
Expand Down Expand Up @@ -473,6 +484,7 @@ def check_columns(cls, columns, group_by, report_model, ):
'ref': field,
'type': field.get_internal_type()
}
col_data.update(options)
parsed_columns.append(col_data)
return parsed_columns

Expand Down Expand Up @@ -704,4 +716,3 @@ def get_chart_settings(self, chart_settings=None, default_chart_title=None):
x['engine_name'] = x.get('engine_name', SLICK_REPORTING_DEFAULT_CHARTS_ENGINE)
output.append(x)
return output

10 changes: 5 additions & 5 deletions slick_reporting/static/slick_reporting/ra.highchart.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,19 +441,19 @@
function displayChart(data, $elem, chart_id) {
chart_id = chart_id || $elem.attr('data-report-default-chart') || '';
let chart = $elem;
let chartObject = getObjFromArray(data.chart_settings, 'id', chart_id, true);

// let chartObject = getObjFromArray(data.chart_settings, 'id', chart_id, true);
let cache_key = data.report_slug + ':' + chart_id
try {
let existing_chart = _chart_cache[data.report_slug];
let existing_chart = _chart_cache[cache_key];
if (typeof (existing_chart) !== 'undefined') {
existing_chart.highcharts().destroy()
}
} catch (e) {
console.error(e)
}

chartObject = $.slick_reporting.highcharts.createChartObject(data, chartObject);
_chart_cache[data.report_slug] = chart.highcharts(chartObject);
chartObject = $.slick_reporting.highcharts.createChartObject(data, chart_id);
_chart_cache[cache_key] = chart.highcharts(chartObject);

}

Expand Down
13 changes: 11 additions & 2 deletions slick_reporting/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ def get_form_class(self):
"""
return self.form_class or report_form_factory(self.get_report_model(), crosstab_model=self.crosstab_model,
display_compute_reminder=self.crosstab_compute_reminder,
excluded_fields=self.excluded_fields)
excluded_fields=self.excluded_fields,
initial=self.get_form_initial(),
# required=self.required_fields
)

def get_form_kwargs(self):
"""
Expand Down Expand Up @@ -209,7 +212,8 @@ def filter_results(self, data, for_print=False):
def get_report_slug(cls):
return cls.__name__.lower()

def get_initial(self):
@staticmethod
def get_form_initial():
# todo revise why not actually displaying datetime on screen
return {
'start_date': SLICK_REPORTING_DEFAULT_START_DATE,
Expand All @@ -236,3 +240,8 @@ def __init_subclass__(cls) -> None:
cls.report_generator_class.check_columns(cls.columns, cls.group_by, cls.get_report_model())

super().__init_subclass__()

@staticmethod
def check_chart_settings(chart_settings=None):
#todo check on chart settings
return
Loading

0 comments on commit 64e95ba

Please sign in to comment.