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