Skip to content

Commit 900c6e9

Browse files
feat/BE: import project API (#40)
1 parent 4c8b2cb commit 900c6e9

File tree

10 files changed

+297
-114
lines changed

10 files changed

+297
-114
lines changed

backend/app/controllers/ProjectController.scala

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
package controllers
22

3-
import dto.request.project.CreateProjectRequest
3+
import dto.request.project.{CreateProjectRequest, ImportProjectData}
44
import dto.response.ApiResponse
55
import play.api.i18n.I18nSupport.RequestWithMessagesApi
66
import play.api.i18n.Messages
7-
import play.api.libs.json.{JsValue, Json}
8-
import play.api.mvc.{Action, AnyContent, MessagesAbstractController, MessagesControllerComponents}
7+
import play.api.libs.Files.TemporaryFile
8+
import play.api.libs.json.{JsResultException, JsValue, Json}
9+
import play.api.mvc.{
10+
Action,
11+
AnyContent,
12+
MessagesAbstractController,
13+
MessagesControllerComponents,
14+
MultipartFormData
15+
}
16+
import scoverage.Platform.Source
917
import services.ProjectService
1018
import utils.WritesExtras.unitWrites
1119
import validations.ValidationHandler
1220

1321
import javax.inject.Inject
14-
import scala.concurrent.ExecutionContext
22+
import scala.concurrent.{ExecutionContext, Future}
1523

1624
class ProjectController @Inject()(
1725
cc: MessagesControllerComponents,
@@ -109,7 +117,7 @@ class ProjectController @Inject()(
109117
}
110118
}
111119

112-
/** GET /projects/:projectId */
120+
/** GET /projects/:projectId */
113121
def getProjectById(projectId: Int): Action[AnyContent] =
114122
authenticatedActionWithUser.async { request =>
115123
val userId = request.userToken.userId
@@ -141,4 +149,48 @@ class ProjectController @Inject()(
141149
Ok(Json.toJson(apiResponse))
142150
}
143151
}
152+
153+
def uploadJsonFile(
154+
workspaceId: Int
155+
): Action[MultipartFormData[TemporaryFile]] =
156+
authenticatedActionWithUser.async(parse.multipartFormData) { request =>
157+
val userId = request.userToken.userId
158+
159+
request.body.file("file") match {
160+
case Some(uploadedFile) =>
161+
val filePath = uploadedFile.ref.path.toFile
162+
163+
try {
164+
val fileSource = Source.fromFile(filePath)("UTF-8")
165+
val fileContent = try fileSource.getLines().mkString("\n")
166+
finally fileSource.close()
167+
val json = Json.parse(fileContent)
168+
169+
val data = json.as[ImportProjectData]
170+
171+
projectService
172+
.importProject(data, userId, workspaceId)
173+
.map { res =>
174+
val apiResponse =
175+
ApiResponse.success("Project imported successfully", res)
176+
Ok(Json.toJson(apiResponse))
177+
}
178+
179+
} catch {
180+
case _: JsResultException =>
181+
Future.successful(
182+
BadRequest(Json.obj("message" -> "JSON parse failed"))
183+
)
184+
case _: Throwable =>
185+
Future.successful(
186+
BadRequest(Json.obj("message" -> "Invalid JSON format"))
187+
)
188+
}
189+
190+
case None =>
191+
Future.successful(
192+
BadRequest(Json.obj("message" -> "No file uploaded"))
193+
)
194+
}
195+
}
144196
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dto.request.project
2+
3+
import models.entities.{Column, Project, Task, UserProject, UserTask}
4+
import play.api.libs.json.{Json, OFormat}
5+
6+
case class ImportProjectData(
7+
project: Project,
8+
columns: Seq[Column],
9+
tasks: Seq[Task],
10+
userTasks: Seq[UserTask],
11+
userProjects: Seq[UserProject]
12+
)
13+
object ImportProjectData {
14+
implicit val format: OFormat[ImportProjectData] = Json.format[ImportProjectData]
15+
}
Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
11
package models.entities
22

33
import models.Enums.ColumnStatus.ColumnStatus
4-
import models.Enums.{ColumnStatus, ProjectStatus, ProjectVisibility}
54
import models.Enums.ProjectStatus.ProjectStatus
65
import models.Enums.ProjectVisibility.ProjectVisibility
6+
import models.Enums.{ColumnStatus, ProjectStatus, ProjectVisibility}
7+
import play.api.libs.json.{Json, OFormat}
78

8-
import java.time.{Instant, LocalDateTime}
9-
10-
case class Project(
11-
id: Option[Int] = None,
12-
name: String,
13-
workspaceId: Int,
14-
status: ProjectStatus = ProjectStatus.active,
15-
visibility: ProjectVisibility = ProjectVisibility.Workspace,
16-
createdBy: Option[Int] = None,
17-
updatedBy: Option[Int] = None,
18-
createdAt: Instant = Instant.now(),
19-
updatedAt: Instant = Instant.now()
20-
)
9+
import java.time.Instant
2110

22-
case class Column(
23-
id: Option[Int] = None,
24-
projectId: Int,
11+
case class Project(id: Option[Int] = None,
2512
name: String,
26-
position: Int,
13+
workspaceId: Int,
14+
status: ProjectStatus = ProjectStatus.active,
15+
visibility: ProjectVisibility = ProjectVisibility.Workspace,
16+
createdBy: Option[Int] = None,
17+
updatedBy: Option[Int] = None,
2718
createdAt: Instant = Instant.now(),
28-
updatedAt: Instant = Instant.now(),
29-
status: ColumnStatus = ColumnStatus.active
30-
)
19+
updatedAt: Instant = Instant.now())
20+
object Project {
21+
implicit val projectFormat: OFormat[Project] = Json.format[Project]
22+
}
23+
24+
case class Column(id: Option[Int] = None,
25+
projectId: Int,
26+
name: String,
27+
position: Int,
28+
createdAt: Instant = Instant.now(),
29+
updatedAt: Instant = Instant.now(),
30+
status: ColumnStatus = ColumnStatus.active)
31+
object Column {
32+
implicit val columnFormat: OFormat[Column] = Json.format[Column]
33+
}

backend/app/models/entities/Task.scala

Lines changed: 53 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,65 @@ package models.entities
33
import models.Enums.TaskPriority.TaskPriority
44
import models.Enums.TaskStatus
55
import models.Enums.TaskStatus.TaskStatus
6+
import play.api.libs.json.{Json, OFormat}
67

78
import java.time.{Instant, LocalDateTime}
89

9-
case class Task(
10-
id: Option[Int] = None,
11-
columnId: Int,
12-
name: String,
13-
description: Option[String] = None,
14-
startDate: Option[Instant] = None,
15-
endDate: Option[Instant] = None,
16-
priority: Option[TaskPriority] = None,
17-
position: Int,
18-
createdBy: Option[Int] = None,
19-
updatedBy: Option[Int] = None,
20-
createdAt: Instant = Instant.now(),
21-
updatedAt: Instant = Instant.now(),
22-
status: TaskStatus = TaskStatus.active,
23-
isCompleted: Boolean = false
24-
)
10+
case class Task(id: Option[Int] = None,
11+
columnId: Int,
12+
name: String,
13+
description: Option[String] = None,
14+
startDate: Option[Instant] = None,
15+
endDate: Option[Instant] = None,
16+
priority: Option[TaskPriority] = None,
17+
position: Int,
18+
createdBy: Option[Int] = None,
19+
updatedBy: Option[Int] = None,
20+
createdAt: Instant = Instant.now(),
21+
updatedAt: Instant = Instant.now(),
22+
status: TaskStatus = TaskStatus.active,
23+
isCompleted: Boolean = false)
2524

26-
case class UserTask(
27-
id: Option[Int] = None,
28-
taskId: Int,
29-
assignedTo: Int,
30-
assignedBy: Option[Int] = None,
31-
assignedAt: Instant = Instant.now()
32-
)
25+
object Task {
26+
implicit val projectFormat: OFormat[Task] = Json.format[Task]
27+
}
3328

34-
case class Checklist(
35-
id: Option[Int] = None,
36-
taskId: Option[Int] = None,
37-
name: Option[String] = None,
38-
createdAt: Option[LocalDateTime] = None,
39-
updatedAt: Option[LocalDateTime] = None
40-
)
29+
case class UserTask(id: Option[Int] = None,
30+
taskId: Int,
31+
assignedTo: Int,
32+
assignedBy: Option[Int] = None,
33+
assignedAt: Instant = Instant.now())
4134

42-
case class ChecklistItem(
43-
id: Option[Int] = None,
44-
checklistId: Option[Int] = None,
45-
content: Option[String] = None,
46-
isCompleted: Boolean = false,
47-
createdAt: Option[LocalDateTime] = None,
48-
updatedAt: Option[LocalDateTime] = None
49-
)
35+
object UserTask {
36+
implicit val userTaskFormat: OFormat[UserTask] = Json.format[UserTask]
37+
}
5038

51-
case class TaskComment(
52-
id: Option[Int] = None,
53-
taskId: Option[Int] = None,
54-
userId: Option[Int] = None,
55-
content: Option[String] = None,
56-
createdAt: Option[LocalDateTime] = None,
57-
updatedAt: Option[LocalDateTime] = None
58-
)
39+
case class Checklist(id: Option[Int] = None,
40+
taskId: Option[Int] = None,
41+
name: Option[String] = None,
42+
createdAt: Option[LocalDateTime] = None,
43+
updatedAt: Option[LocalDateTime] = None)
5944

60-
case class Tag(
61-
id: Option[Int] = None,
62-
projectId: Option[Int] = None,
63-
name: Option[String] = None,
64-
color: Option[String] = None,
65-
createdAt: Option[LocalDateTime] = None
66-
)
45+
case class ChecklistItem(id: Option[Int] = None,
46+
checklistId: Option[Int] = None,
47+
content: Option[String] = None,
48+
isCompleted: Boolean = false,
49+
createdAt: Option[LocalDateTime] = None,
50+
updatedAt: Option[LocalDateTime] = None)
6751

68-
case class TaskTag(
69-
id: Option[Int] = None,
70-
taskId: Option[Int] = None,
71-
tagId: Option[Int] = None
72-
)
52+
case class TaskComment(id: Option[Int] = None,
53+
taskId: Option[Int] = None,
54+
userId: Option[Int] = None,
55+
content: Option[String] = None,
56+
createdAt: Option[LocalDateTime] = None,
57+
updatedAt: Option[LocalDateTime] = None)
58+
59+
case class Tag(id: Option[Int] = None,
60+
projectId: Option[Int] = None,
61+
name: Option[String] = None,
62+
color: Option[String] = None,
63+
createdAt: Option[LocalDateTime] = None)
64+
65+
case class TaskTag(id: Option[Int] = None,
66+
taskId: Option[Int] = None,
67+
tagId: Option[Int] = None)

backend/app/models/entities/User.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package models.entities
22

33
import models.Enums.UserProjectRole
44
import models.Enums.UserProjectRole.UserProjectRole
5+
import play.api.libs.json.{Json, OFormat}
56

67
import java.time.{Instant, LocalDateTime}
78

@@ -18,9 +19,18 @@ case class User(
1819
updatedAt: LocalDateTime = LocalDateTime.now()
1920
)
2021

22+
object User {
23+
implicit val userFormat: OFormat[User] = Json.format[User]
24+
}
25+
2126
case class UserProject(id: Option[Int] = None,
2227
userId: Int,
2328
projectId: Int,
2429
role: UserProjectRole = UserProjectRole.member,
2530
invitedBy: Option[Int] = None,
26-
joinedAt: Instant = Instant.now())
31+
joinedAt: Instant = Instant.now())
32+
33+
object UserProject {
34+
implicit val userProjectFormat: OFormat[UserProject] = Json.format[UserProject]
35+
36+
}

backend/app/repositories/ColumnRepository.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,8 @@ class ColumnRepository @Inject()(
130130
db.run((columns returning columns) ++= cols).map(_.toSeq)
131131
}
132132

133+
def importColumnBatch(cols: Seq[Column]): DBIO[Seq[Int]] = {
134+
(columns returning columns.map(_.id)) ++= cols
135+
}
136+
133137
}

backend/app/repositories/ProjectRepository.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ class ProjectRepository @Inject()(
7474
} yield projectId
7575
}
7676

77+
def importProjectWithOwner(project: Project, ownerId: Int): DBIO[Int] = {
78+
for {
79+
projectId <- (projects returning projects.map(_.id)) += project
80+
81+
_ <- DBIO.seq(
82+
userProjects += UserProject(
83+
userId = ownerId,
84+
projectId = projectId,
85+
role = UserProjectRole.owner,
86+
joinedAt = Instant.now()
87+
)
88+
)
89+
} yield projectId
90+
}
91+
7792
def findNonDeletedByWorkspace(
7893
workspaceId: Int
7994
): DBIO[Seq[ProjectSummariesResponse]] = {
@@ -160,6 +175,13 @@ class ProjectRepository @Inject()(
160175
db.run(action)
161176
}
162177

178+
def importUserBatchIntoProject(
179+
entries: Seq[UserProject]
180+
): DBIO[Seq[Int]] = {
181+
val insertQuery = userProjects returning userProjects.map(_.id)
182+
insertQuery ++= entries
183+
}
184+
163185
def getProjectsByUser(userId: Int): DBIO[Seq[Project]] = {
164186
val q = for {
165187
up <- userProjects

backend/app/repositories/TaskRepository.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,14 @@ class TaskRepository@Inject()(
247247
db.run(insertQuery).map(_ => ())
248248
}
249249

250+
def importTaskBatch(tasksChunk: Seq[Task]): DBIO[Seq[Int]] = {
251+
((tasks returning tasks.map(_.id)) ++= tasksChunk).map(_.toSeq)
252+
}
253+
254+
def importUserBatchIntoTask(entries: Seq[UserTask]): DBIO[Seq[Int]]= {
255+
(userTasks returning userTasks.map(_.id) ++= entries).map(_.toSeq)
256+
}
257+
250258
def search(
251259
projectIds: Option[Seq[Int]] = None,
252260
keyword: Option[String] = None,

0 commit comments

Comments
 (0)