Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tasks #2401

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
614d308
feat: tasks
stepan662 Jul 3, 2024
a93ee2a
fix: search field min height
stepan662 Sep 16, 2024
556a1b7
chore: fix BE tests
stepan662 Sep 17, 2024
f095ae2
chore: fix BE tests
stepan662 Sep 17, 2024
378d93e
chore: e2e tests for tasks
stepan662 Sep 18, 2024
3fd0de9
fix: my tasks e2e + fix some bugs
stepan662 Sep 19, 2024
34bdbb8
fix: my tasks e2e + fix some bugs
stepan662 Sep 19, 2024
479a485
feat: task indicator in neutral color, displayed for everyone
stepan662 Sep 19, 2024
0c834fb
feat: task indicator in neutral color, displayed for everyone
stepan662 Sep 19, 2024
f01f11c
feat: improve layout and font sizes
stepan662 Sep 19, 2024
7c050ba
feat: add tooltips and progress green
stepan662 Sep 19, 2024
a4b57e5
feat: assign to me
stepan662 Sep 19, 2024
84b9c80
feat: current user name and project with avatar unified
stepan662 Sep 19, 2024
00e4de1
feat: increase task items paddings
stepan662 Sep 19, 2024
671c0c8
feat: task transition translations
stepan662 Sep 19, 2024
2482e33
feat: assignee placeholder
stepan662 Sep 19, 2024
5648505
feat: update dashboard
stepan662 Sep 19, 2024
1232594
feat: update dashboard
stepan662 Sep 20, 2024
fea46df
chore: fix e2e tests
stepan662 Sep 20, 2024
ec4b31c
chore: add tasks permission tests
stepan662 Sep 20, 2024
e2f64ec
chore: fix e2e tests
stepan662 Sep 20, 2024
8fd6a1c
chore: fix e2e tests
stepan662 Sep 20, 2024
c470489
chore: fix e2e tests
stepan662 Sep 20, 2024
78959e5
chore: fix e2e tests
stepan662 Sep 20, 2024
8ed95d3
fix: translation possibly undefined
stepan662 Sep 20, 2024
52631c9
fix: translation possibly undefined
stepan662 Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ When no languages provided, it translates only untranslated languages.""",
languagesToTranslate: Set<String>,
) {
keyService.checkInProject(key, projectHolder.project.id)
securityService.checkLanguageTranslatePermissionsByTag(languagesToTranslate, projectHolder.project.id)
securityService.checkLanguageTranslatePermissionsByTag(languagesToTranslate, projectHolder.project.id, key.id)
}

private fun getAllLanguagesToTranslate(): Set<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.tolgee.security.ProjectHolder
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authorization.RequiresProjectPermissions
import io.tolgee.security.authorization.UseDefaultPermissions
import io.tolgee.service.TaskService
import io.tolgee.service.language.LanguageService
import io.tolgee.service.project.LanguageStatsService
import io.tolgee.service.project.ProjectService
Expand All @@ -36,6 +37,7 @@ class ProjectStatsController(
private val languageStatsService: LanguageStatsService,
private val languageStatsModelAssembler: LanguageStatsModelAssembler,
private val languageService: LanguageService,
private val taskService: TaskService,
) {
@Operation(summary = "Get project stats")
@GetMapping("", produces = [MediaTypes.HAL_JSON_VALUE])
Expand All @@ -62,6 +64,7 @@ class ProjectStatsController(
projectId = projectStats.id,
languageCount = languageStats.size,
keyCount = projectStats.keyCount,
taskCount = projectStats.taskCount,
baseWordsCount = totals.baseWordsCount,
translatedPercentage = totals.translatedPercent,
reviewedPercentage = totals.reviewedPercent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
package io.tolgee.api.v2.controllers

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.activity.RequestActivity
import io.tolgee.activity.data.ActivityType
import io.tolgee.dtos.request.task.*
import io.tolgee.dtos.request.userAccount.UserAccountPermissionsFilters
import io.tolgee.hateoas.task.TaskModel
import io.tolgee.hateoas.task.TaskModelAssembler
import io.tolgee.hateoas.task.TaskPerUserReportModel
import io.tolgee.hateoas.task.TaskPerUserReportModelAssembler
import io.tolgee.hateoas.userAccount.SimpleUserAccountModel
import io.tolgee.hateoas.userAccount.SimpleUserAccountModelAssembler
import io.tolgee.model.UserAccount
import io.tolgee.model.enums.Scope
import io.tolgee.model.enums.TaskState
import io.tolgee.model.views.KeysScopeView
import io.tolgee.model.views.TaskWithScopeView
import io.tolgee.openApiDocs.OpenApiOrderExtension
import io.tolgee.security.ProjectHolder
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authorization.RequiresProjectPermissions
import io.tolgee.security.authorization.UseDefaultPermissions
import io.tolgee.service.TaskService
import io.tolgee.service.security.SecurityService
import io.tolgee.service.security.UserAccountService
import jakarta.validation.Valid
import org.springdoc.core.annotations.ParameterObject
import org.springframework.core.io.ByteArrayResource
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PagedResourcesAssembler
import org.springframework.hateoas.PagedModel
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(
value = [
"/v2/projects/{projectId}/tasks",
"/v2/projects/tasks",
],
)
@Tag(name = "Tasks", description = "Manipulates tasks")
@OpenApiOrderExtension(7)
class TaskController(
private val taskService: TaskService,
private val taskModelAssembler: TaskModelAssembler,
private val pagedTaskResourcesAssembler: PagedResourcesAssembler<TaskWithScopeView>,
private val projectHolder: ProjectHolder,
private val userAccountService: UserAccountService,
private val userAccountModelAssembler: SimpleUserAccountModelAssembler,
private val pagedUserResourcesAssembler: PagedResourcesAssembler<UserAccount>,
private val taskPerUserReportModelAssembler: TaskPerUserReportModelAssembler,
private val securityService: SecurityService,
) {
@GetMapping("")
@Operation(summary = "Get tasks")
@RequiresProjectPermissions([Scope.TASKS_VIEW])
@AllowApiAccess
fun getTasks(
@ParameterObject
filters: TaskFilters,
@ParameterObject
pageable: Pageable,
@RequestParam("search", required = false)
search: String?,
): PagedModel<TaskModel> {
val tasks = taskService.getAllPaged(projectHolder.projectEntity, pageable, search, filters)
return pagedTaskResourcesAssembler.toModel(tasks, taskModelAssembler)
}

@PostMapping("")
@Operation(summary = "Create task")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
@RequestActivity(ActivityType.TASK_CREATE)
fun createTask(
@RequestBody @Valid
dto: CreateTaskRequest,
@ParameterObject
filters: TranslationScopeFilters,
): TaskModel {
val task = taskService.createTask(projectHolder.projectEntity, dto, filters)
return taskModelAssembler.toModel(task)
}

@GetMapping("/{taskNumber}")
@Operation(summary = "Get task")
@UseDefaultPermissions
@AllowApiAccess
fun getTask(
@PathVariable
taskNumber: Long,
): TaskModel {
// user can view tasks assigned to him
securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber)
val task = taskService.getTask(projectHolder.projectEntity, taskNumber)
return taskModelAssembler.toModel(task)
}

@PutMapping("/{taskNumber}")
@Operation(summary = "Update task")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
@RequestActivity(ActivityType.TASK_UPDATE)
fun updateTask(
@PathVariable
taskNumber: Long,
@RequestBody @Valid
dto: UpdateTaskRequest,
): TaskModel {
val task = taskService.updateTask(projectHolder.projectEntity, taskNumber, dto)
return taskModelAssembler.toModel(task)
}

@GetMapping("/{taskNumber}/per-user-report")
@Operation(summary = "Report who did what")
@UseDefaultPermissions
@AllowApiAccess
fun getPerUserReport(
@PathVariable
taskNumber: Long,
): List<TaskPerUserReportModel> {
securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber)

val result = taskService.getReport(projectHolder.projectEntity, taskNumber)
return result.map { taskPerUserReportModelAssembler.toModel(it) }
}

@GetMapping("/{taskNumber}/csv-report")
@Operation(summary = "Report who did what")
@UseDefaultPermissions
@AllowApiAccess
fun getCsvReport(
@PathVariable
taskNumber: Long,
): ResponseEntity<ByteArrayResource> {
securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber)
val byteArray = taskService.getExcelFile(projectHolder.projectEntity, taskNumber)
val resource = ByteArrayResource(byteArray)

val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_OCTET_STREAM
headers.setContentDispositionFormData("attachment", "report.xlsx")
headers.contentLength = byteArray.size.toLong()

return ResponseEntity(resource, headers, HttpStatus.OK)
}

@GetMapping("/{taskNumber}/keys")
@Operation(summary = "Get task keys")
@UseDefaultPermissions
@AllowApiAccess
fun getTaskKeys(
@PathVariable
taskNumber: Long,
): TaskKeysResponse {
securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber)
return TaskKeysResponse(
keys = taskService.getTaskKeys(projectHolder.projectEntity, taskNumber),
)
}

@PutMapping("/{taskNumber}/keys")
@Operation(summary = "Add or remove task keys")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
@RequestActivity(ActivityType.TASK_KEYS_UPDATE)
fun updateTaskKeys(
@PathVariable
taskNumber: Long,
@RequestBody @Valid
dto: UpdateTaskKeysRequest,
) {
taskService.updateTaskKeys(projectHolder.projectEntity, taskNumber, dto)
}

@GetMapping("/{taskNumber}/blocking-tasks")
@Operation(summary = "Get task ids which block this task")
@UseDefaultPermissions
@AllowApiAccess
fun getBlockingTasks(
@PathVariable
taskNumber: Long,
): List<Long> {
return taskService.getBlockingTasks(projectHolder.projectEntity, taskNumber)
}

@PostMapping("/{taskNumber}/finish")
@Operation(summary = "Finish task")
// permissions checked inside
@UseDefaultPermissions
@AllowApiAccess
@RequestActivity(ActivityType.TASK_FINISH)
fun finishTask(
@PathVariable
taskNumber: Long,
): TaskModel {
// user can only finish tasks assigned to him
securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber)
val task = taskService.setTaskState(projectHolder.projectEntity, taskNumber, TaskState.DONE)
return taskModelAssembler.toModel(task)
}

@PostMapping("/{taskNumber}/close")
@Operation(summary = "Close task")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
@RequestActivity(ActivityType.TASK_CLOSE)
fun closeTask(
@PathVariable
taskNumber: Long,
): TaskModel {
val task = taskService.setTaskState(projectHolder.projectEntity, taskNumber, TaskState.CLOSED)
return taskModelAssembler.toModel(task)
}

@PostMapping("/{taskNumber}/reopen")
@Operation(summary = "Reopen task")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
@RequestActivity(ActivityType.TASK_REOPEN)
fun reopenTask(
@PathVariable
taskNumber: Long,
): TaskModel {
val task = taskService.setTaskState(projectHolder.projectEntity, taskNumber, TaskState.IN_PROGRESS)
return taskModelAssembler.toModel(task)
}

@PutMapping("/{taskNumber}/keys/{keyId}")
@Operation(summary = "Update task key")
// permissions checked inside
@UseDefaultPermissions
@AllowApiAccess
fun updateTaskKey(
@PathVariable
taskNumber: Long,
@PathVariable
keyId: Long,
@RequestBody @Valid
dto: UpdateTaskKeyRequest,
): UpdateTaskKeyResponse {
// user can only update tasks assigned to him
securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber)
return taskService.updateTaskKey(projectHolder.projectEntity, taskNumber, keyId, dto)
}

@PostMapping("/create-multiple")
@Operation(summary = "Create multiple tasks")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
@RequestActivity(ActivityType.TASKS_CREATE)
fun createTasks(
@RequestBody @Valid
dto: CreateMultipleTasksRequest,
@ParameterObject
filters: TranslationScopeFilters,
) {
taskService.createMultipleTasks(projectHolder.projectEntity, dto.tasks, filters)
}

@PostMapping("/calculate-scope")
@Operation(summary = "Calculate scope")
@RequiresProjectPermissions([Scope.TASKS_VIEW])
@AllowApiAccess
fun calculateScope(
@RequestBody @Valid
dto: CalculateScopeRequest,
@ParameterObject
filters: TranslationScopeFilters,
): KeysScopeView {
return taskService.calculateScope(projectHolder.projectEntity, dto, filters)
}

@GetMapping("/possible-assignees")
@RequiresProjectPermissions([Scope.TASKS_EDIT])
@AllowApiAccess
fun getPossibleAssignees(
@ParameterObject
filters: UserAccountPermissionsFilters,
@ParameterObject
pageable: Pageable,
@RequestParam("search", required = false)
search: String?,
): PagedModel<SimpleUserAccountModel> {
val users =
userAccountService.findWithMinimalPermissions(
filters,
projectHolder.projectEntity.id,
search,
pageable,
)
return pagedUserResourcesAssembler.toModel(users, userAccountModelAssembler)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.tolgee.api.v2.controllers

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.dtos.request.task.TaskFilters
import io.tolgee.hateoas.task.TaskWithProjectModel
import io.tolgee.hateoas.task.TaskWithProjectModelAssembler
import io.tolgee.model.views.TaskWithScopeView
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authentication.AuthenticationFacade
import io.tolgee.security.authorization.UseDefaultPermissions
import io.tolgee.service.TaskService
import org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PagedResourcesAssembler
import org.springframework.hateoas.PagedModel
import org.springframework.web.bind.annotation.*

@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/user-tasks"])
@Tag(name = "User tasks")
class UserTasksController(
private val taskService: TaskService,
private val authenticationFacade: AuthenticationFacade,
private val pagedTaskResourcesAssembler: PagedResourcesAssembler<TaskWithScopeView>,
private val taskWithProjectModelAssembler: TaskWithProjectModelAssembler,
) {
@GetMapping("")
@Operation(summary = "Get user tasks")
@UseDefaultPermissions
@AllowApiAccess
fun getTasks(
@ParameterObject
filters: TaskFilters,
@ParameterObject
pageable: Pageable,
@RequestParam("search", required = false)
search: String?,
): PagedModel<TaskWithProjectModel> {
val user = authenticationFacade.authenticatedUser
val tasks = taskService.getUserTasksPaged(user.id, pageable, search, filters)
return pagedTaskResourcesAssembler.toModel(tasks, taskWithProjectModelAssembler)
}
}
Loading
Loading