Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Task assignment #89

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
18 changes: 17 additions & 1 deletion backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def add_api_endpoints(app):
UsersQueriesInterestsAPI,
UsersRecommendedProjectsAPI,
)
from backend.api.users.tasks import UsersTasksAPI
from backend.api.users.tasks import UsersTasksAPI, UserAssignedTasksAPI
from backend.api.users.actions import (
UsersActionsSetUsersAPI,
UsersActionsSetLevelAPI,
Expand All @@ -245,6 +245,8 @@ def add_api_endpoints(app):
UsersActionsVerifyEmailAPI,
UsersActionsRegisterEmailAPI,
UsersActionsSetInterestsAPI,
UserActionsAssignTasksAPI,
UserActionsUnassignTasksAPI,
)
from backend.api.users.openstreetmap import UsersOpenStreetMapAPI
from backend.api.users.statistics import (
Expand Down Expand Up @@ -677,6 +679,7 @@ def add_api_endpoints(app):
# Users REST endpoint
api.add_resource(UsersAllAPI, format_url("users/"))
api.add_resource(UsersRestAPI, format_url("users/<int:user_id>/"))

api.add_resource(
UsersQueriesUsernameFilterAPI,
format_url("users/queries/filter/<string:username>/"),
Expand All @@ -696,6 +699,14 @@ def add_api_endpoints(app):
# Users Actions endpoint
api.add_resource(UsersActionsSetUsersAPI, format_url("users/me/actions/set-user/"))

api.add_resource(
UserActionsAssignTasksAPI, format_url("project/<int:project_id>/assign")
)

api.add_resource(
UserActionsUnassignTasksAPI, format_url("project/<int:project_id>/unassign")
)

api.add_resource(
UsersActionsSetLevelAPI,
format_url("users/<string:username>/actions/set-level/<string:level>/"),
Expand All @@ -712,6 +723,11 @@ def add_api_endpoints(app):
)

api.add_resource(UsersTasksAPI, format_url("users/<int:user_id>/tasks/"))

api.add_resource(
UserAssignedTasksAPI, format_url("user/<string:username>/assigned-tasks")
)

api.add_resource(
UsersActionsVerifyEmailAPI, format_url("users/me/actions/verify-email/")
)
Expand Down
177 changes: 176 additions & 1 deletion backend/api/users/actions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from flask_restful import Resource, current_app, request
from schematics.exceptions import DataError

from backend.models.dtos.user_dto import UserDTO, UserRegisterEmailDTO
from backend.models.dtos.user_dto import (
UserDTO,
UserRegisterEmailDTO,
AssignTasksDTO,
UnassignTasksDTO,
)
from backend.services.messaging.message_service import MessageService
from backend.services.users.authentication_service import token_auth, tm
from backend.services.users.user_service import UserService, UserServiceError, NotFound
from backend.services.interests_service import InterestService
from backend.services.mapping_service import MappingServiceError, MappingService
from backend.services.validator_service import ValidatorServiceError
from backend.models.postgis.utils import UserLicenseError


class UsersActionsSetUsersAPI(Resource):
Expand Down Expand Up @@ -400,3 +408,170 @@ def post(self):
error_msg = f"User relationship POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": error_msg}, 500


class UserActionsAssignTasksAPI(Resource):
@tm.pm_only()
@token_auth.login_required
def post(self, project_id):
"""
Manually assign tasks to a user
---
tags:
- project-admin
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: project_id
in: path
description: The ID of the project the task is associated with
required: true
type: integer
default: 1
- name: username
in: query
description: The username to assign the task to
required: true
type: string
default: Thinkwhere
- in: body
name: tasks
required: true
description: JSON object for locking task(s)
schema:
properties:
taskIds:
type: array
items:
type: integer
description: Array of taskIds for locking
default: [1,2]
responses:
200:
description: Task(s) assigned to user
401:
description: Unauthorized - Invalid credentials
404:
description: Task(s) or User not found
500:
description: Internal Server Error
"""
try:
assign_tasks_dto = AssignTasksDTO(request.get_json())
assign_tasks_dto.assigner_id = tm.authenticated_user_id
user_id = UserService.get_user_by_username(request.args.get("username")).id
assign_tasks_dto.assignee_id = user_id
assign_tasks_dto.project_id = project_id
assign_tasks_dto.preferred_locale = request.environ.get(
"HTTP_ACCEPT_LANGUAGE"
)
assign_tasks_dto.validate()

except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
return str(e), 400

try:
task = MappingService.assign_tasks(assign_tasks_dto)
return task.to_primitive(), 200
except NotFound:
return {"Error": "Task Not Found"}, 404
except (MappingServiceError, ValidatorServiceError) as e:
return {"Error": str(e)}, 403
except UserLicenseError:
return {"Error": "User not accepted license terms"}, 409
except Exception as e:
error_msg = f"Task Assign API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": error_msg}, 500


class UserActionsUnassignTasksAPI(Resource):
@tm.pm_only()
@token_auth.login_required
def post(self, project_id):
"""
Manually unassign tasks
---
tags:
- project-admin
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: project_id
in: path
description: The ID of the project the task is associated with
required: true
type: integer
default: 1
- in: body
name: tasks
required: true
description: JSON object for unassigning task(s)
schema:
properties:
taskIds:
type: array
items:
type: integer
description: Array of taskIds for unassigning
default: [1,2]
responses:
200:
description: Task(s) unassigned
401:
description: Unauthorized - Invalid credentials
404:
description: Task(s) not found
500:
description: Internal Server Error
"""
try:
unassign_tasks_dto = UnassignTasksDTO(request.get_json())
unassign_tasks_dto.project_id = project_id
unassign_tasks_dto.assigner_id = tm.authenticated_user_id
unassign_tasks_dto.preferred_locale = request.environ.get(
"HTTP_ACCEPT_LANGUAGE"
)
unassign_tasks_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
return str(e), 400

try:
task = MappingService.unassign_tasks(unassign_tasks_dto)
return task.to_primitive(), 200
except NotFound:
return {"Error": "Task Not Found"}, 404
except (MappingServiceError, ValidatorServiceError) as e:
return {"Error": str(e)}, 403
except UserLicenseError:
return {"Error": "User not accepted license terms"}, 409
except Exception as e:
error_msg = f"Task UnAssign API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": error_msg}, 500
113 changes: 112 additions & 1 deletion backend/api/users/tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask_restful import Resource, current_app, request
from dateutil.parser import parse as date_parse

from backend.services.users.authentication_service import token_auth
from backend.services.users.authentication_service import token_auth, tm
from backend.services.users.user_service import UserService, NotFound


Expand Down Expand Up @@ -116,3 +116,114 @@ def get(self, user_id):
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"error": error_msg}, 500


class UserAssignedTasksAPI(Resource):
@tm.pm_only(False)
@token_auth.login_required
def get(self, username):
"""
Get assigned tasks either assigned to or assigned by user
---
tags:
- user
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: asAssigner
description: treats user as assigner, rather than assignee, if true
type: string
- in: query
name: sortBy
description: field to sort by, defaults to assigned_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 assignments
type: boolean
responses:
200:
description: User's assigned tasks
404:
description: No assigned tasks
500:
description: Internal Server Error
"""
try:
sort_column_map = {
"assignedDate": "assigned_date",
"projectId": "project_id",
}
sort_column = sort_column_map.get(
request.args.get("sortBy"), sort_column_map["assignedDate"]
)

# 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

# task status needs to be set to None or one of the statuses
task_status = request.args.get("taskStatus") or None

# 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"

assigned_tasks = UserService.get_user_assigned_tasks(
request.args.get("asAssigner") == "true",
username,
request.environ.get("HTTP_ACCEPT_LANGUAGE"),
closed,
task_status,
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 assigned_tasks.to_primitive(), 200
except NotFound:
return {"Error": "No assigned tasks"}, 404
except Exception as e:
error_msg = f"Assigned Tasks API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": error_msg}, 500
3 changes: 3 additions & 0 deletions backend/models/dtos/project_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ class ProjectDTO(Model):
serialized_name="validationPermission",
validators=[is_known_validation_permission],
)
enforce_assignment = BooleanType(
required=True, default=False, serialized_name="enforceAssignment"
)
enforce_random_task_selection = BooleanType(
required=False, default=False, serialized_name="enforceRandomTaskSelection"
)
Expand Down
Loading