diff --git a/.travis.yml b/.travis.yml index 35a98e7..c3b81a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ language: python +sudo: required +dist: xenial python: - "2.7" - "3.6" + - "3.7" services: - mysql @@ -16,11 +19,16 @@ env: - DJANGO_VERSION=1.10 - DJANGO_VERSION=1.11 - DJANGO_VERSION=2.0 + - DJANGO_VERSION=2.1 matrix: exclude: - python: "2.7" env: DJANGO_VERSION=2.0 + - python: "2.7" + env: DJANGO_VERSION=2.1 + - python: "3.7" + env: DJANGO_VERSION=1.11 install: - pip install -q Django==$DJANGO_VERSION diff --git a/setup.py b/setup.py index ee8bb8e..a65fd31 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='django-speedinfo', - version='1.3.6', + version='1.3.7', packages=['speedinfo', 'speedinfo.migrations'], include_package_data=True, license='MIT', @@ -28,13 +28,16 @@ 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.1', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], keywords='django profiler views performance', + install_requires=['Django>=1.8'] ) diff --git a/speedinfo/admin.py b/speedinfo/admin.py index 60f2c0c..51a05f4 100644 --- a/speedinfo/admin.py +++ b/speedinfo/admin.py @@ -10,7 +10,6 @@ from django.conf.urls import url from django.contrib import admin from django.core.exceptions import PermissionDenied -from django.db.models import F, FloatField, ExpressionWrapper from django.http import HttpResponseRedirect, HttpResponse from speedinfo import profiler, settings @@ -22,6 +21,23 @@ from django.core.urlresolvers import reverse +def field_wrapper(col): + """Helper function to dynamically create list display method + for :class:`ViewProfilerAdmin` to control value formating + and sort order. + + :type col: :data:`settings.ReportColumnFormat` + :rtype: function + """ + def field_format(obj): + return col.format.format(getattr(obj, col.attr_name)) + + field_format.short_description = col.name + field_format.admin_order_field = col.attr_name + + return field_format + + class ViewProfilerAdmin(admin.ModelAdmin): list_display_links = None actions = None @@ -32,38 +48,29 @@ class Media: 'all': ('speedinfo/css/admin.css',) } - def get_queryset(self, request): - qs = super(ViewProfilerAdmin, self).get_queryset(request) - return qs.annotate( - percall=ExpressionWrapper(F('total_time') / F('total_calls'), output_field=FloatField()) - ) - - def get_list_display(self, request): - """Creates and returns list of callables to represent - model field values with a custom format. - - :param request: Request object - :type request: :class:`django.http.HttpRequest` - :return: list containing fields to be displayed on the changelist + def __init__(self, *args, **kwargs): + """Initializes the list of visible columns and + the way they are formating the values. """ - list_display = [] + super(ViewProfilerAdmin, self).__init__(*args, **kwargs) + self.list_display = [] for rc in settings.SPEEDINFO_REPORT_COLUMNS_FORMAT: if rc.attr_name in settings.SPEEDINFO_REPORT_COLUMNS: + method_name = '{}_wrapper'.format(rc.attr_name) + setattr(self, method_name, field_wrapper(rc)) + self.list_display.append(method_name) - def wrapper(col): - def field_format(obj): - return col.format.format(getattr(obj, col.attr_name)) - field_format.short_description = col.name - field_format.admin_order_field = col.order_field - - return field_format + def get_queryset(self, request): + qs = super(ViewProfilerAdmin, self).get_queryset(request) - list_display.append( - wrapper(rc) - ) + for rc in settings.SPEEDINFO_REPORT_COLUMNS_FORMAT: + if (rc.attr_name in settings.SPEEDINFO_REPORT_COLUMNS) and not isinstance(rc.order_field, str): + qs = qs.annotate(**{ + rc.attr_name: rc.order_field + }) - return list_display + return qs def change_view(self, *args, **kwargs): raise PermissionDenied @@ -105,7 +112,7 @@ def export(self, request): csv_writer = csv.writer(output) csv_writer.writerow([col.name for col in export_columns]) - for row in profiler.data.all(): + for row in self.get_queryset(request): csv_writer.writerow([ col.format.format(getattr(row, col.attr_name)) for col in export_columns diff --git a/speedinfo/models.py b/speedinfo/models.py index 02c310c..dc44c6b 100644 --- a/speedinfo/models.py +++ b/speedinfo/models.py @@ -19,63 +19,3 @@ class ViewProfiler(models.Model): class Meta: verbose_name_plural = 'Views' unique_together = ('view_name', 'method') - - @property - def anon_calls_ratio(self): - """Anonymous calls ratio. - - :return: anonymous calls ratio percent - :rtype: float - """ - if self.total_calls > 0: - return 100 * self.anon_calls / float(self.total_calls) - else: - return 0 - - @property - def cache_hits_ratio(self): - """Cache hits ratio. - - :return: cache hits ratio percent - :rtype: float - """ - if self.total_calls > 0: - return 100 * self.cache_hits / float(self.total_calls) - else: - return 0 - - @property - def sql_time_ratio(self): - """SQL time per call ratio. - - :return: SQL time per call ratio percent - :rtype: float - """ - if self.total_time > 0: - return 100 * self.sql_total_time / float(self.total_time) - else: - return 0 - - @property - def sql_count_per_call(self): - """SQL queries count per call. - - :return: SQL queries count per call - :rtype: int - """ - if self.total_calls > 0: - return int(round(self.sql_total_count / float(self.total_calls))) - else: - return 0 - - @property - def time_per_call(self): - """Time per call. - - :return: time per call - :rtype: float - """ - if self.total_calls > 0: - return self.total_time / float(self.total_calls) - else: - return 0 diff --git a/speedinfo/profiler.py b/speedinfo/profiler.py index e4871bb..57fd92a 100644 --- a/speedinfo/profiler.py +++ b/speedinfo/profiler.py @@ -38,15 +38,6 @@ def add(self, view_name, method, is_anon_call, is_cache_hit, sql_time, sql_count total_time=F('total_time') + view_execution_time ) - def all(self): - """Returns iterable object over all profiling data. - - :return: profiling data - :rtype: :class:`typing.Iterable` - """ - from speedinfo.models import ViewProfiler - return ViewProfiler.objects.order_by('-total_time') - def reset(self): """Deletes all profiling data""" from speedinfo.models import ViewProfiler diff --git a/speedinfo/settings.py b/speedinfo/settings.py index c90bdef..3212d1d 100644 --- a/speedinfo/settings.py +++ b/speedinfo/settings.py @@ -2,6 +2,7 @@ from collections import namedtuple from django.conf import settings +from django.db.models import ExpressionWrapper, F, FloatField, IntegerField SPEEDINFO_CACHED_RESPONSE_ATTR_NAME = getattr(settings, 'SPEEDINFO_CACHED_RESPONSE_ATTR_NAME', 'is_cached') SPEEDINFO_EXCLUDE_URLS = getattr(settings, 'SPEEDINFO_EXCLUDE_URLS', []) @@ -13,11 +14,16 @@ SPEEDINFO_REPORT_COLUMNS_FORMAT = [ ReportColumnFormat('View name', '{}', 'view_name', 'view_name'), ReportColumnFormat('HTTP method', '{}', 'method', 'method'), - ReportColumnFormat('Anonymous calls', '{:.1f}%', 'anon_calls_ratio', None), - ReportColumnFormat('Cache hits', '{:.1f}%', 'cache_hits_ratio', None), - ReportColumnFormat('SQL queries per call', '{}', 'sql_count_per_call', None), - ReportColumnFormat('SQL time', '{:.1f}%', 'sql_time_ratio', None), + ReportColumnFormat('Anonymous calls', '{:.1f}%', 'anon_calls_ratio', + ExpressionWrapper(100.0 * F('anon_calls') / F('total_calls'), output_field=FloatField())), + ReportColumnFormat('Cache hits', '{:.1f}%', 'cache_hits_ratio', + ExpressionWrapper(100.0 * F('cache_hits') / F('total_calls'), output_field=FloatField())), + ReportColumnFormat('SQL queries per call', '{}', 'sql_count_per_call', + ExpressionWrapper(F('sql_total_count') / F('total_calls'), output_field=IntegerField())), + ReportColumnFormat('SQL time', '{:.1f}%', 'sql_time_ratio', + ExpressionWrapper(100.0 * F('sql_total_time') / F('total_time'), output_field=FloatField())), ReportColumnFormat('Total calls', '{}', 'total_calls', 'total_calls'), - ReportColumnFormat('Time per call', '{:.8f}', 'time_per_call', 'percall'), + ReportColumnFormat('Time per call', '{:.8f}', 'time_per_call', + ExpressionWrapper(F('total_time') / F('total_calls'), output_field=FloatField())), ReportColumnFormat('Total time', '{:.4f}', 'total_time', 'total_time'), ]