Exemple #1
0
class LabelViewSet(DecoratorMixin(drf_api_endpoint), viewsets.ModelViewSet):
    serializer_class = LabelSerializer
    queryset = Label.objects.none()
    filter_backends = (LabelFilterBackend, )
    pagination_class = FakePaginiation

    _organization = None

    def get_organization(self):
        if self._organization is None:
            try:
                self._organization = self.request.user.orgs.get(
                    pk=self.request.query_params["organization_id"], )
            except (KeyError, ObjectDoesNotExist):
                self._organization = self.request.user.orgs.all()[0]
        return self._organization

    def get_queryset(self):
        return Label.objects.filter(super_organization=self.get_organization()
                                    ).order_by("name").distinct()

    def get_serializer(self, *args, **kwargs):
        kwargs['super_organization'] = self.get_organization()
        building_snapshots = BuildingFilterBackend().filter_queryset(
            request=self.request,
            queryset=BuildingSnapshot.objects.all(),
            view=self,
        )
        kwargs['building_snapshots'] = building_snapshots
        return super(LabelViewSet, self).get_serializer(*args, **kwargs)
Exemple #2
0
class LabelViewSet(DecoratorMixin(drf_api_endpoint), viewsets.ModelViewSet):
    serializer_class = LabelSerializer
    renderer_classes = (JSONRenderer, )
    parser_classes = (JSONParser, )
    queryset = Label.objects.none()
    filter_backends = (LabelFilterBackend, )
    pagination_class = NoPagination

    _organization = None

    def get_parent_organization(self):
        org = self.get_organization()
        if org.is_parent:
            return org
        else:
            return org.parent_org

    def get_organization(self):
        if self._organization is None:
            try:
                self._organization = self.request.user.orgs.get(
                    pk=self.request.query_params["organization_id"], )
            except (KeyError, ObjectDoesNotExist):
                self._organization = self.request.user.orgs.all()[0]
        return self._organization

    def get_queryset(self):
        labels = Label.objects.filter(
            super_organization=self.get_parent_organization()).order_by(
                "name").distinct()
        return labels

    # TODO update for new data model
    def get_serializer(self, *args, **kwargs):
        kwargs['super_organization'] = self.get_organization()
        inventory = InventoryFilterBackend().filter_queryset(
            request=self.request, )
        kwargs['inventory'] = inventory
        return super(LabelViewSet, self).get_serializer(*args, **kwargs)

    def _get_labels(self, request):
        qs = self.get_queryset()
        super_organization = self.get_organization()
        inventory = InventoryFilterBackend().filter_queryset(
            request=self.request, )
        results = [
            LabelSerializer(q,
                            super_organization=super_organization,
                            inventory=inventory).data for q in qs
        ]
        status_code = status.HTTP_200_OK
        return response.Response(results, status=status_code)

    @list_route(methods=['POST'])
    def filter(self, request):
        return self._get_labels(request)

    def list(self, request):
        return self._get_labels(request)
Exemple #3
0
class SEEDOrgReadOnlyModelViewSet(DecoratorMixin(drf_api_endpoint),
                                  OrgQuerySetMixin, ReadOnlyModelViewSet):
    """Viewset class customized with SEED standard attributes.

    Attributes:
        renderer_classes: Tuple of classes, default set to SEEDJSONRenderer.
        parser_classes: Tuple of classes, default set to drf's JSONParser.
        authentication_classes: Tuple of classes, default set to drf's
            SessionAuthentication and SEEDAuthentication.
    """
    renderer_classes = RENDERER_CLASSES
    parser_classes = PARSER_CLASSES
    authentication_classes = AUTHENTICATION_CLASSES
    permission_classes = PERMISSIONS_CLASSES
Exemple #4
0
class LabelViewSet(DecoratorMixin(drf_api_endpoint),
                   SEEDOrgNoPatchOrOrgCreateModelViewSet):
    """
    retrieve:
        Return a label instance by pk if it is within user`s specified org.

    list:
        Return all labels available to user through user`s specified org.

    create:
        Create a new label within user`s specified org.

    delete:
        Remove an existing label.

    update:
        Update a label record.
    """
    serializer_class = LabelSerializer
    renderer_classes = (JSONRenderer, )
    parser_classes = (JSONParser, FormParser)
    queryset = Label.objects.none()
    filter_backends = (LabelFilterBackend, )
    pagination_class = None
    _organization = None

    def get_queryset(self):
        labels = Label.objects.filter(super_organization=self.get_parent_org(
            self.request)).order_by("name").distinct()
        return labels

    def get_serializer(self, *args, **kwargs):
        kwargs['super_organization'] = self.get_organization(self.request)
        inventory = filter_labels_for_inv_type(request=self.request)
        kwargs['inventory'] = inventory
        return super().get_serializer(*args, **kwargs)
Exemple #5
0
class Report(DecoratorMixin(drf_api_endpoint), ViewSet):
    renderer_classes = (JSONRenderer, )
    parser_classes = (JSONParser, )

    def get_cycles(self, start, end):
        organization_id = self.request.GET['organization_id']
        if not isinstance(start, type(end)):
            raise TypeError('start and end not same types')
        # if of type int or convertable  assume they are cycle ids
        try:
            start = int(start)
            end = int(end)
        except ValueError as error:  # noqa
            # assume string is JS date
            if isinstance(start, basestring):
                start_datetime = dateutil.parser.parse(start)
                end_datetime = dateutil.parser.parse(end)
            else:
                raise Exception('Date is not a string')
        # get date times from cycles
        if isinstance(start, int):
            cycle = Cycle.objects.get(pk=start,
                                      organization_id=organization_id)
            start_datetime = cycle.start
            if start == end:
                end_datetime = cycle.end
            else:
                end_datetime = Cycle.objects.get(
                    pk=end, organization_id=organization_id).end
        return Cycle.objects.filter(
            start__gte=start_datetime,
            end__lte=end_datetime,
            organization_id=organization_id).order_by('start')

    def get_data(self, property_view, x_var, y_var):
        result = None
        state = property_view.state
        if getattr(state, x_var, None) and getattr(state, y_var, None):
            result = {
                "id": property_view.property_id,
                "x": getattr(state, x_var),
                "y": getattr(state, y_var),
            }
        return result

    def get_raw_report_data(self, organization_id, cycles, x_var, y_var,
                            campus_only):
        all_property_views = PropertyView.objects.select_related(
            'property',
            'state').filter(property__organization_id=organization_id,
                            cycle_id__in=cycles)
        organization = Organization.objects.get(pk=organization_id)
        results = []
        for cycle in cycles:
            property_views = all_property_views.filter(cycle_id=cycle)
            count_total = []
            count_with_data = []
            data = []
            for property_view in property_views:
                property_pk = property_view.property_id
                if property_view.property.campus and campus_only:
                    count_total.append(property_pk)
                    result = self.get_data(property_view, x_var, y_var)
                    if result:
                        result['yr_e'] = cycle.end.strftime('%Y')
                        data.append(result)
                        count_with_data.append(property_pk)
                elif not property_view.property.campus:
                    count_total.append(property_pk)
                    result = self.get_data(property_view, x_var, y_var)
                    if result:
                        result['yr_e'] = cycle.end.strftime('%Y')
                        de_unitted_result = apply_display_unit_preferences(
                            organization, result)
                        data.append(de_unitted_result)
                        count_with_data.append(property_pk)
            result = {
                "cycle_id": cycle.pk,
                "chart_data": data,
                "property_counts": {
                    "yr_e": cycle.end.strftime('%Y'),
                    "num_properties": len(count_total),
                    "num_properties_w-data": len(count_with_data),
                },
            }
            results.append(result)
        return results

    def get_property_report_data(self, request):
        campus_only = request.query_params.get('campus_only', False)
        params = {}
        missing_params = []
        error = ''
        for param in ['x_var', 'y_var', 'organization_id', 'start', 'end']:
            val = request.query_params.get(param, None)
            if not val:
                missing_params.append(param)
            else:
                params[param] = val
        if missing_params:
            error = "{} Missing params: {}".format(error,
                                                   ", ".join(missing_params))
        if error:
            status_code = status.HTTP_400_BAD_REQUEST
            result = {'status': 'error', 'message': error}
        else:
            cycles = self.get_cycles(params['start'], params['end'])
            data = self.get_raw_report_data(params['organization_id'], cycles,
                                            params['x_var'], params['y_var'],
                                            campus_only)
            for datum in data:
                if datum['property_counts']['num_properties_w-data'] != 0:
                    break
            property_counts = []
            chart_data = []
            for datum in data:
                property_counts.append(datum['property_counts'])
                chart_data.extend(datum['chart_data'])
            data = {
                'property_counts': property_counts,
                'chart_data': chart_data,
            }
            result = {'status': 'success', 'data': data}
            status_code = status.HTTP_200_OK
        return Response(result, status=status_code)

    def get_aggregated_property_report_data(self, request):
        campus_only = request.query_params.get('campus_only', False)
        valid_y_values = ['gross_floor_area', 'use_description', 'year_built']
        params = {}
        missing_params = []
        empty = True
        error = ''
        for param in ['x_var', 'y_var', 'organization_id', 'start', 'end']:
            val = request.query_params.get(param, None)
            if not val:
                missing_params.append(param)
            elif param == 'y_var' and val not in valid_y_values:
                error = "{} {} is not a valid value for {}.".format(
                    error, val, param)
            else:
                params[param] = val
        if missing_params:
            error = "{} Missing params: {}".format(error,
                                                   ", ".join(missing_params))
        if error:
            status_code = status.HTTP_400_BAD_REQUEST
            result = {'status': 'error', 'message': error}
        else:
            cycles = self.get_cycles(params['start'], params['end'])
            x_var = params['x_var']
            y_var = params['y_var']
            data = self.get_raw_report_data(params['organization_id'], cycles,
                                            x_var, y_var, campus_only)
            for datum in data:
                if datum['property_counts']['num_properties_w-data'] != 0:
                    empty = False
                    break
            if empty:
                result = {'status': 'error', 'message': 'No data found'}
                status_code = status.HTTP_404_NOT_FOUND
        if not empty or not error:
            chart_data = []
            property_counts = []
            for datum in data:
                buildings = datum['chart_data']
                yr_e = datum['property_counts']['yr_e']
                chart_data.extend(self.aggregate_data(yr_e, y_var, buildings)),
                property_counts.append(datum['property_counts'])
            # Send back to client
            aggregated_data = {
                'chart_data': chart_data,
                'property_counts': property_counts
            }
            result = {
                'status': 'success',
                'aggregated_data': aggregated_data,
            }
            status_code = status.HTTP_200_OK
        return Response(result, status=status_code)

    def aggregate_data(self, yr_e, y_var, buildings):
        aggregation_method = {
            'use_description': self.aggregate_use_description,
            'year_built': self.aggregate_year_built,
            'gross_floor_area': self.aggregate_gross_floor_area,
        }
        return aggregation_method[y_var](yr_e, buildings)

    def aggregate_use_description(self, yr_e, buildings):
        # Group buildings in this year_ending group into uses
        chart_data = []
        grouped_uses = defaultdict(list)
        for b in buildings:
            grouped_uses[str(b['y']).lower()].append(b)

        # Now iterate over use groups to make each chart item
        for use, buildings_in_uses in grouped_uses.items():
            chart_data.append({
                'x': median([b['x'] for b in buildings_in_uses]),
                'y': use.capitalize(),
                'yr_e': yr_e
            })
        return chart_data

    def aggregate_year_built(self, yr_e, buildings):
        # Group buildings in this year_ending group into decades
        chart_data = []
        grouped_decades = defaultdict(list)
        for b in buildings:
            grouped_decades['%s0' % str(b['y'])[:-1]].append(b)

        # Now iterate over decade groups to make each chart item
        for decade, buildings_in_decade in grouped_decades.items():
            chart_data.append({
                'x':
                median([b['x'] for b in buildings_in_decade]),
                'y':
                '%s-%s' % (decade, '%s9' % str(decade)[:-1]),  # 1990-1999
                'yr_e':
                yr_e
            })
        return chart_data

    def aggregate_gross_floor_area(self, yr_e, buildings):
        chart_data = []
        y_display_map = {
            0: '0-99k',
            100000: '100-199k',
            200000: '200k-299k',
            300000: '300k-399k',
            400000: '400-499k',
            500000: '500-599k',
            600000: '600-699k',
            700000: '700-799k',
            800000: '800-899k',
            900000: '900-999k',
            1000000: 'over 1,000k',
        }
        max_bin = max(y_display_map)

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

        # Now iterate over range groups to make each chart item
        for range_floor, buildings_in_range in grouped_ranges.items():
            chart_data.append({
                'x': median([b['x'] for b in buildings_in_range]),
                'y': y_display_map[range_floor],
                'yr_e': yr_e
            })
        return chart_data
Exemple #6
0
class LabelViewSet(DecoratorMixin(drf_api_endpoint), viewsets.ModelViewSet):
    """API endpoint for viewing and creating labels.

            Returns::
                [
                    {
                        'id': Label's primary key
                        'name': Name given to label
                        'color': Color of label,
                        'organization_id': Id of organization label belongs to,
                        'is_applied': Will be empty array if not applied to property/taxlots
                    }
                ]

    ---
    """
    serializer_class = LabelSerializer
    renderer_classes = (JSONRenderer, )
    parser_classes = (JSONParser, FormParser)
    queryset = Label.objects.none()
    filter_backends = (LabelFilterBackend, )
    pagination_class = NoPagination

    _organization = None

    def get_parent_organization(self):
        org = self.get_organization()
        if org.is_parent:
            return org
        else:
            return org.parent_org

    def get_organization(self):
        if self._organization is None:
            try:
                self._organization = self.request.user.orgs.get(
                    pk=self.request.query_params["organization_id"], )
            except (KeyError, ObjectDoesNotExist):
                self._organization = self.request.user.orgs.all()[0]
        return self._organization

    def get_queryset(self):
        labels = Label.objects.filter(
            super_organization=self.get_parent_organization()).order_by(
                "name").distinct()
        return labels

    def get_serializer(self, *args, **kwargs):
        kwargs['super_organization'] = self.get_organization()
        inventory = InventoryFilterBackend().filter_queryset(
            request=self.request, )
        kwargs['inventory'] = inventory
        return super().get_serializer(*args, **kwargs)

    def _get_labels(self, request):
        qs = self.get_queryset()
        super_organization = self.get_organization()
        inventory = InventoryFilterBackend().filter_queryset(
            request=self.request, )
        results = [
            LabelSerializer(q,
                            super_organization=super_organization,
                            inventory=inventory).data for q in qs
        ]
        status_code = status.HTTP_200_OK
        return response.Response(results, status=status_code)

    @list_route(methods=['POST'])
    def filter(self, request):
        return self._get_labels(request)

    def list(self, request):
        return self._get_labels(request)
Exemple #7
0
class ProjectViewSet(DecoratorMixin(drf_api_endpoint), viewsets.ModelViewSet):
    serializer_class = ProjectSerializer
    renderer_classes = (JSONRenderer,)
    parser_classes = (JSONParser,)
    query_set = Project.objects.none()
    ProjectViewModels = {
        'property': ProjectPropertyView, 'taxlot': ProjectTaxLotView
    }
    ViewModels = {
        'property': PropertyView, 'taxlot': TaxLotView
    }

    # helper methods
    def get_error(self, error, key=None, val=None):
        """Return error message and corresponding http status code."""
        errors = {
            'not found': (
                'Could not find project with {}: {}'.format(key, val),
                status.HTTP_404_NOT_FOUND
            ),
            'permission denied': (
                'Permission denied', status.HTTP_403_FORBIDDEN
            ),
            'bad request': (
                'Incorrect {}'.format(key), status.HTTP_400_BAD_REQUEST
            ),
            'missing param': (
                'Required parameter(s) missing: {}'.format(key),
                status.HTTP_400_BAD_REQUEST
            ),
            'conflict': (
                '{} already exists for {}'.format(key, val),
                status.HTTP_409_CONFLICT
            ),
            'missing inventory': (
                'No {} views found'.format(key),
                status.HTTP_404_NOT_FOUND
            ),
            'missing instance': (
                'Could not find instance of {} with pk {}'.format(key, val),
                status.HTTP_404_NOT_FOUND
            ),
            'misc': (key, status.HTTP_400_BAD_REQUEST)
        }
        return errors[error]

    def get_key(self, pk):
        """Determine where to use slug or pk to identify project."""
        try:
            pk = int(pk)
            key = 'id'
        except ValueError:
            key = 'slug'
        return key

    def get_params(self, keys):
        """
        Get required params from post etc body.

        Returns dict of params and list of missing params.
        """
        rdict = {
            key: self.request.data.get(key) for key in keys
            if self.request.data.get(key, None) is not None
        }
        missing = [key for key in keys if key not in rdict]
        return rdict, missing

    def get_project(self, key, pk):
        """Get project for view."""
        # convert to int if number and look up by pk, otherwise slug
        filter_dict = {key: pk}
        return self.get_queryset().filter(
            **filter_dict
        )

    def get_organization(self):
        """Get org id from query param or request.user."""
        if not getattr(self, '_organization', None):
            try:
                self._organization = self.request.user.orgs.get(
                    pk=self.request.query_params["organization_id"],
                ).pk
            except (KeyError, ObjectDoesNotExist):
                self._organization = self.request.user.orgs.all()[0].pk
        return self._organization

    def get_queryset(self):
        return Project.objects.filter(
            super_organization_id=self.get_organization()
        ).order_by("name").distinct()

    def get_status(self, status):
        """Get status from string or int"""
        try:
            status = int(status)
        except ValueError:
            status = STATUS_LOOKUP[status.lower()]
        return status

    def project_view_factory(self, inventory_type, project_id, view_id):
        """ProjectPropertyView/ProjectTaxLotView factory."""
        Model = self.ProjectViewModels[inventory_type]
        create_dict = {
            'project_id': project_id,
            '{}_view_id'.format(inventory_type): view_id
        }
        return Model(**create_dict)

    # CRUD Views
    @api_endpoint_class
    @has_perm_class('requires_viewer')
    def list(self, request):
        """
        Retrieves all projects for a given organization.

        :GET: Expects organization_id in query string.

        parameters:
            - name: organization_id
              description: The organization_id for this user's organization
              required: true
              paramType: query

        Returns::

            {
                'status': 'success',
                'projects': [
                    {
                        'id': project's primary key,
                        'name': project's name,
                        'slug': project's identifier,
                        'status': 'active',
                        'number_of_buildings': Count of buildings associated with project
                        'last_modified': Timestamp when project last changed
                        'last_modified_by': {
                            'first_name': first name of user that made last change,
                            'last_name': last name,
                            'email': email address,
                        },
                        'is_compliance': True if project is a compliance project,
                        'compliance_type': Description of compliance type,
                        'deadline_date': Timestamp of when compliance is due,
                        'end_date': Timestamp of end of project,
                        'property_count': number of property views associated with project,
                        'taxlot_count':  number of taxlot views associated with project,
                    }...
                ]
            }
        """
        projects = [
            ProjectSerializer(proj).data for proj in self.get_queryset()
        ]
        status_code = status.HTTP_200_OK
        result = {
            'status': 'success',
            'projects': projects
        }
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_viewer')
    def retrieve(self, request, pk):
        """
        Retrieves details about a project.

        :GET: Expects organization_id in query string.
        ---
        parameter_strategy: replace
        parameters:
            - name: organization_id
              description: The organization_id for this user's organization
              required: true
              paramType: query
            - name: project slug or pk
              description: The project slug identifier or primary key for this project
              required: true
              paramType: path

        Returns::

            {
             'id': project's primary key,
             'name': project's name,
             'slug': project's identifier,
             'status': 'active',
             'number_of_buildings': Count of buildings associated with project
             'last_modified': Timestamp when project last changed
             'last_modified_by': {
                'first_name': first name of user that made last change,
                'last_name': last name,
                'email': email address,
                },
             'is_compliance': True if project is a compliance project,
             'compliance_type': Description of compliance type,
             'deadline_date': Timestamp of when compliance is due,
             'end_date': Timestamp of end of project
             'property_count': number of property views associated with project,
             'taxlot_count':  number of taxlot views associated with project,
             'property_views': [list of serialized property views associated with the project...],
             'taxlot_views': [list of serialized taxlot views associated with the project...],
            }

        """

        error = None
        status_code = status.HTTP_200_OK
        key = self.get_key(pk)
        project = self.get_project(key, pk)
        cycle = request.query_params.get('cycle', None)
        if not project:
            error, status_code = self.get_error(
                'not found', key=key, val=pk
            )
            result = {'status': 'error', 'message': error}
        else:
            project = project[0]
            property_views = project.property_views.all()
            taxlot_views = project.taxlot_views.all()
            if cycle:
                property_views = property_views.filter(
                    cycle_id=cycle
                )
                taxlot_views = taxlot_views.filter(
                    cycle_id=cycle
                )
            project_data = ProjectSerializer(project).data
            project_data['property_views'] = [
                PropertyViewSerializer(property_view).data
                for property_view in property_views
            ]
            project_data['taxlot_views'] = [
                TaxLotViewSerializer(taxlot_view).data
                for taxlot_view in taxlot_views
            ]
            result = {
                'status': 'success',
                'project': project_data,
            }
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def create(self, request):
        """
        Creates a new project

        :POST: Expects organization_id in query string.
        ---
        parameters:
            - name: organization_id
              description: ID of organization to associate new project with
              type: integer
              required: true
              paramType: query
            - name: name
              description: name of the new project
              type: string
              required: true
            - name: is_compliance
              description: add compliance data if true
              type: bool
              required: true
            - name: compliance_type
              description: description of type of compliance
              type: string
              required: true if is_compliance else false
            - name: description
              description: description of new project
              type: string
              required: true if is_compliance else false
            - name: end_date
              description: Timestamp for when project ends
              type: string
              required: true if is_compliance else false
            - name: deadline_date
              description: Timestamp for compliance deadline
              type: string
              required: true if is_compliance else false
        Returns::
            {
                'status': 'success',
                'project': {
                        'id': project's primary key,
                        'name': project's name,
                        'slug': project's identifier,
                        'status': 'active',
                        'number_of_buildings': Count of buildings associated with project
                        'last_modified': Timestamp when project last changed
                        'last_modified_by': {
                            'first_name': first name of user that made last change,
                            'last_name': last name,
                            'email': email address,
                        },
                        'is_compliance': True if project is a compliance project,
                        'compliance_type': Description of compliance type,
                        'deadline_date': Timestamp of when compliance is due,
                        'end_date': Timestamp of end of project,
                        'property_count': 0,
                        'taxlot_count':  0,
                    }
            }
        """
        error = None
        status_code = status.HTTP_200_OK
        super_organization_id = self.get_organization()
        project_data, missing = self.get_params(PROJECT_KEYS)
        project_data.update({
            'owner': request.user,
            'super_organization_id': super_organization_id,
        })
        is_compliance = project_data.pop('is_compliance', None)
        if missing:
            error, status_code = self.get_error(
                'missing param', key=", ".join(missing)
            )
        else:
            try:
                # convert to int equivalent
                project_data['status'] = self.get_status(project_data['status'])
            except KeyError:
                error, status_code = self.get_status(
                    'bad request', key='status'
                )
            if not error and is_compliance:
                compliance_data, missing = self.get_params(
                    COMPLIANCE_KEYS
                )
                if missing:
                    error, status_code = self.get_error(
                        'missing param', key=", ".join(missing)
                    )
                else:
                    compliance_data = convert_dates(
                        compliance_data, ['end_date', 'deadline_date']
                    )
        if not error and Project.objects.filter(
            name=project_data['name'],
            super_organization_id=super_organization_id
        ).exists():
            error, status_code = self.get_error(
                'conflict', key='project', val='organization'
            )
        if not error:
            if Project.objects.filter(
                name=project_data['name'], owner=request.user,
                super_organization_id=super_organization_id,
            ).exists():
                error, status_code = self.get_error(
                    'conflict', key='organization/user'
                )
            else:
                project = Project.objects.create(**project_data)
                if is_compliance:
                    compliance_data['project'] = project
                    Compliance.objects.create(**compliance_data)
        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {
                'status': 'success',
                'project': ProjectSerializer(project).data
            }

        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def destroy(self, request, pk):
        """
        Delete a project.

        :DELETE: Expects organization_id in query string.
        ---
        parameter_strategy: replace
        parameters:
            - name: organization_id
              description: The organization_id for this user's organization
              required: true
              paramType: query
            - name: project slug or pk
              description: The project slug identifier or primary key for this project
              required: true
              paramType: path

        Returns::
            {
                'status': 'success',
            }
        """
        error = None
        # DRF uses this, but it causes nothing to be returned
        # status_code = status.HTTP_204_NO_CONTENT
        status_code = status.HTTP_200_OK
        organization_id = request.query_params.get('organization_id', None)
        if not organization_id:
            error, status_code = self.get_error(
                'missing param', key='organization_id'
            )
        elif not int(organization_id) == self.get_organization():
            error, status_code = self.get_error(
                'bad request', key='organization_id'
            )

        if not error:
            key = self.get_key(pk)
            project = self.get_project(key, pk)
            if not project:
                error, status_code = self.get_error(
                    'not found', key=key, val=pk
                )
            else:
                project = project[0]
        if not error:
            if project.super_organization_id != int(organization_id):
                error, status_code = self.get_error('permssion denied')
            else:
                ProjectPropertyView.objects.filter(project=project).delete()
                ProjectTaxLotView.objects.filter(project=project).delete()
                project.delete()

        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {'status': 'success'}
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def update(self, request, pk):
        """
        Updates a project

        :PUT: Expects organization_id in query string.
        ---
        parameters:
            - name: organization_id
              description: ID of organization to associate new project with
              type: integer
              required: true
              paramType: query
            - name: project slug or pk
              description: The project slug identifier or primary key for this project
              required: true
              paramType: path
            - name: name
              description: name of the new project
              type: string
              required: true
            - name: is_compliance
              description: add compliance data if true
              type: bool
              required: true
            - name: compliance_type
              description: description of type of compliance
              type: string
              required: true if is_compliance else false
            - name: description
              description: description of new project
              type: string
              required: true if is_compliance else false
            - name: end_date
              description: Timestamp for when project ends
              type: string
              required: true if is_compliance else false
            - name: deadline_date
              description: Timestamp for compliance deadline
              type: string
              required: true if is_compliance else false
        Returns::
            {
                'status': 'success',
                'project': {
                        'id': project's primary key,
                        'name': project's name,
                        'slug': project's identifier,
                        'status': 'active',
                        'number_of_buildings': Count of buildings associated with project
                        'last_modified': Timestamp when project last changed
                        'last_modified_by': {
                            'first_name': first name of user that made last change,
                            'last_name': last name,
                            'email': email address,
                        },
                        'is_compliance': True if project is a compliance project,
                        'compliance_type': Description of compliance type,
                        'deadline_date': Timestamp of when compliance is due,
                        'end_date': Timestamp of end of project,
                        'property_count': number of property views associated with project,
                        'taxlot_count':  number of taxlot views associated with project,
                }
            }
        """
        error = None
        status_code = status.HTTP_200_OK
        project_data, missing = self.get_params(PROJECT_KEYS)
        project_data['last_modified_by'] = request.user
        if missing:
            error, status_code = self.get_error(
                'missing param', key=", ".join(missing)
            )
        else:
            # convert to int equivalent
            project_data['status'] = self.get_status(project_data['status'])
            is_compliance = project_data.pop('is_compliance')
            if is_compliance:
                compliance_data, missing = self.get_params(COMPLIANCE_KEYS)
                compliance_data = convert_dates(
                    compliance_data, ['end_date', 'deadline_date']
                )
            if missing:
                error, status_code = self.get_error(
                    'missing param', key=", ".join(missing)
                )
        if not error:
            key = self.get_key(pk)
            project = self.get_project(key, pk)
            if not project:
                error, status_code = self.get_error(
                    'not found', key=key, val=pk
                )
            else:
                project = project[0]
                compliance = project.get_compliance()
                if is_compliance:
                    if not compliance:
                        compliance = Compliance(project=project)
                    compliance = update_model(
                        compliance, compliance_data
                    )
                project = update_model(project, project_data)
                if is_compliance:
                    compliance.save()
                # delete compliance if one exists
                elif compliance:
                    compliance.delete()
                project.save()
        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {
                'status': 'success',
                'project': ProjectSerializer(project).data
            }
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def partial_update(self, request, pk):
        """
        Updates a project. Allows partial update, i.e. only updated param s need be supplied.

        :PUT: Expects organization_id in query string.
        ---
        parameters:
            - name: organization_id
              description: ID of organization to associate new project with
              type: integer
              required: true
              paramType: query
            - name: project slug or pk
              description: The project slug identifier or primary key for this project
              required: true
              paramType: path
            - name: name
              description: name of the new project
              type: string
              required: false
            - name: is_compliance
              description: add compliance data if true
              type: bool
              required: false
            - name: compliance_type
              description: description of type of compliance
              type: string
              required: true if is_compliance else false
            - name: description
              description: description of new project
              type: string
              required: true if is_compliance else false
            - name: end_date
              description: Timestamp for when project ends
              type: string
              required: true if is_compliance else false
            - name: deadline_date
              description: Timestamp for compliance deadline
              type: string
              required: true if is_compliance else false
        Returns::
            {
                'status': 'success',
                'project': {
                        'id': project's primary key,
                        'name': project's name,
                        'slug': project's identifier,
                        'status': 'active',
                        'number_of_buildings': Count of buildings associated with project
                        'last_modified': Timestamp when project last changed
                        'last_modified_by': {
                            'first_name': first name of user that made last change,
                            'last_name': last name,
                            'email': email address,
                        },
                        'is_compliance': True if project is a compliance project,
                        'compliance_type': Description of compliance type,
                        'deadline_date': Timestamp of when compliance is due,
                        'end_date': Timestamp of end of project,
                        'property_count': number of property views associated with project,
                        'taxlot_count':  number of taxlot views associated with project,
                }
            }
        """
        error = None
        status_code = status.HTTP_200_OK
        project_data, _ = self.get_params(PROJECT_KEYS)
        project_data['last_modified_by'] = request.user
        if 'status' in project_data:
            # convert to int equivalent
            project_data['status'] = self.get_status(project_data['status'])
        is_compliance = project_data.pop('is_compliance', None)
        if is_compliance:
            compliance_data, _ = self.get_params(COMPLIANCE_KEYS)
            compliance_data = convert_dates(
                compliance_data, ['end_date', 'deadline_date']
            )
        key = self.get_key(pk)
        project = self.get_project(key, pk)
        if not project:
            error, status_code = self.get_error(
                'not found', key=key, val=pk
            )
        else:
            project = project[0]
            compliance = project.get_compliance()
            if is_compliance:
                if not compliance:
                    compliance = Compliance(project=project)
                compliance = update_model(
                    compliance, compliance_data
                )
            project = update_model(project, project_data)
            if is_compliance:
                compliance.save()
            # delete compliance if one exists
            elif is_compliance == 'False':
                compliance.delete()
            project.save()
        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {
                'status': 'success',
                'project': ProjectSerializer(project).data
            }
        return Response(result, status=status_code)

    # Action views

    @api_endpoint_class
    @has_perm_class('requires_member')
    def add(self, request, pk):
        """
        Add inventory to project
        :PUT: Expects organization_id in query string.
        ---
        parameters:
            - name: organization_id
              description: ID of organization to associate new project with
              type: integer
              required: true
            - name: inventory_type
              description: type of inventory to add: 'property' or 'taxlot'
              type: string
              required: true
              paramType: query
            - name: project slug or pk
              description: The project slug identifier or primary key for this project
              required: true
              paramType: path
            - name:  selected
              description: ids of property or taxlot views to add
              type: array[int]
              required: true
        Returns:
            {
                'status': 'success',
                'added': [list of property/taxlot view ids added]
            }
        """
        error = None
        inventory = None
        status_code = status.HTTP_200_OK
        inventory_type = request.query_params.get(
            'inventory_type', request.data.get('inventory_type', None)
        )
        if not inventory_type:
            error, status_code = self.get_error(
                'missing param', 'inventory_type'
            )
        else:
            key = self.get_key(pk)
            project = self.get_project(key, pk)
            if not project:
                error, status_code = self.get_error(
                    'not found', key=key, val=pk
                )
        if not error:
            project = project[0]
            view_type = "{}_view".format(inventory_type)
            request.data['inventory_type'] = view_type
            params = search.process_search_params(
                request.data, request.user, is_api_request=False
            )
            organization_id = self.get_organization()
            params['organization_id'] = organization_id
            qs = search.inventory_search_filter_sort(
                view_type, params=params, user=request.user
            )
            if request.data.get('selected', None) \
                    and isinstance(request.data.get('selected'), list):
                inventory = qs.filter(pk__in=request.data.get('selected'))
            # TODO is this still relevant
            elif request.data.get('select_all_checkbox', None):
                inventory = qs

            if not inventory:
                error, status_code = self.get_error(
                    'missing inventory', key=inventory_type
                )
        if error:
            result = {'status': 'error', 'message': error}
        else:
            Model = self.ProjectViewModels[inventory_type]
            new_project_views = [
                self.project_view_factory(inventory_type, project.id, view.id)
                for view in inventory
            ]
            Model.objects.bulk_create(new_project_views)
            added = [view.id for view in inventory]
            project.last_modified_by = request.user
            project.save()
            result = {'status': 'success', 'added': added}
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def remove(self, request, pk):
        """
        Remove inventory from  project
        :PUT: Expects organization_id in query string.
        ---
        parameters:
            - name: organization_id
              description: ID of organization to associate new project with
              type: integer
              required: true
            - name: inventory_type
              description: type of inventory to add: 'property' or 'taxlot'
              type: string
              required: true
              paramType: query
            - name: project slug or pk
              description: The project slug identifier or primary key for this project
              required: true
              paramType: path
            - name:  selected
              description: ids of property or taxlot views to add
              type: array[int]
              required: true
        Returns:
            {
                'status': 'success',
                'removed': [list of property/taxlot view ids removed]
            }
        """
        error = None
        status_code = status.HTTP_200_OK
        inventory_type = request.query_params.get(
            'inventory_type', request.data.get('inventory_type', None)
        )
        selected = request.data.get('selected', None)
        missing = []
        if not inventory_type:
            missing.append('inventory_type')
        if selected is None:
            missing.append('selected')
        if missing:
            error, status_code = self.get_error(
                'missing param', ",".join(missing)
            )
        else:
            key = self.get_key(pk)
            project = self.get_project(key, pk)
            if not project:
                error, status_code = self.get_error(
                    'not found', key=key, val=pk
                )
        if not error:
            project = project[0]
            ViewModel = self.ViewModels[inventory_type]
            if selected:
                filter_dict = {
                    'id__in': request.data.get(
                        'selected'
                    )
                }
            elif selected == []:
                super_organization_id = self.get_organization()
                filter_dict = {
                    "state__super_organization_id": super_organization_id
                }
                for key in [inventory_type, 'cycle', 'state']:
                    val = request.data.get(key, None)
                    if val:
                        if isinstance(val, list):
                            filter_dict['{}_id__in'.format(key)] = val
                        else:
                            filter_dict['{}_id'.format(key)] = val
            views = ViewModel.objects.filter(**filter_dict).values_list('id')
            if not views:
                error, status_code = self.get_error(
                    'missing inventory', key=inventory_type
                )
            else:
                Model = self.ProjectViewModels[inventory_type]
                filter_dict = {
                    "project_id": project.pk,
                    "{}_view__in".format(inventory_type): views
                }
                project_views = Model.objects.filter(**filter_dict)
                removed = [view.pk for view in project_views]
                project_views.delete()
                project.last_modified_by = request.user
                project.save()
        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {'status': 'success', 'removed': removed}
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_viewer')
    @action(detail=False, methods=['GET'])
    def count(self, request):
        """
        Returns the number of projects within the org tree to which
        a user belongs.  Counts projects in parent orgs and sibling orgs.

        :GET: Expects organization_id in query string.
        ---
        parameters:
            - name: organization_id
              description: The organization_id for this user's organization
              required: true
              paramType: query
        type:
            status:
                type: string
                description: success, or error
            count:
                type: integer
                description: number of projects
        """
        status_code = status.HTTP_200_OK
        super_organization_id = self.get_organization()
        count = Project.objects.filter(
            super_organization_id=super_organization_id
        ).distinct().count()
        result = {'status': 'success', 'count': count}
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def update_details(self, request, pk):
        """
        Updates extra information about the inventory/project relationship.
        In particular, whether the property/taxlot  is compliant
        and who approved it.

        :PUT: Expects organization_id in query string.
        ---
        parameter_strategy: replace
        parameters:
            - name: organization_id
              description: The organization_id for this user's organization
              required: true
              type: integer
              paramType: query
            - name: inventory_type
              description: type of inventory to add: 'property' or 'taxlot'
              required: true
              type: string
              paramType: query
            - name: id
              description: id of property/taxlot  view to update
              required: true
              type: integer
              paramType: string
            - name: compliant
              description: is compliant
              required: true
              type: bool
              paramType: string

        Returns::
            {
                 'status': 'success',
                 'approved_date': Timestamp of change (now),
                 'approver': Email address of user making change
            }
        """
        error = None
        status_code = status.HTTP_200_OK
        params, missing = self.get_params(
            ['id', 'compliant']
        )
        inventory_type = request.query_params.get(
            'inventory_type', request.data.get('inventory_type', None)
        )
        if not inventory_type:
            missing.append('inventory_type')
        if missing:
            error, status_code = self.get_error(
                'missing param', key=", ".join(missing)
            )
        else:
            if not isinstance(params['compliant'], bool):
                error, status_code = self.get_error(
                    'misc', key='compliant must be of type bool',
                )
        if not error:
            key = self.get_key(pk)
            project = self.get_project(key, pk)
            if not project:
                error, status_code = self.get_error(
                    'not found', key=key, val=pk
                )
        if not error:
            project = project[0]
            Model = self.ProjectViewModels[inventory_type]
            filter_dict = {
                "project_id": project.id,
                "{}_view_id".format(inventory_type): params['id']
            }
            try:
                view = Model.objects.get(**filter_dict)
            except Model.DoesNotExist:
                error, status_code = self.get_error(
                    'missing inventory', key=inventory_type
                )
        if not error:
            view.approved_date = timezone.now()
            view.approver = request.user
            view.compliant = params['compliant']
            view.save()
        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {
                'status': 'success',
                'approved_date': view.approved_date.strftime("%m/%d/%Y"),
                'approver': view.approver.email,
            }
        return Response(result, status=status_code)

    @api_endpoint_class
    @has_perm_class('requires_member')
    def transfer(self, request, pk, action):
        """
        Move or copy inventory from one project to another

        :PUT: Expects organization_id in query string.
        ---
        parameter_strategy: replace
        parameters:
            - name: organization_id
              description: The organization_id for this user's organization
              required: true
              type: integer
              paramType: query
            - name: inventory_type
              description: type of inventory to add: 'property' or 'taxlot'
              required: true
              type: string
              paramType: query
            - name: copy or move
              description: Whether to move or copy inventory
              required: true
              paramType: path
              required: true
            -name: target
              type: string or int
              description: target project slug/id  to move/copy to.
              required: true
            - name: selected
              description: JSON array, list of property/taxlot views to be transferred
              paramType: array[int]
              required: true
        """
        error = None
        status_code = status.HTTP_200_OK
        params, missing = self.get_params(
            ['selected', 'target']
        )
        inventory_type = request.query_params.get(
            'inventory_type', request.data.get('inventory_type', None)
        )
        if not inventory_type:
            missing.append('inventory_type')
        if missing:
            error, status_code = self.get_error(
                'missing param', key=", ".join(missing)
            )
        else:
            key = self.get_key(pk)
            project = self.get_project(key, pk)
            if not project:
                error, status_code = self.get_error(
                    'not found', key=key, val=pk
                )
            else:
                target_key = self.get_key(params['target'])
                target = self.get_project(target_key, params['target'])
                if not target:
                    error, status_code = self.get_error(
                        'not found', key=target_key, val=params['target']
                    )
                    error += 'for target'

        if not error:
            project = project[0]
            target = target[0]
            ProjectViewModel = self.ProjectViewModels[inventory_type]
            filter_dict = {
                "{}_view_id__in".format(inventory_type):
                    params['selected'],
                'project_id': project.id
            }
            old_project_views = ProjectViewModel.objects.filter(
                **filter_dict
            )

            if action == 'copy':
                new_project_views = []
                # set pk to None to create a copy of the django instance
                for view in old_project_views:
                    view.pk = None
                    view.project = target
                    new_project_views.append(view)
                try:
                    ProjectViewModel.objects.bulk_create(
                        new_project_views
                    )
                except IntegrityError:
                    error, status_code = self.get_error(
                        'conflict',
                        key="One or more {}".format(
                            PLURALS[inventory_type]
                        ),
                        val='target project'
                    )

            else:
                try:
                    old_project_views.update(project=target)

                except IntegrityError:
                    error, status_code = self.get_error(
                        'conflict',
                        key="One or more {}".format(
                            PLURALS[inventory_type]
                        ),
                        val='target project'
                    )
        if error:
            result = {'status': 'error', 'message': error}
        else:
            result = {'status': 'success'}
        return Response(result, status=status_code)