...
 
Commits (13)
## 6.0
- Added django 2.0 support. Droped support for Django 1.8 and 1.10 as Django no longer supports them
- Bug fixes
## 5.0
- Potentially breaking change - Rebuilt frontend in Angular 5. Should only affect those who customize the frontend.
......
......@@ -8,6 +8,11 @@ Targets sys admins and capable end users who might not be able to program or gai
# News
## 6.0
- Added django 2.0 support. Droped support for Django 1.8 and 1.10 as Django no longer supports them
- Bug fixes
## 5.0
* Complete rewrite of the frontend in Angular CLI
......@@ -15,16 +20,6 @@ Targets sys admins and capable end users who might not be able to program or gai
* Other minor improvements and fixes
* For anyone who has written a custom frontend: we made a few changes to the django template that you might need to look at. The API has remained the same - one additional route was added that returns information as JSON that was previously serialized in the django template.
## 4.0
* Removed python 2.7 support, please use 3.6 for python 2.
* Added scheduled reports
## 3.6
* Fix bug affecting Django 1.10 and 1.11
* Moved to tox for testing
# What is Django Report Builder?
![](docs/screenshots/reportbuilderscreen.jpg)
......
......@@ -9,7 +9,7 @@ See [installation instructions.](quickstart.md)
Code
----
https://github.com/burke-software/django-report-builder
https://gitlab.com/burke-software/django-report-builder/
Discussion
--------
......
{
"/report_builder": {
"/report_builder/api": {
"target": "http://localhost:8000",
"secure": false
},
......
......@@ -17,11 +17,14 @@
<div class="mat-header-cell narrow">Total</div>
<div class="mat-header-cell narrow">Group</div>
</div>
<tree-root [nodes]="getFields()" [options]="treeOptions">
<tree-root [nodes]="fields" [options]="treeOptions">
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
<app-display-tab-row [field]="node.data" [formatOptions]="formatOptions" (updateField)="updateField.emit($event)" (deleteField)="deleteField.emit($event)"
(click)="node.mouseAction('click', $event)" (treeDrop)="node.onDrop($event)" [treeAllowDrop]="node.allowDrop" [treeDrag]="node"
[treeDragEnabled]="node.allowDrag()">></app-display-tab-row>
<div class="node-wrapper report-builder-display-grid">
<app-display-tab-row [field]="node.data" [formatOptions]="formatOptions" (updateField)="updateField.emit($event)" (deleteField)="deleteField.emit($event)"
(click)="node.mouseAction('click', $event)" (treeDrop)="node.onDrop($event)" [treeAllowDrop]="node.allowDrop" [treeDrag]="node"
[treeDragEnabled]="node.allowDrag()">
</app-display-tab-row>
</div>
</ng-template>
</tree-root>
</div>
......@@ -59,8 +59,4 @@ export class DisplayTabComponent {
},
} as IActionMapping,
};
getFields() {
return this.fields.map(x => ({ ...x }));
}
}
......@@ -37,3 +37,8 @@ body {
.mat-selection-list .mat-list-item .mat-line {
white-space: normal;
}
/* report builder - display grid */
.report-builder-display-grid{
display: grid;
}
from django.contrib import admin
from django.contrib.admin import SimpleListFilter
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from report_builder.models import Report, Format
from django.conf import settings
try:
from django.core.urlresolvers import reverse
except ModuleNotFoundError:
from django.urls import reverse
static_url = getattr(settings, 'STATIC_URL', '/static/')
......
......@@ -183,7 +183,7 @@ class FieldsView(RelatedFieldsView):
filters = None
extra = None
defaults = None
meta = getattr(self.model_class, 'ReportBuilder', None)
meta = getattr(field_data['model'], 'ReportBuilder', None)
if meta is not None:
fields = getattr(meta, 'fields', None)
filters = getattr(meta, 'filters', None)
......@@ -239,7 +239,7 @@ class FieldsView(RelatedFieldsView):
else:
extra_fields = extra
for field in extra_fields:
field_attr = getattr(self.model_class, field, None)
field_attr = getattr(field_data['model'], field, None)
if isinstance(field_attr, (property, cached_property)):
result += [{
'name': field,
......
......@@ -77,10 +77,10 @@ class Migration(migrations.Migration):
('distinct', models.BooleanField(default=False)),
('report_file', models.FileField(upload_to=b'report_files', blank=True)),
('report_file_creation', models.DateTimeField(null=True, blank=True)),
('root_model', models.ForeignKey(to='contenttypes.ContentType')),
('root_model', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)),
('starred', models.ManyToManyField(help_text=b'These users have starred this report for easy reference.', related_name='report_starred_set', to=settings.AUTH_USER_MODEL, blank=True)),
('user_created', models.ForeignKey(blank=True, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('user_modified', models.ForeignKey(related_name='report_modified_set', blank=True, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('user_created', models.ForeignKey(blank=True, editable=False, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)),
('user_modified', models.ForeignKey(related_name='report_modified_set', blank=True, editable=False, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)),
],
options={
},
......@@ -89,19 +89,19 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='filterfield',
name='report',
field=models.ForeignKey(to='report_builder.Report'),
field=models.ForeignKey(to='report_builder.Report', on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AddField(
model_name='displayfield',
name='display_format',
field=models.ForeignKey(blank=True, to='report_builder.Format', null=True),
field=models.ForeignKey(blank=True, to='report_builder.Format', null=True, on_delete=models.SET_NULL),
preserve_default=True,
),
migrations.AddField(
model_name='displayfield',
name='report',
field=models.ForeignKey(to='report_builder.Report'),
field=models.ForeignKey(to='report_builder.Report', on_delete=models.CASCADE),
preserve_default=True,
),
]
# Generated by Django 2.0.4 on 2018-04-13 07:47
from django.db import migrations, models
import django.db.models.deletion
import report_builder.models
class Migration(migrations.Migration):
dependencies = [
('report_builder', '0005_add_delta_filtering'),
]
operations = [
migrations.AlterField(
model_name='report',
name='root_model',
field=models.ForeignKey(limit_choices_to=report_builder.models.get_limit_choices_to_callable, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
]
......@@ -536,6 +536,7 @@ class GetFieldsMixin(object):
properties = get_properties_from_model(model_class)
custom_fields = get_custom_fields_from_model(model_class)
app_label = model_class._meta.app_label
model = model_class
if field_name != '':
field = model_class._meta.get_field(field_name)
......@@ -563,6 +564,7 @@ class GetFieldsMixin(object):
custom_fields = get_custom_fields_from_model(new_model)
properties = get_properties_from_model(new_model)
app_label = new_model._meta.app_label
model = new_model
return {
'fields': fields,
......@@ -571,6 +573,7 @@ class GetFieldsMixin(object):
'path': path,
'path_verbose': path_verbose,
'app_label': app_label,
'model': model,
}
def get_related_fields(self, model_class, field_name, path="", path_verbose=""):
......
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.utils.safestring import mark_safe
......@@ -21,6 +20,11 @@ import time
import datetime
import re
try:
from django.core.urlresolvers import reverse
except ModuleNotFoundError:
from django.urls import reverse
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
......@@ -73,14 +77,16 @@ class Report(models.Model):
slug = models.SlugField(verbose_name="Short Name")
description = models.TextField(blank=True)
root_model = models.ForeignKey(
ContentType, limit_choices_to=get_limit_choices_to_callable)
ContentType, limit_choices_to=get_limit_choices_to_callable,
on_delete=models.CASCADE)
created = models.DateField(auto_now_add=True)
modified = models.DateField(auto_now=True)
user_created = models.ForeignKey(
AUTH_USER_MODEL, editable=False, blank=True, null=True)
AUTH_USER_MODEL, editable=False, blank=True, null=True,
on_delete=models.SET_NULL)
user_modified = models.ForeignKey(
AUTH_USER_MODEL, editable=False, blank=True, null=True,
related_name="report_modified_set")
related_name="report_modified_set", on_delete=models.SET_NULL)
distinct = models.BooleanField(default=False)
report_file = models.FileField(upload_to="report_files", blank=True)
report_file_creation = models.DateTimeField(blank=True, null=True)
......@@ -497,7 +503,7 @@ class Format(models.Model):
class AbstractField(models.Model):
report = models.ForeignKey(Report)
report = models.ForeignKey(Report, on_delete=models.CASCADE)
path = models.CharField(max_length=2000, blank=True)
path_verbose = models.CharField(max_length=2000, blank=True)
field = models.CharField(max_length=2000)
......@@ -545,7 +551,8 @@ class DisplayField(AbstractField):
)
total = models.BooleanField(default=False)
group = models.BooleanField(default=False)
display_format = models.ForeignKey(Format, blank=True, null=True)
display_format = models.ForeignKey(Format, blank=True, null=True,
on_delete=models.SET_NULL)
def get_choices(self, model, field_name):
try:
......
......@@ -17,7 +17,12 @@ class RelationUtilityFunctionTests(TestCase):
Test that the initial assumption about the ManyToOneRel field_name is
correct
"""
self.assertEquals(Waiter.restaurant.field.rel.field_name, "place")
field_name = (
Waiter.restaurant.field.rel.field_name
if hasattr(Waiter.restaurant.field, 'rel')
else Waiter.restaurant.field.target_field.name
)
self.assertEquals(field_name, "place")
def test_get_relation_fields_from_model_does_not_change_field_name(self):
"""
......@@ -31,8 +36,13 @@ class RelationUtilityFunctionTests(TestCase):
ManyToManyRel objects are not affected.
"""
get_relation_fields_from_model(Restaurant)
self.assertEquals(Waiter.restaurant.field.rel.field_name, "place")
Waiter.restaurant.field.rel.get_related_field()
field_name = (
Waiter.restaurant.field.rel.field_name
if hasattr(Waiter.restaurant.field, 'rel')
else Waiter.restaurant.field.target_field.name
)
self.assertEquals(field_name, "place")
# Waiter.restaurant.field.rel.get_related_field()
class UtilityFunctionTests(TestCase):
......
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db.models.query import QuerySet
from django.test import TestCase
from django.test.utils import override_settings
......@@ -20,6 +19,12 @@ from freezegun import freeze_time
from datetime import date, datetime, timedelta, time as dtime
from django.contrib.auth import get_user_model
try:
from django.core.urlresolvers import reverse
except ModuleNotFoundError:
from django.urls import reverse
User = get_user_model()
......@@ -80,7 +85,7 @@ class ReportBuilderTests(TestCase):
self.assertTrue(callable(get_limit_choices_to_callable))
self.assertTrue(isinstance(lookup_dict['pk__in'], QuerySet))
self.assertQuerysetEqual(lookup_dict['pk__in'], map(repr, models), ordered=False)
def test_report_builder_reports(self):
url = '/report_builder/api/reports/'
no_auth_client = APIClient()
......@@ -112,6 +117,30 @@ class ReportBuilderTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'pizza')
def test_report_builder_fields_from_related_with_hidden_field(self):
ct = ContentType.objects.get(model="bar", app_label="demo_models")
response = self.client.post(
'/report_builder/api/fields/',
{"model": ct.id,
"path": "",
"path_verbose": "",
"field": "foos"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'char_field')
self.assertNotContains(response, 'char_field2')
def test_report_builder_fields_from_related_with_properties(self):
ct = ContentType.objects.get(model="foo", app_label="demo_models")
response = self.client.post(
'/report_builder/api/fields/',
{"model": ct.id,
"path": "",
"path_verbose": "",
"field": "bar_set"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'i_want_char_field')
self.assertContains(response, 'i_need_char_field')
def test_report_builder_fields_from_related_fields(self):
ct = ContentType.objects.get(model="place", app_label="demo_models")
response = self.client.post(
......@@ -187,7 +216,7 @@ class ReportBuilderTests(TestCase):
report = Report.objects.create(
name="foo report",
root_model=ct)
display_field = DisplayField.objects.create(
name='foo',
report=report,
......@@ -543,13 +572,13 @@ class ReportTests(TestCase):
"""
Make a mock report using the People demo model.
Creates mock People instances and returns a report including the
Creates mock People instances and returns a report including the
following DisplayFields:
first_name (CharField)
last_modifed (DateField)
birth_date (DateTimeField)
hammer_time (TimeField)
"""
"""
self.make_people()
model = ContentType.objects.get(model="person",
app_label="demo_models")
......@@ -601,7 +630,7 @@ class ReportTests(TestCase):
# DateField
ff = FilterField.objects.create(
report=people_report,
field='last_modifed',
field='last_modifed',
filter_type='lte',
filter_value=str(date.today() - timedelta(seconds=self.day * 10)),
)
......@@ -612,7 +641,7 @@ class ReportTests(TestCase):
self.assertEquals(len(response.data['data']), 3)
# TimeField
ff.field = 'hammer_time'
ff.field = 'hammer_time'
ff.filter_value = str(dtime(hour=13))
ff.save()
......@@ -620,7 +649,7 @@ class ReportTests(TestCase):
self.assertEquals(len(response.data['data']), 1)
# DateTimeField
ff.field = 'birth_date'
ff.field = 'birth_date'
ff.filter_value = str(datetime.today() - timedelta(seconds=self.day * 40))
ff.save()
......@@ -631,8 +660,8 @@ class ReportTests(TestCase):
def test_filter_datetime_range(self):
"""
Test filtering 'DateTime' field types using a range filter.
Each FilterField accepts 2 values of the respective field type to
Each FilterField accepts 2 values of the respective field type to
create the range.
Ex. Filter a TimeField for 'user logins between 10am - 1pm':
......@@ -645,7 +674,7 @@ class ReportTests(TestCase):
# DateField
ff = FilterField.objects.create(
report=people_report,
field='last_modifed',
field='last_modifed',
filter_type='range',
filter_value=str(date.today() - timedelta(seconds=self.day * 7)),
filter_value2=str(date.today()),
......@@ -656,7 +685,7 @@ class ReportTests(TestCase):
self.assertEquals(len(response.data['data']), 1)
# TimeField
ff.field = 'hammer_time'
ff.field = 'hammer_time'
ff.filter_value = str(dtime(hour=10))
ff.filter_value2 = str(dtime(hour=13))
ff.save()
......@@ -665,7 +694,7 @@ class ReportTests(TestCase):
self.assertEquals(len(response.data['data']), 1)
# DateTimeField
ff.field = 'birth_date'
ff.field = 'birth_date'
ff.filter_value = str(datetime.now() - timedelta(seconds=self.day * 50))
ff.filter_value2 = str(datetime.now() - timedelta(seconds=self.day * 70))
ff.save()
......@@ -678,8 +707,8 @@ class ReportTests(TestCase):
"""
Test filtering 'DateTime' field types using a relative range filter.
Each FilterField accepts a delta value (in seconds) which represents
the range (positive or negative) off of the current date.
Each FilterField accepts a delta value (in seconds) which represents
the range (positive or negative) off of the current date.
Ex. Filter a TimeField for 'user logins in the last 3 hours':
filter_type='relative_range',
......@@ -700,7 +729,7 @@ class ReportTests(TestCase):
self.assertEquals(len(response.data['data']), 1)
# DateField w/ partial day
ff.filter_delta = self.day * -7 + 5
ff.filter_delta = self.day * -7 + 5
ff.save()
response = self.client.get(generate_url)
......@@ -739,7 +768,7 @@ class ReportTests(TestCase):
def test_filter_datefield_relative_range_over_time(self):
"""
Test filtering DateField types using a relative range filter
over time.
over time.
"""
people_report = self.make_people_report()
generate_url = reverse('generate_report', args=[people_report.id])
......@@ -766,15 +795,15 @@ class ReportTests(TestCase):
def test_filter_timefield_relative_range_over_time(self):
"""
Test filtering TimeField field types using a relative range filter
over time.
over time.
"""
people_report = self.make_people_report()
generate_url = reverse('generate_report', args=[people_report.id])
initial_today = datetime(2017, 11, 1, 12)
four_hours_later_today = datetime(2017, 11, 1, 16)
# TimeField with login 'now'
with freeze_time(initial_today) as frozen_today:
FilterField.objects.create(
......@@ -794,11 +823,11 @@ class ReportTests(TestCase):
def test_filter_datetimefield_relative_range_over_time(self):
"""
Test filtering DateTimeField field types using a relative range filter
over time.
over time.
"""
people_report = self.make_people_report()
generate_url = reverse('generate_report', args=[people_report.id])
initial_today = datetime(2017, 10, 1, 12)
one_month_later = datetime(2017, 11, 1, 12)
......
......@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='FooExclude',
fields=[
('foo_ptr', models.OneToOneField(auto_created=True, serialize=False, parent_link=True, primary_key=True, to='demo_models.Foo')),
('foo_ptr', models.OneToOneField(auto_created=True, serialize=False, parent_link=True, primary_key=True, to='demo_models.Foo', on_delete=models.CASCADE)),
],
options={
},
......
......@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Restaurant',
fields=[
('place', models.OneToOneField(primary_key=True, to='demo_models.Place', serialize=False)),
('place', models.OneToOneField(primary_key=True, to='demo_models.Place', serialize=False, on_delete=models.CASCADE)),
('serves_hot_dogs', models.BooleanField(default=False)),
('serves_pizza', models.BooleanField(default=False)),
],
......@@ -37,6 +37,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='waiter',
name='restaurant',
field=models.ForeignKey(to='demo_models.Restaurant'),
field=models.ForeignKey(to='demo_models.Restaurant', on_delete=models.CASCADE),
),
]
......@@ -32,11 +32,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='bar',
name='check_mate_status',
field=models.CharField(default=b'CH', max_length=2, choices=[(b'CH', b'CHECK'), (b'MA', b'CHECKMATE')]),
field=models.CharField(default='CH', max_length=2, choices=[('CH', 'CHECK'), ('MA', 'CHECKMATE')]),
),
migrations.AddField(
model_name='child',
name='parent',
field=models.ForeignKey(related_name='children', to='demo_models.Person'),
field=models.ForeignKey(related_name='children', to='demo_models.Person', on_delete=models.CASCADE),
),
]
......@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
('object_pk', models.TextField()),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)),
],
),
migrations.AlterField(
......
......@@ -20,7 +20,7 @@ class FooExclude(Foo):
class Bar(models.Model):
char_field = models.CharField(max_length=50, blank=True)
foos = models.ManyToManyField(Foo, blank=True)
foos = models.ManyToManyField(Foo, blank=True, related_name='bar_set')
CHECK = 'CH'
MATE = 'MA'
......@@ -69,7 +69,7 @@ class Place(models.Model):
class Restaurant(models.Model):
place = models.OneToOneField(Place, primary_key=True)
place = models.OneToOneField(Place, primary_key=True, on_delete=models.CASCADE)
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
......@@ -78,7 +78,7 @@ class Restaurant(models.Model):
class Waiter(models.Model):
restaurant = models.ForeignKey(Restaurant)
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
days_worked = models.IntegerField(blank=True, null=True, default=None)
......@@ -104,7 +104,7 @@ class Person(models.Model):
class Child(models.Model):
parent = models.ForeignKey(Person, related_name='children')
parent = models.ForeignKey(Person, related_name='children', on_delete=models.CASCADE)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
age = models.IntegerField(null=True, blank=True, default=None)
......@@ -117,7 +117,7 @@ class Child(models.Model):
class Comment(models.Model):
""" django-contrib-comments like model """
content_type = models.ForeignKey('contenttypes.ContentType')
content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE)
object_pk = models.TextField()
content_object = GenericForeignKey(
ct_field="content_type", fk_field="object_pk")
......@@ -44,14 +44,15 @@ INSTALLED_APPS = (
'django_extensions',
)
MIDDLEWARE_CLASSES = (
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
]
ROOT_URLCONF = 'report_builder_demo.urls'
......
from django.conf.urls import include, url
from django.conf.urls import url, include
from django.conf import settings
from django.conf.urls.static import static
......@@ -6,7 +7,7 @@ from django.contrib import admin
admin.autodiscover()
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^admin/', admin.site.urls),
url(r'^report_builder/', include('report_builder_scheduled.urls')),
url(r'^report_builder/', include('report_builder.urls')),
]
......
from django.contrib import admin
from django.core.urlresolvers import reverse
from .models import ScheduledReport
try:
from django.core.urlresolvers import reverse
except ModuleNotFoundError:
from django.urls import reverse
@admin.register(ScheduledReport)
class ScheduledReportAdmin(admin.ModelAdmin):
......
# Generated by Django 2.0.4 on 2018-04-13 07:47
from django.conf import settings
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('report_builder_scheduled', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='scheduledreport',
name='last_run_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AlterField(
model_name='scheduledreport',
name='users',
field=models.ManyToManyField(blank=True, help_text='Staff users to notify', limit_choices_to={'is_staff': True}, to=settings.AUTH_USER_MODEL),
),
]
......@@ -12,7 +12,9 @@ class ScheduledReport(models.Model):
""" A scheduled report that runs and emails itself to various users on
a recurring basis. Requires celery. """
is_active = models.BooleanField(default=True)
report = models.ForeignKey('report_builder.Report')
report = models.ForeignKey(
'report_builder.Report', on_delete=models.CASCADE
)
users = models.ManyToManyField(
AUTH_USER_MODEL,
limit_choices_to={'is_staff': True},
......
......@@ -2,13 +2,17 @@ import django
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core import mail
from django.core.urlresolvers import reverse
from django.test import TestCase
from .models import ScheduledReport
from report_builder.models import Report
from .tasks import report_builder_run_scheduled_report
from unittest import skipIf
try:
from django.core.urlresolvers import reverse
except ModuleNotFoundError:
from django.urls import reverse
User = get_user_model()
......
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from .tasks import report_builder_run_scheduled_report
from .models import ScheduledReport
try:
from django.core.urlresolvers import reverse
except ModuleNotFoundError:
from django.urls import reverse
@staff_member_required
def run_scheduled_report(request, pk):
""" Manually run a scheduled report - useful for testing or one off situations """
......
......@@ -5,10 +5,11 @@ Werkzeug
ipdb
celery[redis]
flake8
django-money==0.11
djangorestframework==3.6.3
django_celery_beat==1.0.1
coverage==4.4.1
django-money==0.13.1
djangorestframework==3.8.2
django_celery_beat==1.1.1
coverage==4.5.1
tox
model_mommy
freezegun
openpyxl
\ No newline at end of file
......@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="django-report-builder",
version="5.0.1",
version="6.0.0",
author="David Burke",
author_email="david@burkesoftware.com",
description=("Query and Report builder for Django ORM"),
......
[tox]
toxworkdir={env:TOX_WORK_DIR:.tox}
envlist = py{36}-django{18,110,111}
envlist = py{36}-django{111,20}
[testenv]
passenv = *
install_command = pip install {opts} {packages}
deps =
django18: django>=1.8,<1.9
django110: django>=1.10,<1.11
django111: django>=1.11,<1.12
django20: django>=2.0,<2.1
-r{toxinidir}/requirements.txt
commands =
{envpython} {toxinidir}/manage.py test --noinput
{envpython} {toxinidir}/manage.py test --noinput