backend/api/tasks/resources.py (193 lines of code) (raw):

import io from distutils.util import strtobool from flask import send_file, Response from flask_restful import Resource, current_app, request from schematics.exceptions import DataError from backend.services.mapping_service import MappingService, NotFound from backend.models.dtos.grid_dto import GridDTO from backend.services.users.authentication_service import token_auth, tm from backend.services.users.user_service import UserService from backend.services.validator_service import ValidatorService from backend.services.project_service import ProjectService, ProjectServiceError from backend.services.grid.grid_service import GridService from backend.models.postgis.statuses import UserRole from backend.models.postgis.utils import InvalidGeoJson class TasksRestAPI(Resource): def get(self, project_id, task_id): """ Get a task's metadata --- tags: - tasks produces: - application/json parameters: - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 responses: 200: description: Task found 404: description: Task not found 500: description: Internal Server Error """ try: preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE") task = MappingService.get_task_as_dto(task_id, project_id, preferred_locale) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except Exception as e: error_msg = f"TasksRestAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task"}, 500 class TasksQueriesJsonAPI(Resource): def get(self, project_id): """ Get all tasks for a project as JSON --- tags: - tasks produces: - application/json parameters: - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - in: query name: tasks type: string description: List of tasks; leave blank to retrieve all default: 1,2 - in: query name: as_file type: boolean description: Set to true if file download preferred default: True responses: 200: description: Project found 403: description: Forbidden 404: description: Project not found 500: description: Internal Server Error """ try: tasks = request.args.get("tasks") if request.args.get("tasks") else None as_file = ( strtobool(request.args.get("as_file")) if request.args.get("as_file") else True ) tasks_json = ProjectService.get_project_tasks(int(project_id), tasks) if as_file: tasks_json = str(tasks_json).encode("utf-8") return send_file( io.BytesIO(tasks_json), mimetype="application/json", as_attachment=True, attachment_filename=f"{str(project_id)}-tasks.geojson", ) return tasks_json, 200 except NotFound: return {"Error": "Project or Task Not Found"}, 404 except ProjectServiceError as e: return {"Error": str(e)}, 403 except Exception as e: current_app.logger.critical(e) return {"Error": "Unable to fetch task JSON"}, 500 @token_auth.login_required def delete(self, project_id): """ Delete a list of tasks from a project --- tags: - tasks produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - in: body name: body required: true description: JSON object with a list of tasks to delete schema: properties: tasks: type: array items: type: integer default: [ 1, 2 ] responses: 200: description: Task(s) deleted 400: description: Bad request 403: description: Forbidden 404: description: Project or Task Not Found 500: description: Internal Server Error """ user_id = token_auth.current_user() user = UserService.get_user_by_id(user_id) if user.role != UserRole.ADMIN.value: return {"Error": "This endpoint action is restricted to ADMIN users."}, 403 tasks_ids = request.get_json().get("tasks") if tasks_ids is None: return {"Error": "Tasks ids not provided"}, 400 if type(tasks_ids) != list: return {"Error": "Tasks were not provided as a list"}, 400 try: ProjectService.delete_tasks(project_id, tasks_ids) return {"Success": "Task(s) deleted"}, 200 except NotFound as e: return {"Error": f"Project or Task Not Found: {e}"}, 404 except ProjectServiceError as e: return {"Error": str(e)}, 403 except Exception as e: current_app.logger.critical(e) return {"Error": "Unable to delete tasks"}, 500 class TasksQueriesXmlAPI(Resource): def get(self, project_id): """ Get all tasks for a project as OSM XML --- tags: - tasks produces: - application/xml parameters: - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - in: query name: tasks type: string description: List of tasks; leave blank to retrieve all default: 1,2 - in: query name: as_file type: boolean description: Set to true if file download preferred default: False responses: 200: description: OSM XML 400: description: Client Error 404: description: No mapped tasks 500: description: Internal Server Error """ try: tasks = request.args.get("tasks") if request.args.get("tasks") else None as_file = ( strtobool(request.args.get("as_file")) if request.args.get("as_file") else False ) xml = MappingService.generate_osm_xml(project_id, tasks) if as_file: return send_file( io.BytesIO(xml), mimetype="text.xml", as_attachment=True, attachment_filename=f"HOT-project-{project_id}.osm", ) return Response(xml, mimetype="text/xml", status=200) except NotFound: return ( {"Error": "Not found; please check the project and task numbers."}, 404, ) except Exception as e: error_msg = f"TasksQueriesXmlAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task XML"}, 500 class TasksQueriesGpxAPI(Resource): def get(self, project_id): """ Get all tasks for a project as GPX --- tags: - tasks produces: - application/xml parameters: - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - in: query name: tasks type: string description: List of tasks; leave blank for all default: 1,2 - in: query name: as_file type: boolean description: Set to true if file download preferred default: False responses: 200: description: GPX XML 400: description: Client error 404: description: No mapped tasks 500: description: Internal Server Error """ try: current_app.logger.debug("GPX Called") tasks = request.args.get("tasks") as_file = ( strtobool(request.args.get("as_file")) if request.args.get("as_file") else False ) xml = MappingService.generate_gpx(project_id, tasks) if as_file: return send_file( io.BytesIO(xml), mimetype="text.xml", as_attachment=True, attachment_filename=f"HOT-project-{project_id}.gpx", ) return Response(xml, mimetype="text/xml", status=200) except NotFound: return ( {"Error": "Not found; please check the project and task numbers."}, 404, ) except Exception as e: error_msg = f"TasksQueriesGpxAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task GPX"}, 500 class TasksQueriesAoiAPI(Resource): @tm.pm_only() @token_auth.login_required def put(self): """ Get task tiles intersecting with the aoi provided --- tags: - tasks produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: body name: body required: true description: JSON object containing aoi and tasks and bool flag for controlling clip grid to aoi schema: properties: clipToAoi: type: boolean default: true areaOfInterest: schema: properties: type: type: string default: FeatureCollection features: type: array items: schema: $ref: "#/definitions/GeoJsonFeature" grid: schema: properties: type: type: string default: FeatureCollection features: type: array items: schema: $ref: "#/definitions/GeoJsonFeature" responses: 200: description: Intersecting tasks found successfully 400: description: Client Error - Invalid Request 500: description: Internal Server Error """ try: grid_dto = GridDTO(request.get_json()) grid_dto.validate() except DataError as e: current_app.logger.error(f"error validating request: {str(e)}") return {"Error": "Unable to fetch tiles interesecting AOI"}, 400 try: grid = GridService.trim_grid_to_aoi(grid_dto) return grid, 200 except InvalidGeoJson as e: return {"Error": f"{str(e)}"}, 400 except Exception as e: error_msg = f"TasksQueriesAoiAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch tiles intersecting AOI"}, 500 class TasksQueriesMappedAPI(Resource): def get(self, project_id): """ Get all mapped tasks for a project grouped by username --- tags: - tasks produces: - application/json parameters: - name: project_id in: path description: Unique project ID required: true type: integer default: 1 responses: 200: description: Mapped tasks returned 500: description: Internal Server Error """ try: mapped_tasks = ValidatorService.get_mapped_tasks_by_user(project_id) return mapped_tasks.to_primitive(), 200 except Exception as e: error_msg = f"Task Lock API - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch mapped tasks"}, 500 class TasksQueriesOwnInvalidatedAPI(Resource): @tm.pm_only(False) @token_auth.login_required def get(self, username): """ Get invalidated tasks either mapped by user or invalidated by user --- tags: - tasks produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: username in: path description: The users username required: true type: string - in: query name: asValidator description: treats user as validator, rather than mapper, if true type: string - in: query name: sortBy description: field to sort by, defaults to action_date type: string - in: query name: sortDirection description: direction of sort, defaults to desc type: string - in: query name: page description: Page of results user requested type: integer - in: query name: pageSize description: Size of page, defaults to 10 type: integer - in: query name: project description: Optional project filter type: integer - in: query name: closed description: Optional filter for open/closed invalidations type: boolean responses: 200: description: Invalidated tasks user has invalidated 404: description: No invalidated tasks 500: description: Internal Server Error """ try: sort_column = {"updatedDate": "updated_date", "projectId": "project_id"} if request.args.get("sortBy", "updatedDate") in sort_column: sort_column = sort_column[request.args.get("SortBy", "updatedDate")] else: sort_column = sort_column["updatedDate"] # closed needs to be set to True, False, or None closed = None if request.args.get("closed") == "true": closed = True elif request.args.get("closed") == "false": closed = False # sort direction should only be desc or asc if request.args.get("sortDirection") in ["asc", "desc"]: sort_direction = request.args.get("sortDirection") else: sort_direction = "desc" invalidated_tasks = ValidatorService.get_user_invalidated_tasks( request.args.get("asValidator") == "true", username, request.environ.get("HTTP_ACCEPT_LANGUAGE"), closed, request.args.get("project", None, type=int), request.args.get("page", None, type=int), request.args.get("pageSize", None, type=int), sort_column, sort_direction, ) return invalidated_tasks.to_primitive(), 200 except NotFound: return {"Error": "No invalidated tasks"}, 404 except Exception as e: error_msg = f"TasksQueriesMappedAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch invalidated tasks for user"}, 500