Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 62 additions & 51 deletions backend/app/controllers/TaskController.scala
Original file line number Diff line number Diff line change
@@ -1,63 +1,62 @@
package controllers

import dto.request.task.{AssignMemberRequest, CreateTaskRequest, UpdateTaskRequest}
import dto.request.task.{
AssignMemberRequest,
CreateTaskRequest,
UpdateTaskRequest
}
import dto.response.ApiResponse
import play.api.i18n.I18nSupport.RequestWithMessagesApi
import play.api.i18n.Messages
import play.api.libs.json.{JsValue, Json}
import play.api.mvc.{Action, AnyContent, MessagesAbstractController, MessagesControllerComponents}
import play.api.mvc.{
Action,
AnyContent,
MessagesAbstractController,
MessagesControllerComponents
}
import services.TaskService
import validations.ValidationHandler
import utils.WritesExtras.unitWrites
import validations.ValidationHandler

import javax.inject.Inject
import scala.concurrent.ExecutionContext

class TaskController @Inject()(
cc: MessagesControllerComponents,
taskService: TaskService,
authenticatedActionWithUser: AuthenticatedActionWithUser
)(implicit ec: ExecutionContext)
extends MessagesAbstractController(cc)
cc: MessagesControllerComponents,
taskService: TaskService,
authenticatedActionWithUser: AuthenticatedActionWithUser
)(implicit ec: ExecutionContext)
extends MessagesAbstractController(cc)
with ValidationHandler {

def create(columnId: Int): Action[JsValue] =
authenticatedActionWithUser.async(parse.json) { request =>
implicit val messages: Messages = request.messages
val createdBy = request.userToken.userId
handleJsonValidation[CreateTaskRequest](request.body) {
createColumnDto =>
taskService
.createNewTask(createColumnDto, columnId, createdBy)
.map { taskId =>
Created(
Json.toJson(
ApiResponse[Unit](
s"Task created successfully with ID: $taskId"
)
)
handleJsonValidation[CreateTaskRequest](request.body) { createColumnDto =>
taskService
.createNewTask(createColumnDto, columnId, createdBy)
.map { taskId =>
Created(
Json.toJson(
ApiResponse[Unit](s"Task created successfully with ID: $taskId")
)
}
)
}
}
}

def update(taskId: Int): Action[JsValue] =
authenticatedActionWithUser.async(parse.json) { request =>
implicit val messages: Messages = request.messages
val updatedBy = request.userToken.userId
handleJsonValidation[UpdateTaskRequest](request.body) {
updateTaskDto =>
taskService
.updateTask(taskId, updateTaskDto, updatedBy)
.map { _ =>
Ok(
Json.toJson(
ApiResponse[Unit](
s"Task updated successfully"
)
)
)
}
handleJsonValidation[UpdateTaskRequest](request.body) { updateTaskDto =>
taskService
.updateTask(taskId, updateTaskDto, updatedBy)
.map { _ =>
Ok(Json.toJson(ApiResponse[Unit](s"Task updated successfully")))
}
}
}

Expand All @@ -81,13 +80,7 @@ class TaskController @Inject()(
taskService
.archiveTask(taskId, archivedBy)
.map { _ =>
Ok(
Json.toJson(
ApiResponse[Unit](
s"Task archived successfully"
)
)
)
Ok(Json.toJson(ApiResponse[Unit](s"Task archived successfully")))
}
}

Expand All @@ -97,13 +90,7 @@ class TaskController @Inject()(
taskService
.restoreTask(taskId, restoredBy)
.map { _ =>
Ok(
Json.toJson(
ApiResponse[Unit](
s"Task restored successfully"
)
)
)
Ok(Json.toJson(ApiResponse[Unit](s"Task restored successfully")))
}
}

Expand All @@ -117,7 +104,7 @@ class TaskController @Inject()(
}
}

def assignMember(projectId:Int, taskId: Int): Action[JsValue] = {
def assignMember(projectId: Int, taskId: Int): Action[JsValue] = {
authenticatedActionWithUser.async(parse.json) { request =>
val assignedBy = request.userToken.userId
handleJsonValidation[AssignMemberRequest](request.body) {
Expand All @@ -136,6 +123,22 @@ class TaskController @Inject()(
}
}

def unassignMember(projectId: Int,
taskId: Int,
userId: Int): Action[AnyContent] =
authenticatedActionWithUser.async { request =>
val unassignedBy = request.userToken.userId
taskService
.unassignMemberFromTask(projectId, taskId, userId, unassignedBy)
.map { _ =>
Ok(
Json.toJson(
ApiResponse[Unit](s"Member unassigned from task successfully")
)
)
}
}

def getArchivedTasks(projectId: Int): Action[AnyContent] =
authenticatedActionWithUser.async { request =>
implicit val messages: Messages = request.messages
Expand All @@ -145,7 +148,8 @@ class TaskController @Inject()(
.map { tasks =>
Ok(
Json.toJson(
ApiResponse.success("Archived tasks retrieved successfully", tasks)
ApiResponse
.success("Archived tasks retrieved successfully", tasks)
)
)
}
Expand All @@ -166,7 +170,10 @@ class TaskController @Inject()(
}
}

def search(page: Int, size: Int, keyword: String, projectIds: Option[List[Int]]): Action[AnyContent] =
def search(page: Int,
size: Int,
keyword: String,
projectIds: Option[List[Int]]): Action[AnyContent] =
authenticatedActionWithUser.async { request =>
val userId = request.userToken.userId
taskService
Expand All @@ -178,7 +185,11 @@ class TaskController @Inject()(
userId
)
.map { tasks =>
Ok(Json.toJson(ApiResponse.success("Tasks retrieved successfully", tasks)))
Ok(
Json.toJson(
ApiResponse.success("Tasks retrieved successfully", tasks)
)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import play.api.libs.json.{Json, OFormat}

case class AssignMemberToTaskResponse(userId: Int,
username: String,
columnId: Int)
taskId: Int)

object AssignMemberToTaskResponse {
implicit val format: OFormat[AssignMemberToTaskResponse] = Json.format[AssignMemberToTaskResponse]
Expand Down
1 change: 1 addition & 0 deletions backend/app/dto/websocket/board/BoardMessage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object BoardMessage {
case TaskMoved(p) => Json.toJson(p)
case TaskCreated(p) => Json.toJson(p)
case MemberAssignedToTask(p) => Json.toJson(p)
case MemberUnassignedFromTask(p) => Json.toJson(p)
case TaskUpdated(p) => Json.toJson(p)
case TaskStatusUpdated(p) => Json.toJson(p)
case ColumnCreated(p) => Json.toJson(p)
Expand Down
1 change: 1 addition & 0 deletions backend/app/dto/websocket/board/BoardMessageTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ object BoardMessageTypes {
final val TASK_MOVED: String = "TASK_MOVED"
final val TASK_CREATED: String = "TASK_CREATED"
final val MEMBER_ASSIGNED_TO_TASK: String = "MEMBER_ASSIGNED_TO_TASK"
final val MEMBER_UNASSIGNED_TO_TASK: String = "MEMBER_UNASSIGNED_TO_TASK"
final val TASK_UPDATED: String = "TASK_UPDATED"
final val TASK_STATUS_UPDATED: String = "TASK_STATUS_UPDATED"
final val COLUMN_CREATED: String = "COLUMN_CREATED"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dto.websocket.board.messageTypes

import dto.websocket.board.{BoardMessage, BoardMessageTypes}
import play.api.libs.json.{Json, OFormat}

case class MemberUnassignedFromTask(payload: MemberUnassignedFromTaskPayload)
extends BoardMessage {
override val messageType: String = BoardMessageTypes.MEMBER_UNASSIGNED_TO_TASK
}

case class MemberUnassignedFromTaskPayload(
taskId: Int,
userId: Int
)
object MemberUnassignedFromTaskPayload {
implicit val memberAssignedToTaskPayloadFormat
: OFormat[MemberUnassignedFromTaskPayload] =
Json.format[MemberUnassignedFromTaskPayload]
}
13 changes: 8 additions & 5 deletions backend/app/repositories/TaskRepository.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package repositories

import db.MyPostgresProfile.api.{columnStatusTypeMapper, projectStatusTypeMapper, taskStatusTypeMapper}
import dto.response.task.{AssignMemberToTaskResponse, AssignedMemberResponse, TaskSummaryResponse}
import dto.response.task.AssignMemberToTaskResponse
import dto.response.task.TaskSummaryResponse
import models.Enums.TaskStatus.TaskStatus
import models.Enums.{ColumnStatus, ProjectStatus, TaskStatus}
import models.entities.{Task, UserTask}
Expand Down Expand Up @@ -68,6 +66,11 @@ class TaskRepository@Inject()(
)
userTasks += userTask
}

def unassignMemberFromTask(taskId: Int, userId: Int): DBIO[Int] = {
userTasks.filter(ut => ut.taskId === taskId && ut.assignedTo === userId).delete
}

def findArchivedTasksByProjectId(projectId: Int): DBIO[Seq[TaskSummaryResponse]] = {
val query = for {
((t, c), p) <- tasks
Expand All @@ -93,10 +96,10 @@ class TaskRepository@Inject()(
up <- userProjects if up.userId === userId && up.projectId === c.projectId
u <- users if u.id === userId
if !(userTasks.filter(ut => ut.taskId === taskId && ut.assignedTo === userId)).exists
} yield (u.id, u.name, c.id)
} yield (u.id, u.name, t.id)

query.result.headOption.map(_.map { case (uid, uname, colId) =>
AssignMemberToTaskResponse(uid, uname, colId)
query.result.headOption.map(_.map { case (uid, uname, taskId) =>
AssignMemberToTaskResponse(uid, uname, taskId)
})
}

Expand Down
35 changes: 35 additions & 0 deletions backend/app/services/TaskService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,41 @@ class TaskService @Inject()(taskRepository: TaskRepository,
db.run(action)
}

def unassignMemberFromTask(projectId:Int, taskId: Int, userId: Int, unassignedBy: Int): Future[Int] = {
val action = for {
isUserInProject <- projectRepository.isUserInActiveProject(
unassignedBy,
projectId
)

_ <- if (isUserInProject) {
DBIO.successful(())
} else {
DBIO.failed(AppException(
message = s"You do not have permission to unassign members from this task",
statusCode = Status.FORBIDDEN))
}

deletionCheck <- taskRepository.unassignMemberFromTask(taskId, userId)
_ <- deletionCheck match {
case 1 => {
val unassignedMemberFromTaskMessage = MemberUnassignedFromTask(MemberUnassignedFromTaskPayload(
taskId,
userId)
)
broadcastService.broadcastToProject(projectId, unassignedMemberFromTaskMessage)
DBIO.successful(())
}
case 0 => DBIO.failed(AppException(
message = s"User with ID $userId is not in the project or not assigned to the task",
statusCode = Status.BAD_REQUEST)
)
}
} yield deletionCheck

db.run(action)
}

def getActiveTasksInProject(projectId: Int, userId: Int): Future[Seq[TaskSummaryResponse]] = {
val action = for {
isUserInActiveProject <- projectRepository.isUserInActiveProject(
Expand Down
1 change: 1 addition & 0 deletions backend/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ PATCH /api/tasks/:taskId/archive controllers.TaskController.archive(taskId
PATCH /api/tasks/:taskId/restore controllers.TaskController.restore(taskId: Int)
DELETE /api/tasks/:taskId controllers.TaskController.delete(taskId: Int)
POST /api/projects/:projectId/tasks/:taskId/members controllers.TaskController.assignMember(projectId: Int, taskId: Int)
DELETE /api/projects/:projectId/tasks/:taskId/members/:userId controllers.TaskController.unassignMember(projectId: Int, taskId: Int, userId: Int)
GET /api/projects/:projectId/columns/tasks/archived controllers.TaskController.getArchivedTasks(projectId: Int)
GET /api/projects/:projectId/columns/tasks/active controllers.TaskController.getActiveTasksInProject(projectId: Int)
GET /api/tasks controllers.TaskController.search(page: Int ?= 2, size: Int ?= 10, keyword: String ?= "", projectIds: Option[List[Int]])
Expand Down
42 changes: 42 additions & 0 deletions backend/test/controllers/TaskControllerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,48 @@ class TaskControllerSpec
ex.message must include("Task with ID 0 does not exist")
}

"unassign member from a task successfully" in {
val request = FakeRequest(DELETE, "/api/projects/1/tasks/1/members/1")
.withCookies(Cookie(cookieName, fakeToken))

val result = route(app, request).get

status(result) mustBe OK
(contentAsJson(result) \ "message").as[String] must include(
"Member unassigned from task successfully"
)
}

"fail when unassigning member from a task with user not assign to a task" in {
val request = FakeRequest(DELETE, "/api/projects/1/tasks/1/members/0")
.withCookies(Cookie(cookieName, fakeToken))

val result = route(app, request).get

val ex = intercept[AppException] {
await(result)
}
ex.statusCode mustBe BAD_REQUEST
ex.message must include(
"User with ID 0 is not in the project or not assigned to the task"
)
}

"fail when unassigning member from a task with user not in project" in {
val request = FakeRequest(DELETE, "/api/projects/2/tasks/1/members/0")
.withCookies(Cookie(cookieName, fakeToken))

val result = route(app, request).get

val ex = intercept[AppException] {
await(result)
}
ex.statusCode mustBe FORBIDDEN
ex.message must include(
"You do not have permission to unassign members from this task"
)
}

"fail when creating task with duplicate position in same column" in {
val body = Json.toJson(CreateTaskRequest("Task 1", 1))
val request = FakeRequest(POST, "/api/columns/1/tasks")
Expand Down