dataviz: allow setting time range using template (#57617)

This commit is contained in:
Valentin Deniaud 2021-10-06 17:09:49 +02:00
parent 981f2ee8a1
commit 9f6d68477a
6 changed files with 168 additions and 6 deletions

View File

@ -14,12 +14,14 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
from collections import OrderedDict from collections import OrderedDict
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from combo.utils import cache_during_request, requests, spooler from combo.utils import cache_during_request, requests, spooler
@ -69,6 +71,8 @@ class ChartNgForm(forms.ModelForm):
'time_range', 'time_range',
'time_range_start', 'time_range_start',
'time_range_end', 'time_range_end',
'time_range_start_template',
'time_range_end_template',
'chart_type', 'chart_type',
'height', 'height',
'sort_order', 'sort_order',
@ -85,9 +89,14 @@ class ChartNgForm(forms.ModelForm):
field_ids = list(self._meta.fields) field_ids = list(self._meta.fields)
if not self.instance.statistic or self.instance.statistic.service_slug == 'bijoe': if not self.instance.statistic or self.instance.statistic.service_slug == 'bijoe':
field_ids = [ exclude = (
x for x in field_ids if x not in ('time_range', 'time_range_start', 'time_range_end') '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'] stat_field = self.fields['statistic']
if not self.instance.statistic: if not self.instance.statistic:
@ -142,3 +151,18 @@ class ChartNgForm(forms.ModelForm):
for choice in self.time_intervals: for choice in self.time_intervals:
if choice[0].strip('_') not in choice_ids: if choice[0].strip('_') not in choice_ids:
self.fields['time_interval'].choices.append(choice) 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.'))

View File

@ -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.',
),
),
]

View File

@ -25,6 +25,7 @@ from dateutil.relativedelta import MO, relativedelta
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.db import models, transaction from django.db import models, transaction
from django.template import Context, Template
from django.template.defaultfilters import date as format_date from django.template.defaultfilters import date as format_date
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -36,7 +37,7 @@ from django.utils.translation import ungettext
from requests.exceptions import HTTPError, RequestException from requests.exceptions import HTTPError, RequestException
from combo.data.library import register_cell_class 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 from combo.utils import get_templated_url, requests, spooler
@ -158,7 +159,8 @@ TIME_FILTERS = (
('previous-week', _('Previous week')), ('previous-week', _('Previous week')),
('current-week', _('Current week')), ('current-week', _('Current week')),
('next-week', _('Next 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_start = models.DateField(_('From'), null=True, blank=True)
time_range_end = models.DateField(_('To'), 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 = models.CharField(
_('Chart Type'), _('Chart Type'),
max_length=20, max_length=20,
@ -394,6 +411,16 @@ class ChartNgCell(CellBase):
params['start'] = self.time_range_start params['start'] = self.time_range_start
if self.time_range_end: if self.time_range_end:
params['end'] = 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('_'): if 'time_interval' in params and params['time_interval'].startswith('_'):
params['time_interval'] = 'day' params['time_interval'] = 'day'
return params return params

View File

@ -11,6 +11,8 @@
$(function () { $(function () {
start_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start'); start_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start');
end_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end'); 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() { $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range').change(function() {
if(this.value == 'range') { if(this.value == 'range') {
start_field.parent().show(); start_field.parent().show();
@ -19,6 +21,13 @@
start_field.parent().hide(); start_field.parent().hide();
end_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(); }).change();
}); });
</script> </script>

View File

@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings from django.conf import settings
from combo.apps.pwa.models import PwaSettings from combo.apps.pwa.models import PwaSettings

View File

@ -1098,6 +1098,8 @@ def test_chartng_cell_manager(app, admin_user, statistics):
assert 'time_range' not in resp.form.fields assert 'time_range' not in resp.form.fields
assert 'time_range_start' 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_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.statistic = Statistic.objects.get(slug='example')
cell.save() 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' not in resp.form.fields
assert 'time_range_start' 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_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.statistic = Statistic.objects.get(slug='unavailable-stat')
cell.save() cell.save()
@ -1200,6 +1204,49 @@ def test_chartng_cell_manager_new_api(app, admin_user, new_api_statistics):
assert cell.time_range == '' 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) @with_httmock(new_api_mock)
def test_chartng_cell_manager_new_api_dynamic_fields(app, admin_user, new_api_statistics): def test_chartng_cell_manager_new_api_dynamic_fields(app, admin_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index') 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] request = new_api_mock.call['requests'][-1]
assert 'start=2020-10-01' in request.url and 'end=2020-11-03' in request.url 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) @with_httmock(new_api_mock)
def test_chartng_cell_new_api_filter_params_month(new_api_statistics, nocache, freezer): def test_chartng_cell_new_api_filter_params_month(new_api_statistics, nocache, freezer):