• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SEED-platform / seed / #6426

pending completion
#6426

push

coveralls-python

web-flow
Merge pull request #3682 from SEED-platform/Fix-legend

Fix legend

15655 of 22544 relevant lines covered (69.44%)

0.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.04
/seed/views/reports.py
1
# !/usr/bin/env python
2
# encoding: utf-8
3
"""
1✔
4
:copyright (c) 2014 - 2022, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Department of Energy) and contributors. All rights reserved.
5
:author
6
"""
7
from collections import defaultdict
1✔
8
from io import BytesIO
1✔
9

10
import dateutil
1✔
11
from django.http import HttpResponse
12
from past.builtins import basestring
1✔
13
from rest_framework import status
1✔
14
from rest_framework.parsers import JSONParser
1✔
15
from rest_framework.renderers import JSONRenderer
1✔
16
from rest_framework.response import Response
1✔
17
from rest_framework.viewsets import ViewSet
1✔
18
from xlsxwriter import Workbook
1✔
19

20
from seed.decorators import DecoratorMixin
1✔
21
from seed.lib.superperms.orgs.models import Organization
1✔
22
from seed.models import Cycle, PropertyView
1✔
23
from seed.serializers.pint import apply_display_unit_preferences
1✔
24
from seed.utils.api import drf_api_endpoint
1✔
25
from seed.utils.generic import median, round_down_hundred_thousand
1✔
26

27

28
class Report(DecoratorMixin(drf_api_endpoint), ViewSet):  # type: ignore[misc]
1✔
29
    renderer_classes = (JSONRenderer,)
1✔
30
    parser_classes = (JSONParser,)
1✔
31

32
    def get_cycles(self, start, end):
1✔
33
        organization_id = self.request.GET['organization_id']
×
34
        if not isinstance(start, type(end)):
×
35
            raise TypeError('start and end not same types')
×
36
        # if of type int or convertable  assume they are cycle ids
37
        try:
×
38
            start = int(start)
×
39
            end = int(end)
×
40
        except ValueError:
×
41
            # assume string is JS date
42
            if isinstance(start, basestring):
×
43
                start_datetime = dateutil.parser.parse(start)
×
44
                end_datetime = dateutil.parser.parse(end)
×
45
            else:
46
                raise Exception('Date is not a string')
×
47
        # get date times from cycles
48
        if isinstance(start, int):
×
49
            cycle = Cycle.objects.get(pk=start, organization_id=organization_id)
×
50
            start_datetime = cycle.start
×
51
            if start == end:
×
52
                end_datetime = cycle.end
×
53
            else:
54
                end_datetime = Cycle.objects.get(
×
55
                    pk=end, organization_id=organization_id
56
                ).end
57
        return Cycle.objects.filter(
×
58
            start__gte=start_datetime, end__lte=end_datetime,
59
            organization_id=organization_id
60
        ).order_by('start')
61

62
    def get_data(self, property_view, x_var, y_var):
1✔
63
        result = None
×
64
        state = property_view.state
×
65
        if getattr(state, x_var, None) and getattr(state, y_var, None):
×
66
            result = {
×
67
                "id": property_view.property_id,
68
                "x": getattr(state, x_var),
69
                "y": getattr(state, y_var),
70
            }
71
        return result
×
72

73
    def get_raw_report_data(self, organization_id, cycles, x_var, y_var):
1✔
74
        all_property_views = PropertyView.objects.select_related(
×
75
            'property', 'state'
76
        ).filter(
77
            property__organization_id=organization_id,
78
            cycle_id__in=cycles
79
        )
80
        organization = Organization.objects.get(pk=organization_id)
×
81
        results = []
×
82
        for cycle in cycles:
×
83
            property_views = all_property_views.filter(cycle_id=cycle)
×
84
            count_total = []
×
85
            count_with_data = []
×
86
            data = []
×
87
            for property_view in property_views:
×
88
                property_pk = property_view.property_id
×
89
                count_total.append(property_pk)
×
90
                result = self.get_data(property_view, x_var, y_var)
×
91
                if result:
×
92
                    result['yr_e'] = cycle.end.strftime('%Y')
×
93
                    de_unitted_result = apply_display_unit_preferences(organization, result)
×
94
                    data.append(de_unitted_result)
×
95
                    count_with_data.append(property_pk)
×
96
            result = {
×
97
                "cycle_id": cycle.pk,
98
                "chart_data": data,
99
                "property_counts": {
100
                    "yr_e": cycle.end.strftime('%Y'),
101
                    "num_properties": len(count_total),
102
                    "num_properties_w-data": len(count_with_data),
103
                },
104
            }
105
            results.append(result)
×
106
        return results
×
107

108
    def get_property_report_data(self, request):
1✔
109
        params = {}
×
110
        missing_params = []
×
111
        error = ''
×
112
        for param in ['x_var', 'y_var', 'organization_id', 'start', 'end']:
×
113
            val = request.query_params.get(param, None)
×
114
            if not val:
×
115
                missing_params.append(param)
×
116
            else:
117
                params[param] = val
×
118
        if missing_params:
×
119
            error = "{} Missing params: {}".format(
×
120
                error, ", ".join(missing_params)
121
            )
122
        if error:
×
123
            status_code = status.HTTP_400_BAD_REQUEST
×
124
            result = {'status': 'error', 'message': error}
×
125
        else:
126
            cycles = self.get_cycles(params['start'], params['end'])
×
127
            data = self.get_raw_report_data(
×
128
                params['organization_id'], cycles,
129
                params['x_var'], params['y_var']
130
            )
131
            for datum in data:
×
132
                if datum['property_counts']['num_properties_w-data'] != 0:
×
133
                    break
×
134
            property_counts = []
×
135
            chart_data = []
×
136
            for datum in data:
×
137
                property_counts.append(datum['property_counts'])
×
138
                chart_data.extend(datum['chart_data'])
×
139
            data = {
×
140
                'property_counts': property_counts,
141
                'chart_data': chart_data,
142
            }
143
            result = {'status': 'success', 'data': data}
×
144
            status_code = status.HTTP_200_OK
×
145
        return Response(result, status=status_code)
×
146

147
    def export_reports_data(self, request):
1✔
148
        params = {}
×
149
        missing_params = []
×
150
        error = ''
×
151
        for param in ['x_var', 'x_label', 'y_var', 'y_label', 'organization_id', 'start', 'end']:
×
152
            val = request.query_params.get(param, None)
×
153
            if not val:
×
154
                missing_params.append(param)
×
155
            else:
156
                params[param] = val
×
157
        if missing_params:
×
158
            error = "{} Missing params: {}".format(
×
159
                error, ", ".join(missing_params)
160
            )
161
        if error:
×
162
            status_code = status.HTTP_400_BAD_REQUEST
×
163
            result = {'status': 'error', 'message': error}
×
164
            return Response(result, status=status_code)
×
165

166
        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
×
167
        response['Content-Disposition'] = 'attachment; filename="report-data"'
×
168

169
        # Create WB
170
        output = BytesIO()
×
171
        wb = Workbook(output, {'remove_timezone': True})
×
172

173
        # Create sheets
174
        count_sheet = wb.add_worksheet('Counts')
×
175
        base_sheet = wb.add_worksheet('Raw')
×
176
        agg_sheet = wb.add_worksheet('Agg')
×
177

178
        # Enable bold format and establish starting cells
179
        bold = wb.add_format({'bold': True})
×
180
        data_row_start = 0
×
181
        data_col_start = 0
×
182

183
        # Write all headers across all sheets
184
        count_sheet.write(data_row_start, data_col_start, 'Year Ending', bold)
×
185
        count_sheet.write(data_row_start, data_col_start + 1, 'Properties with Data', bold)
×
186
        count_sheet.write(data_row_start, data_col_start + 2, 'Total Properties', bold)
×
187

188
        base_sheet.write(data_row_start, data_col_start, 'ID', bold)
×
189
        base_sheet.write(data_row_start, data_col_start + 1, request.query_params.get('x_label'), bold)
×
190
        base_sheet.write(data_row_start, data_col_start + 2, request.query_params.get('y_label'), bold)
×
191
        base_sheet.write(data_row_start, data_col_start + 3, 'Year Ending', bold)
×
192

193
        agg_sheet.write(data_row_start, data_col_start, request.query_params.get('x_label'), bold)
×
194
        agg_sheet.write(data_row_start, data_col_start + 1, request.query_params.get('y_label'), bold)
×
195
        agg_sheet.write(data_row_start, data_col_start + 2, 'Year Ending', bold)
×
196

197
        # Gather base data
198
        cycles = self.get_cycles(params['start'], params['end'])
×
199
        data = self.get_raw_report_data(
×
200
            params['organization_id'], cycles,
201
            params['x_var'], params['y_var']
202
        )
203

204
        base_row = data_row_start + 1
×
205
        agg_row = data_row_start + 1
×
206
        count_row = data_row_start + 1
×
207

208
        for cycle_results in data:
×
209
            total_count = cycle_results['property_counts']['num_properties']
×
210
            with_data_count = cycle_results['property_counts']['num_properties_w-data']
×
211
            yr_e = cycle_results['property_counts']['yr_e']
×
212

213
            # Write Counts
214
            count_sheet.write(count_row, data_col_start, yr_e)
×
215
            count_sheet.write(count_row, data_col_start + 1, with_data_count)
×
216
            count_sheet.write(count_row, data_col_start + 2, total_count)
×
217

218
            count_row += 1
×
219

220
            # Write Base/Raw Data
221
            data_rows = cycle_results['chart_data']
×
222
            for datum in data_rows:
×
223
                base_sheet.write(base_row, data_col_start, datum.get('id'))
×
224
                base_sheet.write(base_row, data_col_start + 1, datum.get('x'))
×
225
                base_sheet.write(base_row, data_col_start + 2, datum.get('y'))
×
226
                base_sheet.write(base_row, data_col_start + 3, datum.get('yr_e'))
×
227

228
                base_row += 1
×
229

230
            # Gather and write Agg data
231
            for agg_datum in self.aggregate_data(yr_e, params['y_var'], data_rows):
×
232
                agg_sheet.write(agg_row, data_col_start, agg_datum.get('x'))
×
233
                agg_sheet.write(agg_row, data_col_start + 1, agg_datum.get('y'))
×
234
                agg_sheet.write(agg_row, data_col_start + 2, agg_datum.get('yr_e'))
×
235

236
                agg_row += 1
×
237

238
        wb.close()
×
239

240
        xlsx_data = output.getvalue()
×
241

242
        response.write(xlsx_data)
×
243

244
        return response
×
245

246
    def get_aggregated_property_report_data(self, request):
1✔
247
        valid_y_values = ['gross_floor_area', 'property_type', 'year_built']
×
248
        params = {}
×
249
        missing_params = []
×
250
        empty = True
×
251
        error = ''
×
252
        for param in ['x_var', 'y_var', 'organization_id', 'start', 'end']:
×
253
            val = request.query_params.get(param, None)
×
254
            if not val:
×
255
                missing_params.append(param)
×
256
            elif param == 'y_var' and val not in valid_y_values:
×
257
                error = "{} {} is not a valid value for {}.".format(
×
258
                    error, val, param
259
                )
260
            else:
261
                params[param] = val
×
262
        if missing_params:
×
263
            error = "{} Missing params: {}".format(
×
264
                error, ", ".join(missing_params)
265
            )
266
        if error:
×
267
            status_code = status.HTTP_400_BAD_REQUEST
×
268
            result = {'status': 'error', 'message': error}
×
269
        else:
270
            cycles = self.get_cycles(params['start'], params['end'])
×
271
            x_var = params['x_var']
×
272
            y_var = params['y_var']
×
273
            data = self.get_raw_report_data(
×
274
                params['organization_id'], cycles, x_var, y_var
275
            )
276
            for datum in data:
×
277
                if datum['property_counts']['num_properties_w-data'] != 0:
×
278
                    empty = False
×
279
                    break
×
280
            if empty:
×
281
                result = {'status': 'error', 'message': 'No data found'}
×
282
                status_code = status.HTTP_404_NOT_FOUND
×
283
        if not empty or not error:
×
284
            chart_data = []
×
285
            property_counts = []
×
286
            for datum in data:
×
287
                buildings = datum['chart_data']
×
288
                yr_e = datum['property_counts']['yr_e']
×
289
                chart_data.extend(self.aggregate_data(yr_e, y_var, buildings)),
×
290
                property_counts.append(datum['property_counts'])
×
291
            # Send back to client
292
            aggregated_data = {
×
293
                'chart_data': chart_data,
294
                'property_counts': property_counts
295
            }
296
            result = {
×
297
                'status': 'success',
298
                'aggregated_data': aggregated_data,
299
            }
300
            status_code = status.HTTP_200_OK
×
301
        return Response(result, status=status_code)
×
302

303
    def aggregate_data(self, yr_e, y_var, buildings):
1✔
304
        aggregation_method = {
×
305
            'property_type': self.aggregate_property_type,
306
            'year_built': self.aggregate_year_built,
307
            'gross_floor_area': self.aggregate_gross_floor_area,
308

309

310
        }
311
        return aggregation_method[y_var](yr_e, buildings)
×
312

313
    def aggregate_property_type(self, yr_e, buildings):
1✔
314
        # Group buildings in this year_ending group into uses
315
        chart_data = []
×
316
        grouped_uses = defaultdict(list)
×
317
        for b in buildings:
×
318
            grouped_uses[str(b['y']).lower()].append(b)
×
319

320
        # Now iterate over use groups to make each chart item
321
        for use, buildings_in_uses in grouped_uses.items():
×
322
            chart_data.append({
×
323
                'x': median([b['x'] for b in buildings_in_uses]),
324
                'y': use.capitalize(),
325
                'yr_e': yr_e
326
            })
327
        return chart_data
×
328

329
    def aggregate_year_built(self, yr_e, buildings):
1✔
330
        # Group buildings in this year_ending group into decades
331
        chart_data = []
×
332
        grouped_decades = defaultdict(list)
×
333
        for b in buildings:
×
334
            grouped_decades['%s0' % str(b['y'])[:-1]].append(b)
×
335

336
        # Now iterate over decade groups to make each chart item
337
        for decade, buildings_in_decade in grouped_decades.items():
×
338
            chart_data.append({
×
339
                'x': median(
340
                    [b['x'] for b in buildings_in_decade]
341
                ),
342
                'y': '%s-%s' % (decade, '%s9' % str(decade)[:-1]),  # 1990-1999
343
                'yr_e': yr_e
344
            })
345
        return chart_data
×
346

347
    def aggregate_gross_floor_area(self, yr_e, buildings):
1✔
348
        chart_data = []
×
349
        y_display_map = {
×
350
            0: '0-99k',
351
            100000: '100-199k',
352
            200000: '200k-299k',
353
            300000: '300k-399k',
354
            400000: '400-499k',
355
            500000: '500-599k',
356
            600000: '600-699k',
357
            700000: '700-799k',
358
            800000: '800-899k',
359
            900000: '900-999k',
360
            1000000: 'over 1,000k',
361
        }
362
        max_bin = max(y_display_map)
×
363

364
        # Group buildings in this year_ending group into ranges
365
        grouped_ranges = defaultdict(list)
×
366
        for b in buildings:
×
367
            area = b['y']
×
368
            # make sure anything greater than the biggest bin gets put in
369
            # the biggest bin
370
            range_bin = min(max_bin, round_down_hundred_thousand(area))
×
371
            grouped_ranges[range_bin].append(b)
×
372

373
        # Now iterate over range groups to make each chart item
374
        for range_floor, buildings_in_range in grouped_ranges.items():
×
375
            chart_data.append({
×
376
                'x': median(
377
                    [b['x'] for b in buildings_in_range]
378
                ),
379
                'y': y_display_map[range_floor],
380
                'yr_e': yr_e
381
            })
382
        return chart_data
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc