diff --git a/combo/apps/dataviz/forms.py b/combo/apps/dataviz/forms.py
index e6f29f8e..7a35c645 100644
--- a/combo/apps/dataviz/forms.py
+++ b/combo/apps/dataviz/forms.py
@@ -14,12 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import datetime
from collections import OrderedDict
from django import forms
from django.conf import settings
from django.db import transaction
from django.db.models import Q
+from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
from django.utils.translation import ugettext_lazy as _
from combo.utils import cache_during_request, requests, spooler
@@ -69,6 +71,8 @@ class ChartNgForm(forms.ModelForm):
'time_range',
'time_range_start',
'time_range_end',
+ 'time_range_start_template',
+ 'time_range_end_template',
'chart_type',
'height',
'sort_order',
@@ -85,9 +89,14 @@ class ChartNgForm(forms.ModelForm):
field_ids = list(self._meta.fields)
if not self.instance.statistic or self.instance.statistic.service_slug == 'bijoe':
- field_ids = [
- x for x in field_ids if x not in ('time_range', 'time_range_start', 'time_range_end')
- ]
+ exclude = (
+ 'time_range',
+ 'time_range_start',
+ 'time_range_end',
+ 'time_range_start_template',
+ 'time_range_end_template',
+ )
+ field_ids = [x for x in field_ids if x not in exclude]
stat_field = self.fields['statistic']
if not self.instance.statistic:
@@ -142,3 +151,18 @@ class ChartNgForm(forms.ModelForm):
for choice in self.time_intervals:
if choice[0].strip('_') not in choice_ids:
self.fields['time_interval'].choices.append(choice)
+
+ def clean(self):
+ for template_field in ('time_range_start_template', 'time_range_end_template'):
+ if not self.cleaned_data.get(template_field):
+ continue
+ context = {'now': datetime.datetime.now, 'today': datetime.datetime.now}
+ try:
+ date = Template('{{ %s|date:"Y-m-d" }}' % self.cleaned_data[template_field]).render(
+ Context(context)
+ )
+ except (VariableDoesNotExist, TemplateSyntaxError) as e:
+ self.add_error(template_field, e)
+ else:
+ if not date:
+ self.add_error(template_field, _('Template does not evaluate to a valid date.'))
diff --git a/combo/apps/dataviz/migrations/0019_auto_20211006_1525.py b/combo/apps/dataviz/migrations/0019_auto_20211006_1525.py
new file mode 100644
index 00000000..b81f4774
--- /dev/null
+++ b/combo/apps/dataviz/migrations/0019_auto_20211006_1525.py
@@ -0,0 +1,36 @@
+# Generated by Django 2.2.19 on 2021-10-06 13:25
+
+from django.db import migrations, models
+
+import combo.data.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dataviz', '0018_auto_20210723_1318'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='chartngcell',
+ name='time_range_end_template',
+ field=models.CharField(
+ blank=True,
+ max_length=200,
+ validators=[combo.data.models.django_template_validator],
+ verbose_name='To',
+ ),
+ ),
+ migrations.AddField(
+ model_name='chartngcell',
+ name='time_range_start_template',
+ field=models.CharField(
+ blank=True,
+ max_length=200,
+ validators=[combo.data.models.django_template_validator],
+ verbose_name='From',
+ help_text='Template code returning a date. For example, Monday in two weeks would be today|add_days:"14"|adjust_to_week_monday.',
+ ),
+ ),
+ ]
diff --git a/combo/apps/dataviz/models.py b/combo/apps/dataviz/models.py
index 95a8374d..81f309e1 100644
--- a/combo/apps/dataviz/models.py
+++ b/combo/apps/dataviz/models.py
@@ -25,6 +25,7 @@ from dateutil.relativedelta import MO, relativedelta
from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.db import models, transaction
+from django.template import Context, Template
from django.template.defaultfilters import date as format_date
from django.urls import reverse
from django.utils import timezone
@@ -36,7 +37,7 @@ from django.utils.translation import ungettext
from requests.exceptions import HTTPError, RequestException
from combo.data.library import register_cell_class
-from combo.data.models import CellBase
+from combo.data.models import CellBase, django_template_validator
from combo.utils import get_templated_url, requests, spooler
@@ -158,7 +159,8 @@ TIME_FILTERS = (
('previous-week', _('Previous week')),
('current-week', _('Current week')),
('next-week', _('Next week')),
- ('range', _('Free range')),
+ ('range', _('Free range (date)')),
+ ('range-template', _('Free range (template)')),
)
@@ -185,6 +187,21 @@ class ChartNgCell(CellBase):
)
time_range_start = models.DateField(_('From'), null=True, blank=True)
time_range_end = models.DateField(_('To'), null=True, blank=True)
+ time_range_start_template = models.CharField(
+ _('From'),
+ max_length=200,
+ blank=True,
+ validators=[django_template_validator],
+ help_text=_(
+ 'Template code returning a date. For example, Monday in two weeks would be today|add_days:"14"|adjust_to_week_monday.'
+ ),
+ )
+ time_range_end_template = models.CharField(
+ _('To'),
+ max_length=200,
+ blank=True,
+ validators=[django_template_validator],
+ )
chart_type = models.CharField(
_('Chart Type'),
max_length=20,
@@ -394,6 +411,16 @@ class ChartNgCell(CellBase):
params['start'] = self.time_range_start
if self.time_range_end:
params['end'] = self.time_range_end
+ elif self.time_range == 'range-template':
+ context = {'now': datetime.now, 'today': datetime.now}
+ if self.time_range_start_template:
+ params['start'] = Template('{{ %s|date:"Y-m-d" }}' % self.time_range_start_template).render(
+ Context(context)
+ )
+ if self.time_range_end_template:
+ params['end'] = Template('{{ %s|date:"Y-m-d" }}' % self.time_range_end_template).render(
+ Context(context)
+ )
if 'time_interval' in params and params['time_interval'].startswith('_'):
params['time_interval'] = 'day'
return params
diff --git a/combo/apps/dataviz/templates/combo/chartngcell_form.html b/combo/apps/dataviz/templates/combo/chartngcell_form.html
index c883058b..9d79145c 100644
--- a/combo/apps/dataviz/templates/combo/chartngcell_form.html
+++ b/combo/apps/dataviz/templates/combo/chartngcell_form.html
@@ -11,6 +11,8 @@
$(function () {
start_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start');
end_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end');
+ start_field_template = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start_template');
+ end_field_template = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end_template');
$('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range').change(function() {
if(this.value == 'range') {
start_field.parent().show();
@@ -19,6 +21,13 @@
start_field.parent().hide();
end_field.parent().hide();
}
+ if(this.value == 'range_template') {
+ start_field_template.parent().show();
+ end_field_template.parent().show();
+ } else {
+ start_field_template.parent().hide();
+ end_field_template.parent().hide();
+ }
}).change();
});
diff --git a/combo/context_processors.py b/combo/context_processors.py
index eafd630a..2b947800 100644
--- a/combo/context_processors.py
+++ b/combo/context_processors.py
@@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
from django.conf import settings
from combo.apps.pwa.models import PwaSettings
diff --git a/tests/test_dataviz.py b/tests/test_dataviz.py
index a6ec5485..dbb507cb 100644
--- a/tests/test_dataviz.py
+++ b/tests/test_dataviz.py
@@ -1098,6 +1098,8 @@ def test_chartng_cell_manager(app, admin_user, statistics):
assert 'time_range' not in resp.form.fields
assert 'time_range_start' not in resp.form.fields
assert 'time_range_end' not in resp.form.fields
+ assert 'time_range_start_template' not in resp.form.fields
+ assert 'time_range_end_template_end' not in resp.form.fields
cell.statistic = Statistic.objects.get(slug='example')
cell.save()
@@ -1111,6 +1113,8 @@ def test_chartng_cell_manager(app, admin_user, statistics):
assert 'time_range' not in resp.form.fields
assert 'time_range_start' not in resp.form.fields
assert 'time_range_end' not in resp.form.fields
+ assert 'time_range_start_template' not in resp.form.fields
+ assert 'time_range_end_template_end' not in resp.form.fields
cell.statistic = Statistic.objects.get(slug='unavailable-stat')
cell.save()
@@ -1200,6 +1204,49 @@ def test_chartng_cell_manager_new_api(app, admin_user, new_api_statistics):
assert cell.time_range == ''
+@with_httmock(new_api_mock)
+@pytest.mark.freeze_time('2021-10-06')
+def test_chartng_cell_manager_new_api_time_range_templates(app, admin_user, new_api_statistics):
+ page = Page.objects.create(title='One', slug='index')
+ cell = ChartNgCell(page=page, order=1, placeholder='content')
+ cell.statistic = Statistic.objects.get(slug='one-serie')
+ cell.save()
+
+ app = login(app)
+ resp = app.get('/manage/pages/%s/' % page.id)
+ field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
+
+ resp.form[field_prefix + 'time_range'] = 'range-template'
+ resp.form[field_prefix + 'time_range_start_template'] = 'today|add_days:"7"|adjust_to_week_monday'
+ resp.form[field_prefix + 'time_range_end_template'] = 'now|add_days:"14"|adjust_to_week_monday'
+ resp = resp.form.submit().follow()
+ cell.refresh_from_db()
+ assert cell.time_range == 'range-template'
+ assert cell.time_range_start_template == 'today|add_days:"7"|adjust_to_week_monday'
+ assert cell.time_range_end_template == 'now|add_days:"14"|adjust_to_week_monday'
+
+ resp.form[field_prefix + 'time_range_start_template'] = ''
+ resp.form[field_prefix + 'time_range_end_template'] = ''
+ resp = resp.form.submit().follow()
+ cell.refresh_from_db()
+ assert cell.time_range_start_template == ''
+ assert cell.time_range_end_template == ''
+
+ resp.form[field_prefix + 'time_range_start_template'] = 'xxx'
+ resp = resp.form.submit()
+ assert 'Template does not evaluate to a valid date.' in resp.text
+
+ resp = app.get('/manage/pages/%s/' % page.id)
+ resp.form[field_prefix + 'time_range_start_template'] = 'today|xxx'
+ resp = resp.form.submit()
+ assert 'Invalid filter' in resp.text
+
+ resp = app.get('/manage/pages/%s/' % page.id)
+ resp.form[field_prefix + 'time_range_start_template'] = 'today|date:xxx'
+ resp = resp.form.submit()
+ assert 'Failed lookup for key [xxx]' in resp.text
+
+
@with_httmock(new_api_mock)
def test_chartng_cell_manager_new_api_dynamic_fields(app, admin_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index')
@@ -1435,6 +1482,26 @@ def test_chartng_cell_new_api_filter_params(new_api_statistics, nocache, freezer
request = new_api_mock.call['requests'][-1]
assert 'start=2020-10-01' in request.url and 'end=2020-11-03' in request.url
+ cell.time_range = 'range-template'
+ cell.save()
+ cell.get_chart()
+ request = new_api_mock.call['requests'][-1]
+ assert 'start' not in urllib.parse.parse_qs(urllib.parse.urlparse(request.url).query)
+ assert 'end' not in urllib.parse.parse_qs(urllib.parse.urlparse(request.url).query)
+
+ cell.time_range_start_template = 'today|add_days:"7"|adjust_to_week_monday'
+ cell.save()
+ cell.get_chart()
+ request = new_api_mock.call['requests'][-1]
+ assert 'start=2020-03-09' in request.url
+ assert 'end' not in urllib.parse.parse_qs(urllib.parse.urlparse(request.url).query)
+
+ cell.time_range_end_template = 'today|add_days:"14"|adjust_to_week_monday'
+ cell.save()
+ cell.get_chart()
+ request = new_api_mock.call['requests'][-1]
+ assert 'start=2020-03-09' in request.url and 'end=2020-03-16' in request.url
+
@with_httmock(new_api_mock)
def test_chartng_cell_new_api_filter_params_month(new_api_statistics, nocache, freezer):