From 08f2fb834ce09efcac284876a0b998801bd87c57 Mon Sep 17 00:00:00 2001 From: mustafaulker Date: Wed, 18 Dec 2024 17:27:56 +0300 Subject: [PATCH 1/4] Add release 6.6.0 release notes --- CHANGELOG | 3 +++ README.md | 11 +++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6a91c819..0a487749 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +## 6.6.0 +- Split files and zip them when the row count exceeds one million. + ## 6.5.7 - Remove mis-import diff --git a/README.md b/README.md index 5757caf7..91a054f7 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Targets sys admins and capable end users who might not be able to program or gai # News +## 6.6.0 +- Split files and zip them when the row count exceeds one million. + ## 6.5.7 - Remove mis-import @@ -19,14 +22,6 @@ Targets sys admins and capable end users who might not be able to program or gai - Remove six dependency - Replace deprecated openpyxl save_virtual_workbook func. (Cherry-picked from @tmszi's fork) -## 6.5.4 - -- Create AutoField to BigAutoField convertion migration - -## 6.5.3 - -- Fix "Router with basename "report" is already registered" error - ## 6.5 - Make compatible for Django >4. Project's GitLab Repo merged into this fork and Django 4 compatibility changes were made. From d1f60e45398c70ec70d6678b1b69d8ce85c3507a Mon Sep 17 00:00:00 2001 From: mustafaulker Date: Wed, 18 Dec 2024 18:06:22 +0300 Subject: [PATCH 2/4] Upgrade Django and Python syntax Ran django-upgrade and pyupgrade --- report_builder/admin.py | 12 ++-- report_builder/api/serializers.py | 2 +- report_builder/api/views.py | 2 +- report_builder/migrations/0001_initial.py | 3 - .../migrations/0002_auto_20150201_1809.py | 3 - .../migrations/0003_auto_20150720_1549.py | 3 - .../migrations/0004_auto_20170915_2046.py | 2 - .../migrations/0005_add_delta_filtering.py | 2 - report_builder/mixins.py | 6 +- report_builder/models.py | 14 ++--- report_builder/tests/test_utils.py | 16 +++--- report_builder/tests/test_views.py | 2 +- report_builder/tests/tests.py | 38 ++++++------- report_builder/unique_slugify.py | 6 +- report_builder/urls.py | 57 ++++++++++++------- report_builder/views.py | 10 ++-- report_builder_demo/__init__.py | 1 - report_builder_demo/celery.py | 2 - .../demo_models/migrations/0001_initial.py | 3 - .../demo_models/migrations/0002_bar_foos.py | 3 - .../migrations/0003_auto_20150419_2110.py | 3 - .../migrations/0004_bar_date_field.py | 3 - .../migrations/0004_waiter_days_worked.py | 3 - .../migrations/0005_auto_20150622_1840.py | 3 - .../demo_models/migrations/0006_account.py | 3 - .../demo_models/migrations/0006_merge.py | 3 - .../migrations/0007_auto_20150712_1752.py | 3 - .../demo_models/migrations/0008_merge.py | 3 - .../migrations/0009_auto_20151209_2136.py | 3 - .../0010_adding_sample_datefields.py | 2 - report_builder_demo/demo_models/models.py | 2 +- report_builder_demo/settings.py | 1 - report_builder_demo/urls.py | 9 ++- .../migrations/0001_initial.py | 2 - report_builder_scheduled/tests.py | 2 +- report_builder_scheduled/urls.py | 4 +- 36 files changed, 99 insertions(+), 137 deletions(-) diff --git a/report_builder/admin.py b/report_builder/admin.py index 0fb3b7c4..93c2687b 100644 --- a/report_builder/admin.py +++ b/report_builder/admin.py @@ -55,21 +55,21 @@ class Media: def response_add(self, request, obj, post_url_continue=None): if '_easy' in request.POST: return HttpResponseRedirect(obj.get_absolute_url()) - return super(ReportAdmin, self).response_add(request, obj, post_url_continue) + return super().response_add(request, obj, post_url_continue) def response_change(self, request, obj): if '_easy' in request.POST: return HttpResponseRedirect(obj.get_absolute_url()) - return super(ReportAdmin, self).response_change(request, obj) + return super().response_change(request, obj) def change_view(self, request, object_id, extra_context=None): if getattr(settings, 'REPORT_BUILDER_ASYNC_REPORT', False) and 'report_file' not in self.fields: self.fields += ['report_file', 'report_file_creation'] - return super(ReportAdmin, self).change_view(request, object_id, extra_context=None) + return super().change_view(request, object_id, extra_context=None) def changelist_view(self, request, extra_context=None): self.user = request.user - return super(ReportAdmin, self).changelist_view(request, extra_context=extra_context) + return super().changelist_view(request, extra_context=extra_context) @admin.display( description="Starred" @@ -81,7 +81,7 @@ def ajax_starred(self, obj): else: img = static('report_builder/img/unstar.png') return mark_safe( - ''.format( + ''.format( reverse('ajax_add_star', args=[obj.id]), img) ) @@ -111,7 +111,7 @@ def export_to_report(modeladmin, request, queryset): selected.append(str(s)) ct = ContentType.objects.get_for_model(queryset.model) return HttpResponseRedirect( - reverse('export_to_report') + "?ct=%s&admin_url=%s&ids=%s" % (ct.pk, admin_url, ",".join(selected)) + reverse('export_to_report') + "?ct={}&admin_url={}&ids={}".format(ct.pk, admin_url, ",".join(selected)) ) diff --git a/report_builder/api/serializers.py b/report_builder/api/serializers.py index 91e72605..dec96254 100644 --- a/report_builder/api/serializers.py +++ b/report_builder/api/serializers.py @@ -26,7 +26,7 @@ class Meta: read_only_fields = ('id',) def to_internal_value(self, data): - if data.get('sort') is '': + if data.get('sort') == '': data['sort'] = None return super().to_internal_value(data) diff --git a/report_builder/api/views.py b/report_builder/api/views.py index ee7c04bf..f54570f0 100644 --- a/report_builder/api/views.py +++ b/report_builder/api/views.py @@ -81,7 +81,7 @@ def perform_update(self, serializer): def copy_report(self, request, pk=None): report = self.get_object() new_report = duplicate(report, changes=( - ('name', '{0} (copy)'.format(report.name)), + ('name', '{} (copy)'.format(report.name)), ('user_created', request.user), ('user_modified', request.user), )) diff --git a/report_builder/migrations/0001_initial.py b/report_builder/migrations/0001_initial.py index 575a18c6..2cb32e2d 100644 --- a/report_builder/migrations/0001_initial.py +++ b/report_builder/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations from django.conf import settings diff --git a/report_builder/migrations/0002_auto_20150201_1809.py b/report_builder/migrations/0002_auto_20150201_1809.py index 5c6e9025..b50acd0d 100644 --- a/report_builder/migrations/0002_auto_20150201_1809.py +++ b/report_builder/migrations/0002_auto_20150201_1809.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations from django.conf import settings diff --git a/report_builder/migrations/0003_auto_20150720_1549.py b/report_builder/migrations/0003_auto_20150720_1549.py index 496c3d9f..8850f84f 100644 --- a/report_builder/migrations/0003_auto_20150720_1549.py +++ b/report_builder/migrations/0003_auto_20150720_1549.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder/migrations/0004_auto_20170915_2046.py b/report_builder/migrations/0004_auto_20170915_2046.py index eec8d812..0a72c320 100644 --- a/report_builder/migrations/0004_auto_20170915_2046.py +++ b/report_builder/migrations/0004_auto_20170915_2046.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.1 on 2017-09-15 20:46 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/report_builder/migrations/0005_add_delta_filtering.py b/report_builder/migrations/0005_add_delta_filtering.py index e0eced5a..cb2e957d 100644 --- a/report_builder/migrations/0005_add_delta_filtering.py +++ b/report_builder/migrations/0005_add_delta_filtering.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.1 on 2017-11-21 03:32 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/report_builder/mixins.py b/report_builder/mixins.py index 485be458..27d171f7 100644 --- a/report_builder/mixins.py +++ b/report_builder/mixins.py @@ -41,7 +41,7 @@ def generate_filename(title, ends_with): return title -class DataExportMixin(object): +class DataExportMixin: def build_sheet(self, data, ws, sheet_name='report', header=None, widths=None): first_row = 1 column_base = 1 @@ -289,7 +289,7 @@ def can_change_or_view(model): display_totals[display_field_key] = Decimal(0) else: - message += 'Error: Permission denied on access to {0}.'.format( + message += 'Error: Permission denied on access to {}.'.format( display_field.name ) @@ -524,7 +524,7 @@ def sort_helper(self, value, default): return value -class GetFieldsMixin(object): +class GetFieldsMixin: def get_fields(self, model_class, field_name='', path='', path_verbose=''): """ Get fields and meta data from a model :param model_class: A django model class diff --git a/report_builder/models.py b/report_builder/models.py index 9177bbd5..9cded8f7 100644 --- a/report_builder/models.py +++ b/report_builder/models.py @@ -107,7 +107,7 @@ def __str__(self): def save(self, *args, **kwargs): if not self.id: unique_slugify(self, self.name) - super(Report, self).save(*args, **kwargs) + super().save(*args, **kwargs) def add_aggregates(self, queryset, display_fields=None): agg_funcs = { @@ -372,7 +372,7 @@ def get_query(self): for filter_field in report.filterfield_set.order_by('position'): if filter_field.filter_type in ('max', 'min'): func = {'max': Max, 'min': Min}[filter_field.filter_type] - column_name = '{0}{1}__{2}'.format( + column_name = '{}{}__{}'.format( filter_field.path, filter_field.field, filter_field.field_type @@ -396,7 +396,7 @@ def get_absolute_url(self): def edit(self): return mark_safe( - ''.format( + ''.format( self.get_absolute_url(), static('report_builder/img/edit.svg') ) @@ -405,14 +405,14 @@ def edit(self): def download_xlsx(self): if getattr(settings, 'REPORT_BUILDER_ASYNC_REPORT', False): return mark_safe( - ''.format( + ''.format( self.id, static('report_builder/img/download.svg') ) ) else: return mark_safe( - ''.format( + ''.format( reverse('report_download_file', args=[self.id]), static('report_builder/img/download.svg') ) @@ -422,7 +422,7 @@ def download_xlsx(self): def copy_report(self): return mark_safe( - ''.format( + ''.format( reverse('report_builder_create_copy', args=[self.id]), static('report_builder/img/copy.svg') ) @@ -686,7 +686,7 @@ def clean(self): elif self.field_type == 'DateField' and self.filter_type != 'isnull': self.filter_value = str(self.parse_datetime_fields(self.filter_value)) - return super(FilterField, self).clean() + return super().clean() def parse_datetime_fields(self, dt_type): """Clean and parse datetime filter_value inputs.""" diff --git a/report_builder/tests/test_utils.py b/report_builder/tests/test_utils.py index 744b267d..c7a4e9a1 100644 --- a/report_builder/tests/test_utils.py +++ b/report_builder/tests/test_utils.py @@ -22,7 +22,7 @@ def test_a_initial_rel_field_name(self): if hasattr(Waiter.restaurant.field, 'rel') else Waiter.restaurant.field.target_field.name ) - self.assertEquals(field_name, "place") + self.assertEqual(field_name, "place") def test_get_relation_fields_from_model_does_not_change_field_name(self): """ @@ -41,7 +41,7 @@ def test_get_relation_fields_from_model_does_not_change_field_name(self): if hasattr(Waiter.restaurant.field, 'rel') else Waiter.restaurant.field.target_field.name ) - self.assertEquals(field_name, "place") + self.assertEqual(field_name, "place") # Waiter.restaurant.field.rel.get_related_field() @@ -68,7 +68,7 @@ def test_get_relation_fields_from_model(self): self.assertTrue('displayfield' in names or 'report_builder:displayfield' in names) self.assertTrue('filterfield' in names or 'report_builder:filterfield' in names) self.assertTrue('root_model' in names) - self.assertEquals(len(names), 7) + self.assertEqual(len(names), 7) def test_get_model_from_path_string(self): result = get_model_from_path_string(Restaurant, 'waiter__name') @@ -87,7 +87,7 @@ def test_get_direct_fields_from_model(self): self.assertTrue('description' in names) self.assertTrue('distinct' in names) self.assertTrue('id' in names) - self.assertEquals(len(names), 9) + self.assertEqual(len(names), 9) def test_get_fields(self): """ Test GetFieldsMixin.get_fields """ @@ -102,8 +102,8 @@ def test_get_gfk_fields_from_model(self): def test_get_properties_from_model(self): properties = get_properties_from_model(DisplayField) - self.assertEquals(properties[0]['label'], 'choices') - self.assertEquals(properties[1]['label'], 'choices_dict') + self.assertEqual(properties[0]['label'], 'choices') + self.assertEqual(properties[1]['label'], 'choices_dict') def test_filter_property(self): # Not a very complete test - only tests one type of filter @@ -113,7 +113,7 @@ def test_filter_property(self): def test_custom_global_model_manager(self): """ test for custom global model manager """ if getattr(settings, 'REPORT_BUILDER_MODEL_MANAGER', False): - self.assertEquals( + self.assertEqual( self.report._get_model_manager(), settings.REPORT_BUILDER_MODEL_MANAGER) @@ -131,4 +131,4 @@ def test_custom_model_manager(self): # coverage of get_query objects = self.report.get_query() # expect custom manager to return correct object with filters - self.assertEquals(objects[0], self.report) + self.assertEqual(objects[0], self.report) diff --git a/report_builder/tests/test_views.py b/report_builder/tests/test_views.py index 66b15ef1..daffdff1 100644 --- a/report_builder/tests/test_views.py +++ b/report_builder/tests/test_views.py @@ -34,7 +34,7 @@ def test_email_report_with_template(self): email_report(report_url, user) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, email_subject) - self.assertEqual(mail.outbox[0].alternatives[0][0], "

Hello {0},

\n
\n

The report is here

".format(username, report_url)) + self.assertEqual(mail.outbox[0].alternatives[0][0], "

Hello {},

\n
\n

The report is here

".format(username, report_url)) settings.REPORT_BUILDER_EMAIL_NOTIFICATION = None settings.REPORT_BUILDER_EMAIL_TEMPLATE = None mail.outbox = [] diff --git a/report_builder/tests/tests.py b/report_builder/tests/tests.py index cc686b7a..761be2fe 100644 --- a/report_builder/tests/tests.py +++ b/report_builder/tests/tests.py @@ -237,7 +237,7 @@ def test_report_builder_understands_empty_string(self): response = self.client.put(f'/report_builder/api/report/{report.id}/', data=json.dumps(data), content_type='application/json', - HTTP_X_REQUESTED_WWITH='XMLHttpRequest') + headers={"x-requested-wwith": 'XMLHttpRequest'}) self.assertEqual(response.status_code, 200) self.assertIsNone(report.displayfield_set.all()[0].sort) @@ -646,7 +646,7 @@ def test_filter_datetime_lte_filter(self): generate_url = reverse('generate_report', args=[people_report.id]) response = self.client.get(generate_url) # filter from 4 to 2 people - self.assertEquals(len(response.data['data']), 3) + self.assertEqual(len(response.data['data']), 3) # TimeField ff.field = 'hammer_time' @@ -654,7 +654,7 @@ def test_filter_datetime_lte_filter(self): ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # DateTimeField ff.field = 'birth_date' @@ -662,7 +662,7 @@ def test_filter_datetime_lte_filter(self): ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 2) + self.assertEqual(len(response.data['data']), 2) @freeze_time("2017-11-01 12:00:00") def test_filter_datetime_range(self): @@ -690,7 +690,7 @@ def test_filter_datetime_range(self): generate_url = reverse('generate_report', args=[people_report.id]) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # TimeField ff.field = 'hammer_time' @@ -699,7 +699,7 @@ def test_filter_datetime_range(self): ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # DateTimeField ff.field = 'birth_date' @@ -708,7 +708,7 @@ def test_filter_datetime_range(self): ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) @freeze_time("2017-11-01 12:00:00") def test_filter_datetime_relative_range(self): @@ -734,14 +734,14 @@ def test_filter_datetime_relative_range(self): generate_url = reverse('generate_report', args=[people_report.id]) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # DateField w/ partial day ff.filter_delta = self.day * -7 + 5 ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # TimeField w/ hour delta ff.field = 'hammer_time' @@ -749,14 +749,14 @@ def test_filter_datetime_relative_range(self): ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 2) + self.assertEqual(len(response.data['data']), 2) # TimeField w/ sec delta ff.filter_delta = -5 ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # DateTimeField w/ full day ff.field = 'birth_date' @@ -764,14 +764,14 @@ def test_filter_datetime_relative_range(self): ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 2) + self.assertEqual(len(response.data['data']), 2) # # DateTimeField w/ partial day ff.filter_delta = (self.day + self.hour) * -1 ff.save() response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) def test_filter_datefield_relative_range_over_time(self): """ @@ -793,12 +793,12 @@ def test_filter_datefield_relative_range_over_time(self): filter_delta=self.day * -16, ) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 3) + self.assertEqual(len(response.data['data']), 3) # login again 10 days later frozen_today.move_to(ten_days_later) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) def test_filter_timefield_relative_range_over_time(self): """ @@ -821,12 +821,12 @@ def test_filter_timefield_relative_range_over_time(self): filter_delta=self.hour * -10, ) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # login 4 hours later frozen_today.move_to(four_hours_later_today) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 2) + self.assertEqual(len(response.data['data']), 2) def test_filter_datetimefield_relative_range_over_time(self): """ @@ -849,12 +849,12 @@ def test_filter_datetimefield_relative_range_over_time(self): ) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 1) + self.assertEqual(len(response.data['data']), 1) # # login one month later frozen_today.move_to(one_month_later) response = self.client.get(generate_url) - self.assertEquals(len(response.data['data']), 2) + self.assertEqual(len(response.data['data']), 2) def test_groupby_id(self): self.make_people() diff --git a/report_builder/unique_slugify.py b/report_builder/unique_slugify.py index 505b0508..cdb08e29 100644 --- a/report_builder/unique_slugify.py +++ b/report_builder/unique_slugify.py @@ -38,11 +38,11 @@ def unique_slugify(instance, value, slug_field_name='slug', queryset=None, next = 2 while not slug or queryset.filter(**{slug_field_name: slug}): slug = original_slug - end = '%s%s' % (slug_separator, next) + end = '{}{}'.format(slug_separator, next) if slug_len and len(slug) + len(end) > slug_len: slug = slug[:slug_len - len(end)] slug = _slug_strip(slug, slug_separator) - slug = '%s%s' % (slug, end) + slug = '{}{}'.format(slug, end) next += 1 setattr(instance, slug_field.attname, slug) @@ -69,5 +69,5 @@ def _slug_strip(value, separator='-'): if separator: if separator != '-': re_sep = re.escape(separator) - value = re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value) + value = re.sub(r'^{}+|{}+$'.format(re_sep, re_sep), '', value) return value diff --git a/report_builder/urls.py b/report_builder/urls.py index e592da76..72bec9a6 100644 --- a/report_builder/urls.py +++ b/report_builder/urls.py @@ -1,6 +1,6 @@ from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required -from django.urls import re_path, include +from django.urls import path, include from rest_framework import routers from . import views @@ -14,28 +14,45 @@ router.register(r'contenttypes', api_views.ContentTypeViewSet) urlpatterns = [ - re_path(r'^report/(?P\d+)/download_file/$', views.DownloadFileView.as_view(), name="report_download_file"), - re_path(r'^report/(?P\d+)/download_file/(?P.+)/$', views.DownloadFileView.as_view(), + path('report//download_file/', views.DownloadFileView.as_view(), name="report_download_file"), + path('report//download_file//', views.DownloadFileView.as_view(), name="report_download_file"), - re_path(r'^report/(?P\d+)/check_status/(?P.+)/$', views.check_status, name="report_check_status"), - re_path(r'^report/(?P\d+)/add_star/$', views.ajax_add_star, name="ajax_add_star"), - re_path(r'^report/(?P\d+)/create_copy/$', views.create_copy, name="report_builder_create_copy"), - re_path(r'^export_to_report/$', views.ExportToReport.as_view(), name="export_to_report"), - re_path(r'^api/', include(router.urls)), - re_path(r'^api/config/', api_views.ConfigView.as_view()), - re_path(r'^api/api-auth/', include('rest_framework.urls', namespace='rest_framework')), - re_path(r'^api/related_fields', staff_member_required(api_views.RelatedFieldsView.as_view()), - name="related_fields"), - re_path(r'^api/fields', staff_member_required(api_views.FieldsView.as_view()), name="fields"), - re_path(r'^api/report/(?P\w+)/generate/', staff_member_required(api_views.GenerateReport.as_view()), - name="generate_report"), - re_path(r'^api/report/(?P\d+)/download_file/(?P.+)/$', views.DownloadFileView.as_view(), + path('report//check_status//', views.check_status, name="report_check_status"), + path('report//add_star/', views.ajax_add_star, name="ajax_add_star"), + path('report//create_copy/', views.create_copy, name="report_builder_create_copy"), + path('export_to_report/', views.ExportToReport.as_view(), name="export_to_report"), + path('api/', include(router.urls)), + path( + 'api/config/', + api_views.ConfigView.as_view(), + name="config", + ), + path('api/api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path( + 'api/related_fields/', + staff_member_required(api_views.RelatedFieldsView.as_view()), + name="related_fields", + ), + path( + 'api/fields/', + staff_member_required(api_views.FieldsView.as_view()), + name="fields", + ), + path( + 'api/report//generate/', + staff_member_required(api_views.GenerateReport.as_view()), + name="generate_report" + ), + path('api/report//download_file//', views.DownloadFileView.as_view(), name="report_download_file"), - re_path(r'^api/report/(?P\d+)/check_status/(?P.+)/$', views.check_status, name="report_check_status"), - re_path('^report/(?P\d+)/$', views.ReportSPAView.as_view(), name="report_update_view"), + path('api/report//check_status//', views.check_status, name="report_check_status"), + path('report//', views.ReportSPAView.as_view(), name="report_update_view"), ] if not hasattr(settings, 'REPORT_BUILDER_FRONTEND') or settings.REPORT_BUILDER_FRONTEND: urlpatterns += [ - re_path(r'^', staff_member_required(views.ReportSPAView.as_view()), name="report_builder"), - ] + path( + '', + staff_member_required(views.ReportSPAView.as_view()), + name="report_builder", + ), ] diff --git a/report_builder/views.py b/report_builder/views.py index 7fc11923..d06241a5 100644 --- a/report_builder/views.py +++ b/report_builder/views.py @@ -23,7 +23,7 @@ class ReportSPAView(TemplateView): template_name = "report_builder/spa.html" def get_context_data(self, **kwargs): - context = super(ReportSPAView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['ASYNC_REPORT'] = getattr( settings, 'REPORT_BUILDER_ASYNC_REPORT', False ) @@ -35,7 +35,7 @@ def fieldset_string_to_field(fieldset_dict, model): fieldset_dict['fields'] = list(fieldset_dict['fields']) i = 0 for dict_field in fieldset_dict['fields']: - if isinstance(dict_field, string_types): + if isinstance(dict_field, str): fieldset_dict['fields'][i] = model._meta.get_field_by_name( dict_field)[0] elif isinstance(dict_field, list) or isinstance(dict_field, tuple): @@ -58,7 +58,7 @@ class DownloadFileView(DataExportMixin, View): @method_decorator(staff_member_required) def dispatch(self, *args, **kwargs): - return super(DownloadFileView, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) def process_report(self, report_id, user_id, file_type, to_response, queryset=None): @@ -106,7 +106,7 @@ def create_copy(request, pk): """ Copy a report including related fields """ report = get_object_or_404(Report, pk=pk) new_report = duplicate(report, changes=( - ('name', '{0} (copy)'.format(report.name)), + ('name', '{} (copy)'.format(report.name)), ('user_created', request.user), ('user_modified', request.user), )) @@ -133,7 +133,7 @@ class ExportToReport(DownloadFileView, TemplateView): template_name = "report_builder/export_to_report.html" def get_context_data(self, **kwargs): - ctx = super(ExportToReport, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) ctx['admin_url'] = self.request.GET.get('admin_url', '/') ct = ContentType.objects.get_for_id(self.request.GET['ct']) ids = self.request.GET['ids'].split(',') diff --git a/report_builder_demo/__init__.py b/report_builder_demo/__init__.py index 43459b18..fa002332 100644 --- a/report_builder_demo/__init__.py +++ b/report_builder_demo/__init__.py @@ -1,3 +1,2 @@ # flake8: noqa -from __future__ import absolute_import from .celery import app as celery_app diff --git a/report_builder_demo/celery.py b/report_builder_demo/celery.py index 325bb63e..25db39f0 100644 --- a/report_builder_demo/celery.py +++ b/report_builder_demo/celery.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import os from celery import Celery diff --git a/report_builder_demo/demo_models/migrations/0001_initial.py b/report_builder_demo/demo_models/migrations/0001_initial.py index 07216569..c3cfa3bf 100644 --- a/report_builder_demo/demo_models/migrations/0001_initial.py +++ b/report_builder_demo/demo_models/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0002_bar_foos.py b/report_builder_demo/demo_models/migrations/0002_bar_foos.py index 17f4ca89..ad6e6b9d 100644 --- a/report_builder_demo/demo_models/migrations/0002_bar_foos.py +++ b/report_builder_demo/demo_models/migrations/0002_bar_foos.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0003_auto_20150419_2110.py b/report_builder_demo/demo_models/migrations/0003_auto_20150419_2110.py index 40169468..67e68070 100644 --- a/report_builder_demo/demo_models/migrations/0003_auto_20150419_2110.py +++ b/report_builder_demo/demo_models/migrations/0003_auto_20150419_2110.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0004_bar_date_field.py b/report_builder_demo/demo_models/migrations/0004_bar_date_field.py index 5a284380..690d7907 100644 --- a/report_builder_demo/demo_models/migrations/0004_bar_date_field.py +++ b/report_builder_demo/demo_models/migrations/0004_bar_date_field.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0004_waiter_days_worked.py b/report_builder_demo/demo_models/migrations/0004_waiter_days_worked.py index 52331563..2de4556b 100644 --- a/report_builder_demo/demo_models/migrations/0004_waiter_days_worked.py +++ b/report_builder_demo/demo_models/migrations/0004_waiter_days_worked.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0005_auto_20150622_1840.py b/report_builder_demo/demo_models/migrations/0005_auto_20150622_1840.py index 4bdc1526..dd865674 100644 --- a/report_builder_demo/demo_models/migrations/0005_auto_20150622_1840.py +++ b/report_builder_demo/demo_models/migrations/0005_auto_20150622_1840.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0006_account.py b/report_builder_demo/demo_models/migrations/0006_account.py index 631f4d08..6bf3c6d6 100644 --- a/report_builder_demo/demo_models/migrations/0006_account.py +++ b/report_builder_demo/demo_models/migrations/0006_account.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import djmoney.models.fields diff --git a/report_builder_demo/demo_models/migrations/0006_merge.py b/report_builder_demo/demo_models/migrations/0006_merge.py index c1801c2d..cc28ca86 100644 --- a/report_builder_demo/demo_models/migrations/0006_merge.py +++ b/report_builder_demo/demo_models/migrations/0006_merge.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0007_auto_20150712_1752.py b/report_builder_demo/demo_models/migrations/0007_auto_20150712_1752.py index 21e8232b..16f3c639 100644 --- a/report_builder_demo/demo_models/migrations/0007_auto_20150712_1752.py +++ b/report_builder_demo/demo_models/migrations/0007_auto_20150712_1752.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0008_merge.py b/report_builder_demo/demo_models/migrations/0008_merge.py index fedf8e3d..d636df80 100644 --- a/report_builder_demo/demo_models/migrations/0008_merge.py +++ b/report_builder_demo/demo_models/migrations/0008_merge.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/report_builder_demo/demo_models/migrations/0009_auto_20151209_2136.py b/report_builder_demo/demo_models/migrations/0009_auto_20151209_2136.py index 4fc3295b..c77870d6 100644 --- a/report_builder_demo/demo_models/migrations/0009_auto_20151209_2136.py +++ b/report_builder_demo/demo_models/migrations/0009_auto_20151209_2136.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import djmoney.models.fields diff --git a/report_builder_demo/demo_models/migrations/0010_adding_sample_datefields.py b/report_builder_demo/demo_models/migrations/0010_adding_sample_datefields.py index 635d0e37..5a7e82dd 100644 --- a/report_builder_demo/demo_models/migrations/0010_adding_sample_datefields.py +++ b/report_builder_demo/demo_models/migrations/0010_adding_sample_datefields.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.1 on 2017-11-21 03:46 -from __future__ import unicode_literals from django.db import migrations, models import djmoney.models.fields diff --git a/report_builder_demo/demo_models/models.py b/report_builder_demo/demo_models/models.py index 3c7169ea..235200bd 100644 --- a/report_builder_demo/demo_models/models.py +++ b/report_builder_demo/demo_models/models.py @@ -83,7 +83,7 @@ class Waiter(models.Model): days_worked = models.IntegerField(blank=True, null=True, default=None) def __str__(self): - return "%s the waiter at %s" % (self.name, self.restaurant) + return "{} the waiter at {}".format(self.name, self.restaurant) class Person(models.Model): diff --git a/report_builder_demo/settings.py b/report_builder_demo/settings.py index c7000aa6..2450b83d 100644 --- a/report_builder_demo/settings.py +++ b/report_builder_demo/settings.py @@ -81,7 +81,6 @@ USE_I18N = True -USE_L10N = True USE_TZ = True diff --git a/report_builder_demo/urls.py b/report_builder_demo/urls.py index 8662123c..9e924b88 100644 --- a/report_builder_demo/urls.py +++ b/report_builder_demo/urls.py @@ -1,15 +1,14 @@ from django.conf import settings -from django.conf.urls import include from django.conf.urls.static import static from django.contrib import admin -from django.urls import re_path +from django.urls import include, path admin.autodiscover() urlpatterns = [ - re_path(r'^admin/', admin.site.urls), - re_path(r'^report_builder/', include('report_builder_scheduled.urls')), - re_path(r'^report_builder/', include('report_builder.urls')), + path('admin/', admin.site.urls), + path('report_builder/', include('report_builder_scheduled.urls')), + path('report_builder/', include('report_builder.urls')), ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/report_builder_scheduled/migrations/0001_initial.py b/report_builder_scheduled/migrations/0001_initial.py index 92a6500a..e3a0dbab 100644 --- a/report_builder_scheduled/migrations/0001_initial.py +++ b/report_builder_scheduled/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.1 on 2017-09-15 20:46 -from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models diff --git a/report_builder_scheduled/tests.py b/report_builder_scheduled/tests.py index f628d6c0..b2f5a7c5 100644 --- a/report_builder_scheduled/tests.py +++ b/report_builder_scheduled/tests.py @@ -15,7 +15,7 @@ IS_D18 = False -if django.VERSION[0] is 1 and django.VERSION[1] is 8: +if django.VERSION[0] is 1 and django.VERSION[1] == 8: IS_D18 = True diff --git a/report_builder_scheduled/urls.py b/report_builder_scheduled/urls.py index 2a603305..fc306864 100644 --- a/report_builder_scheduled/urls.py +++ b/report_builder_scheduled/urls.py @@ -1,7 +1,7 @@ -from django.urls import re_path +from django.urls import path from .views import run_scheduled_report urlpatterns = [ - re_path(r'^report/(?P\d+)/run_scheduled_report/$', run_scheduled_report, name="run_scheduled_report"), + path('report//run_scheduled_report/', run_scheduled_report, name="run_scheduled_report"), ] From a655953e5dd24fb7b0c43162ee129b84e7c981c1 Mon Sep 17 00:00:00 2001 From: mustafaulker Date: Wed, 18 Dec 2024 18:29:24 +0300 Subject: [PATCH 3/4] Format the whole project Ran pre-commit & Ruff - Create pre-commit and Ruff conf. files --- .pre-commit-config.yaml | 27 ++++ manage.py | 1 + report_builder/admin.py | 19 +-- report_builder/api/serializers.py | 78 +++++++--- report_builder/api/tests.py | 5 +- report_builder/api/views.py | 190 ++++++++++++----------- report_builder/email.py | 27 ++-- report_builder/mixins.py | 110 ++++++------- report_builder/models.py | 237 ++++++++++++++--------------- report_builder/tasks.py | 3 +- report_builder/tests/test_utils.py | 44 +++--- report_builder/tests/test_views.py | 10 +- report_builder/tests/tests.py | 216 +++++++++++++------------- report_builder/unique_slugify.py | 15 +- report_builder/urls.py | 22 ++- report_builder/utils.py | 66 ++++---- report_builder/views.py | 87 +++++------ report_builder_scheduled/admin.py | 2 +- report_builder_scheduled/models.py | 44 ++++-- report_builder_scheduled/tasks.py | 10 +- report_builder_scheduled/tests.py | 7 +- report_builder_scheduled/urls.py | 1 + report_builder_scheduled/views.py | 2 +- ruff.toml | 82 ++++++++++ setup.py | 5 +- 25 files changed, 758 insertions(+), 552 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..66c538fe --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +default_language_version: + python: python3.12 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v5.0.0 + hooks: + - id: check-merge-conflict + + - repo: https://github.com/astral-sh/ruff-pre-commit.git + rev: v0.8.3 + hooks: + - id: ruff + args: [ --fix, --unsafe-fixes ] + - id: ruff-format + args: [ -- ] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.19.1 + hooks: + - id: pyupgrade + + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.22.2 + hooks: + - id: django-upgrade + args: [ --target-version, "5.0" ] \ No newline at end of file diff --git a/manage.py b/manage.py index 9277547f..e3281a50 100755 --- a/manage.py +++ b/manage.py @@ -2,6 +2,7 @@ import os import sys + if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "report_builder_demo.settings") diff --git a/report_builder/admin.py b/report_builder/admin.py index 93c2687b..62794b16 100644 --- a/report_builder/admin.py +++ b/report_builder/admin.py @@ -7,7 +7,7 @@ from django.urls import reverse from django.utils.safestring import mark_safe -from report_builder.models import Report, Format +from report_builder.models import Format, Report class StarredFilter(SimpleListFilter): @@ -15,9 +15,7 @@ class StarredFilter(SimpleListFilter): parameter_name = 'starred' def lookups(self, request, model_admin): - return ( - ('Starred', 'Starred Reports'), - ) + return (('Starred', 'Starred Reports'),) def queryset(self, request, queryset): if self.value() == 'Starred': @@ -38,7 +36,9 @@ class ReportAdmin(admin.ModelAdmin): 'download_xlsx', 'copy_report', ) - readonly_fields = ['slug', ] + readonly_fields = [ + 'slug', + ] fields = ['name', 'description', 'root_model', 'slug'] search_fields = ('name', 'description') list_filter = (StarredFilter, 'root_model', 'created', 'modified', 'root_model__app_label') @@ -72,17 +72,18 @@ def changelist_view(self, request, extra_context=None): return super().changelist_view(request, extra_context=extra_context) @admin.display( - description="Starred" + description="Starred", ) def ajax_starred(self, obj): if obj.starred.filter(id=self.user.id): - img = static('report_builder/img/star.png') else: img = static('report_builder/img/unstar.png') return mark_safe( ''.format( - reverse('ajax_add_star', args=[obj.id]), img) + reverse('ajax_add_star', args=[obj.id]), + img, + ), ) def save_model(self, request, obj, form, change): @@ -111,7 +112,7 @@ def export_to_report(modeladmin, request, queryset): selected.append(str(s)) ct = ContentType.objects.get_for_model(queryset.model) return HttpResponseRedirect( - reverse('export_to_report') + "?ct={}&admin_url={}&ids={}".format(ct.pk, admin_url, ",".join(selected)) + reverse('export_to_report') + "?ct={}&admin_url={}&ids={}".format(ct.pk, admin_url, ",".join(selected)), ) diff --git a/report_builder/api/serializers.py b/report_builder/api/serializers.py index dec96254..c7169a8e 100644 --- a/report_builder/api/serializers.py +++ b/report_builder/api/serializers.py @@ -5,7 +5,8 @@ from django.db import transaction from rest_framework import serializers -from report_builder.models import Report, DisplayField, FilterField, Format +from report_builder.models import DisplayField, FilterField, Format, Report + User = get_user_model() @@ -19,12 +20,26 @@ class Meta: class DisplayFieldSerializer(serializers.ModelSerializer): class Meta: model = DisplayField - fields = ('id', 'path', 'path_verbose', 'field', 'field_verbose', - 'name', 'sort', 'sort_reverse', 'width', 'aggregate', - 'position', 'total', 'group', 'report', 'display_format', - 'field_type') + fields = ( + 'id', + 'path', + 'path_verbose', + 'field', + 'field_verbose', + 'name', + 'sort', + 'sort_reverse', + 'width', + 'aggregate', + 'position', + 'total', + 'group', + 'report', + 'display_format', + 'field_type', + ) read_only_fields = ('id',) - + def to_internal_value(self, data): if data.get('sort') == '': data['sort'] = None @@ -32,7 +47,8 @@ def to_internal_value(self, data): class NonStrictCharField(serializers.CharField): - """ Allow booleans to be turned into strings instead of error """ + """Allow booleans to be turned into strings instead of error""" + def to_internal_value(self, value): if value is True: return "True" @@ -44,9 +60,21 @@ def to_internal_value(self, value): class FilterFieldSerializer(serializers.ModelSerializer): class Meta: model = FilterField - fields = ('id', 'path', 'path_verbose', 'field', 'field_verbose', - 'field_type', 'filter_type', 'filter_value', 'filter_value2', - 'exclude', 'position', 'report', 'filter_delta') + fields = ( + 'id', + 'path', + 'path_verbose', + 'field', + 'field_verbose', + 'field_type', + 'filter_type', + 'filter_value', + 'filter_value2', + 'exclude', + 'position', + 'report', + 'filter_delta', + ) read_only_fields = ('id', 'field_type') filter_value = NonStrictCharField(allow_blank=True) @@ -65,15 +93,13 @@ class Meta: class ReportSerializer(serializers.HyperlinkedModelSerializer): - root_model = serializers.PrimaryKeyRelatedField( - queryset=Report.allowed_models()) + root_model = serializers.PrimaryKeyRelatedField(queryset=Report.allowed_models()) root_model_name = serializers.StringRelatedField(source='root_model') user_created = UserSerializer(read_only=True) class Meta: model = Report - fields = ('id', 'name', 'modified', 'root_model', 'root_model_name', - 'user_created') + fields = ('id', 'name', 'modified', 'root_model', 'root_model_name', 'user_created') class ReportNestedSerializer(ReportSerializer): @@ -85,10 +111,20 @@ class ReportNestedSerializer(ReportSerializer): class Meta: model = Report fields = ( - 'id', 'name', 'description', 'modified', 'root_model', - 'root_model_name', 'displayfield_set', 'distinct', 'user_created', - 'user_modified', 'filterfield_set', 'report_file', - 'report_file_creation') + 'id', + 'name', + 'description', + 'modified', + 'root_model', + 'root_model_name', + 'displayfield_set', + 'distinct', + 'user_created', + 'user_modified', + 'filterfield_set', + 'report_file', + 'report_file_creation', + ) read_only_fields = ('report_file', 'report_file_creation') def validate(self, data): @@ -107,10 +143,8 @@ def update(self, instance, validated_data): with transaction.atomic(): instance.name = validated_data.get('name', instance.name) - instance.description = validated_data.get( - 'description', instance.description) - instance.distinct = validated_data.get( - 'distinct', instance.distinct) + instance.description = validated_data.get('description', instance.description) + instance.distinct = validated_data.get('distinct', instance.distinct) instance.modified = datetime.date.today() instance.save() instance.displayfield_set.all().delete() diff --git a/report_builder/api/tests.py b/report_builder/api/tests.py index 384b414b..6915c330 100644 --- a/report_builder/api/tests.py +++ b/report_builder/api/tests.py @@ -1,11 +1,11 @@ from django.contrib.auth import get_user_model from django.test import TestCase from model_bakery import baker -from rest_framework.test import APIRequestFactory -from rest_framework.test import force_authenticate +from rest_framework.test import APIRequestFactory, force_authenticate from .views import ContentTypeViewSet + User = get_user_model() @@ -17,6 +17,5 @@ def test_content_viewset(self): request = factory.get('/report_builder/api/contenttypes/') force_authenticate(request, user=user) response = view(request) - print(response.data) self.assertEqual(response.status_code, 200) self.assertTrue(response.data, "should return some content types") diff --git a/report_builder/api/views.py b/report_builder/api/views.py index f54570f0..07ac6c44 100644 --- a/report_builder/api/views.py +++ b/report_builder/api/views.py @@ -11,35 +11,41 @@ from rest_framework.response import Response from rest_framework.views import APIView -from .serializers import ( - ReportNestedSerializer, ReportSerializer, FormatSerializer, - FilterFieldSerializer, ContentTypeSerializer) -from ..mixins import GetFieldsMixin, DataExportMixin -from ..models import Report, Format, FilterField, get_allowed_models +from ..mixins import DataExportMixin, GetFieldsMixin +from ..models import FilterField, Format, Report, get_allowed_models from ..utils import duplicate +from .serializers import ( + ContentTypeSerializer, + FilterFieldSerializer, + FormatSerializer, + ReportNestedSerializer, + ReportSerializer, +) def find_exact_position(fields_list, item): current_position = 0 for i in fields_list: - if (i.name == item.name and - i.get_internal_type() == item.get_internal_type()): + if i.name == item.name and i.get_internal_type() == item.get_internal_type(): return current_position current_position += 1 return -1 class ReportBuilderViewMixin: - """ Set up explicit settings so that project defaults - don't interfer with report builder's api. """ + """Set up explicit settings so that project defaults + don't interfer with report builder's api. + """ + permission_classes = (IsAdminUser,) pagination_class = None + class ConfigView(ReportBuilderViewMixin, APIView): def get(self, request): data = { - 'async_report': getattr( settings, 'REPORT_BUILDER_ASYNC_REPORT', False ), - 'formats': FormatSerializer(Format.objects.all(), many=True).data + 'async_report': getattr(settings, 'REPORT_BUILDER_ASYNC_REPORT', False), + 'formats': FormatSerializer(Format.objects.all(), many=True).data, } return JsonResponse(data) @@ -55,9 +61,10 @@ class FilterFieldViewSet(ReportBuilderViewMixin, viewsets.ModelViewSet): class ContentTypeViewSet(ReportBuilderViewMixin, viewsets.ReadOnlyModelViewSet): - """ Read only view of content types. + """Read only view of content types. Used to populate choices for new report root model. """ + queryset = get_allowed_models() serializer_class = ContentTypeSerializer @@ -80,11 +87,14 @@ def perform_update(self, serializer): @action(methods=['post'], detail=True) def copy_report(self, request, pk=None): report = self.get_object() - new_report = duplicate(report, changes=( - ('name', '{} (copy)'.format(report.name)), - ('user_created', request.user), - ('user_modified', request.user), - )) + new_report = duplicate( + report, + changes=( + ('name', f'{report.name} (copy)'), + ('user_created', request.user), + ('user_modified', request.user), + ), + ) # duplicate does not get related for display in report.displayfield_set.all(): @@ -101,11 +111,10 @@ def copy_report(self, request, pk=None): serializer = ReportNestedSerializer(new_report) return JsonResponse(serializer.data) - - class RelatedFieldsView(ReportBuilderViewMixin, GetFieldsMixin, APIView): - """ Get related fields from an ORM model """ + """Get related fields from an ORM model""" + def get_data_from_request(self, request): self.model = request.data['model'] self.path = request.data['path'] @@ -119,7 +128,8 @@ def post(self, request): self.model_class, self.field, self.path, - self.path_verbose,) + self.path_verbose, + ) result = [] for new_field in new_fields: included_model = True @@ -134,42 +144,44 @@ def post(self, request): app_label = split_name[0] model_name = split_name[1] if getattr(settings, 'REPORT_BUILDER_INCLUDE', False): - includes = getattr(settings, 'REPORT_BUILDER_INCLUDE') + includes = settings.REPORT_BUILDER_INCLUDE # If it is not included as 'foo' and not as 'demo_models.foo' - if (model_name not in includes and - model_information not in includes): + if model_name not in includes and model_information not in includes: included_model = False if getattr(settings, 'REPORT_BUILDER_EXCLUDE', False): - excludes = getattr(settings, 'REPORT_BUILDER_EXCLUDE') + excludes = settings.REPORT_BUILDER_EXCLUDE # If it is excluded as 'foo' and as 'demo_models.foo' - if (model_name in excludes or model_information in excludes): + if model_name in excludes or model_information in excludes: included_model = False verbose_name = getattr(new_field, 'verbose_name', None) if verbose_name is None: verbose_name = new_field.get_accessor_name() - result += [{ - 'field_name': new_field.field_name, - 'verbose_name': verbose_name, - 'path': path, - 'help_text': getattr(new_field, 'help_text', ''), - 'model_id': model_ct.id, - 'parent_model_name': model_name, - 'parent_model_app_label': app_label, - 'included_model': included_model, - }] + result += [ + { + 'field_name': new_field.field_name, + 'verbose_name': verbose_name, + 'path': path, + 'help_text': getattr(new_field, 'help_text', ''), + 'model_id': model_ct.id, + 'parent_model_name': model_name, + 'parent_model_app_label': app_label, + 'included_model': included_model, + }, + ] return Response(result) class FieldsView(RelatedFieldsView): - """ Get direct fields and properties on an ORM model - """ + """Get direct fields and properties on an ORM model""" + def post(self, request): self.get_data_from_request(request) field_data = self.get_fields( self.model_class, self.field, self.path, - self.path_verbose,) + self.path_verbose, + ) # External packages might cause duplicates. This clears it up new_set = [] @@ -196,7 +208,7 @@ def post(self, request): if field.name not in fields: index = find_exact_position( field_data['fields'], - field + field, ) if index != -1: field_data['fields'].pop(index) @@ -205,7 +217,7 @@ def post(self, request): if field.name in exclude: index = find_exact_position( field_data['fields'], - field + field, ) if index != -1: field_data['fields'].pop(index) @@ -216,20 +228,20 @@ def post(self, request): verbose_name = getattr(new_field, 'verbose_name', None) if not verbose_name: verbose_name = new_field.get_accessor_name() - result += [{ - 'name': new_field.name, - 'field': new_field.name, - 'field_verbose': verbose_name, - 'field_type': new_field.get_internal_type(), - 'is_default': True if defaults is None or - new_field.name in defaults else False, - 'field_choices': new_field.choices, - 'can_filter': True if filters is None or - new_field.name in filters else False, - 'path': field_data['path'], - 'path_verbose': field_data['path_verbose'], - 'help_text': new_field.help_text, - }] + result += [ + { + 'name': new_field.name, + 'field': new_field.name, + 'field_verbose': verbose_name, + 'field_type': new_field.get_internal_type(), + 'is_default': True if defaults is None or new_field.name in defaults else False, + 'field_choices': new_field.choices, + 'can_filter': True if filters is None or new_field.name in filters else False, + 'path': field_data['path'], + 'path_verbose': field_data['path_verbose'], + 'help_text': new_field.help_text, + }, + ] # Add properties if fields is not None or extra is not None: if fields and extra: @@ -240,41 +252,41 @@ def post(self, request): extra_fields = extra for field in extra_fields: field_attr = getattr(field_data['model'], field, None) - if isinstance(field_attr, (property, cached_property)): - result += [{ - 'name': field, - 'field': field, - 'field_verbose': field, - 'field_type': 'Property', - 'field_choices': None, - 'can_filter': True if filters is None or - field in filters else False, - 'path': field_data['path'], - 'path_verbose': field_data['path_verbose'], - 'is_default': True if defaults is None or - field in defaults else False, - 'help_text': 'Adding this property will ' - 'significantly increase the time it takes to run a ' - 'report.' - }] + if isinstance(field_attr, property | cached_property): + result += [ + { + 'name': field, + 'field': field, + 'field_verbose': field, + 'field_type': 'Property', + 'field_choices': None, + 'can_filter': True if filters is None or field in filters else False, + 'path': field_data['path'], + 'path_verbose': field_data['path_verbose'], + 'is_default': True if defaults is None or field in defaults else False, + 'help_text': 'Adding this property will ' + 'significantly increase the time it takes to run a ' + 'report.', + }, + ] # Add custom fields custom_fields = field_data.get('custom_fields', None) if custom_fields: for field in custom_fields: - result += [{ - 'name': field.name, - 'field': field.name, - 'field_verbose': field.name, - 'field_type': 'Custom Field', - 'field_choices': getattr(field, 'choices', None), - 'can_filter': True if filters is None or - field.name in filters else False, - 'path': field_data['path'], - 'path_verbose': field_data['path_verbose'], - 'is_default': True if defaults is None or - field.name in defaults else False, - 'help_text': 'This is a custom field.', - }] + result += [ + { + 'name': field.name, + 'field': field.name, + 'field_verbose': field.name, + 'field_type': 'Custom Field', + 'field_choices': getattr(field, 'choices', None), + 'can_filter': True if filters is None or field.name in filters else False, + 'path': field_data['path'], + 'path_verbose': field_data['path_verbose'], + 'is_default': True if defaults is None or field.name in defaults else False, + 'help_text': 'This is a custom field.', + }, + ] return Response(result) @@ -287,9 +299,9 @@ def post(self, request, report_id=None): objects_list = report.report_to_list( user=request.user, - preview=True,) - display_fields = report.get_good_display_fields().values_list( - 'name', flat=True) + preview=True, + ) + display_fields = report.get_good_display_fields().values_list('name', flat=True) response = { 'data': objects_list, 'meta': {'titles': display_fields}, diff --git a/report_builder/email.py b/report_builder/email.py index 1cb43060..e81c1a3e 100644 --- a/report_builder/email.py +++ b/report_builder/email.py @@ -1,13 +1,14 @@ from django.conf import settings -from django.core.mail import send_mail, EmailMultiAlternatives +from django.core.mail import EmailMultiAlternatives, send_mail from django.template.loader import get_template def email_report(report_url, user=None, email=None): - if ((getattr(settings, 'EMAIL_BACKEND', False) or - getattr(settings, 'EMAIL_HOST', False)) and - getattr(settings, 'DEFAULT_FROM_EMAIL', False)): - + if (getattr(settings, 'EMAIL_BACKEND', False) or getattr(settings, 'EMAIL_HOST', False)) and getattr( + settings, + 'DEFAULT_FROM_EMAIL', + False, + ): name = None if user: email = user.email @@ -16,27 +17,25 @@ def email_report(report_url, user=None, email=None): if get_template('email/email_report.html'): email_template = get_template('email/email_report.html') msg = EmailMultiAlternatives( - getattr(settings, 'REPORT_BUILDER_EMAIL_SUBJECT', False) or - 'Report is ready', + getattr(settings, 'REPORT_BUILDER_EMAIL_SUBJECT', False) or 'Report is ready', report_url, - getattr(settings, 'DEFAULT_FROM_EMAIL'), + settings.DEFAULT_FROM_EMAIL, [email], ) - htmlParameters = { + html_parameters = { 'name': name, 'report': report_url, } msg.attach_alternative( - email_template.render(htmlParameters), - "text/html" + email_template.render(html_parameters), + "text/html", ) msg.send() else: send_mail( - getattr(settings, 'REPORT_BUILDER_EMAIL_SUBJECT', False) or - 'Report is ready', + getattr(settings, 'REPORT_BUILDER_EMAIL_SUBJECT', False) or 'Report is ready', str(report_url), - getattr(settings, 'DEFAULT_FROM_EMAIL'), + settings.DEFAULT_FROM_EMAIL, [email], fail_silently=True, ) diff --git a/report_builder/mixins.py b/report_builder/mixins.py index 27d171f7..0081ced0 100644 --- a/report_builder/mixins.py +++ b/report_builder/mixins.py @@ -9,7 +9,7 @@ from tempfile import NamedTemporaryFile from django.contrib.contenttypes.models import ContentType -from django.db.models import Avg, Count, Sum, Max, Min +from django.db.models import Avg, Count, Max, Min, Sum from django.db.models.fields.related_descriptors import ManyToManyDescriptor from django.http import HttpResponse from openpyxl.styles import Font @@ -17,13 +17,12 @@ from openpyxl.workbook import Workbook from .utils import ( - get_relation_fields_from_model, - get_properties_from_model, + get_custom_fields_from_model, get_direct_fields_from_model, get_model_from_path_string, - get_custom_fields_from_model, + get_properties_from_model, + get_relation_fields_from_model, ) -from datetime import datetime DisplayField = namedtuple( @@ -35,7 +34,7 @@ def generate_filename(title, ends_with): title = title.split('.')[0] title.replace(' ', '_') - title += ('_' + datetime.now().strftime("%m%d_%H%M")) + title += '_' + datetime.datetime.now().strftime("%m%d_%H%M") if not title.endswith(ends_with): title += ends_with return title @@ -78,37 +77,33 @@ def build_sheet(self, data, ws, sheet_name='report', header=None, widths=None): ws.append(['Unknown Error']) def build_xlsx_response(self, wb, title="report"): - """ Take a workbook and return an xlsx file response """ + """Take a workbook and return an xlsx file response""" title = generate_filename(title, '.xlsx') with NamedTemporaryFile() as tmp: wb.save(tmp.name) tmp.seek(0) stream = tmp.read() stream_size = tmp.tell() - response = HttpResponse( - stream, - content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') - response['Content-Disposition'] = 'attachment; filename=%s' % title + response = HttpResponse(stream, content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + response['Content-Disposition'] = f'attachment; filename={title}' response['Content-Length'] = stream_size return response def build_csv_response(self, wb, title="report"): - """ Take a workbook and return a csv file response """ + """Take a workbook and return a csv file response""" title = generate_filename(title, '.csv') myfile = StringIO() sh = wb.active c = csv.writer(myfile) for r in sh.rows: c.writerow([cell.value for cell in r]) - response = HttpResponse( - myfile.getvalue(), - content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=%s' % title + response = HttpResponse(myfile.getvalue(), content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename={title}' response['Content-Length'] = myfile.tell() return response def list_to_workbook(self, data, title='report', header=None, widths=None): - """ Create just a openpxl workbook from a list of data """ + """Create just a openpxl workbook from a list of data""" wb = Workbook() title = re.sub(r'\W+', '', title)[:30] @@ -118,8 +113,7 @@ def list_to_workbook(self, data, title='report', header=None, widths=None): if i > 0: wb.create_sheet() ws = wb.worksheets[i] - self.build_sheet( - sheet_data, ws, sheet_name=sheet_name, header=header) + self.build_sheet(sheet_data, ws, sheet_name=sheet_name, header=header) i += 1 else: ws = wb.worksheets[0] @@ -136,7 +130,7 @@ def list_to_xlsx_file(self, data, title, header=None, widths=None): for row in data: cleaned_row = [] for value in row: - if isinstance(value, datetime): + if isinstance(value, datetime.datetime): value = value.replace(tzinfo=None) cleaned_row.append(value) ws.append(cleaned_row) @@ -147,8 +141,7 @@ def list_to_xlsx_file(self, data, title, header=None, widths=None): return file_buffer def list_to_csv_file(self, data, title='report', header=None, widths=None): - """ Make a list into a csv response for download. - """ + """Make a list into a csv response for download.""" wb = self.list_to_workbook(data, title, header, widths) if not title.endswith('.csv'): title += '.csv' @@ -159,25 +152,26 @@ def list_to_csv_file(self, data, title='report', header=None, widths=None): c.writerow([cell.value for cell in r]) return myfile - def list_to_xlsx_response(self, data, title='report', header=None, - widths=None): - """ Make 2D list into a xlsx response for download + def list_to_xlsx_response(self, data, title='report', header=None, widths=None): + """Make 2D list into a xlsx response for download data can be a 2d array or a dict of 2d arrays like {'sheet_1': [['A1', 'B1']]} """ wb = self.list_to_workbook(data, title, header, widths) return self.build_xlsx_response(wb, title=title) - def list_to_csv_response(self, data, title='report', header=None, - widths=None): - """ Make 2D list into a csv response for download data. - """ + def list_to_csv_response(self, data, title='report', header=None, widths=None): + """Make 2D list into a csv response for download data.""" wb = self.list_to_workbook(data, title, header, widths) return self.build_csv_response(wb, title=title) def add_aggregates(self, queryset, display_fields): agg_funcs = { - 'Avg': Avg, 'Min': Min, 'Max': Max, 'Count': Count, 'Sum': Sum + 'Avg': Avg, + 'Min': Min, + 'Max': Max, + 'Count': Count, + 'Sum': Sum, } for display_field in display_fields: @@ -188,8 +182,8 @@ def add_aggregates(self, queryset, display_fields): return queryset - def report_to_list(self, queryset, display_fields, user=None, property_filters=[], preview=False): - """ Create list from a report with all data filtering. + def report_to_list(self, queryset, display_fields, user=None, property_filters=None, preview=False): + """Create list from a report with all data filtering. queryset: initial queryset to generate results display_fields: list of field references or DisplayField models user: requesting user. If left as None - there will be no permission check @@ -197,11 +191,14 @@ def report_to_list(self, queryset, display_fields, user=None, property_filters=[ preview: return only first 50 rows Returns list, message in case of issues. """ + if property_filters is None: + property_filters = [] model_class = queryset.model def can_change_or_view(model): - """ Return True iff `user` has either change or view permission - for `model`. """ + """Return True iff `user` has either change or view permission + for `model`. + """ if user is None: return True model_name = model._meta.model_name @@ -230,9 +227,19 @@ def can_change_or_view(model): new_model = get_model_from_path_string(model_class, path) model_field = new_model._meta.get_field_by_name(field)[0] choices = model_field.choices - new_display_fields.append(DisplayField( - path, '', field, '', '', None, None, choices, '' - )) + new_display_fields.append( + DisplayField( + path, + '', + field, + '', + '', + None, + None, + choices, + '', + ), + ) display_fields = new_display_fields @@ -289,12 +296,10 @@ def can_change_or_view(model): display_totals[display_field_key] = Decimal(0) else: - message += 'Error: Permission denied on access to {}.'.format( - display_field.name - ) + message += f'Error: Permission denied on access to {display_field.name}.' def increment_total(display_field_key, val): - """ Increment display total by `val` if given `display_field_key` in + """Increment display total by `val` if given `display_field_key` in `display_totals`. """ if display_field_key in display_totals: @@ -314,7 +319,7 @@ def increment_total(display_field_key, val): display_field_paths.insert(0, 'pk') m2m_relations = [] - for position, property_path in property_list.items(): + for _position, property_path in property_list.items(): property_root = property_path.split('__')[0] root_class = model_class @@ -323,17 +328,14 @@ def increment_total(display_field_key, val): except AttributeError: # django-hstore schema compatibility continue - if type(property_root_class) == ManyToManyDescriptor: - display_field_paths.insert(1, '%s__pk' % property_root) + if type(property_root_class) is ManyToManyDescriptor: + display_field_paths.insert(1, f'{property_root}__pk') m2m_relations.append(property_root) if group: values = objects.values(*group) values = self.add_aggregates(values, display_fields) - filtered_report_rows = [ - [row[field] for field in display_field_paths] - for row in values - ] + filtered_report_rows = [[row[field] for field in display_field_paths] for row in values] for row in filtered_report_rows: for pos, field in enumerate(display_field_paths): increment_total(field, row[pos]) @@ -364,7 +366,7 @@ def increment_total(display_field_key, val): val = None else: if property_filter.field_type == 'Custom Field': - for relation in property_filter.path.split('__'): + for _relation in property_filter.path.split('__'): if hasattr(obj, root_relation): obj = getattr(obj, root_relation) val = obj.get_custom_value(property_filter.field) @@ -418,7 +420,7 @@ def increment_total(display_field_key, val): defaults = { None: str, datetime.date: lambda: datetime.date(datetime.MINYEAR, 1, 1), - datetime: lambda: datetime(datetime.MINYEAR, 1, 1), + datetime: lambda: datetime.datetime(datetime.MINYEAR, 1, 1), } # Order sort fields in reverse order so that ascending, descending @@ -496,7 +498,7 @@ def formatter(value, style): if display_totals: display_totals_row = [] - fields_and_properties = list(display_field_paths[0 if group else 1:]) + fields_and_properties = list(display_field_paths[0 if group else 1 :]) for position, value in property_list.items(): fields_and_properties.insert(position, value) @@ -510,7 +512,7 @@ def formatter(value, style): display_totals_row[pos] = formatter(display_totals_row[pos], style) values_and_properties_list.append( - ['TOTALS'] + (len(fields_and_properties) - 1) * [''] + ['TOTALS'] + (len(fields_and_properties) - 1) * [''], ) values_and_properties_list.append(display_totals_row) @@ -526,7 +528,7 @@ def sort_helper(self, value, default): class GetFieldsMixin: def get_fields(self, model_class, field_name='', path='', path_verbose=''): - """ Get fields and meta data from a model + """Get fields and meta data from a model :param model_class: A django model class :param field_name: The field name to get sub fields from :param path: path of our field in format @@ -585,7 +587,7 @@ def get_fields(self, model_class, field_name='', path='', path_verbose=''): } def get_related_fields(self, model_class, field_name, path="", path_verbose=""): - """ Get fields for a given model """ + """Get fields for a given model""" if field_name: field = model_class._meta.get_field(field_name) direct = field.concrete diff --git a/report_builder/models.py b/report_builder/models.py index 9cded8f7..be476ee0 100644 --- a/report_builder/models.py +++ b/report_builder/models.py @@ -1,33 +1,29 @@ import datetime import re import time +import zipfile from decimal import Decimal from functools import reduce +from io import BytesIO from dateutil import parser from django import forms from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError, ObjectDoesNotExist, FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError from django.core.files.base import ContentFile from django.db import models -from django.db.models import Avg, Min, Max, Count, Sum, F +from django.db.models import Avg, Count, F, Max, Min, Sum from django.templatetags.static import static from django.urls import reverse from django.utils.functional import cached_property from django.utils.safestring import mark_safe from report_builder.unique_slugify import unique_slugify + from .email import email_report -from .mixins import generate_filename, DataExportMixin -from .utils import ( - get_model_from_path_string, - sort_data, - increment_total, - formatter -) -import zipfile -from io import BytesIO +from .mixins import DataExportMixin, generate_filename +from .utils import formatter, get_model_from_path_string, increment_total, sort_data AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') @@ -64,42 +60,32 @@ def get_limit_choices_to_callable(): class Report(models.Model): - """ A saved report with queryset and descriptive fields - """ - - def _get_model_manager(self): - """ Get manager from settings else use objects - """ - model_manager = 'objects' - if getattr(settings, 'REPORT_BUILDER_MODEL_MANAGER', False): - model_manager = settings.REPORT_BUILDER_MODEL_MANAGER - return model_manager - - @staticmethod - def allowed_models(): - return get_allowed_models() + """A saved report with queryset and descriptive fields""" name = models.CharField(max_length=255) 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, - on_delete=models.CASCADE) + root_model = models.ForeignKey(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, - on_delete=models.SET_NULL) + user_created = models.ForeignKey(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", on_delete=models.SET_NULL) + AUTH_USER_MODEL, + editable=False, + blank=True, + null=True, + 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) starred = models.ManyToManyField( - AUTH_USER_MODEL, blank=True, + AUTH_USER_MODEL, + blank=True, help_text="These users have starred this report for easy reference.", - related_name="report_starred_set") + related_name="report_starred_set", + ) def __str__(self): return self.name @@ -109,13 +95,30 @@ def save(self, *args, **kwargs): unique_slugify(self, self.name) super().save(*args, **kwargs) + def get_absolute_url(self): + return reverse("report_update_view", args=(self.id,)) + + def _get_model_manager(self): + """Get manager from settings else use objects""" + model_manager = 'objects' + if getattr(settings, 'REPORT_BUILDER_MODEL_MANAGER', False): + model_manager = settings.REPORT_BUILDER_MODEL_MANAGER + return model_manager + + @staticmethod + def allowed_models(): + return get_allowed_models() + def add_aggregates(self, queryset, display_fields=None): agg_funcs = { - 'Avg': Avg, 'Min': Min, 'Max': Max, 'Count': Count, 'Sum': Sum + 'Avg': Avg, + 'Min': Min, + 'Max': Max, + 'Count': Count, + 'Sum': Sum, } if display_fields is None: - display_fields = self.displayfield_set.filter( - aggregate__isnull=False) + display_fields = self.displayfield_set.filter(aggregate__isnull=False) for display_field in display_fields: if display_field.aggregate: func = agg_funcs[display_field.aggregate] @@ -129,12 +132,11 @@ def root_model_class(self): return self.root_model.model_class() def get_field_type(self, field_name, path=""): - """ Get field type for given field name. + """Get field type for given field name. Field_name is the full path of the field path is optional """ - model = get_model_from_path_string( - self.root_model_class, path + field_name) + model = get_model_from_path_string(self.root_model_class, path + field_name) # Is it an ORM field? try: @@ -143,7 +145,7 @@ def get_field_type(self, field_name, path=""): pass # Is it a property? field_attr = getattr(model, field_name, None) - if isinstance(field_attr, (property, cached_property)): + if isinstance(field_attr, property | cached_property): return "Property" # Is it a custom field? try: @@ -154,7 +156,7 @@ def get_field_type(self, field_name, path=""): return "Invalid" def get_good_display_fields(self): - """ Returns only valid display fields """ + """Returns only valid display fields""" display_fields = self.displayfield_set.all() bad_display_fields = [] for display_field in display_fields: @@ -190,9 +192,7 @@ def report_to_list(self, queryset=None, user=None, preview=False): insert_property_indexes.append(i) else: if display_field.aggregate: - display_field_paths += [ - display_field.field_key + - '__' + display_field.aggregate.lower()] + display_field_paths += [display_field.field_key + '__' + display_field.aggregate.lower()] else: display_field_paths += [display_field.field_key] i += 1 @@ -206,12 +206,8 @@ def report_to_list(self, queryset=None, user=None, preview=False): choice_lists[display_field.position] = choice_list # Build display format list - if ( - hasattr(display_field, 'display_format') and - display_field.display_format - ): - display_formats[display_field.position] = \ - display_field.display_format + if hasattr(display_field, 'display_format') and display_field.display_format: + display_formats[display_field.position] = display_field.display_format property_filters = [] for filter_field in self.filterfield_set.all(): @@ -286,7 +282,7 @@ def report_to_list(self, queryset=None, user=None, preview=False): break for display_field in display_fields.filter( - sort__gt=0 + sort__gt=0, ).order_by('-sort'): data_list = sort_data(data_list, display_field) @@ -304,8 +300,8 @@ def report_to_list(self, queryset=None, user=None, preview=False): display_totals_row[pos] = formatter(display_totals_row[pos], style) data_list += [ - ['TOTALS'] + (len(display_fields) - 1) * [''] - ] + [display_totals_row] + ['TOTALS'] + (len(display_fields) - 1) * [''], + ] + [display_totals_row] return data_list @@ -315,7 +311,7 @@ def get_query(self): # Check for report_builder_model_manger property on the model if getattr(model_class, 'report_builder_model_manager', False): - objects = getattr(model_class, 'report_builder_model_manager').all() + objects = model_class.report_builder_model_manager.all() else: # Get global model manager manager = report._get_model_manager() @@ -344,8 +340,7 @@ def get_query(self): filter_string += '__' + fs # Check for special types such as isnull - if (filter_field.filter_type == "isnull" and - filter_field.filter_value in ["0", "False"]): + if filter_field.filter_type == "isnull" and filter_field.filter_value in ["0", "False"]: filter_ = {filter_string: False} elif filter_field.filter_type == "in": filter_ = {filter_string: filter_field.filter_value.split(',')} @@ -372,11 +367,7 @@ def get_query(self): for filter_field in report.filterfield_set.order_by('position'): if filter_field.filter_type in ('max', 'min'): func = {'max': Max, 'min': Min}[filter_field.filter_type] - column_name = '{}{}__{}'.format( - filter_field.path, - filter_field.field, - filter_field.field_type - ) + column_name = f'{filter_field.path}{filter_field.field}__{filter_field.field_type}' filter_string = filter_field.path + filter_field.field annotate_args = {column_name: func(filter_string)} filter_args = {column_name: F(filter_field.field)} @@ -391,15 +382,12 @@ def get_query(self): return objects - def get_absolute_url(self): - return reverse("report_update_view", args=(self.id,)) - def edit(self): return mark_safe( ''.format( self.get_absolute_url(), - static('report_builder/img/edit.svg') - ) + static('report_builder/img/edit.svg'), + ), ) def download_xlsx(self): @@ -407,15 +395,15 @@ def download_xlsx(self): return mark_safe( ''.format( self.id, - static('report_builder/img/download.svg') - ) + static('report_builder/img/download.svg'), + ), ) else: return mark_safe( ''.format( reverse('report_download_file', args=[self.id]), - static('report_builder/img/download.svg') - ) + static('report_builder/img/download.svg'), + ), ) download_xlsx.short_description = "Download" @@ -424,15 +412,14 @@ def copy_report(self): return mark_safe( ''.format( reverse('report_builder_create_copy', args=[self.id]), - static('report_builder/img/copy.svg') - ) + static('report_builder/img/copy.svg'), + ), ) copy_report.short_description = "Copy" def check_report_display_field_positions(self): - """ After a report is saved, make sure positions are sane - """ + """After a report is saved, make sure positions are sane""" for i, display_field in enumerate(self.displayfield_set.all()): if display_field.position != i + 1: display_field.position = i + 1 @@ -452,15 +439,22 @@ def async_report_save(self, chunks, title, header, widths, user=None, file_type= single_chunk = chunks[0] if file_type == 'csv': csv_file = data_export.list_to_csv_file( - single_chunk, title, header, widths + single_chunk, + title, + header, + widths, ) file_name = generate_filename(title, '.csv') self.report_file.save( - file_name, ContentFile(csv_file.getvalue().encode()) + file_name, + ContentFile(csv_file.getvalue().encode()), ) elif file_type == 'xlsx': xlsx_file = data_export.list_to_xlsx_file( - single_chunk, title, header, widths + single_chunk, + title, + header, + widths, ) file_name = generate_filename(title, '.xlsx') self.report_file.save(file_name, ContentFile(xlsx_file.read())) @@ -474,12 +468,18 @@ def async_report_save(self, chunks, title, header, widths, user=None, file_type= chunk_title = f'{title}_part{index + 1}.{file_type}' if file_type == 'csv': csv_file = data_export.list_to_csv_file( - chunk, chunk_title, header, widths + chunk, + chunk_title, + header, + widths, ) zip_file.writestr(chunk_title, csv_file.getvalue().encode()) elif file_type == 'xlsx': xlsx_file = data_export.list_to_xlsx_file( - chunk, chunk_title, header, widths + chunk, + chunk_title, + header, + widths, ) zip_file.writestr(chunk_title, xlsx_file.read()) zip_filename = f'{title}.zip' @@ -497,10 +497,8 @@ def async_report_save(self, chunks, title, header, widths, user=None, file_type= def chunk_data(data, chunk_size): for i in range(0, len(data), chunk_size): yield data[i : i + chunk_size] - - def run_report(self, file_type, user=None, queryset=None, asynchronous=False, scheduled=False, - email_to: str = None): + def run_report(self, file_type, user=None, queryset=None, asynchronous=False, scheduled=False, email_to: str = None): """Generate this report file""" if not queryset: queryset = self.get_query() @@ -508,18 +506,17 @@ def run_report(self, file_type, user=None, queryset=None, asynchronous=False, sc display_fields = self.get_good_display_fields() data_export = DataExportMixin() - objects_list, message = data_export.report_to_list( - queryset, display_fields, user, preview=False) + objects_list, message = data_export.report_to_list(queryset, display_fields, user, preview=False) title = re.sub(r'\W+', '', self.name)[:30] header = [] widths = [] for field in display_fields: header.append(field.name) widths.append(field.width) - + chunk_size = 1000000 chunks = list(self.chunk_data(objects_list, chunk_size)) - + if scheduled: self.async_report_save(chunks, title, header, widths, user, file_type) elif asynchronous: @@ -528,20 +525,20 @@ def run_report(self, file_type, user=None, queryset=None, asynchronous=False, sc self.async_report_save(chunks, title, header, widths, user, file_type) else: if file_type == 'csv': - return data_export.list_to_csv_response( - objects_list, title, header, widths) + return data_export.list_to_csv_response(objects_list, title, header, widths) else: - return data_export.list_to_xlsx_response( - objects_list, title, header, widths) + return data_export.list_to_xlsx_response(objects_list, title, header, widths) class Format(models.Model): - """ A specifies a Python string format for e.g. `DisplayField`s. - """ + """A specifies a Python string format for e.g. `DisplayField`s.""" + name = models.CharField(max_length=50, blank=True, default='') string = models.CharField( - max_length=300, blank=True, default='', - help_text='Python string format. Ex ${} would place a $ in front of the result.' + max_length=300, + blank=True, + default='', + help_text='Python string format. Ex ${} would place a $ in front of the result.', ) def __str__(self): @@ -566,20 +563,19 @@ def field_type(self): @property def field_key(self): - """ This key can be passed to a Django ORM values_list """ + """This key can be passed to a Django ORM values_list""" return self.path + self.field @property def choices(self): if self.pk: - model = get_model_from_path_string( - self.report.root_model.model_class(), self.path) + model = get_model_from_path_string(self.report.root_model.model_class(), self.path) return self.get_choices(model, self.field) class DisplayField(AbstractField): - """ A display field to show in a report. Always belongs to a Report - """ + """A display field to show in a report. Always belongs to a Report""" + name = models.CharField(max_length=2000) sort = models.IntegerField(blank=True, null=True) sort_reverse = models.BooleanField(verbose_name="Reverse", default=False) @@ -593,12 +589,14 @@ class DisplayField(AbstractField): ('Max', 'Max'), ('Min', 'Min'), ), - blank=True + blank=True, ) total = models.BooleanField(default=False) group = models.BooleanField(default=False) - display_format = models.ForeignKey(Format, blank=True, null=True, - on_delete=models.SET_NULL) + display_format = models.ForeignKey(Format, blank=True, null=True, on_delete=models.SET_NULL) + + def __str__(self): + return self.name def get_choices(self, model, field_name): try: @@ -617,9 +615,6 @@ def choices_dict(self): choices_dict.update({choice[0]: choice[1]}) return choices_dict - def __str__(self): - return self.name - class FilterField(AbstractField): """ @@ -660,6 +655,9 @@ class FilterField(AbstractField): filter_value2 = models.CharField(max_length=2000, blank=True) exclude = models.BooleanField(default=False) + def __str__(self): + return self.field + def clean(self): dt_types = ['DateField', 'DateTimeField', 'TimeField'] @@ -670,13 +668,12 @@ def clean(self): # field type if self.filter_type == 'relative_range' and self.field_type not in dt_types: raise ValidationError( - 'Relative Range filtering is only currently supported for' - ' the following field types: {}.'.format(dt_types)) + 'Relative Range filtering is only currently supported for' f' the following field types: {dt_types}.', + ) # Check for required relative range filter_delta if self.filter_type == 'relative_range' and self.filter_delta is None: - raise ValidationError( - 'Relative Range filters must have value and delta inputs.') + raise ValidationError('Relative Range filters must have value and delta inputs.') if self.filter_type in ('max', 'min'): # These filter types ignore their value. @@ -711,12 +708,12 @@ def get_choices(self, model, field_name): return model_field.choices def filter_property(self, value): - """ Determine if passed value should be filtered or not """ + """Determine if passed value should be filtered or not""" filter_field = self filter_type = filter_field.filter_type filter_value = filter_field.filter_value filtered = True - WEEKDAY_INTS = { + weekday_ints = { 'monday': 0, 'tuesday': 1, 'wednesday': 2, @@ -761,7 +758,7 @@ def filter_property(self, value): filtered = False if filter_type == 'range' and value in [int(x) for x in filter_value]: filtered = False - if filter_type == 'week_day' and WEEKDAY_INTS.get(str(filter_value).lower()) == value.weekday: + if filter_type == 'week_day' and weekday_ints.get(str(filter_value).lower()) == value.weekday: filtered = False if filter_type == 'isnull' and value is None: filtered = False @@ -781,7 +778,8 @@ def get_relative_range(self): With: self.filter_type = 'relative_range' self.filter_delta = -60 * 60 * 24 * 2 (i.e. -2 days) - Return: + + Return: # a 'negative' two day range from filter_value ["2017-01-01", "2017-01-03"] """ @@ -789,8 +787,7 @@ def get_relative_range(self): if self.field_type == 'DateField': if abs(self.filter_delta) < day: - raise ValidationError( - 'DateField delta must be at least 1 day.') + raise ValidationError('DateField delta must be at least 1 day.') first = datetime.date.today() second = first + datetime.timedelta(seconds=self.filter_delta) output_range = sorted([first, second]) @@ -820,9 +817,5 @@ def field_type(self): @property def choices(self): if self.pk: - model = get_model_from_path_string( - self.report.root_model.model_class(), self.path) + model = get_model_from_path_string(self.report.root_model.model_class(), self.path) return self.get_choices(model, self.field) - - def __str__(self): - return self.field diff --git a/report_builder/tasks.py b/report_builder/tasks.py index 3e62efb6..29a3de24 100644 --- a/report_builder/tasks.py +++ b/report_builder/tasks.py @@ -3,7 +3,8 @@ @shared_task def report_builder_file_async_report_save(report_id, user_id, file_type): - """ Start a report task """ + """Start a report task""" from .views import DownloadFileView + view = DownloadFileView() view.process_report(report_id, user_id, file_type, to_response=False) diff --git a/report_builder/tests/test_utils.py b/report_builder/tests/test_utils.py index c7a4e9a1..96e64f7a 100644 --- a/report_builder/tests/test_utils.py +++ b/report_builder/tests/test_utils.py @@ -1,14 +1,17 @@ -from django.contrib.contenttypes.models import ContentType from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from ..utils import ( - get_properties_from_model, get_direct_fields_from_model, - get_relation_fields_from_model, get_model_from_path_string) +from report_builder_demo.demo_models.models import Bar, Comment, Place, Restaurant, Waiter + from ..mixins import GetFieldsMixin -from ..models import Report, DisplayField, FilterField -from report_builder_demo.demo_models.models import ( - Bar, Restaurant, Waiter, Comment, Place) +from ..models import DisplayField, FilterField, Report +from ..utils import ( + get_direct_fields_from_model, + get_model_from_path_string, + get_properties_from_model, + get_relation_fields_from_model, +) class RelationUtilityFunctionTests(TestCase): @@ -46,18 +49,16 @@ def test_get_relation_fields_from_model_does_not_change_field_name(self): class UtilityFunctionTests(TestCase): - def setUp(self): self.report_ct = ContentType.objects.get_for_model(Report) - self.report = Report.objects.create( - name="foo report", - root_model=self.report_ct) + self.report = Report.objects.create(name="foo report", root_model=self.report_ct) self.filter_field = FilterField.objects.create( report=self.report, field="X", field_verbose="stuff", filter_type='contains', - filter_value='Lots of spam') + filter_value='Lots of spam', + ) def get_fields_names(self, fields): return [field.name for field in fields] @@ -79,7 +80,6 @@ def test_get_model_from_path_string_one_to_one(self): result = get_model_from_path_string(Restaurant, 'place__serves_pizza') self.assertEqual(result, Place) - def test_get_direct_fields_from_model(self): fields = get_direct_fields_from_model(Report) names = self.get_fields_names(fields) @@ -90,7 +90,7 @@ def test_get_direct_fields_from_model(self): self.assertEqual(len(names), 9) def test_get_fields(self): - """ Test GetFieldsMixin.get_fields """ + """Test GetFieldsMixin.get_fields""" obj = GetFieldsMixin() obj.get_fields( Bar, @@ -98,7 +98,7 @@ def test_get_fields(self): ) def test_get_gfk_fields_from_model(self): - fields = get_direct_fields_from_model(Comment) + get_direct_fields_from_model(Comment) def test_get_properties_from_model(self): properties = get_properties_from_model(DisplayField) @@ -111,18 +111,16 @@ def test_filter_property(self): self.assertTrue(result) def test_custom_global_model_manager(self): - """ test for custom global model manager """ + """Test for custom global model manager""" if getattr(settings, 'REPORT_BUILDER_MODEL_MANAGER', False): - self.assertEqual( - self.report._get_model_manager(), - settings.REPORT_BUILDER_MODEL_MANAGER) + self.assertEqual(self.report._get_model_manager(), settings.REPORT_BUILDER_MODEL_MANAGER) def test_custom_model_manager(self): - """ test for custom model manager """ + """Test for custom model manager""" if getattr( - self.report.root_model.model_class(), - 'report_builder_model_manager', - True + self.report.root_model.model_class(), + 'report_builder_model_manager', + True, ): # change setup to use actual field and value self.filter_field.field = 'name' diff --git a/report_builder/tests/test_views.py b/report_builder/tests/test_views.py index daffdff1..ac1839d3 100644 --- a/report_builder/tests/test_views.py +++ b/report_builder/tests/test_views.py @@ -1,12 +1,11 @@ -from django.contrib.auth import get_user_model from django.conf import settings +from django.contrib.auth import get_user_model from django.core import mail from django.test import TestCase from django.test.utils import override_settings - from model_bakery import baker -from report_builder.tasks import report_builder_file_async_report_save +from report_builder.tasks import report_builder_file_async_report_save from ..email import email_report @@ -34,7 +33,10 @@ def test_email_report_with_template(self): email_report(report_url, user) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, email_subject) - self.assertEqual(mail.outbox[0].alternatives[0][0], "

Hello {},

\n
\n

The report is here

".format(username, report_url)) + self.assertEqual( + mail.outbox[0].alternatives[0][0], + f"

Hello {username},

\n
\n

The report is here

", + ) settings.REPORT_BUILDER_EMAIL_NOTIFICATION = None settings.REPORT_BUILDER_EMAIL_TEMPLATE = None mail.outbox = [] diff --git a/report_builder/tests/tests.py b/report_builder/tests/tests.py index 761be2fe..bf4b9017 100644 --- a/report_builder/tests/tests.py +++ b/report_builder/tests/tests.py @@ -2,7 +2,8 @@ import json import time import unittest -from datetime import date, datetime, timedelta, time as dtime +from datetime import date, datetime, timedelta +from datetime import time as dtime from io import StringIO from django.conf import settings @@ -16,22 +17,10 @@ from rest_framework.test import APIClient from report_builder.api.serializers import ReportNestedSerializer -from report_builder_demo.demo_models.models import ( - Bar, - Place, - Restaurant, - Waiter, - Person, - Child -) -from ..models import ( - Report, - DisplayField, - FilterField, - Format, - get_allowed_models, - get_limit_choices_to_callable -) +from report_builder_demo.demo_models.models import Bar, Child, Person, Place, Restaurant, Waiter + +from ..models import DisplayField, FilterField, Format, Report, get_allowed_models, get_limit_choices_to_callable + User = get_user_model() @@ -73,9 +62,7 @@ def test_get_allowed_models_for_include(self): def test_get_allowed_models_for_exclude(self): pre_exclude_duplicates = find_duplicates_in_contexttype() - settings.REPORT_BUILDER_EXCLUDE = ( - 'demo_second_app.bar', - ) + settings.REPORT_BUILDER_EXCLUDE = ('demo_second_app.bar',) post_exclude_duplicates = find_duplicates_in_contexttype() settings.REPORT_BUILDER_EXCLUDE = None self.assertEqual(pre_exclude_duplicates, ['bar']) @@ -109,7 +96,8 @@ def test_report_builder_fields(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": ""}) + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'char_field') self.assertNotContains(response, 'char_field2') @@ -118,10 +106,8 @@ def test_report_builder_fields_from_related(self): ct = ContentType.objects.get(model="place", app_label="demo_models") response = self.client.post( '/report_builder/api/fields/', - {"model": ct.id, - "path": "", - "path_verbose": "", - "field": "restaurant"}) + {"model": ct.id, "path": "", "path_verbose": "", "field": "restaurant"}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'pizza') @@ -129,10 +115,8 @@ 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"}) + {"model": ct.id, "path": "", "path_verbose": "", "field": "foos"}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'char_field') self.assertNotContains(response, 'char_field2') @@ -141,10 +125,8 @@ 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"}) + {"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') @@ -153,10 +135,8 @@ def test_report_builder_fields_from_related_fields(self): ct = ContentType.objects.get(model="place", app_label="demo_models") response = self.client.post( '/report_builder/api/related_fields/', - {"model": ct.id, - "path": "", - "path_verbose": "", - "field": "restaurant"}) + {"model": ct.id, "path": "", "path_verbose": "", "field": "restaurant"}, + ) self.assertContains(response, '"parent_model_name"') self.assertContains(response, '"parent_model_app_label"') self.assertContains(response, '"included_model"') @@ -166,7 +146,8 @@ def test_report_builder_exclude(self): ct = ContentType.objects.get(model="fooexclude", app_label="demo_models") response = self.client.post( '/report_builder/api/fields/', - {"model": ct.id, "path": "", "path_verbose": "", "field": ""}) + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'char_field') self.assertNotContains(response, 'char_field2') @@ -175,7 +156,7 @@ def test_report_builder_extra(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": ""} + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'char_field') @@ -186,7 +167,8 @@ def test_report_builder_is_default(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": ""}) + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'is_default') @@ -194,7 +176,8 @@ def test_report_builder_choices(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": ""}) + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'field_choices') self.assertContains(response, '[["CH","CHECK"],["MA","CHECKMATE"]]') @@ -203,7 +186,8 @@ def test_report_builder_can_filter(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": ""}) + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, + ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'can_filter') for field in response.data: @@ -215,35 +199,37 @@ def test_report_builder_can_filter(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": ""}) + {"model": ct.id, "path": "", "path_verbose": "", "field": ""}, + ) for field in response.data: self.assertEqual(field['can_filter'], True) def test_report_builder_understands_empty_string(self): ct = ContentType.objects.get_for_model(Report) - report = Report.objects.create( - name="foo report", - root_model=ct) + report = Report.objects.create(name="foo report", root_model=ct) - display_field = DisplayField.objects.create( + DisplayField.objects.create( name='foo', report=report, field="X", field_verbose="stuff", sort=None, - position=1) + position=1, + ) data = ReportNestedSerializer(report).data data['displayfield_set'][0]['sort'] = '' - response = self.client.put(f'/report_builder/api/report/{report.id}/', - data=json.dumps(data), - content_type='application/json', - headers={"x-requested-wwith": 'XMLHttpRequest'}) + response = self.client.put( + f'/report_builder/api/report/{report.id}/', + data=json.dumps(data), + content_type='application/json', + headers={"x-requested-wwith": 'XMLHttpRequest'}, + ) self.assertEqual(response.status_code, 200) self.assertIsNone(report.displayfield_set.all()[0].sort) -class ReportTests(TestCase): +class ReportTests(TestCase): def setUp(self): user = User.objects.get_or_create(username='testy')[0] user.is_staff = True @@ -279,7 +265,7 @@ def test_property_position(self): report_list = self.report.report_to_list(self.report.get_query()) self.assertEqual( report_list[0], - [bar.i_want_char_field, bar.i_need_char_field] + [bar.i_want_char_field, bar.i_need_char_field], ) def test_property_and_field_position(self): @@ -309,7 +295,7 @@ def test_property_and_field_position(self): report_list = self.report.report_to_list(self.report.get_query()) self.assertEqual( report_list[0], - [bar.char_field, bar.i_want_char_field, bar.i_need_char_field, bar.char_field] + [bar.char_field, bar.i_want_char_field, bar.i_need_char_field, bar.char_field], ) def test_property_display(self): @@ -403,8 +389,9 @@ def make_lots_of_foos(self): bar.foos.create(char_field="a") def test_performance(self): - """ Test getting a report with ORM and property fields. - Provides baseline on performance testing. """ + """Test getting a report with ORM and property fields. + Provides baseline on performance testing. + """ self.make_lots_of_foos() DisplayField.objects.create( report=self.report, @@ -419,13 +406,13 @@ def test_performance(self): start = time.time() response = self.client.get(self.generate_url) run_time = time.time() - start - print('report builder report time is {}'.format(run_time)) self.assertEqual(response.status_code, 200) self.assertLess(run_time, 1.0) def test_performance_filter(self): - """ Test getting a report with ORM and property fields. - Provides baseline on performance testing. """ + """Test getting a report with ORM and property fields. + Provides baseline on performance testing. + """ self.make_lots_of_foos() DisplayField.objects.create( report=self.report, @@ -452,7 +439,6 @@ def test_performance_filter(self): start = time.time() response = self.client.get(self.generate_url) run_time = time.time() - start - print('report builder report time is {}'.format(run_time)) self.assertEqual(response.status_code, 200) self.assertLess(run_time, 1.0) @@ -470,10 +456,12 @@ def make_tiny_town(self): for row in data: place = Place.objects.create(name=row[0], address=row[1]) restaurant = Restaurant.objects.create( - place=place, serves_hot_dogs=row[2], serves_pizza=row[3] + place=place, + serves_hot_dogs=row[2], + serves_pizza=row[3], ) - for count in range(row[4]): + for _count in range(row[4]): days = None if not (total % 3 | total % 2) else total % 3 Waiter.objects.create( @@ -485,7 +473,7 @@ def make_tiny_town(self): total += 1 def test_total_accounting(self): - """ Test accounting total fields. + """Test accounting total fields. Nullable fields should be totalled as 0. """ self.make_tiny_town() @@ -532,33 +520,52 @@ def make_people(self): color (CharField) """ people = ( - ('John', 'Doe', ( - ('Will', 'Doe', 5, 'R'), - ('James', 'Doe', 8, ''), - ('Robert', 'Doe', 3, 'G'), - ), date.today() - timedelta(seconds=self.day * 5), + ( + 'John', + 'Doe', + ( + ('Will', 'Doe', 5, 'R'), + ('James', 'Doe', 8, ''), + ('Robert', 'Doe', 3, 'G'), + ), + date.today() - timedelta(seconds=self.day * 5), datetime.today() - timedelta(seconds=self.day), - dtime(hour=12)), - ('Maria', 'Smith', ( - ('Susan', 'Smith', 1, 'Y'), - ('Karen', 'Smith', 4, 'B'), - ), date.today() - timedelta(seconds=self.day * 10), + dtime(hour=12), + ), + ( + 'Maria', + 'Smith', + ( + ('Susan', 'Smith', 1, 'Y'), + ('Karen', 'Smith', 4, 'B'), + ), + date.today() - timedelta(seconds=self.day * 10), datetime.today() - timedelta(seconds=self.day * 30), - dtime(hour=16)), - ('Donald', 'King', ( - ('Charles', 'King', None, ''), - ('Helen', 'King', 7, 'G'), - ('Mark', 'King', 2, 'Y'), - ('Karen', 'King', 4, 'R'), - ('Larry', 'King', 5, 'R'), - ('Lisa', 'King', 3, 'R'), - ), datetime.today() - timedelta(seconds=self.day * 15), + dtime(hour=16), + ), + ( + 'Donald', + 'King', + ( + ('Charles', 'King', None, ''), + ('Helen', 'King', 7, 'G'), + ('Mark', 'King', 2, 'Y'), + ('Karen', 'King', 4, 'R'), + ('Larry', 'King', 5, 'R'), + ('Lisa', 'King', 3, 'R'), + ), + datetime.today() - timedelta(seconds=self.day * 15), datetime.today() - timedelta(seconds=self.day * 60), - dtime(hour=20)), - ('Paul', 'Nelson', (), + dtime(hour=20), + ), + ( + 'Paul', + 'Nelson', + (), date.today() - timedelta(seconds=self.day * 20), datetime.today() - timedelta(seconds=self.day * 90), - dtime(hour=22)), + dtime(hour=22), + ), ) for first, last, cn, lm, bd, ht in people: person = Person( @@ -566,12 +573,16 @@ def make_people(self): last_name=last, last_modifed=lm, # DateField birth_date=bd, # DateTimeField - hammer_time=ht) # TimeField + hammer_time=ht, + ) # TimeField person.save() for child_first, child_last, age, color in cn: child = Child( - parent=person, first_name=child_first, last_name=child_last, - age=age, color=color + parent=person, + first_name=child_first, + last_name=child_last, + age=age, + color=color, ) child.save() @@ -588,11 +599,8 @@ def make_people_report(self): hammer_time (TimeField) """ self.make_people() - model = ContentType.objects.get(model="person", - app_label="demo_models") - people_report = Report.objects.create( - root_model=model, - name="A report of people") + model = ContentType.objects.get(model="person", app_label="demo_models") + people_report = Report.objects.create(root_model=model, name="A report of people") DisplayField.objects.create( report=people_report, @@ -805,7 +813,6 @@ def test_filter_timefield_relative_range_over_time(self): Test filtering TimeField field types using a relative range filter over time. """ - people_report = self.make_people_report() generate_url = reverse('generate_report', args=[people_report.id]) @@ -845,7 +852,7 @@ def test_filter_datetimefield_relative_range_over_time(self): report=people_report, field='birth_date', filter_type='relative_range', - filter_delta=self.day * -30 + filter_delta=self.day * -30, ) response = self.client.get(generate_url) @@ -904,7 +911,9 @@ def test_groupby_id(self): generate_url = reverse('generate_report', args=[report.id]) response = self.client.get(generate_url) - data = '"data":[[1,"John","Doe",3],[3,"Donald","King",6],[2,"Maria","Smith",2],["TOTALS","","",""],["","","",11.0]]' + data = ( + '"data":[[1,"John","Doe",3],[3,"Donald","King",6],[2,"Maria","Smith",2],["TOTALS","","",""],["","","",11.0]]' + ) self.assertContains(response, data) @@ -1192,17 +1201,18 @@ def test_admin_sync(self): self.assertEqual(response.status_code, 200) def test_report_builder_related_fields(self): - ''' + """ Test for Django 1.8 support via https://github.com/burke-software/django-report-builder/issues/144 - ''' + """ ct = ContentType.objects.get(model='place') response = self.client.post( - '/report_builder/api/related_fields/', { + '/report_builder/api/related_fields/', + { 'model': ct.id, 'path': '', - 'field': 'restaurant' - } + 'field': 'restaurant', + }, ) self.assertContains(response, '"field_name":"waiter"') self.assertContains(response, '"verbose_name":"waiter_set"') @@ -1378,4 +1388,4 @@ def test_annotation_filter_max(self): def test_get_config(self): settings.REPORT_BUILDER_ASYNC_REPORT = True response = self.client.get('/report_builder/api/config/') - self.assertContains(response,'"async_report": true') + self.assertContains(response, '"async_report": true') diff --git a/report_builder/unique_slugify.py b/report_builder/unique_slugify.py index cdb08e29..9b8ac2bb 100644 --- a/report_builder/unique_slugify.py +++ b/report_builder/unique_slugify.py @@ -3,8 +3,7 @@ from django.template.defaultfilters import slugify -def unique_slugify(instance, value, slug_field_name='slug', queryset=None, - slug_separator='-'): +def unique_slugify(instance, value, slug_field_name='slug', queryset=None, slug_separator='-'): """ Calculates and stores a unique slug of ``value`` for an instance. @@ -38,11 +37,11 @@ def unique_slugify(instance, value, slug_field_name='slug', queryset=None, next = 2 while not slug or queryset.filter(**{slug_field_name: slug}): slug = original_slug - end = '{}{}'.format(slug_separator, next) + end = f'{slug_separator}{next}' if slug_len and len(slug) + len(end) > slug_len: - slug = slug[:slug_len - len(end)] + slug = slug[: slug_len - len(end)] slug = _slug_strip(slug, slug_separator) - slug = '{}{}'.format(slug, end) + slug = f'{slug}{end}' next += 1 setattr(instance, slug_field.attname, slug) @@ -60,14 +59,14 @@ def _slug_strip(value, separator='-'): if separator == '-' or not separator: re_sep = '-' else: - re_sep = '(?:-|%s)' % re.escape(separator) + re_sep = f'(?:-|{re.escape(separator)})' # Remove multiple instances and if an alternate separator is provided, # replace the default '-' separator. if separator != re_sep: - value = re.sub('%s+' % re_sep, separator, value) + value = re.sub(f'{re_sep}+', separator, value) # Remove separator from the beginning and end of the slug. if separator: if separator != '-': re_sep = re.escape(separator) - value = re.sub(r'^{}+|{}+$'.format(re_sep, re_sep), '', value) + value = re.sub(rf'^{re_sep}+|{re_sep}+$', '', value) return value diff --git a/report_builder/urls.py b/report_builder/urls.py index 72bec9a6..4d523309 100644 --- a/report_builder/urls.py +++ b/report_builder/urls.py @@ -1,11 +1,12 @@ from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required -from django.urls import path, include +from django.urls import include, path from rest_framework import routers from . import views from .api import views as api_views + router = routers.DefaultRouter() router.register(r'reports', api_views.ReportViewSet) router.register(r'report', api_views.ReportNestedViewSet, basename="report-nested") @@ -15,8 +16,11 @@ urlpatterns = [ path('report//download_file/', views.DownloadFileView.as_view(), name="report_download_file"), - path('report//download_file//', views.DownloadFileView.as_view(), - name="report_download_file"), + path( + 'report//download_file//', + views.DownloadFileView.as_view(), + name="report_download_file", + ), path('report//check_status//', views.check_status, name="report_check_status"), path('report//add_star/', views.ajax_add_star, name="ajax_add_star"), path('report//create_copy/', views.create_copy, name="report_builder_create_copy"), @@ -41,10 +45,13 @@ path( 'api/report//generate/', staff_member_required(api_views.GenerateReport.as_view()), - name="generate_report" + name="generate_report", + ), + path( + 'api/report//download_file//', + views.DownloadFileView.as_view(), + name="report_download_file", ), - path('api/report//download_file//', views.DownloadFileView.as_view(), - name="report_download_file"), path('api/report//check_status//', views.check_status, name="report_check_status"), path('report//', views.ReportSPAView.as_view(), name="report_update_view"), ] @@ -55,4 +62,5 @@ '', staff_member_required(views.ReportSPAView.as_view()), name="report_builder", - ), ] + ), + ] diff --git a/report_builder/utils.py b/report_builder/utils.py index 3f1a44cc..c53025d0 100644 --- a/report_builder/utils.py +++ b/report_builder/utils.py @@ -1,12 +1,13 @@ +import copy +import datetime +import inspect from decimal import Decimal from itertools import chain from numbers import Number + +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist -from django.conf import settings -import copy -import datetime -import inspect def javascript_date_format(python_date_format): @@ -19,9 +20,10 @@ def javascript_date_format(python_date_format): def duplicate(obj, changes=None): - """ Duplicates any object including m2m fields + """Duplicates any object including m2m fields changes: any changes that should occur, example - changes = (('fullname','name (copy)'), ('do not copy me', ''))""" + changes = (('fullname','name (copy)'), ('do not copy me', '')) + """ if not obj.pk: raise ValueError('Instance must be saved before it can be cloned.') duplicate = copy.copy(obj) @@ -46,8 +48,8 @@ def duplicate(obj, changes=None): def sort_helper(x, sort_key, sort_type): - """ Sadly, python 3 makes it very hard to sort mixed types - We can work around this by forcing the types + """Sadly, python 3 makes it very hard to sort mixed types + We can work around this by forcing the types """ result = x[sort_key] if result is None: @@ -61,7 +63,9 @@ def sort_helper(x, sort_key, sort_type): def sort_data(data_list, display_field): - """ Sort data based on display_field settings + """ + + Sort data based on display_field settings data_list - 2d array of data display_field - report_builder.DisplayField object returns sorted data_list @@ -73,14 +77,14 @@ def sort_data(data_list, display_field): if sample_data is None: sample_data = data_list[-1][position] sort_type = None - if isinstance(sample_data, (datetime.date, datetime.datetime)): + if isinstance(sample_data, datetime.date | datetime.datetime): sort_type = DATE - elif isinstance(sample_data, (int, float, complex)): + elif isinstance(sample_data, int | float | complex): sort_type = NUMBER return sorted( data_list, key=lambda x: sort_helper(x, position, sort_type), - reverse=is_reverse + reverse=is_reverse, ) @@ -96,7 +100,7 @@ def increment_total(display_field, data_row): def formatter(value, style): - """ Convert value to Decimal to apply numeric formats. + """Convert value to Decimal to apply numeric formats. value - The value we wish to format. style - report_builder.Format object """ @@ -119,19 +123,19 @@ def isprop(v): def get_properties_from_model(model_class): - """ Show properties from a model """ + """Show properties from a model""" properties = [] attr_names = [name for (name, value) in inspect.getmembers(model_class, isprop)] for attr_name in attr_names: if attr_name.endswith('pk'): attr_names.remove(attr_name) else: - properties.append(dict(label=attr_name, name=attr_name.strip('_').replace('_', ' '))) + properties.append({'label': attr_name, 'name': attr_name.strip('_').replace('_', ' ')}) return sorted(properties, key=lambda k: k['label']) def get_relation_fields_from_model(model_class): - """ get related fields (m2m, fk, and reverse fk) """ + """Get related fields (m2m, fk, and reverse fk)""" relation_fields = [] all_fields_names = get_all_field_names(model_class) for field_name in all_fields_names: @@ -149,18 +153,22 @@ def get_relation_fields_from_model(model_class): def get_all_field_names(model_class): - """ Restores a function from django<1.10 """ - return list(set(chain.from_iterable( - (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) - for field in model_class._meta.get_fields() - # For complete backwards compatibility, you may want to exclude - # GenericForeignKey from the results. - if not (field.many_to_one and field.related_model is None) - ))) + """Restores a function from django<1.10""" + return list( + set( + chain.from_iterable( + (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) + for field in model_class._meta.get_fields() + # For complete backwards compatibility, you may want to exclude + # GenericForeignKey from the results. + if not (field.many_to_one and field.related_model is None) + ), + ), + ) def get_direct_fields_from_model(model_class): - """ Direct, not m2m, not FK """ + """Direct, not m2m, not FK""" direct_fields = [] all_fields_names = get_all_field_names(model_class) for field_name in all_fields_names: @@ -173,13 +181,15 @@ def get_direct_fields_from_model(model_class): def get_custom_fields_from_model(model_class): - """ django-custom-fields support """ + """django-custom-fields support""" if 'custom_field' in settings.INSTALLED_APPS: from custom_field.models import CustomField + try: content_type = ContentType.objects.get( model=model_class._meta.model_name, - app_label=model_class._meta.app_label) + app_label=model_class._meta.app_label, + ) except ContentType.DoesNotExist: content_type = None custom_fields = CustomField.objects.filter(content_type=content_type) @@ -187,7 +197,7 @@ def get_custom_fields_from_model(model_class): def get_model_from_path_string(root_model, path): - """ Return a model class for a related model + """Return a model class for a related model root_model is the class of the initial model path is like foo__bar where bar is related to foo """ diff --git a/report_builder/views.py b/report_builder/views.py index d06241a5..790c144b 100644 --- a/report_builder/views.py +++ b/report_builder/views.py @@ -6,26 +6,27 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.views.generic import TemplateView, View -from six import string_types from .mixins import DataExportMixin from .models import Report from .utils import duplicate + User = get_user_model() class ReportSPAView(TemplateView): - template_name = "report_builder/spa.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['ASYNC_REPORT'] = getattr( - settings, 'REPORT_BUILDER_ASYNC_REPORT', False + settings, + 'REPORT_BUILDER_ASYNC_REPORT', + False, ) return context @@ -36,8 +37,7 @@ def fieldset_string_to_field(fieldset_dict, model): i = 0 for dict_field in fieldset_dict['fields']: if isinstance(dict_field, str): - fieldset_dict['fields'][i] = model._meta.get_field_by_name( - dict_field)[0] + fieldset_dict['fields'][i] = model._meta.get_field_by_name(dict_field)[0] elif isinstance(dict_field, list) or isinstance(dict_field, tuple): dict_field[1]['recursive'] = True fieldset_string_to_field(dict_field[1], model) @@ -45,51 +45,44 @@ def fieldset_string_to_field(fieldset_dict, model): def get_fieldsets(model): - """ fieldsets are optional, they are defined in the Model. - """ + """Fieldsets are optional, they are defined in the Model.""" fieldsets = getattr(model, 'report_builder_fieldsets', None) if fieldsets: - for fieldset_name, fieldset_dict in model.report_builder_fieldsets: + for _fieldset_name, fieldset_dict in model.report_builder_fieldsets: fieldset_string_to_field(fieldset_dict, model) return fieldsets class DownloadFileView(DataExportMixin, View): - @method_decorator(staff_member_required) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) - def process_report(self, report_id, user_id, - file_type, to_response, queryset=None): + def process_report(self, report_id, user_id, file_type, to_response, queryset=None): report = get_object_or_404(Report, pk=report_id) user = User.objects.get(pk=user_id) if to_response: return report.run_report(file_type, user, queryset) else: - report.run_report(file_type, user, queryset, asynchronous=True) + report.run_report(file_type, user, queryset, asynchronous=True) def get(self, request, *args, **kwargs): report_id = kwargs['pk'] file_type = kwargs.get('filetype') if getattr(settings, 'REPORT_BUILDER_ASYNC_REPORT', False): from .tasks import report_builder_file_async_report_save - report_task = report_builder_file_async_report_save.delay( - report_id, request.user.pk, file_type) + + report_task = report_builder_file_async_report_save.delay(report_id, request.user.pk, file_type) task_id = report_task.task_id - return HttpResponse( - json.dumps({'task_id': task_id}), - content_type="application/json") + return HttpResponse(json.dumps({'task_id': task_id}), content_type="application/json") else: - return self.process_report( - report_id, request.user.pk, file_type, to_response=True) + return self.process_report(report_id, request.user.pk, file_type, to_response=True) @staff_member_required def ajax_add_star(request, pk): - """ Star or unstar report for user - """ + """Star or unstar report for user""" report = get_object_or_404(Report, pk=pk) user = request.user if user in report.starred.all(): @@ -103,13 +96,16 @@ def ajax_add_star(request, pk): @staff_member_required def create_copy(request, pk): - """ Copy a report including related fields """ + """Copy a report including related fields""" report = get_object_or_404(Report, pk=pk) - new_report = duplicate(report, changes=( - ('name', '{} (copy)'.format(report.name)), - ('user_created', request.user), - ('user_modified', request.user), - )) + new_report = duplicate( + report, + changes=( + ('name', f'{report.name} (copy)'), + ('user_created', request.user), + ('user_modified', request.user), + ), + ) # duplicate does not get related for display in report.displayfield_set.all(): new_display = copy.copy(display) @@ -125,11 +121,12 @@ def create_copy(request, pk): class ExportToReport(DownloadFileView, TemplateView): - """ Export objects (by ID and content type) to an existing or new report + """Export objects (by ID and content type) to an existing or new report In effect, this runs the report with its display fields. It ignores filters and filters instead of the provided ID's. It can be selected as a global admin action. """ + template_name = "report_builder/export_to_report.html" def get_context_data(self, **kwargs): @@ -140,8 +137,7 @@ def get_context_data(self, **kwargs): ctx['ids'] = ",".join(map(str, ids)) ctx['ct'] = ct.id ctx['number_objects'] = len(ids) - ctx['object_list'] = Report.objects.filter( - root_model=ct).order_by('-modified') + ctx['object_list'] = Report.objects.filter(root_model=ct).order_by('-modified') ctx['mode'] = ct.model_class()._meta.verbose_name return ctx @@ -152,7 +148,8 @@ def get(self, request, *args, **kwargs): report = get_object_or_404(Report, pk=request.GET['download']) queryset = ct.model_class().objects.filter(pk__in=ids) return self.process_report( - report.id, request.user.pk, + report.id, + request.user.pk, to_response=True, queryset=queryset, file_type="xlsx", @@ -163,21 +160,25 @@ def get(self, request, *args, **kwargs): @staff_member_required def check_status(request, pk, task_id): - """ Check if the asynchronous report is ready to download """ + """Check if the asynchronous report is ready to download""" from celery.result import AsyncResult + res = AsyncResult(task_id) link = '' if res.state == 'SUCCESS': report = get_object_or_404(Report, pk=pk) link = report.report_file.url return HttpResponse( - json.dumps({ - 'state': res.state, - 'link': link, - 'email': getattr( - settings, - 'REPORT_BUILDER_EMAIL_NOTIFICATION', - False - ) - }), - content_type="application/json") + json.dumps( + { + 'state': res.state, + 'link': link, + 'email': getattr( + settings, + 'REPORT_BUILDER_EMAIL_NOTIFICATION', + False, + ), + }, + ), + content_type="application/json", + ) diff --git a/report_builder_scheduled/admin.py b/report_builder_scheduled/admin.py index 705af6f9..77821a6a 100644 --- a/report_builder_scheduled/admin.py +++ b/report_builder_scheduled/admin.py @@ -12,7 +12,7 @@ class ScheduledReportAdmin(admin.ModelAdmin): readonly_fields = ('last_run_at',) @admin.display( - description='' + description='', ) def run_report_url(self, obj): url = reverse('run_scheduled_report', kwargs={'pk': obj.id}) diff --git a/report_builder_scheduled/models.py b/report_builder_scheduled/models.py index d312e9be..ab3fba7b 100644 --- a/report_builder_scheduled/models.py +++ b/report_builder_scheduled/models.py @@ -5,46 +5,61 @@ import report_builder_scheduled.tasks + AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') class ScheduledReport(models.Model): - """ A scheduled report that runs and emails itself to various users on - a recurring basis. Requires celery. """ + """ + + 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', on_delete=models.CASCADE + 'report_builder.Report', + on_delete=models.CASCADE, ) users = models.ManyToManyField( AUTH_USER_MODEL, limit_choices_to={'is_staff': True}, blank=True, - help_text="Staff users to notify") + help_text="Staff users to notify", + ) other_emails = models.CharField( max_length=1000, blank=True, - help_text="comma separated list of emails to send to in addition to users") + help_text="comma separated list of emails to send to in addition to users", + ) last_run_at = models.DateTimeField(auto_now_add=True, editable=False) interval = models.ForeignKey( - 'django_celery_beat.IntervalSchedule', on_delete=models.CASCADE, - null=True, blank=True, verbose_name=_('interval'), + 'django_celery_beat.IntervalSchedule', + on_delete=models.CASCADE, + null=True, + blank=True, + verbose_name=_('interval'), ) crontab = models.ForeignKey( - 'django_celery_beat.CrontabSchedule', on_delete=models.CASCADE, null=True, blank=True, - verbose_name=_('crontab'), help_text=_('Use one of interval/crontab'), + 'django_celery_beat.CrontabSchedule', + on_delete=models.CASCADE, + null=True, + blank=True, + verbose_name=_('crontab'), + help_text=_('Use one of interval/crontab'), ) def __str__(self): return str(self.report) def _get_list_of_emails(self) -> [str]: - """ Get list of emails for all users to return """ + """Get list of emails for all users to return""" emails = list(self.users.exclude(email="").values_list('email', flat=True)) emails += [x.strip() for x in self.other_emails.split(',') if x != ''] return emails def _is_due(self) -> bool: - """Check if due to run, check against both cron and interval """ + """Check if due to run, check against both cron and interval""" if self.interval: is_due, next = self.interval.schedule.is_due(self.last_run_at) if is_due: @@ -60,7 +75,10 @@ def run_report(self): self.save() def run_from_schedule(self): - """Run this only from a celery task - check if report needs to run and - if so run it as it's own celery task """ + """ + + Run this only from a celery task - check if report needs to run and + if so run it as it's own celery task + """ if self._is_due(): report_builder_scheduled.tasks.report_builder_run_scheduled_report.delay(self.id) diff --git a/report_builder_scheduled/tasks.py b/report_builder_scheduled/tasks.py index 81cd4d48..232fc226 100644 --- a/report_builder_scheduled/tasks.py +++ b/report_builder_scheduled/tasks.py @@ -8,9 +8,13 @@ def report_builder_run_scheduled_report(scheduled_report_id: int): report = report_builder_scheduled.models.ScheduledReport.objects.get(pk=scheduled_report_id) report.run_report() + @shared_task def report_builder_check_if_scheduled_report(): - """Run any reports that need run - this will kick off another task for - the actual reports - so this should always run pretty fast """ + """ + + Run any reports that need run - this will kick off another task for + the actual reports - so this should always run pretty fast + """ for scheduled in report_builder_scheduled.models.ScheduledReport.objects.filter(is_active=True): - scheduled.run_from_schedule() \ No newline at end of file + scheduled.run_from_schedule() diff --git a/report_builder_scheduled/tests.py b/report_builder_scheduled/tests.py index b2f5a7c5..b1edfb29 100644 --- a/report_builder_scheduled/tests.py +++ b/report_builder_scheduled/tests.py @@ -8,14 +8,16 @@ from django.urls import reverse from report_builder.models import Report + from .models import ScheduledReport from .tasks import report_builder_run_scheduled_report + User = get_user_model() IS_D18 = False -if django.VERSION[0] is 1 and django.VERSION[1] == 8: +if django.VERSION[0] == 1 and django.VERSION[1] == 8: IS_D18 = True @@ -51,7 +53,8 @@ def test_run_scheduled_report_view(self): class AdminViewTests(TestCase): - """ Basic sanity check that admin views work """ + """Basic sanity check that admin views work""" + @skipIf(IS_D18, "Django 1.8 does not support force_login") def test_scheduled_report_admin(self): url = reverse('admin:report_builder_scheduled_scheduledreport_changelist') diff --git a/report_builder_scheduled/urls.py b/report_builder_scheduled/urls.py index fc306864..2d3cf209 100644 --- a/report_builder_scheduled/urls.py +++ b/report_builder_scheduled/urls.py @@ -2,6 +2,7 @@ from .views import run_scheduled_report + urlpatterns = [ path('report//run_scheduled_report/', run_scheduled_report, name="run_scheduled_report"), ] diff --git a/report_builder_scheduled/views.py b/report_builder_scheduled/views.py index 94bc81cb..70900af9 100644 --- a/report_builder_scheduled/views.py +++ b/report_builder_scheduled/views.py @@ -10,7 +10,7 @@ @staff_member_required def run_scheduled_report(request, pk): - """ Manually run a scheduled report - useful for testing or one-off situations """ + """Manually run a scheduled report - useful for testing or one-off situations""" scheduled_report = get_object_or_404(ScheduledReport, pk=pk) report_builder_run_scheduled_report.delay(scheduled_report.id) messages.success(request, "Ran scheduled report") diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..5b86c889 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,82 @@ +target-version = "py312" +line-length = 121 +indent-width = 4 + +extend-exclude = [ + "node_modules", + "venv", + ".ipynb_checkpoints", + ".pyenv", + ".pytest_cache", + ".vscode", + "build", + "site-packages", + "_deploymanifests", + "migrations", + "Dockerfile", + "buildspec.yml", + "report_builder_demo", + "tests", +] + +[lint] +select = [ + "F", # pyflakes + "W", # pycodestyle warnings + "N", # pep8-naming + "B", # flake8-bugbear + "I", # isort + "UP", # pyupgrade + "COM", # flake8-commas + "DJ", # flake8-django + "T20", # flake8-print +] +ignore = [ + "C417", # unnecessary-list-comp + "D100", # missing-docstring + "D101", # missing-class-docstring + "D102", # missing-function-docstring + "D103", # missing-method-docstring + "D104", # missing-package-docstring + "D105", # missing-magic-method-docstring + "D106", # missing-docstring-in-init + "D107", # missing-docstring-in-nested-function + "D200", # one-liner-needed + "D212", # multi-line-docstring-first-line + "D415", # capitalized-ends-in-period + "E203", # whitespace-before-colon + "E231", # missing-whitespace-after-comma + "E501", # line-too-long + "E731", # do-not-assign-lambda + "F403", # from-import-star + "F405", # import-star-usage + "Q000", # quotes + "COM819", # prohibited-trailing-comma +] + +[format] +docstring-code-format = true +quote-style = "preserve" + +[lint.pydocstyle] +convention = "google" + +[lint.isort] +lines-after-imports = 2 + +[lint.isort.sections] +"django" = ["django"] + +section-order = [ + "future", + "standard-library", + "third-party", + "django", + "first-party", + "local-folder", +] + +[lint.per-file-ignores] +"__init__.py" = ["F401"] +"settings/*.py" = ["F401"] +"manage.py" = ["D"] diff --git a/setup.py b/setup.py index 9d3e6af6..576a5ccb 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup + setup( name="django-report-builder", @@ -28,5 +29,5 @@ 'openpyxl>=3.1.0', 'python-dateutil', 'djangorestframework>=3.8.0', - ] + ], ) From 829a13c0a3f704bc93393947ad45876dff9488dd Mon Sep 17 00:00:00 2001 From: mustafaulker Date: Wed, 18 Dec 2024 18:32:57 +0300 Subject: [PATCH 4/4] Update version and release notes --- CHANGELOG | 3 +++ README.md | 3 +++ pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0a487749..195ab552 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +## 6.6.2 +- Run pyupgrade and django-upgrade. Format the whole project using pre-commit & ruff + ## 6.6.0 - Split files and zip them when the row count exceeds one million. diff --git a/README.md b/README.md index 91a054f7..cd13b658 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Targets sys admins and capable end users who might not be able to program or gai # News +## 6.6.2 +- Run pyupgrade and django-upgrade. Format the whole project using pre-commit & ruff + ## 6.6.0 - Split files and zip them when the row count exceeds one million. diff --git a/pyproject.toml b/pyproject.toml index cfb526ed..10cd48ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-report-builder" -version = "6.6.0" +version = "6.6.2" description = "Query and Report builder for Django ORM" authors = ["David Burke "] packages = [ diff --git a/setup.py b/setup.py index 576a5ccb..4dc3da81 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="django-report-builder", - version="6.6.0", + version="6.6.2", author="David Burke", author_email="david@burkesoftware.com", maintainer="Mustafa Ülker",