|  | 
|  | 1 | +from collections import defaultdict | 
|  | 2 | +from datetime import timedelta | 
|  | 3 | + | 
|  | 4 | +from django import forms | 
|  | 5 | +from django.contrib import admin | 
|  | 6 | +from django.db.models.functions import TruncDay, TruncWeek, TruncMonth, TruncHour | 
|  | 7 | +from django.http import JsonResponse | 
|  | 8 | +from django.shortcuts import render | 
|  | 9 | +from django.urls import path | 
|  | 10 | +from django.utils import timezone | 
|  | 11 | +from django.utils.html import format_html | 
|  | 12 | + | 
|  | 13 | +from ..models import TimeSeriesChart | 
|  | 14 | + | 
|  | 15 | +SCALE_MAPPING = { | 
|  | 16 | +    'HOUR': TruncHour, | 
|  | 17 | +    'DAY': TruncDay, | 
|  | 18 | +    'WEEK': TruncWeek, | 
|  | 19 | +    'MONTH': TruncMonth, | 
|  | 20 | +} | 
|  | 21 | + | 
|  | 22 | + | 
|  | 23 | +class ChartStandardForm(forms.Form): | 
|  | 24 | +    time_range = forms.CharField(widget=forms.Select(choices=TimeSeriesChart.TimeRange.choices), required=False) | 
|  | 25 | +    scale = forms.CharField(widget=forms.Select(choices=TimeSeriesChart.Scale.choices), required=False) | 
|  | 26 | + | 
|  | 27 | + | 
|  | 28 | +class ChartFilterForm(forms.Form): | 
|  | 29 | +    time_range = forms.CharField(widget=forms.Select(choices=TimeSeriesChart.TimeRange.choices), required=False) | 
|  | 30 | +    scale = forms.CharField(widget=forms.Select(choices=TimeSeriesChart.Scale.choices), required=False) | 
|  | 31 | +    filters = forms.ChoiceField(required=False) | 
|  | 32 | + | 
|  | 33 | + | 
|  | 34 | +@admin.register(TimeSeriesChart) | 
|  | 35 | +class TimeSeriesChartAdmin(admin.ModelAdmin): | 
|  | 36 | +    list_display = ('display_chart_url', 'name', 'chart_type', 'app_label', 'model_name') | 
|  | 37 | +    list_display_links = ('name',) | 
|  | 38 | +    search_fields = ('name',) | 
|  | 39 | +    fieldsets = ( | 
|  | 40 | +        ( | 
|  | 41 | +            None, | 
|  | 42 | +            { | 
|  | 43 | +                'fields': ( | 
|  | 44 | +                    'name', 'description', | 
|  | 45 | +                    ('chart_type', 'stacked') | 
|  | 46 | +                ) | 
|  | 47 | +            }, | 
|  | 48 | +        ), | 
|  | 49 | +        ( | 
|  | 50 | +            'Target model', | 
|  | 51 | +            { | 
|  | 52 | +                'fields': ( | 
|  | 53 | +                    ('app_label', 'model_name', 'time_field'), | 
|  | 54 | +                    ('aggregate', 'aggregate_field', 'aggregate_label'), | 
|  | 55 | +                    ('split_field', 'filter_field', 'filters') | 
|  | 56 | +                ) | 
|  | 57 | +            } | 
|  | 58 | +        ), | 
|  | 59 | +        ( | 
|  | 60 | +            'Time options', | 
|  | 61 | +            { | 
|  | 62 | +                'fields': ( | 
|  | 63 | +                    'default_time_range', | 
|  | 64 | +                    'default_scale', | 
|  | 65 | +                ) | 
|  | 66 | +            } | 
|  | 67 | +        ), | 
|  | 68 | +    ) | 
|  | 69 | + | 
|  | 70 | +    def get_urls(self): | 
|  | 71 | +        urls = super().get_urls() | 
|  | 72 | +        my_urls = [ | 
|  | 73 | +            path('<int:chart_id>/chart/', self.admin_site.admin_view(self.chart_view), name='admin_chart_chart'), | 
|  | 74 | +            path('<int:chart_id>/metrics/', self.admin_site.admin_view(self.metrics_api), name='admin_chart_metrics'), | 
|  | 75 | +        ] | 
|  | 76 | +        return my_urls + urls | 
|  | 77 | + | 
|  | 78 | +    def metrics_api(self, request, chart_id): | 
|  | 79 | +        chart = TimeSeriesChart.objects.get(id=chart_id) | 
|  | 80 | +        data = { | 
|  | 81 | +            'time_range': request.GET.get('time_range', chart.default_time_range), | 
|  | 82 | +            'scale': request.GET.get('scale', chart.default_scale), | 
|  | 83 | +            'filters': request.GET.get('filters', None) | 
|  | 84 | +        } | 
|  | 85 | + | 
|  | 86 | +        if chart.filter_field: | 
|  | 87 | +            form = ChartFilterForm(data) | 
|  | 88 | +            StatsModel = chart.get_stats_model() | 
|  | 89 | +            choices = StatsModel.objects.values_list(chart.filter_field, flat=True).distinct() | 
|  | 90 | +            choices = [(x, x) for x in choices] | 
|  | 91 | +            form.fields['filters'].choices = choices | 
|  | 92 | +        else: | 
|  | 93 | +            form = ChartStandardForm(data) | 
|  | 94 | + | 
|  | 95 | +        if form.is_valid(): | 
|  | 96 | +            time_range = int(form.cleaned_data.get('time_range')) | 
|  | 97 | +            scale = form.cleaned_data.get('scale') | 
|  | 98 | +            filter_value = form.cleaned_data.get('filters', None) | 
|  | 99 | +        else: | 
|  | 100 | +            raise Exception('filter is invalid') | 
|  | 101 | + | 
|  | 102 | +        if scale == TimeSeriesChart.Scale.HOUR: | 
|  | 103 | +            date_format = '%Y-%m-%d %H:%M' | 
|  | 104 | +        else: | 
|  | 105 | +            date_format = '%Y-%m-%d' | 
|  | 106 | + | 
|  | 107 | +        if time_range: | 
|  | 108 | +            start_time = timezone.now() - timedelta(days=time_range) | 
|  | 109 | +        else: | 
|  | 110 | +            start_time = None | 
|  | 111 | +        stats = chart.get_queryset(start_time, scale, filter_value) | 
|  | 112 | + | 
|  | 113 | +        labels = [] | 
|  | 114 | + | 
|  | 115 | +        if not chart.split_field: | 
|  | 116 | +            data = [] | 
|  | 117 | +            for item in stats: | 
|  | 118 | +                labels.append(item['time'].strftime(date_format)) | 
|  | 119 | +                data.append(str(item['total'] if item['total'] is not None else 0)) | 
|  | 120 | +            datasets = [ | 
|  | 121 | +                { | 
|  | 122 | +                    'label': chart.aggregate_label, | 
|  | 123 | +                    'data': data | 
|  | 124 | +                } | 
|  | 125 | +            ] | 
|  | 126 | +        else: | 
|  | 127 | +            labels = [] | 
|  | 128 | +            last_labels = None | 
|  | 129 | +            data = defaultdict(dict) | 
|  | 130 | +            for item in stats: | 
|  | 131 | +                label = item['time'].strftime(date_format) | 
|  | 132 | +                if item['time'] != last_labels: | 
|  | 133 | +                    labels.append(label) | 
|  | 134 | +                    last_labels = item['time'] | 
|  | 135 | +                data[item['split']][label] = item['total'] | 
|  | 136 | + | 
|  | 137 | +            split_names = data.keys() | 
|  | 138 | +            datasets = defaultdict(list) | 
|  | 139 | + | 
|  | 140 | +            for label in labels: | 
|  | 141 | +                for name in split_names: | 
|  | 142 | +                    datasets[name].append(data[name].get(label, 0)) | 
|  | 143 | + | 
|  | 144 | +            datasets = [{'label': k, 'data': v} for k, v in datasets.items()] | 
|  | 145 | + | 
|  | 146 | +        return JsonResponse({ | 
|  | 147 | +            'chart_type': chart.chart_type, | 
|  | 148 | +            'stacked': chart.stacked, | 
|  | 149 | +            'labels': labels, | 
|  | 150 | +            'datasets': datasets | 
|  | 151 | +        }) | 
|  | 152 | + | 
|  | 153 | +    def chart_view(self, request, chart_id): | 
|  | 154 | +        context = { | 
|  | 155 | +            **admin.site.each_context(request), | 
|  | 156 | +        } | 
|  | 157 | + | 
|  | 158 | +        chart = TimeSeriesChart.objects.get(id=chart_id) | 
|  | 159 | + | 
|  | 160 | +        data = { | 
|  | 161 | +            'time_range': request.GET.get('time_range', chart.default_time_range), | 
|  | 162 | +            'scale': request.GET.get('scale', chart.default_scale), | 
|  | 163 | +        } | 
|  | 164 | + | 
|  | 165 | +        if chart.filter_field: | 
|  | 166 | +            form = ChartFilterForm(data) | 
|  | 167 | +            StatsModel = chart.get_stats_model() | 
|  | 168 | +            choices = StatsModel.objects.values_list(chart.filter_field, flat=True).distinct() | 
|  | 169 | +            choices = [(x, x) for x in choices] | 
|  | 170 | +            choices = [('', 'All')] + choices | 
|  | 171 | +            form.fields['filters'].choices = choices | 
|  | 172 | +            form.fields['filters'].label = chart.filter_field.title() | 
|  | 173 | +        else: | 
|  | 174 | +            form = ChartStandardForm(data) | 
|  | 175 | + | 
|  | 176 | +        context["chart"] = chart | 
|  | 177 | +        context["chart_title"] = chart.name | 
|  | 178 | +        context["form"] = form | 
|  | 179 | + | 
|  | 180 | +        return render(request, 'admin/chart.html', context) | 
|  | 181 | + | 
|  | 182 | +    @admin.display(description='View Chart') | 
|  | 183 | +    def display_chart_url(self, obj): | 
|  | 184 | +        return format_html('<a href="/admin/admin_extended/timeserieschart/{}/chart/">View Chart</a>', obj.id) | 
0 commit comments