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)
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)
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
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)
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
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)
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)