• 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

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

11
import json
1✔
12

13
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
14
from django.http import JsonResponse
15
from rest_framework import status
1✔
16
from rest_framework.decorators import action
1✔
17
from rest_framework.renderers import JSONRenderer
1✔
18
from rest_framework.viewsets import ViewSet
1✔
19

20
from seed.decorators import ajax_request_class
1✔
21
from seed.lib.superperms.orgs.decorators import has_perm_class
1✔
22
from seed.lib.superperms.orgs.models import Organization
1✔
23
from seed.models import (
1✔
24
    AUDIT_USER_EDIT,
25
    DATA_STATE_MATCHING,
26
    MERGE_STATE_DELETE,
27
    MERGE_STATE_MERGED,
28
    MERGE_STATE_NEW,
29
    VIEW_LIST,
30
    VIEW_LIST_TAXLOT,
31
    Column,
32
    ColumnListProfile,
33
    ColumnListProfileColumn,
34
    Cycle,
35
    Note,
36
    PropertyView,
37
    StatusLabel,
38
    TaxLot,
39
    TaxLotAuditLog,
40
    TaxLotProperty,
41
    TaxLotState,
42
    TaxLotView
43
)
44
from seed.serializers.pint import (
1✔
45
    add_pint_unit_suffix,
46
    apply_display_unit_preferences
47
)
48
from seed.serializers.properties import PropertyViewSerializer
1✔
49
from seed.serializers.taxlots import (
1✔
50
    TaxLotSerializer,
51
    TaxLotStateSerializer,
52
    TaxLotViewSerializer
53
)
54
from seed.utils.api import ProfileIdMixin, api_endpoint_class
1✔
55
from seed.utils.match import match_merge_link
1✔
56
from seed.utils.merge import merge_taxlots
1✔
57
from seed.utils.properties import (
1✔
58
    get_changed_fields,
59
    pair_unpair_property_taxlot,
60
    update_result_with_master
61
)
62
from seed.utils.taxlots import taxlots_across_cycles
1✔
63

64
# Global toggle that controls whether or not to display the raw extra
65
# data fields in the columns returned for the view.
66
DISPLAY_RAW_EXTRADATA = True
1✔
67
DISPLAY_RAW_EXTRADATA_TIME = True
1✔
68

69

70
class TaxLotViewSet(ViewSet, ProfileIdMixin):
1✔
71
    renderer_classes = (JSONRenderer,)
1✔
72
    serializer_class = TaxLotSerializer
1✔
73

74
    def _get_filtered_results(self, request, profile_id):
1✔
75
        page = request.query_params.get('page', 1)
×
76
        per_page = request.query_params.get('per_page', 1)
×
77
        org_id = request.query_params.get('organization_id', None)
×
78
        cycle_id = request.query_params.get('cycle')
×
79
        # check if there is a query parameter for the profile_id. If so, then use that one
80
        profile_id = request.query_params.get('profile_id', profile_id)
×
81
        if not org_id:
×
82
            return JsonResponse(
×
83
                {'status': 'error', 'message': 'Need to pass organization_id as query parameter'},
84
                status=status.HTTP_400_BAD_REQUEST)
85

86
        if cycle_id:
×
87
            cycle = Cycle.objects.get(organization_id=org_id, pk=cycle_id)
×
88
        else:
89
            cycle = Cycle.objects.filter(organization_id=org_id).order_by('name')
×
90
            if cycle:
×
91
                cycle = cycle.first()
×
92
            else:
93
                return JsonResponse({
×
94
                    'status': 'error',
95
                    'message': 'Could not locate cycle',
96
                    'pagination': {
97
                        'total': 0
98
                    },
99
                    'cycle_id': None,
100
                    'results': []
101
                })
102

103
        # Return taxlot views limited to the 'inventory_ids' list.  Otherwise, if selected is empty, return all
104
        if 'inventory_ids' in request.data and request.data['inventory_ids']:
×
105
            taxlot_views_list = TaxLotView.objects.select_related('taxlot', 'state', 'cycle') \
×
106
                .filter(taxlot_id__in=request.data['inventory_ids'], taxlot__organization_id=org_id,
107
                        cycle=cycle) \
108
                .order_by('id')
109
        else:
110
            taxlot_views_list = TaxLotView.objects.select_related('taxlot', 'state', 'cycle') \
×
111
                .filter(taxlot__organization_id=org_id, cycle=cycle) \
112
                .order_by('id')
113

114
        paginator = Paginator(taxlot_views_list, per_page)
×
115

116
        try:
×
117
            taxlot_views = paginator.page(page)
×
118
            page = int(page)
×
119
        except PageNotAnInteger:
×
120
            taxlot_views = paginator.page(1)
×
121
            page = 1
×
122
        except EmptyPage:
×
123
            taxlot_views = paginator.page(paginator.num_pages)
×
124
            page = paginator.num_pages
×
125

126
        org = Organization.objects.get(pk=org_id)
×
127

128
        # Retrieve all the columns that are in the db for this organization
129
        columns_from_database = Column.retrieve_all(org_id, 'taxlot', False)
×
130

131
        # This uses an old method of returning the show_columns. There is a new method that
132
        # is preferred in v2.1 API with the ProfileIdMixin.
133
        if profile_id is None:
×
134
            show_columns = None
×
135
        elif profile_id == -1:
×
136
            show_columns = list(Column.objects.filter(
×
137
                organization_id=org_id
138
            ).values_list('id', flat=True))
139
        else:
140
            try:
×
141
                profile = ColumnListProfile.objects.get(
×
142
                    organization=org,
143
                    id=profile_id,
144
                    profile_location=VIEW_LIST,
145
                    inventory_type=VIEW_LIST_TAXLOT
146
                )
147
                show_columns = list(ColumnListProfileColumn.objects.filter(
×
148
                    column_list_profile_id=profile.id
149
                ).values_list('column_id', flat=True))
150
            except ColumnListProfile.DoesNotExist:
×
151
                show_columns = None
×
152

153
        related_results = TaxLotProperty.serialize(taxlot_views, show_columns,
×
154
                                                   columns_from_database)
155

156
        # collapse units here so we're only doing the last page; we're already a
157
        # realized list by now and not a lazy queryset
158
        unit_collapsed_results = [apply_display_unit_preferences(org, x) for x in related_results]
×
159

160
        response = {
×
161
            'pagination': {
162
                'page': page,
163
                'start': paginator.page(page).start_index(),
164
                'end': paginator.page(page).end_index(),
165
                'num_pages': paginator.num_pages,
166
                'has_next': paginator.page(page).has_next(),
167
                'has_previous': paginator.page(page).has_previous(),
168
                'total': paginator.count
169
            },
170
            'cycle_id': cycle.id,
171
            'results': unit_collapsed_results
172
        }
173

174
        return JsonResponse(response)
×
175

176
    # @require_organization_id
177
    # @require_organization_membership
178
    @api_endpoint_class
1✔
179
    @ajax_request_class
1✔
180
    @has_perm_class('requires_viewer')
1✔
181
    def list(self, request):
1✔
182
        """
183
        List all the properties
184
        ---
185
        parameters:
186
            - name: organization_id
187
              description: The organization_id for this user's organization
188
              required: true
189
              paramType: query
190
            - name: cycle
191
              description: The ID of the cycle to get taxlots
192
              required: true
193
              paramType: query
194
            - name: page
195
              description: The current page of taxlots to return
196
              required: false
197
              paramType: query
198
            - name: per_page
199
              description: The number of items per page to return
200
              required: false
201
              paramType: query
202
        """
203
        return self._get_filtered_results(request, profile_id=-1)
×
204

205
    @api_endpoint_class
1✔
206
    @ajax_request_class
1✔
207
    @has_perm_class('requires_viewer')
1✔
208
    @action(detail=False, methods=['POST'])
1✔
209
    def cycles(self, request):
1✔
210
        """
211
        List all the taxlots with all columns
212
        ---
213
        parameters:
214
            - name: organization_id
215
              description: The organization_id for this user's organization
216
              required: true
217
              paramType: query
218
            - name: profile_id
219
              description: Either an id of a list settings profile, or undefined
220
              paramType: body
221
            - name: cycle_ids
222
              description: The IDs of the cycle to get taxlots
223
              required: true
224
              paramType: query
225
        """
226
        org_id = request.data.get('organization_id', None)
×
227
        profile_id = request.data.get('profile_id', -1)
×
228
        cycle_ids = request.data.get('cycle_ids', [])
×
229

230
        if not org_id:
×
231
            return JsonResponse(
×
232
                {'status': 'error', 'message': 'Need to pass organization_id as query parameter'},
233
                status=status.HTTP_400_BAD_REQUEST)
234

235
        response = taxlots_across_cycles(org_id, profile_id, cycle_ids)
×
236

237
        return JsonResponse(response)
×
238

239
    # @require_organization_id
240
    # @require_organization_membership
241
    @api_endpoint_class
1✔
242
    @ajax_request_class
1✔
243
    @has_perm_class('requires_viewer')
1✔
244
    @action(detail=False, methods=['POST'])
1✔
245
    def filter(self, request):
1✔
246
        """
247
        List all the properties
248
        ---
249
        parameters:
250
            - name: organization_id
251
              description: The organization_id for this user's organization
252
              required: true
253
              paramType: query
254
            - name: cycle
255
              description: The ID of the cycle to get taxlots
256
              required: true
257
              paramType: query
258
            - name: page
259
              description: The current page of taxlots to return
260
              required: false
261
              paramType: query
262
            - name: per_page
263
              description: The number of items per page to return
264
              required: false
265
              paramType: query
266
            - name: profile_id
267
              description: Either an id of a list settings profile, or undefined
268
              paramType: body
269
        """
270
        if 'profile_id' not in request.data:
×
271
            profile_id = None
×
272
        else:
273
            if request.data['profile_id'] == 'None':
×
274
                profile_id = None
×
275
            else:
276
                profile_id = request.data['profile_id']
×
277

278
        return self._get_filtered_results(request, profile_id=profile_id)
×
279

280
    @api_endpoint_class
1✔
281
    @ajax_request_class
1✔
282
    @has_perm_class('can_modify_data')
1✔
283
    @action(detail=False, methods=['POST'])
1✔
284
    def merge(self, request):
1✔
285
        """
286
        Merge multiple tax lot records into a single new record, and run this
287
        new record through a match and merge round within it's current Cycle.
288
        ---
289
        parameters:
290
            - name: organization_id
291
              description: The organization_id for this user's organization
292
              required: true
293
              paramType: query
294
            - name: state_ids
295
              description: Array containing tax lot state ids to merge
296
              paramType: body
297
        """
298
        body = request.data
×
299

300
        state_ids = body.get('state_ids', [])
×
301
        organization_id = int(request.query_params.get('organization_id', None))
×
302

303
        # Check the number of state_ids to merge
304
        if len(state_ids) < 2:
×
305
            return JsonResponse({
×
306
                'status': 'error',
307
                'message': 'At least two ids are necessary to merge'
308
            }, status=status.HTTP_400_BAD_REQUEST)
309

310
        merged_state = merge_taxlots(state_ids, organization_id, 'Manual Match')
×
311

312
        merge_count, link_count, view_id = match_merge_link(merged_state.taxlotview_set.first().id, 'TaxLotState')
×
313

314
        result = {
×
315
            'status': 'success'
316
        }
317

318
        result.update({
×
319
            'match_merged_count': merge_count,
320
            'match_link_count': link_count,
321
        })
322

323
        return result
×
324

325
    @api_endpoint_class
1✔
326
    @ajax_request_class
1✔
327
    @has_perm_class('can_modify_data')
1✔
328
    @action(detail=True, methods=['POST'])
1✔
329
    def unmerge(self, request, pk=None):
1✔
330
        """
331
        Unmerge a taxlot view into two taxlot views
332
        ---
333
        parameters:
334
            - name: organization_id
335
              description: The organization_id for this user's organization
336
              required: true
337
              paramType: query
338
        """
339
        try:
×
340
            old_view = TaxLotView.objects.select_related(
×
341
                'taxlot', 'cycle', 'state'
342
            ).get(
343
                id=pk,
344
                taxlot__organization_id=self.request.GET['organization_id']
345
            )
346
        except TaxLotView.DoesNotExist:
×
347
            return {
×
348
                'status': 'error',
349
                'message': 'taxlot view with id {} does not exist'.format(pk)
350
            }
351

352
        # Duplicate pairing
353
        paired_view_ids = list(TaxLotProperty.objects.filter(taxlot_view_id=old_view.id)
×
354
                               .order_by('property_view_id').values_list('property_view_id',
355
                                                                         flat=True))
356

357
        # Capture previous associated labels
358
        label_ids = list(old_view.labels.all().values_list('id', flat=True))
×
359

360
        notes = old_view.notes.all()
×
361
        for note in notes:
×
362
            note.taxlot_view = None
×
363

364
        merged_state = old_view.state
×
365
        if merged_state.data_state != DATA_STATE_MATCHING or merged_state.merge_state != MERGE_STATE_MERGED:
×
366
            return {
×
367
                'status': 'error',
368
                'message': 'taxlot view with id {} is not a merged taxlot view'.format(pk)
369
            }
370

371
        log = TaxLotAuditLog.objects.select_related('parent_state1', 'parent_state2').filter(
×
372
            state=merged_state
373
        ).order_by('-id').first()
374

375
        if log.parent_state1 is None or log.parent_state2 is None:
×
376
            return {
×
377
                'status': 'error',
378
                'message': 'taxlot view with id {} must have two parent states'.format(pk)
379
            }
380

381
        state1 = log.parent_state1
×
382
        state2 = log.parent_state2
×
383
        cycle_id = old_view.cycle_id
×
384

385
        # Clone the taxlot record twice
386
        old_taxlot = old_view.taxlot
×
387
        new_taxlot = old_taxlot
×
388
        new_taxlot.id = None
×
389
        new_taxlot.save()
×
390

391
        new_taxlot_2 = TaxLot.objects.get(pk=new_taxlot.pk)
×
392
        new_taxlot_2.id = None
×
393
        new_taxlot_2.save()
×
394

395
        # If the canonical TaxLot is NOT associated to another -View
396
        if not TaxLotView.objects.filter(taxlot_id=old_view.taxlot_id).exclude(pk=old_view.id).exists():
×
397
            TaxLot.objects.get(pk=old_view.taxlot_id).delete()
×
398

399
        # Create the views
400
        new_view1 = TaxLotView(
×
401
            cycle_id=cycle_id,
402
            taxlot_id=new_taxlot.id,
403
            state=state1
404
        )
405
        new_view2 = TaxLotView(
×
406
            cycle_id=cycle_id,
407
            taxlot_id=new_taxlot_2.id,
408
            state=state2
409
        )
410

411
        # Mark the merged state as deleted
412
        merged_state.merge_state = MERGE_STATE_DELETE
×
413
        merged_state.save()
×
414

415
        # Change the merge_state of the individual states
416
        if log.parent1.name in ['Import Creation',
×
417
                                'Manual Edit'] and log.parent1.import_filename is not None:
418
            # State belongs to a new record
419
            state1.merge_state = MERGE_STATE_NEW
×
420
        else:
421
            state1.merge_state = MERGE_STATE_MERGED
×
422
        if log.parent2.name in ['Import Creation',
×
423
                                'Manual Edit'] and log.parent2.import_filename is not None:
424
            # State belongs to a new record
425
            state2.merge_state = MERGE_STATE_NEW
×
426
        else:
427
            state2.merge_state = MERGE_STATE_MERGED
×
428
        # In most cases data_state will already be 3 (DATA_STATE_MATCHING), but if one of the parents was a
429
        # de-duplicated record then data_state will be 0. This step ensures that the new states will be 3.
430
        state1.data_state = DATA_STATE_MATCHING
×
431
        state2.data_state = DATA_STATE_MATCHING
×
432
        state1.save()
×
433
        state2.save()
×
434

435
        # Delete the audit log entry for the merge
436
        log.delete()
×
437

438
        old_view.delete()
×
439
        new_view1.save()
×
440
        new_view2.save()
×
441

442
        # Associate labels
443
        label_objs = StatusLabel.objects.filter(pk__in=label_ids)
×
444
        new_view1.labels.set(label_objs)
×
445
        new_view2.labels.set(label_objs)
×
446

447
        # Duplicate notes to the new views
448
        for note in notes:
×
449
            created = note.created
×
450
            updated = note.updated
×
451
            note.id = None
×
452
            note.taxlot_view = new_view1
×
453
            note.save()
×
454
            ids = [note.id]
×
455
            note.id = None
×
456
            note.taxlot_view = new_view2
×
457
            note.save()
×
458
            ids.append(note.id)
×
459
            # Correct the created and updated times to match the original note
460
            Note.objects.filter(id__in=ids).update(created=created, updated=updated)
×
461

462
        for paired_view_id in paired_view_ids:
×
463
            TaxLotProperty(primary=True,
×
464
                           cycle_id=cycle_id,
465
                           taxlot_view_id=new_view1.id,
466
                           property_view_id=paired_view_id).save()
467
            TaxLotProperty(primary=True,
×
468
                           cycle_id=cycle_id,
469
                           taxlot_view_id=new_view2.id,
470
                           property_view_id=paired_view_id).save()
471

472
        return {
×
473
            'status': 'success',
474
            'view_id': new_view1.id
475
        }
476

477
    @api_endpoint_class
1✔
478
    @ajax_request_class
1✔
479
    @has_perm_class('can_modify_data')
1✔
480
    @action(detail=True, methods=['POST'])
1✔
481
    def links(self, request, pk=None):
1✔
482
        """
483
        Get taxlot details for each linked taxlot across org cycles
484
        ---
485
        parameters:
486
            - name: pk
487
              description: The primary key of the TaxLotView
488
              required: true
489
              paramType: path
490
            - name: organization_id
491
              description: The organization_id for this user's organization
492
              required: true
493
              paramType: query
494
        """
495
        organization_id = request.data.get('organization_id', None)
×
496
        base_view = TaxLotView.objects.select_related('cycle').filter(
×
497
            pk=pk,
498
            cycle__organization_id=organization_id
499
        )
500

501
        if base_view.exists():
×
502
            result = {'data': []}
×
503

504
            linked_views = TaxLotView.objects.select_related('cycle').filter(
×
505
                taxlot_id=base_view.get().taxlot_id,
506
                cycle__organization_id=organization_id
507
            ).order_by('-cycle__start')
508
            for linked_view in linked_views:
×
509
                state_data = TaxLotStateSerializer(linked_view.state).data
×
510

511
                state_data['cycle_id'] = linked_view.cycle.id
×
512
                state_data['view_id'] = linked_view.id
×
513
                result['data'].append(state_data)
×
514

515
            return JsonResponse(result, status=status.HTTP_200_OK)
×
516
        else:
517
            result = {
×
518
                'status': 'error',
519
                'message': 'property view with id {} does not exist in given organization'.format(pk)
520
            }
521
            return JsonResponse(result)
×
522

523
    @api_endpoint_class
1✔
524
    @ajax_request_class
1✔
525
    @has_perm_class('can_modify_data')
1✔
526
    @action(detail=True, methods=['POST'])
1✔
527
    def match_merge_link(self, request, pk=None):
1✔
528
        """
529
        Runs match merge link for an individual taxlot.
530

531
        Note that this method can return a view_id of None if the given -View
532
        was not involved in a merge.
533
        """
534
        merge_count, link_count, view_id = match_merge_link(pk, 'TaxLotState')
×
535

536
        result = {
×
537
            'view_id': view_id,
538
            'match_merged_count': merge_count,
539
            'match_link_count': link_count,
540
        }
541

542
        return JsonResponse(result)
×
543

544
    @api_endpoint_class
1✔
545
    @ajax_request_class
1✔
546
    @has_perm_class('can_modify_data')
1✔
547
    @action(detail=True, methods=['PUT'])
1✔
548
    def pair(self, request, pk=None):
1✔
549
        """
550
        Pair a property to this taxlot
551
        ---
552
        parameter_strategy: replace
553
        parameters:
554
            - name: organization_id
555
              description: The organization_id for this user's organization
556
              required: true
557
              paramType: query
558
            - name: property_id
559
              description: The property id to pair up with this taxlot
560
              required: true
561
              paramType: query
562
            - name: pk
563
              description: pk (taxlot ID)
564
              required: true
565
              paramType: path
566
        """
567
        # TODO: Call with PUT /api/v2/taxlots/1/pair/?property_id=1&organization_id=1
568
        organization_id = int(request.query_params.get('organization_id'))
×
569
        property_id = int(request.query_params.get('property_id'))
×
570
        taxlot_id = int(pk)
×
571
        return pair_unpair_property_taxlot(property_id, taxlot_id, organization_id, True)
×
572

573
    @api_endpoint_class
1✔
574
    @ajax_request_class
1✔
575
    @has_perm_class('can_modify_data')
1✔
576
    @action(detail=True, methods=['PUT'])
1✔
577
    def unpair(self, request, pk=None):
1✔
578
        """
579
        Unpair a property from this taxlot
580
        ---
581
        parameter_strategy: replace
582
        parameters:
583
            - name: organization_id
584
              description: The organization_id for this user's organization
585
              required: true
586
              paramType: query
587
            - name: property_id
588
              description: The property id to unpair from this taxlot
589
              required: true
590
              paramType: query
591
            - name: pk
592
              description: pk (taxlot ID)
593
              required: true
594
              paramType: path
595
        """
596
        # TODO: Call with PUT /api/v2/taxlots/1/unpair/?property_id=1&organization_id=1
597
        organization_id = int(request.query_params.get('organization_id'))
×
598
        property_id = int(request.query_params.get('property_id'))
×
599
        taxlot_id = int(pk)
×
600
        return pair_unpair_property_taxlot(property_id, taxlot_id, organization_id, False)
×
601

602
    # @require_organization_id
603
    # @require_organization_membership
604
    @api_endpoint_class
1✔
605
    @ajax_request_class
1✔
606
    @has_perm_class('requires_viewer')
1✔
607
    @action(detail=False, methods=['GET'])
1✔
608
    def columns(self, request):
1✔
609
        """
610
        List all tax lot columns
611
        ---
612
        parameters:
613
            - name: organization_id
614
              description: The organization_id for this user's organization
615
              required: true
616
              paramType: query
617
            - name: used_only
618
              description: Determine whether or not to show only the used fields. Ones that have been mapped
619
              type: boolean
620
              required: false
621
              paramType: query
622
        """
623
        organization_id = int(request.query_params.get('organization_id'))
×
624
        organization = Organization.objects.get(pk=organization_id)
×
625

626
        only_used = json.loads(request.query_params.get('only_used', 'false'))
×
627
        columns = Column.retrieve_all(organization_id, 'taxlot', only_used)
×
628
        unitted_columns = [add_pint_unit_suffix(organization, x) for x in columns]
×
629

630
        return JsonResponse({'status': 'success', 'columns': unitted_columns})
×
631

632
    @api_endpoint_class
1✔
633
    @ajax_request_class
1✔
634
    @has_perm_class('requires_viewer')
1✔
635
    @action(detail=False, methods=['GET'])
1✔
636
    def mappable_columns(self, request):
1✔
637
        """
638
        List only taxlot columns that are mappable
639
        parameters:
640
            - name: organization_id
641
              description: The organization_id for this user's organization
642
              required: true
643
              paramType: query
644
        """
645
        organization_id = int(request.query_params.get('organization_id'))
×
646
        columns = Column.retrieve_mapping_columns(organization_id, 'taxlot')
×
647

648
        return JsonResponse({'status': 'success', 'columns': columns})
×
649

650
    @api_endpoint_class
1✔
651
    @ajax_request_class
1✔
652
    @has_perm_class('can_modify_data')
1✔
653
    @action(detail=False, methods=['DELETE'])
1✔
654
    def batch_delete(self, request):
1✔
655
        """
656
        Batch delete several tax lots
657
        ---
658
        parameters:
659
            - name: selected
660
              description: A list of taxlot ids to delete
661
              many: true
662
              required: true
663
        """
664
        taxlot_states = request.data.get('selected', [])
×
665
        resp = TaxLotState.objects.filter(pk__in=taxlot_states).delete()
×
666

667
        if resp[0] == 0:
×
668
            return JsonResponse({'status': 'warning', 'message': 'No action was taken'})
×
669

670
        return JsonResponse({'status': 'success', 'taxlots': resp[1]['seed.TaxLotState']})
×
671

672
    def _get_taxlot_view(self, taxlot_pk):
1✔
673
        try:
×
674
            taxlot_view = TaxLotView.objects.select_related(
×
675
                'taxlot', 'cycle', 'state'
676
            ).get(
677
                id=taxlot_pk,
678
                taxlot__organization_id=self.request.GET['organization_id']
679
            )
680
            result = {
×
681
                'status': 'success',
682
                'taxlot_view': taxlot_view
683
            }
684
        except TaxLotView.DoesNotExist:
×
685
            result = {
×
686
                'status': 'error',
687
                'message': 'taxlot view with id {} does not exist'.format(taxlot_pk)
688
            }
689
        return result
×
690

691
    def get_history(self, taxlot_view):
1✔
692
        """Return history in reverse order"""
693

694
        # access the history from the property state
695
        history, master = taxlot_view.state.history()
×
696

697
        # convert the history and master states to StateSerializers
698
        master['state'] = TaxLotStateSerializer(master['state_data']).data
×
699
        del master['state_data']
×
700
        del master['state_id']
×
701

702
        for h in history:
×
703
            h['state'] = TaxLotStateSerializer(h['state_data']).data
×
704
            del h['state_data']
×
705
            del h['state_id']
×
706

707
        return history, master
×
708

709
    def _get_properties(self, taxlot_view_pk):
1✔
710
        property_view_pks = TaxLotProperty.objects.filter(
×
711
            taxlot_view_id=taxlot_view_pk
712
        ).values_list('property_view_id', flat=True)
713
        property_views = PropertyView.objects.filter(
×
714
            pk__in=property_view_pks
715
        ).select_related('cycle', 'state')
716
        properties = []
×
717
        for property_view in property_views:
×
718
            properties.append(PropertyViewSerializer(property_view).data)
×
719
        return properties
×
720

721
    @api_endpoint_class
1✔
722
    @ajax_request_class
1✔
723
    @action(detail=True, methods=['GET'])
1✔
724
    def properties(self, pk):
1✔
725
        """
726
        Get related properties for this tax lot
727
        """
728
        return JsonResponse(self._get_properties(pk))
×
729

730
    @api_endpoint_class
1✔
731
    @ajax_request_class
1✔
732
    def retrieve(self, request, pk):
1✔
733
        """
734
        Get taxlot details
735
        ---
736
        parameters:
737
            - name: pk
738
              description: The primary key of the TaxLotView
739
              required: true
740
              paramType: path
741
            - name: organization_id
742
              description: The organization_id for this user's organization
743
              required: true
744
              paramType: query
745
        """
746
        result = self._get_taxlot_view(pk)
×
747
        if result.get('status', None) != 'error':
×
748
            taxlot_view = result.pop('taxlot_view')
×
749
            result.update(TaxLotViewSerializer(taxlot_view).data)
×
750
            # remove TaxLotView id from result
751
            result.pop('id')
×
752

753
            result['state'] = TaxLotStateSerializer(taxlot_view.state).data
×
754
            result['properties'] = self._get_properties(taxlot_view.pk)
×
755
            result['history'], master = self.get_history(taxlot_view)
×
756
            result = update_result_with_master(result, master)
×
757
            return JsonResponse(result, status=status.HTTP_200_OK)
×
758
        else:
759
            return JsonResponse(result, status=status.HTTP_404_NOT_FOUND)
×
760

761
    @api_endpoint_class
1✔
762
    @ajax_request_class
1✔
763
    def update(self, request, pk):
1✔
764
        """
765
        Update a taxlot and run the updated record through a match and merge
766
        round within it's current Cycle.
767
        ---
768
        parameters:
769
            - name: organization_id
770
              description: The organization_id for this user's organization
771
              required: true
772
              paramType: query
773
        """
774
        data = request.data
×
775

776
        result = self._get_taxlot_view(pk)
×
777
        if result.get('status', 'error') != 'error':
×
778
            taxlot_view = result.pop('taxlot_view')
×
779
            taxlot_state_data = TaxLotStateSerializer(taxlot_view.state).data
×
780

781
            # get the taxlot state information from the request
782
            new_taxlot_state_data = data['state']
×
783

784
            # set empty strings to None
785
            for key, val in new_taxlot_state_data.items():
×
786
                if val == '':
×
787
                    new_taxlot_state_data[key] = None
×
788

789
            changed_fields, previous_data = get_changed_fields(taxlot_state_data, new_taxlot_state_data)
×
790
            if not changed_fields:
×
791
                result.update(
×
792
                    {'status': 'success', 'message': 'Records are identical'}
793
                )
794
                return JsonResponse(result, status=status.HTTP_204_NO_CONTENT)
×
795
            else:
796
                # Not sure why we are going through the pain of logging this all right now... need to
797
                # reevaluate this.
798
                log = TaxLotAuditLog.objects.select_related().filter(
×
799
                    state=taxlot_view.state
800
                ).order_by('-id').first()
801

802
                # if checks above pass, create an exact copy of the current state for historical purposes
803
                if log.name == 'Import Creation':
×
804
                    # Add new state by removing the existing ID.
805
                    taxlot_state_data.pop('id')
×
806
                    # Remove the import_file_id for the first edit of a new record
807
                    # If the import file has been deleted and this value remains the serializer won't be valid
808
                    taxlot_state_data.pop('import_file')
×
809
                    new_taxlot_state_serializer = TaxLotStateSerializer(
×
810
                        data=taxlot_state_data
811
                    )
812
                    if new_taxlot_state_serializer.is_valid():
×
813
                        # create the new property state, and perform an initial save / moving relationships
814
                        new_state = new_taxlot_state_serializer.save()
×
815

816
                        # then assign this state to the property view and save the whole view
817
                        taxlot_view.state = new_state
×
818
                        taxlot_view.save()
×
819

820
                        TaxLotAuditLog.objects.create(organization=log.organization,
×
821
                                                      parent1=log,
822
                                                      parent2=None,
823
                                                      parent_state1=log.state,
824
                                                      parent_state2=None,
825
                                                      state=new_state,
826
                                                      name='Manual Edit',
827
                                                      description=None,
828
                                                      import_filename=log.import_filename,
829
                                                      record_type=AUDIT_USER_EDIT)
830

831
                        result.update(
×
832
                            {'state': new_taxlot_state_serializer.data}
833
                        )
834

835
                        # save the property view so that the datetime gets updated on the property.
836
                        taxlot_view.save()
×
837
                    else:
838
                        result.update({
×
839
                            'status': 'error',
840
                            'message': 'Invalid update data with errors: {}'.format(
841
                                new_taxlot_state_serializer.errors)}
842
                        )
843
                        return JsonResponse(result, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
×
844

845
                # redo assignment of this variable in case this was an initial edit
846
                taxlot_state_data = TaxLotStateSerializer(taxlot_view.state).data
×
847

848
                if 'extra_data' in new_taxlot_state_data:
×
849
                    taxlot_state_data['extra_data'].update(
×
850
                        new_taxlot_state_data['extra_data']
851
                    )
852

853
                taxlot_state_data.update(
×
854
                    {k: v for k, v in new_taxlot_state_data.items() if k != 'extra_data'}
855
                )
856

857
                log = TaxLotAuditLog.objects.select_related().filter(
×
858
                    state=taxlot_view.state
859
                ).order_by('-id').first()
860

861
                if log.name in ['Manual Edit', 'Manual Match', 'System Match', 'Merge current state in migration']:
×
862
                    # Convert this to using the serializer to save the data. This will override the
863
                    # previous values in the state object.
864

865
                    # Note: We should be able to use partial update here and pass in the changed
866
                    # fields instead of the entire state_data.
867
                    updated_taxlot_state_serializer = TaxLotStateSerializer(
×
868
                        taxlot_view.state,
869
                        data=taxlot_state_data
870
                    )
871
                    if updated_taxlot_state_serializer.is_valid():
×
872
                        # create the new property state, and perform an initial save / moving
873
                        # relationships
874
                        updated_taxlot_state_serializer.save()
×
875

876
                        result.update(
×
877
                            {'state': updated_taxlot_state_serializer.data}
878
                        )
879

880
                        # save the property view so that the datetime gets updated on the property.
881
                        taxlot_view.save()
×
882

883
                        Note.create_from_edit(request.user.id, taxlot_view, new_taxlot_state_data, previous_data)
×
884

885
                        merge_count, link_count, view_id = match_merge_link(taxlot_view.id, 'TaxLotState')
×
886

887
                        result.update({
×
888
                            'view_id': view_id,
889
                            'match_merged_count': merge_count,
890
                            'match_link_count': link_count,
891
                        })
892

893
                        return JsonResponse(result, status=status.HTTP_200_OK)
×
894
                    else:
895
                        result.update({
×
896
                            'status': 'error',
897
                            'message': 'Invalid update data with errors: {}'.format(
898
                                updated_taxlot_state_serializer.errors)}
899
                        )
900
                        return JsonResponse(result, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
×
901
                else:
902
                    result = {
×
903
                        'status': 'error',
904
                        'message': 'Unrecognized audit log name: ' + log.name
905
                    }
906
                    return JsonResponse(result, status=status.HTTP_204_NO_CONTENT)
×
907
        else:
908
            return JsonResponse(result, status=status.HTTP_404_NOT_FOUND)
×
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