/** * */ package com.qprogramming.tasq.task; import com.google.common.annotations.VisibleForTesting; import com.qprogramming.tasq.account.*; import com.qprogramming.tasq.agile.AgileService; import com.qprogramming.tasq.agile.Sprint; import com.qprogramming.tasq.error.TasqAuthException; import com.qprogramming.tasq.error.TasqException; import com.qprogramming.tasq.events.Event; import com.qprogramming.tasq.events.EventsService; import com.qprogramming.tasq.projects.Project; import com.qprogramming.tasq.projects.ProjectService; import com.qprogramming.tasq.support.PeriodHelper; import com.qprogramming.tasq.support.ResultData; import com.qprogramming.tasq.support.Utils; import com.qprogramming.tasq.support.sorters.ProjectSorter; import com.qprogramming.tasq.support.sorters.TaskSorter; import com.qprogramming.tasq.support.web.MessageHelper; import com.qprogramming.tasq.task.comments.Comment; import com.qprogramming.tasq.task.comments.CommentService; import com.qprogramming.tasq.task.link.TaskLink; import com.qprogramming.tasq.task.link.TaskLinkService; import com.qprogramming.tasq.task.link.TaskLinkType; import com.qprogramming.tasq.task.tag.Tag; import com.qprogramming.tasq.task.tag.TagsRepository; import com.qprogramming.tasq.task.watched.WatchedTask; import com.qprogramming.tasq.task.watched.WatchedTaskService; import com.qprogramming.tasq.task.worklog.LogType; import com.qprogramming.tasq.task.worklog.TaskResolution; import com.qprogramming.tasq.task.worklog.WorkLog; import com.qprogramming.tasq.task.worklog.WorkLogService; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.Hibernate; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.Period; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.*; import java.text.ParseException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.qprogramming.tasq.support.Utils.REDIRECT; import static com.qprogramming.tasq.support.Utils.REDIRECT_TASK; import static com.qprogramming.tasq.task.TaskForm.*; /** * @author romanjak * @date 26 maj 2014 */ @Controller public class TaskController { protected static final Logger LOG = LoggerFactory.getLogger(TaskController.class); private static final String TYPE_TXT = "Type"; private static final String ESTIMATED_TXT = "Estimated "; private static final String REMAINING_TXT = "Remaining"; private static final String ESTIMATE_TXT = "Estimate"; private static final String DESCRIPTION_TXT = "Description"; private static final String NAME_TXT = "Name"; private static final String STORY_POINTS_TXT = "Story points"; private static final String UNASSIGNED = "<i>Unassigned</i>"; private static final String OPEN = "OPEN"; private static final String ALL = "ALL"; private static final String CHANGE_TO = " -> "; private static final String BR = "<br>"; private static final String START = "start"; private static final String STOP = "stop"; private static final String CANCEL = "cancel"; private static final String ERROR_ACCES_RIGHTS = "error.accesRights"; private static final String REFERER = "Referer"; private static final String ERROR_NAME_HTML = "error.name.html"; private TaskService taskSrv; private ProjectService projectSrv; private AccountService accSrv; private WorkLogService wlSrv; private MessageSource msg; private AgileService sprintSrv; private TaskLinkService linkService; private WatchedTaskService watchSrv; private CommentService commSrv; private TagsRepository tagsRepo; private EventsService eventSrv; private LastVisitedService visitedSrv; @Autowired public TaskController(TaskService taskSrv, ProjectService projectSrv, AccountService accSrv, WorkLogService wlSrv, MessageSource msg, AgileService sprintSrv, TaskLinkService linkService, CommentService commSrv, TagsRepository tagsRepo, WatchedTaskService watchSrv, EventsService eventSrv, LastVisitedService visitedSrv) { this.taskSrv = taskSrv; this.projectSrv = projectSrv; this.accSrv = accSrv; this.wlSrv = wlSrv; this.msg = msg; this.sprintSrv = sprintSrv; this.linkService = linkService; this.commSrv = commSrv; this.tagsRepo = tagsRepo; this.watchSrv = watchSrv; this.eventSrv = eventSrv; this.visitedSrv = visitedSrv; } @RequestMapping(value = "task/create", method = RequestMethod.GET) public TaskForm startTaskCreate(Model model) { fillCreateTaskModel(model); return new TaskForm(); } @Transactional @RequestMapping(value = "task/create", method = RequestMethod.POST) public String createTask(@Valid @ModelAttribute("taskForm") TaskForm taskForm, BindingResult errors, @RequestParam(value = "linked", required = false) String linked, RedirectAttributes ra, HttpServletRequest request, Model model) { if (!Roles.isUser()) { throw new TasqAuthException(msg); } if (Utils.containsHTMLTags(taskForm.getName())) { errors.rejectValue(NAME, ERROR_NAME_HTML); } checkEstimatesValues(taskForm, errors); int storyPoints = getStoryPoints(taskForm); if (!StringUtils.isNumeric(taskForm.getStory_points()) || !Utils.validStoryPoint(storyPoints)) { errors.rejectValue(STORY_POINTS, "task.storyPoints.invalid"); } if (errors.hasErrors()) { fillCreateTaskModel(model); return null; } Project project = projectSrv.findByProjectId(taskForm.getProject()); if (project != null) { // check if can edit Task task; task = taskForm.createTask(); if (!projectSrv.canEdit(project)) { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } // build ID long taskCount = project.getLastTaskNo(); taskCount++; String taskID = project.getProjectId() + "-" + taskCount; task.setId(taskID); task.setProject(project); task.setTaskOrder(taskCount); project.getTasks().add(task); project.setLastTaskNo(taskCount); // assigne setCreatedTaskAssignee(taskForm, task); task = taskSrv.save(task);//save before adding rest wlSrv.addActivityLog(task, "", LogType.CREATE); // lookup for sprint // Create log work if (taskForm.getAddToSprint() != null) { Sprint sprint = sprintSrv.findByProjectIdAndSprintNo(project.getId(), taskForm.getAddToSprint()); task.addSprint(sprint); // increase scope if (sprint.isActive()) { if (checkIfNotEstimated(task, project)) { errors.rejectValue("addToSprint", "agile.task2Sprint.Notestimated", new Object[]{"", sprint.getSprintNo()}, "Unable to add not estimated task to active sprint"); fillCreateTaskModel(model); return null; } String message = ""; wlSrv.addActivityLog(task, message, LogType.TASKSPRINTADD); } } task = taskSrv.save(task); projectSrv.save(project); // Save files saveTaskFiles(taskForm.getFiles(), task); watchSrv.startWatching(task); if (StringUtils.isNotBlank(linked)) { Task linkedTask = taskSrv.findById(linked); if (linkedTask != null) { TaskLink link = new TaskLink(linkedTask.getId(), taskID, TaskLinkType.RELATES_TO); linkService.save(link); wlSrv.addWorkLogNoTask(linked + " - " + taskID, project, LogType.TASK_LINK); } } //everything went well , save task taskSrv.save(task); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.create.success", new Object[]{taskID}, Utils.getCurrentLocale())); return REDIRECT_TASK + taskID; } return null; } private void checkEstimatesValues(@Valid @ModelAttribute("taskForm") TaskForm taskForm, BindingResult result) { if (StringUtils.isNotBlank(taskForm.getEstimate())) { if (!Utils.correctEstimate(taskForm.getEstimate())) { result.rejectValue("estimate", "error.estimateFormat"); } else if (PeriodHelper.inFormat(taskForm.getEstimate()).getDays() > 28) { result.rejectValue("estimate", "error.estimateTooLong"); } } } private void setCreatedTaskAssignee(TaskForm taskForm, Task task) { if (StringUtils.isNotBlank(taskForm.getAssignee())) { Account assignee = accSrv.findByEmail(taskForm.getAssignee()); if (assignee != null) { task.setAssignee(assignee); watchSrv.addToWatchers(task, assignee); } } } @RequestMapping(value = "/task/{id}/edit", method = RequestMethod.GET) public TaskForm startEditTask(@PathVariable("id") String id, Model model) { Task task = taskSrv.findById(id); if (projectSrv.canEdit(task.getProject()) && (Roles.isUser() | task.getOwner().equals(Utils.getCurrentAccount()))) { fillModelForEdit(model, task); return new TaskForm(task); } else { throw new TasqAuthException(msg); } } private void fillModelForEdit(Model model, Task task) { model.addAttribute("task", task); model.addAttribute("project", task.getProject()); } @Transactional @RequestMapping(value = "/task/{id}/{subid}/edit", method = RequestMethod.GET) public TaskForm startEditSubTask(@PathVariable("id") String id, @PathVariable("subid") String subid, Model model) { return startEditTask(taskSrv.createSubId(id, subid), model); } @Transactional @RequestMapping(value = "/task/{id}/{subid}/edit", method = RequestMethod.POST) public String editSubTask(@Valid @ModelAttribute("taskForm") TaskForm taskForm, BindingResult errors, RedirectAttributes ra, HttpServletRequest request, Model model) { return editTask(taskForm, errors, ra, request, model); } @Transactional @RequestMapping(value = "/task/{id}/edit", method = RequestMethod.POST) public String editTask(@Valid @ModelAttribute("taskForm") TaskForm taskForm, BindingResult errors, RedirectAttributes ra, HttpServletRequest request, Model model) { String taskID = taskForm.getId(); Task task = taskSrv.findById(taskID); if (task == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.task.notfound", null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } if (Utils.containsHTMLTags(taskForm.getName())) { errors.rejectValue(NAME, ERROR_NAME_HTML); } checkEstimatesValues(taskForm, errors); if (StringUtils.isNotBlank(taskForm.getRemaining()) && !Utils.correctEstimate(taskForm.getRemaining())) { errors.rejectValue(REMAINING, "error.estimateFormat"); } int storyPoints = getStoryPoints(taskForm); if (!task.isSubtask() && !StringUtils.isNumeric(taskForm.getStory_points()) || !Utils.validStoryPoint(storyPoints)) { errors.rejectValue(STORY_POINTS, "task.storyPoints.invalid"); } if (errors.hasErrors()) { fillModelForEdit(model, task); return null; } // check if can edit if (!projectSrv.canEdit(task.getProject()) && (!Roles.isUser() | !task.getOwner().equals(Utils.getCurrentAccount()))) { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } if (task.getState().equals(TaskState.CLOSED)) { ResultData result = taskIsClosed(task); MessageHelper.addWarningAttribute(ra, result.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } StringBuilder message = new StringBuilder(Utils.TABLE); if (nameChanged(taskForm.getName(), task)) { message.append(Utils.changedFromTo(NAME_TXT, task.getName(), taskForm.getName())); task.setName(taskForm.getName()); updateWatched(task); visitedSrv.updateName(task); } if (!task.getDescription().equals(taskForm.getDescription())) { message.append(Utils.changedFromTo(DESCRIPTION_TXT, task.getDescription(), taskForm.getDescription())); task.setDescription(taskForm.getDescription()); } if ((StringUtils.isNotBlank(taskForm.getEstimate()) && (!task.getEstimate().equalsIgnoreCase(taskForm.getEstimate())))) { message.append(changeEstimate(taskForm.getEstimate(), task)); } if (StringUtils.isNotBlank(taskForm.getRemaining()) && (!task.getRemaining().equalsIgnoreCase(taskForm.getRemaining()))) { message.append(changeRemaining(taskForm.getRemaining(), task)); } boolean estimated = !taskForm.getNotEstimated(); if (!task.isEstimated().equals(estimated) && !task.isInSprint()) { message.append( Utils.changedFromTo(ESTIMATED_TXT, task.getEstimated().toString(), Boolean.toString(estimated))); task.setEstimated(estimated); if (!task.isEstimated()) { task.setStory_points(0); } } // Don't check for SP if task is not estimated if (task.isEstimated()) { try { if (task.getStory_points() != null && task.getStory_points() != storyPoints) { if (shouldAddWorklogPointsChanged(task, storyPoints)) { message.append(Utils.changedFromTo(STORY_POINTS_TXT, task.getStory_points().toString(), Integer.toString(storyPoints))); } task.setStory_points(storyPoints); } } catch (NumberFormatException e) { throw new TasqException("Please use only full numbers"); } } if (!task.getDue_date().equalsIgnoreCase(taskForm.getDue_date())) { message.append(Utils.changedFromTo("Due date", task.getDue_date(), taskForm.getDue_date())); task.setDue_date(Utils.convertStringToDate(taskForm.getDue_date())); } TaskType type = TaskType.toType(taskForm.getType()); if (!task.getType().equals(type)) { message.append(Utils.changedFromTo(TYPE_TXT, task.getType().toString(), type.toString())); task.setType(type); updateWatched(task); } LOG.debug(message.toString()); message.append(Utils.TABLE_END); if (message.length() > 43) { wlSrv.addActivityLog(task, message.toString(), LogType.EDITED); } visitedSrv.updateFromToVisitedTask(task, task); taskSrv.save(task); return REDIRECT_TASK + taskID; } private String changeRemaining(String remainingString, Task task) { Period remaining = PeriodHelper.inFormat(remainingString); task.setRemaining(remaining); return Utils.changedFromTo(REMAINING_TXT, task.getRemaining(), remainingString).toString(); } private String changeEstimate(String est, Task task) { String result = ""; Period estimate = PeriodHelper.inFormat(est); Period difference = PeriodHelper.minusPeriods(estimate, task.getRawEstimate()); // only add estimate change event if task is in sprint if (sprintSrv.taskInActiveSprint(task)) { wlSrv.addActivityPeriodLog(task, PeriodHelper.outFormat(difference), difference, LogType.ESTIMATE); } else { result = Utils.changedFromTo(ESTIMATE_TXT, task.getEstimate(), est).toString(); } task.setEstimate(estimate); task.setRemaining(estimate); return result; } @Transactional @ResponseBody @RequestMapping(value = "task/changeEstimateTime", method = RequestMethod.POST) public ResultData changeEstimateTime(@RequestParam(name = "id") String id, @RequestParam String newValue, @RequestParam Boolean estimate) { Task task = taskSrv.findById(id); ResultData result = validateNewTime(newValue, task, estimate); if (ResultData.Code.ERROR.equals(result.code)) { return result; } else { if (estimate) { changeEstimate(newValue, task); result.message = msg.getMessage("task.estimate.changed", new Object[]{id, newValue}, Utils.getCurrentLocale()); } else { changeRemaining(newValue, task); result.message = msg.getMessage("task.remaining.changed", new Object[]{id, newValue}, Utils.getCurrentLocale()); } } return result; } private ResultData validateNewTime(String newValue, Task task, Boolean estimate) { ResultData result = new ResultData(); result.code = ResultData.Code.OK; if (StringUtils.isBlank(newValue) || task == null || (estimate && !task.getLoggedWork().equals("0m")) || task.getEstimate().trim().equalsIgnoreCase(newValue)) { result.code = ResultData.Code.ERROR; result.message = msg.getMessage("error.changeTime", null, Utils.getCurrentLocale()); } else if (!projectSrv.canEdit(task.getProject())) { if (!Roles.isUser() || (!isOwnerOrAssignee(task))) { result.code = ResultData.Code.ERROR; result.message = msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale()); } } return result; } private boolean isOwnerOrAssignee(Task task) { return Utils.getCurrentAccount().equals(task.getOwner()) || Utils.getCurrentAccount().equals(task.getAssignee()); } private int getStoryPoints(TaskForm taskForm) { return (StringUtils.isNotBlank(taskForm.getStory_points()) && StringUtils.isNumeric(taskForm.getStory_points())) ? Integer.parseInt(taskForm.getStory_points()) : 0; } private boolean nameChanged(String newName, Task task) { return !task.getName().equals(newName); } private void updateWatched(Task task) { WatchedTask watched = watchSrv.getByTask(task); if (watched != null) { watched.setName(task.getName()); watched.setType((TaskType) task.getType()); } } @Transactional @RequestMapping(value = "task/{id}/{subId}", method = RequestMethod.GET) public String showSubTaskDetails(@PathVariable(value = "id") String id, @PathVariable(value = "subId") String subId, Model model, RedirectAttributes ra) { return showTaskDetails(taskSrv.createSubId(id, subId), model, ra); } @Transactional @RequestMapping(value = "task/{id}", method = RequestMethod.GET) public String showTaskDetails(@PathVariable String id, Model model, RedirectAttributes ra) { Task task = taskSrv.findById(id); if (task == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("task.notexists", null, Utils.getCurrentLocale())); return "redirect:/tasks"; } Account account = Utils.getCurrentAccount(); visitedSrv.addLastVisited(account.getId(), task); // TASK Set<Comment> comments = commSrv.findByTaskIdOrderByDateDesc(id); Map<TaskLinkType, List<String>> links = linkService.findTaskLinks(id); Map<TaskLinkType, List<Task>> taskLinks = new HashMap<>(); for (Map.Entry<TaskLinkType, List<String>> taskLink : links.entrySet()) { taskLinks.put(taskLink.getKey(), taskLink.getValue().stream().map(s -> taskSrv.findById(s)).collect(Collectors.toList())); } if (!task.isSubtask()) { List<Task> subtasks = taskSrv.findSubtasks(task); // Add all subtasks into remaining work if (!subtasks.isEmpty()) { Period parentEstimate = task.getRawEstimate(); Period parentLoggedWork = task.getRawLoggedWork(); Period parentRemaining = task.getRawRemaining(); taskSrv.addSubtaskTimers(task, subtasks); Collections.sort(subtasks, new TaskSorter(TaskSorter.SORTBY.ID, true)); model.addAttribute("taskEstimate", PeriodHelper.outFormat(parentEstimate)); model.addAttribute("subtasksEstimate", PeriodHelper.outFormat(PeriodHelper.minusPeriods(task.getRawEstimate(), parentEstimate))); model.addAttribute("taskLogged", PeriodHelper.outFormat(parentLoggedWork)); model.addAttribute("subtasksLogged", PeriodHelper.outFormat(PeriodHelper.minusPeriods(task.getRawLoggedWork(), parentLoggedWork))); model.addAttribute("taskRemaining", PeriodHelper.outFormat(parentRemaining)); model.addAttribute("subtasksRemaining", PeriodHelper.outFormat(PeriodHelper.minusPeriods(task.getRawRemaining(), parentRemaining))); } model.addAttribute("subtasks", subtasks); } model.addAttribute("watching", watchSrv.isWatching(task.getId())); model.addAttribute("comments", comments); model.addAttribute("task", task); model.addAttribute("links", taskLinks); model.addAttribute("files", getTaskFiles(task)); return "task/details"; } @Transactional @RequestMapping(value = "tasks", method = RequestMethod.GET) public String listTasks(@RequestParam(value = "projectID", required = false) String projId, @RequestParam(value = "state", required = false) String state, @RequestParam(value = "query", required = false) String query, @RequestParam(value = "priority", required = false) String priority, @RequestParam(value = "type", required = false) String type, @RequestParam(value = "assignee", required = false) String assignee, Model model) { if (StringUtils.isEmpty(state)) { if (query != null) { state = ALL; } else { state = OPEN; } } Account currentAccount = Utils.getCurrentAccount(); List<Project> projects = projectSrv.findAllByUser(); Collections.sort(projects, new ProjectSorter(ProjectSorter.SORTBY.LAST_VISIT, currentAccount.getActiveProject(), true)); Account assigneeAccount = null; if (StringUtils.isNotEmpty(assignee)) { assigneeAccount = accSrv.findByUsername(assignee); if (assigneeAccount != null) { model.addAttribute("assignee", assigneeAccount); } } model.addAttribute("projects", projects); // Get active or choosen project Optional<Project> projectObj; if (projId == null) { projectObj = projects.stream().filter(p -> p.getProjectId().equals(currentAccount.getActiveProject())).findFirst(); } else { projectObj = projects.stream().filter(p -> p.getProjectId().equals(projId)).findFirst(); } if (projectObj.isPresent()) { Project project = projectObj.get(); TaskFilter filter = new TaskFilter(project, state, query, priority, type, assigneeAccount); List<Task> tasks = taskSrv.findBySpecification(filter); if (StringUtils.isNotEmpty(query)) { Tag tag = tagsRepo.findByName(query); List<Task> searchResult = tasks.stream().filter(task -> StringUtils.containsIgnoreCase(task.getId(), query) || StringUtils.containsIgnoreCase(task.getName(), query) || StringUtils.containsIgnoreCase(task.getDescription(), query) || task.getTags().contains(tag)).collect(Collectors.toCollection(LinkedList::new)); tasks = searchResult; } Collections.sort(tasks, new TaskSorter(TaskSorter.SORTBY.ID, false)); model.addAttribute("tasks", tasks); model.addAttribute("active_project", project); } return "task/list"; } @RequestMapping(value = "task/{id}/subtask", method = RequestMethod.GET) public TaskForm startSubTaskCreate(@PathVariable String id, Model model) { Task task = taskSrv.findById(id); if (task != null) { model.addAttribute("project", task.getProject()); model.addAttribute("task", task); return new TaskForm(); } return null; } @Transactional @RequestMapping(value = "task/{id}/subtask", method = RequestMethod.POST) public String createSubTask(@PathVariable String id, @Valid @ModelAttribute("taskForm") TaskForm taskForm, Errors errors, RedirectAttributes ra, HttpServletRequest request, Model model) { if (!Roles.isUser()) { throw new TasqAuthException(msg); } Task task = taskSrv.findById(id); if (task != null) { if (task.getState().equals(TaskState.CLOSED)) { ResultData result = taskIsClosed(task); MessageHelper.addWarningAttribute(ra, result.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } Project project = projectSrv.findByProjectId(taskForm.getProject()); if (Utils.containsHTMLTags(taskForm.getName())) { errors.rejectValue(NAME, ERROR_NAME_HTML); } if (errors.hasErrors()) { model.addAttribute("project", project); model.addAttribute("task", task); return null; } if (!projectSrv.canEdit(project)) { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } Task subTask = taskForm.createSubTask(); // assigne if (StringUtils.isNotBlank(taskForm.getAssignee())) { Account assignee = accSrv.findByEmail(taskForm.getAssignee()); subTask.setAssignee(assignee); } subTask = taskSrv.createSubTask(project, task, subTask); wlSrv.addActivityLog(subTask, "", LogType.SUBTASK); taskSrv.save(subTask); return REDIRECT_TASK + id; } MessageHelper.addErrorAttribute(ra, msg.getMessage("task.notexists", null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } /** * Logs work . If only digits are sent , it's pressumed that those were * hours * * @param taskID - ID of task for which work is logged * @param loggedWork - amount of time spent * @param ra * @param request * @return */ @Transactional @RequestMapping(value = "logwork", method = RequestMethod.POST) public String logWork(@RequestParam(value = "taskID") String taskID, @RequestParam(value = "loggedWork") String loggedWork, @RequestParam(value = REMAINING, required = false) String remainingTxt, @RequestParam("date_logged") String dateLogged, @RequestParam("time_logged") String timeLogged, RedirectAttributes ra, HttpServletRequest request) { loggedWork = Utils.matchTimeFormat(loggedWork); remainingTxt = Utils.matchTimeFormat(remainingTxt); if ((StringUtils.isNotBlank(loggedWork) && !Utils.correctEstimate(loggedWork)) || (StringUtils.isNotBlank(remainingTxt) && !Utils.correctEstimate(remainingTxt))) { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.estimateFormat", null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } Period logged = PeriodHelper.inFormat(loggedWork); if (!validLoggedWork(logged)) { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.logged.minmax", null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } if (StringUtils.isNotBlank(remainingTxt) && PeriodHelper.inFormat(remainingTxt).getDays() > 28) { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.estimateTooLong", null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } Task task = taskSrv.findById(taskID); if (task != null) { if (task.getState().equals(TaskState.CLOSED)) { ResultData result = taskIsClosed(task); MessageHelper.addWarningAttribute(ra, result.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } // check if can edit if (Roles.isPowerUser() | projectSrv.canEdit(task.getProject())) { StringBuilder message = new StringBuilder(loggedWork); Date when = new Date(); if (StringUtils.isNotEmpty(dateLogged) && StringUtils.isNotEmpty(timeLogged)) { when = Utils.convertStringToDateAndTime(dateLogged + " " + timeLogged); message.append(BR); message.append("Date: "); message.append(dateLogged); message.append(" "); message.append(timeLogged); } Period remaining = null; if (StringUtils.isNotEmpty(remainingTxt)) { remaining = PeriodHelper.inFormat(remainingTxt); task = wlSrv.addDatedWorkLog(task, remainingTxt, when, LogType.ESTIMATE); } task = wlSrv.addTimedWorkLog(task, message.toString(), when, remaining, logged, LogType.LOG); task = taskSrv.checkStateAndSave(task); if (!totalLoggedTimeValid(logged, task)) { MessageHelper.addWarningAttribute(ra, msg.getMessage("task.logWork.logged.tooMuch", new Object[]{loggedWork, task.getId()}, Utils.getCurrentLocale())); } else { MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.logWork.logged", new Object[]{loggedWork, task.getId()}, Utils.getCurrentLocale())); } } else { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } } return REDIRECT + request.getHeader(REFERER); } /** * Check if total logged time is not longer than 4 weeks which is total sprint durration * * @param logged * @param task * @return */ private boolean totalLoggedTimeValid(Period logged, Task task) { Period loggedWork = task.getRawLoggedWork(); Period totalWork = PeriodHelper.plusPeriods(loggedWork, logged); return totalWork.getDays() <= 28; } private boolean validLoggedWork(Period logged) { Duration min = PeriodHelper.toStandardDuration(new Period().withMinutes(1)); Duration duration = PeriodHelper.toStandardDuration(logged); return duration.isLongerThan(min) && logged.getDays() <= 5; } @Transactional @RequestMapping(value = "/task/changeState", method = RequestMethod.POST) @ResponseBody public ResponseEntity<ResultData> changeState(@RequestParam(value = "id") String taskID, @RequestParam(value = "state") TaskState state, @RequestParam(value = "zero_checkbox", required = false) Boolean remainingZero, @RequestParam(value = "closesubtasks", required = false) Boolean closeSubtasks, @RequestParam(value = "message", required = false) String commentMessage, @RequestParam(value = "resolution", required = false) TaskResolution resolution) { // check if not admin or user Task task = taskSrv.findById(taskID); if (task != null) { if (state.equals(task.getState())) { String stateText = msg.getMessage(state.getCode(), null, Utils.getCurrentLocale()); return ResponseEntity.ok(new ResultData(ResultData.Code.WARNING, msg.getMessage("task.already.inState", new Object[]{stateText}, Utils.getCurrentLocale()))); } // check if can edit if ((Utils.getCurrentAccount().equals(task.getOwner()) || Utils.getCurrentAccount().equals(task.getAssignee())) || (Roles.isPowerUser() | projectSrv.canEdit(task.getProject()))) { // check if reopening kanban if (task.getState().equals(TaskState.CLOSED) && Project.AgileType.KANBAN.equals(task.getProject().getAgile()) && task.getRelease() != null) { return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("task.changeState.change.kanbanRelease", new Object[]{task.getRelease().getRelease()}, Utils.getCurrentLocale()))); } if (TaskState.TO_DO.equals(state)) { Hibernate.initialize(task.getLoggedWork()); if (!("0m").equals(task.getLoggedWork())) { return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("task.alreadyStarted", new Object[]{taskID}, Utils.getCurrentLocale()))); } } else if (TaskState.CLOSED.equals(state)) { ResultData result = taskSrv.checkTaskCanOperated(task, false); if (result.code.equals(ResultData.Code.ERROR)) { return ResponseEntity.ok(result); } else { stopTimer(task); } } if (closeSubtasks != null && closeSubtasks) { List<Task> subtasks = taskSrv.findSubtasks(task); subtasks.stream().filter(subtask -> !TaskState.CLOSED.equals(subtask.getState())).forEach(subtask -> { wlSrv.addActivityLog(subtask, "", LogType.CLOSED); subtask.setState(TaskState.CLOSED); }); taskSrv.save(subtasks); } //Resolution if (resolution != null) { task.setResolution(resolution); } if (task.getState().equals(TaskState.CLOSED)) { task.setResolution(null); } TaskState oldState = (TaskState) task.getState(); task.setState(state); if (StringUtils.isNotEmpty(commentMessage)) { Hibernate.initialize(task.getComments()); task.addComment(commSrv.addComment(commentMessage, Utils.getCurrentAccount(), task)); } // Zero remaining time if (remainingZero != null && remainingZero) { task.setRemaining(PeriodHelper.inFormat("0m")); } String message = worklogStateChange(state, oldState, task); taskSrv.save(task); return ResponseEntity.ok(new ResultData(ResultData.Code.OK, message)); } return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("error.unknown", null, Utils.getCurrentLocale()))); } else { return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("role.error.task.permission", null, Utils.getCurrentLocale()))); } } @Transactional @RequestMapping(value = "/task/changePoints", method = RequestMethod.POST) @ResponseBody public ResponseEntity<ResultData> changeStoryPoints(@RequestParam(value = "id") String taskID, @RequestParam(value = "points") Integer points) { //check if valud story point if (!Utils.validStoryPoint(points)) { return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("task.storyPoints.invalid", new Object[]{points}, Utils.getCurrentLocale()))); } // check if not admin or user Task task = taskSrv.findById(taskID); if (task != null) { // check if can edit if (!projectSrv.canEdit(task.getProject()) || !Roles.isPowerUser()) { throw new TasqAuthException(msg, "role.error.task.permission"); } // updatepoints if (shouldAddWorklogPointsChanged(task, points)) { StringBuilder message = new StringBuilder(Utils.TABLE); message.append(Utils.changedFromTo(STORY_POINTS_TXT, task.getStory_points().toString(), Integer.toString(points))); message.append(Utils.TABLE_END); wlSrv.addActivityLog(task, message.toString(), LogType.EDITED); } task.setStory_points(points); taskSrv.save(task); return ResponseEntity.ok(new ResultData(ResultData.Code.OK, msg.getMessage("task.storypoints.edited", new Object[]{task.getId(), points}, Utils.getCurrentLocale()))); } return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("error.unknown", null, Utils.getCurrentLocale()))); } @Transactional @RequestMapping(value = "/task/time", method = RequestMethod.GET) public String handleTimer(@RequestParam(value = "id") String taskID, @RequestParam(value = "action") String action, RedirectAttributes ra, HttpServletRequest request) { Utils.setHttpRequest(request); Task task = taskSrv.findById(taskID); if (task != null) { // check if can edit if (Roles.isPowerUser() | projectSrv.canEdit(task.getProject())) { switch (action) { case START: { Account account = Utils.getCurrentAccount(); if (StringUtils.isNotBlank(account.getActiveTask())) { MessageHelper.addWarningAttribute(ra, msg.getMessage("task.stopTime.warning", new Object[]{account.getActiveTask()}, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } account.startTimerOnTask(task); accSrv.update(account); taskSrv.checkStateAndSave(task); break; } case STOP: Period logWork = stopTimer(task); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.logWork.logged", new Object[]{PeriodHelper.outFormat(logWork), task.getId()}, Utils.getCurrentLocale())); break; case CANCEL: { Account account = Utils.getCurrentAccount(); account.clearActiveTask(); accSrv.update(account); return REDIRECT_TASK + taskID; } } } else { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } } else { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.task.notfound", null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } return REDIRECT + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "/task/assign", method = RequestMethod.POST) public String assign(@RequestParam(value = "taskID") String taskID, @RequestParam(value = "email") String email, RedirectAttributes ra, HttpServletRequest request) { Task task = taskSrv.findById(taskID); String previous = getAssignee(task); if (task != null) { if (Roles.isPowerUser() | projectSrv.canEdit(task.getProject())) { if (task.getState().equals(TaskState.CLOSED)) { ResultData result = taskIsClosed(task); MessageHelper.addWarningAttribute(ra, result.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } if (("").equals(email) && task.getAssignee() != null) { task.setAssignee(null); task.setLastUpdate(new Date()); wlSrv.addActivityLog(task, Utils.changedFromTo(previous, UNASSIGNED), LogType.ASSIGNED); taskSrv.save(task); } else { Account assignee = accSrv.findByEmail(email); if (assignee != null && !assignee.equals(task.getAssignee())) { // check if can edit if (!projectSrv.canEdit(task.getProject())) { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } task.setAssignee(assignee); task.setLastUpdate(new Date()); watchSrv.addToWatchers(task, assignee); wlSrv.addActivityLog(task, Utils.changedFromTo(previous, assignee.toString()), LogType.ASSIGNED); taskSrv.save(task); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.assigned", new Object[]{task.getId(), assignee.toString()}, Utils.getCurrentLocale())); } } } else { throw new TasqAuthException(msg); } } return REDIRECT + request.getHeader(REFERER); } private String getAssignee(Task task) { return task.getAssignee() == null ? UNASSIGNED : task.getAssignee().toString(); } @Transactional @RequestMapping(value = "/task/assignMe", method = RequestMethod.GET) public String assignMe(@RequestParam(value = "id") String taskID, RedirectAttributes ra, HttpServletRequest request) { Task task = taskSrv.findById(taskID); if (task != null) { if (task.getState().equals(TaskState.CLOSED)) { ResultData result = taskIsClosed(task); MessageHelper.addWarningAttribute(ra, result.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } assignMeToTask(task); } return REDIRECT + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "/task/assignMe", method = RequestMethod.POST) @ResponseBody public ResponseEntity<ResultData> assignMePOST(@RequestParam(value = "id") String id) { // check if not admin or user if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Task task = taskSrv.findById(id); if (task != null) { if (task.getState().equals(TaskState.CLOSED)) { return ResponseEntity.ok(taskIsClosed(task)); } if (assignMeToTask(task)) { return ResponseEntity.ok(new ResultData(ResultData.Code.OK, msg.getMessage("task.assinged.me", null, Utils.getCurrentLocale()) + " " + id)); } else { return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, msg.getMessage("role.error.task.permission", null, Utils.getCurrentLocale()))); } } return ResponseEntity.ok(new ResultData(ResultData.Code.ERROR, "?")); } @Transactional @RequestMapping(value = "/task/priority", method = RequestMethod.GET) public String changePriority(@RequestParam(value = "id") String taskID, @RequestParam(value = "priority") String priority, RedirectAttributes ra, HttpServletRequest request) { Utils.setHttpRequest(request); Task task = taskSrv.findById(taskID); if (task != null) { if (task.getState().equals(TaskState.CLOSED)) { ResultData result = taskIsClosed(task); MessageHelper.addWarningAttribute(ra, result.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } TaskPriority newPriority = TaskPriority.valueOf(priority); if (!task.getPriority().equals(newPriority) && projectSrv.canEdit(task.getProject()) && Roles.isPowerUser()) { StringBuilder message = new StringBuilder(); String oldPriority = ""; if (task.getPriority() != null) { oldPriority = task.getPriority().toString(); } message.append(oldPriority); message.append(CHANGE_TO); task.setPriority(newPriority); message.append(task.getPriority().toString()); wlSrv.addActivityLog(task, message.toString(), LogType.PRIORITY); taskSrv.save(task); } } return REDIRECT + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "/task/delete", method = RequestMethod.GET) public String deleteTask(@RequestParam(value = "id") String taskID, @RequestParam(value = "force", required = false) boolean force, RedirectAttributes ra, HttpServletRequest request) { Task task = taskSrv.findById(taskID); if (task != null) { Project project = projectSrv.findById(task.getProject().getId()); // Only allow delete for administrators, owner or app admin Locale currentLocale = Utils.getCurrentLocale(); if (isAdmin(task, project) && canForceRemove(force)) { Account currentAccount = Utils.getCurrentAccount(); Set<Account> notify = notifyWhileDeleting(task); String taskName = task.getName(); ResultData result; //if removed task is subtask and it's last one if (task.isSubtask()) { updateSubtaskCount(task); } result = taskSrv.deleteTask(task, force); if (result.code.equals(ResultData.Code.ERROR)) { MessageHelper.addWarningAttribute(ra, result.message, currentLocale); rollBack(); return REDIRECT + request.getHeader(REFERER); } StringBuilder message = new StringBuilder(taskSrv.printID(taskID)); message.append(" - "); message.append(taskName); //send event to owner/assignee if needed for (Account account : notify) { Locale accountLocale = new Locale(account.getLanguage()); String moreDetails = msg.getMessage("log.type.delete.info", new Object[]{currentAccount, taskID, taskName}, accountLocale); eventSrv.addSystemEvent(account, LogType.DELETED, msg.getMessage(LogType.DELETED.getCode(), null, accountLocale), moreDetails); } wlSrv.addWorkLogNoTask(message.toString(), project, LogType.DELETED); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.delete.success", new Object[]{taskID}, currentLocale), currentLocale); } else { throw new TasqAuthException(msg, "role.error.task.permission"); } return "redirect:/"; } return REDIRECT + request.getHeader(REFERER); } /** * If removed task/subtask is deleted converted and it's last subtask , update parent task and remove task count * * @param task - task which potential parent have to be updated */ private void updateSubtaskCount(Task task) { Task parentTask = taskSrv.findById(task.getParent()); if (taskSrv.findSubtasks(task.getParent()).size() == 1) { parentTask.setSubtasks(0); taskSrv.save(parentTask); } } /** * Gets all accounts which should be notified while deleting task, don't include current account * * @param task * @return */ private Set<Account> notifyWhileDeleting(Task task) { Set<Account> notify = new HashSet<>(); notify.add(task.getOwner()); notify.add(task.getAssignee()); notify.remove(Utils.getCurrentAccount()); notify.remove(null); return notify; } private boolean canForceRemove(Boolean force) { return !force || Roles.isPowerUser(); } @VisibleForTesting void rollBack() { TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); } @RequestMapping(value = "/task/attachFiles", method = RequestMethod.POST) public String attacheFiles(@RequestParam String taskID, @RequestParam List<MultipartFile> files, HttpServletRequest request) { Task task = taskSrv.findById(taskID); if (task != null) { // check if can edit if (!projectSrv.canEdit(task.getProject()) && !Roles.isPowerUser()) { throw new TasqAuthException(msg, "role.error.task.permission"); } saveTaskFiles(files, task); } return REDIRECT + request.getHeader(REFERER); } @RequestMapping(value = "/task/{id}/file", method = RequestMethod.GET) public void downloadFile(@PathVariable String id, @RequestParam("get") String filename, HttpServletRequest request, HttpServletResponse response, RedirectAttributes ra) throws IOException { Task task = taskSrv.findById(id); if (task.getProject().getParticipants().contains(Utils.getCurrentAccount())) { File file = new File(taskSrv.getTaskDirectory(task) + File.separator + filename); response.setHeader("content-Disposition", "attachment; filename=" + filename); try (InputStream is = new FileInputStream(file)) { IOUtils.copyLarge(is, response.getOutputStream()); } catch (IOException e) { LOG.error("Error while writing to output stream , filename '{}'", filename, e); } finally { response.flushBuffer(); } } else { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); } } @RequestMapping(value = "/task/{id}/{subid}/file", method = RequestMethod.GET) public void downloadSubtaskFile(@PathVariable String id, @PathVariable String subid, @RequestParam("get") String filename, HttpServletRequest request, HttpServletResponse response, RedirectAttributes ra) throws IOException { downloadFile(id + "/" + subid, filename, request, response, ra); } @RequestMapping(value = "/task/{id}/imgfile", method = RequestMethod.GET) public void showImageFile(@PathVariable String id, @RequestParam("get") String filename, HttpServletResponse response, RedirectAttributes ra) throws IOException { Task task = taskSrv.findById(id); if (task.getProject().getParticipants().contains(Utils.getCurrentAccount())) { File file = new File(taskSrv.getTaskDirectory(task) + File.separator + filename); response.setHeader("content-Disposition", "attachment; filename=" + filename); try (InputStream is = new FileInputStream(file)) { response.setContentType("image/jpeg, image/jpg, image/png, image/gif"); IOUtils.copyLarge(is, response.getOutputStream()); } catch (FileNotFoundException e) { LOG.error("File not found filename '{}'", filename, e); } catch (IOException e) { LOG.error("Error while writing to output stream , filename '{}'", filename, e); } finally { response.flushBuffer(); response.getOutputStream().close(); } } else { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); } } @RequestMapping(value = "/task/{id}/{subid}/imgfile", method = RequestMethod.GET) public void showSubTaskImageFile(@PathVariable String id, @PathVariable String subid, @RequestParam("get") String filename, HttpServletRequest request, HttpServletResponse response, RedirectAttributes ra) throws IOException { showImageFile(id + "/" + subid, filename, response, ra); } @RequestMapping(value = "/task/removeFile", method = RequestMethod.GET) public String removeFile(@RequestParam String id, @RequestParam("file") String filename, HttpServletRequest request, RedirectAttributes ra) { Task task = taskSrv.findById(id); if (!projectSrv.canEdit(task.getProject()) && (!Roles.isUser() || !task.getOwner().equals(Utils.getCurrentAccount()))) { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); } else { File file = new File(taskSrv.getTaskDirectory(task) + File.separator + filename); if (file.exists()) { file.delete(); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.file.deleted", new Object[]{filename}, Utils.getCurrentLocale())); } } return REDIRECT + request.getHeader(REFERER); } /** * Convert subtask into new regular task * * @param id * @param type * @param request * @param ra * @return * @throws IOException */ @Transactional @RequestMapping(value = "/task/conver2task", method = RequestMethod.POST) public String convert2task(@RequestParam("taskid") String id, @RequestParam("type") TaskType type, HttpServletRequest request, RedirectAttributes ra) throws IOException { Task subtask = taskSrv.findById(id); Project project = projectSrv.findById(subtask.getProject().getId()); if (!projectSrv.canEdit(project) && (!Roles.isUser() || !subtask.getOwner().equals(Utils.getCurrentAccount()))) { MessageHelper.addErrorAttribute(ra, msg.getMessage(ERROR_ACCES_RIGHTS, null, Utils.getCurrentLocale())); return REDIRECT + request.getHeader(REFERER); } else { ResultData checkResult = taskSrv.checkTaskCanOperated(subtask, false); if (checkResult.code.equals(ResultData.Code.ERROR)) { MessageHelper.addWarningAttribute(ra, checkResult.message, Utils.getCurrentLocale()); return REDIRECT + request.getHeader(REFERER); } Task parent = taskSrv.findById(subtask.getParent()); long taskCount = project.getLastTaskNo(); taskCount++; String taskID = project.getProjectId() + "-" + taskCount; Task task = cloneTask(subtask, taskID); task.setParent(null); task.setType(type); task.setTaskOrder(taskCount); task.setEstimated(false); taskSrv.save(task); List<Event> events = eventSrv.getTaskEvents(id); for (Event event : events) { event.setTask(taskID); eventSrv.save(event); } List<WorkLog> worklogs = wlSrv.getTaskEvents(id); for (WorkLog workLog : worklogs) { workLog.setTask(task); } Set<Comment> comments = commSrv.findByTaskIdOrderByDateDesc(id); for (Comment comment : comments) { comment.setTask(task); } List<TaskLink> links = linkService.findAllTaskLinks(subtask); for (TaskLink taskLink : links) { if (taskLink.getTaskA().equals(id)) { taskLink.setTaskA(taskID); } if (taskLink.getTaskB().equals(id)) { taskLink.setTaskB(taskID); } linkService.save(taskLink); } // files File oldDir = new File(taskSrv.getTaskDir(subtask)); if (oldDir.exists()) { File newDir = new File(taskSrv.getTaskDir(task)); if (!newDir.mkdirs()) { LOG.error("Failed to create new dir {}", newDir.getAbsolutePath()); } newDir.setWritable(true, false); newDir.setReadable(true, false); FileUtils.copyDirectory(oldDir, newDir); } project.setLastTaskNo(taskCount); project.getTasks().add(task); projectSrv.save(project); // Add log StringBuilder message = new StringBuilder(Utils.TABLE); message.append(Utils.changedFromTo("ID", id, taskID)); message.append(Utils.changedFromTo(TYPE_TXT, subtask.getType().toString(), type.toString())); message.append(Utils.TABLE_END); wlSrv.addActivityLog(task, message.toString(), LogType.SUBTASK2TASK); taskSrv.save(task); // cleanup visitedSrv.updateFromToVisitedTask(subtask, task); updateSubtaskCount(subtask); taskSrv.deleteTask(subtask, false); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.subtasks.2task.success", new Object[]{id, taskID}, Utils.getCurrentLocale())); TaskLink link = new TaskLink(parent.getId(), taskID, TaskLinkType.RELATES_TO); linkService.save(link); return REDIRECT_TASK + taskID; } } private Task cloneTask(Task subtask, String taskID) { Task task = new Task(); BeanUtils.copyProperties(subtask, task); task.setId(taskID); task.setEstimate(subtask.getRawEstimate()); task.setLoggedWork(subtask.getRawLoggedWork()); task.setRemaining(subtask.getRawRemaining()); task.setDue_date(subtask.getRawDue_date()); task.setCreate_date(subtask.getRawCreate_date()); task.setLastUpdate(new Date()); Hibernate.initialize(subtask.getTags()); Set<Tag> tags = subtask.getTags(); task.setTags(tags); Hibernate.initialize(subtask.getSprints()); Set<Sprint> sprints = subtask.getSprints(); task.setSprints(sprints); return task; } @Transactional @RequestMapping(value = "task/delWorklog", method = RequestMethod.GET) public String deleteWorkLog(@RequestParam("id") Long id, RedirectAttributes ra, HttpServletRequest request, Model model) { if (Roles.isAdmin()) { WorkLog wl = wlSrv.findById(id); if (wl != null) { Long projectID = wl.getTask().getProject().getId(); wlSrv.delete(wl); MessageHelper.addSuccessAttribute(ra, msg.getMessage("task.worklog.deleted", null, Utils.getCurrentLocale())); return "redirect:/manage/tasks?project=" + projectID; } else { return REDIRECT + request.getHeader(REFERER); } } else { throw new TasqAuthException(); } } /** * Admin call to update all tasks logged work based on their worklog events * * @param ra * @param model * @return */ @Transactional @RequestMapping(value = "task/updatelogs", method = RequestMethod.GET) public String update(@RequestParam(value = "project", required = false) Long project, RedirectAttributes ra, HttpServletRequest request, Model model) { if (Roles.isAdmin()) { List<Task> list; if (project != null) { list = taskSrv.findAllByProjectId(project); } else { list = taskSrv.findAll(); } StringBuilder console = new StringBuilder("Updating logged work on all tasks within application"); console.append(BR); for (Task task : list) { task.updateLoggedWork(); console.append(task.toString()); console.append(": updated with "); console.append(task.getLoggedWork()); console.append(BR); } model.addAttribute("console", console.toString()); return "other/console"; } else { throw new TasqAuthException(); } } /** * Helper temp method to eliminate depreciated task without finishDate * * @param ra * @param request * @param model * @return */ @Deprecated @Transactional @RequestMapping(value = "task/updateFinish", method = RequestMethod.GET) public String updateFinishDate(@RequestParam(value = "project", required = false) Long project, RedirectAttributes ra, HttpServletRequest request, Model model) { if (Roles.isAdmin()) { List<Task> list; if (project != null) { list = taskSrv.findAllByProjectId(project); } else { list = taskSrv.findAll(); } StringBuilder console = new StringBuilder("Updating logged work on tasks within application"); console.append(BR); for (Task task : list) { List<WorkLog> worklogs = wlSrv.getTaskEvents(task.getId()); WorkLog closing = new WorkLog(); for (WorkLog workLog : worklogs) { if (workLog.getType().equals(LogType.CLOSED)) { closing = workLog; } } task.setFinishDate(closing.getRawTime()); console.append(task.toString()); console.append(": finish date set to :"); console.append(closing.getRawTime()); console.append(BR); } model.addAttribute("console", console.toString()); return "other/console"; } else { throw new TasqAuthException(); } } @Deprecated @Transactional @RequestMapping(value = "task/updateClosed", method = RequestMethod.GET) public String updateMissingEvent(@RequestParam(value = "project") Long project, RedirectAttributes ra, HttpServletRequest request, Model model) { if (Roles.isAdmin()) { if (project != null) { Project projectById = projectSrv.findById(project); List<Task> list = taskSrv.findByProjectAndState(projectById, TaskState.CLOSED); StringBuilder console = new StringBuilder("Updating closed events on tasks within application"); console.append(BR); for (Task task : list) { if (task.getState().equals(TaskState.CLOSED)) { List<WorkLog> worklogs = wlSrv.getTaskEvents(task.getId()); if (worklogs.stream().noneMatch(workLog -> workLog.getType().equals(LogType.CLOSED))) { wlSrv.addActivityLog(task, "", LogType.CLOSED); taskSrv.save(task); console.append(task.toString()); console.append(": added missing closed event"); console.append(BR); } } } model.addAttribute("console", console.toString()); } return "other/console"; } else { throw new TasqAuthException(); } } @RequestMapping(value = "/activeTaskAccounts", method = RequestMethod.GET) @ResponseBody public ResponseEntity<Set<DisplayAccount>> getActiveTaskAccounts(@RequestParam String taskID, HttpServletResponse response) { response.setContentType("application/json"); List<Account> accounts = accSrv.findAllWithActiveTask(taskID); List<Task> subtasks = taskSrv.findSubtasks(taskID); for (Task subtask : subtasks) { accounts.addAll(accSrv.findAllWithActiveTask(subtask.getId())); } return ResponseEntity.ok(accounts.stream().map(DisplayAccount::new).collect(Collectors.toSet())); } private ResultData taskIsClosed(Task task) { String localized = msg.getMessage(((TaskState) task.getState()).getCode(), null, Utils.getCurrentLocale()); return new ResultData(ResultData.Code.ERROR, msg.getMessage("task.closed.cannot.operate", new Object[]{localized}, Utils.getCurrentLocale())); } /** * Fills model with project list and user's active project * * @param model */ private void fillCreateTaskModel(Model model) { if (!Roles.isUser()) { throw new TasqAuthException(msg); } Project project = projectSrv.findUserActiveProject(); if (project == null) { throw new TasqAuthException(msg, "error.noProjects"); } model.addAttribute("project", project); model.addAttribute("projects_list", projectSrv.findAllByUser()); } private boolean checkIfNotEstimated(Task task, Project project) { return task.getStory_points() == 0 && task.isEstimated(); } private String worklogStateChange(TaskState state, TaskState oldState, Task task) { if (TaskState.CLOSED.equals(state)) { task.setFinishDate(new Date()); wlSrv.addActivityLog(task, "", LogType.CLOSED); taskSrv.save(task); return msg.getMessage("task.state.changed.closed", new Object[]{task.getId()}, Utils.getCurrentLocale()); } else if (TaskState.CLOSED.equals(oldState)) { wlSrv.addActivityLog(task, "", LogType.REOPEN); task.setFinishDate(null); taskSrv.save(task); return msg.getMessage("task.state.changed.reopened", new Object[]{task.getId()}, Utils.getCurrentLocale()); } else { taskSrv.changeState(oldState, state, task); String localised = msg.getMessage(state.getCode(), null, Utils.getCurrentLocale()); return msg.getMessage("task.state.changed", new Object[]{task.getId(), localised}, Utils.getCurrentLocale()); } } /** * Checks if worklog point changes should be added. If task is in active * sprint , work log is added and false is returned * * @param task * @param storyPoints * @return */ private boolean shouldAddWorklogPointsChanged(Task task, int storyPoints) { if (sprintSrv.taskInActiveSprint(task)) { wlSrv.addActivityLog(task, Integer.toString(-1 * (task.getStory_points() - storyPoints)), LogType.ESTIMATE); taskSrv.save(task); return false; } else { return true; } } /** * Stops currently running timer on task * * @param task * @return Period logged as worklog */ private Period stopTimer(Task task) { Account account = Utils.getCurrentAccount(); if (StringUtils.isNotBlank(account.getActiveTask()) && task.getId().equals(account.getActiveTask())) { DateTime now = new DateTime(); Period logWork = new Period(account.getActiveTaskTimer(), now); // Only log work if greater than 1 minute if (logWork.toStandardDuration().getMillis() / 1000 / 60 < 1) { logWork = new Period().plusMinutes(1); } wlSrv.addTimedWorkLog(task, PeriodHelper.outFormat(logWork), new Date(), null, logWork, LogType.LOG); account.clearActiveTask(); accSrv.update(account); return logWork; } return null; } /** * Check if is project admin or admin * * @param task * @param project * @return */ private boolean isAdmin(Task task, Project project) { Account currentAccount = Utils.getCurrentAccount(); return project.getAdministrators().contains(currentAccount) || task.getOwner().equals(currentAccount) || Roles.isAdmin(); } private boolean saveTaskFiles(List<MultipartFile> filesArray, Task task) { // Save for (MultipartFile multipartFile : filesArray) { if (!multipartFile.isEmpty()) { String taskDir = taskSrv.getTaskDirectory(task) + File.separator; File file = new File(taskDir + multipartFile.getOriginalFilename()); //check if file exists, if yes , add suffix file = createUniqueFile(taskDir, file); try { FileUtils.writeByteArrayToFile(file, multipartFile.getBytes()); } catch (IOException e) { LOG.error("IOException while saving task files", e); return false; } } } return true; } /** * Creates unique file. If file with such filename already exists in that folder, _# is added , where # is increased until free number is found * * @param taskDir - task directory * @param file file to be created unique * @return */ private File createUniqueFile(String taskDir, File file) { long i; while (file.exists()) { String pathname = FilenameUtils.getBaseName(file.getName()); Pattern p = Pattern.compile("(\\d*)$"); Matcher matcher = p.matcher(pathname); if (matcher.find() && StringUtils.isNotBlank(matcher.group())) { try { i = Integer.parseInt(matcher.group()) + 1; } catch (NumberFormatException e) { LOG.debug("Something went wrong with numbers, adding timestamp. {}", e); i = System.currentTimeMillis(); } } else { i = 0; } pathname = FilenameUtils.getBaseName(pathname).split("(_\\d*)$")[0]; file = new File(taskDir + FilenameUtils.getBaseName(pathname) + "_" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(file.getName())); } return file; } /** * Get's tasks all files * * @param task * @return */ private List<String> getTaskFiles(Task task) { File folder = new File(taskSrv.getTaskDirectory(task)); File[] listOfFiles = folder.listFiles(); List<String> fileNames = new ArrayList<>(); if (listOfFiles != null) { for (File file : listOfFiles) { if (file.isFile()) { String fileName = file.getName(); fileNames.add(fileName); } } } return fileNames; } /** * Assigns currently logged user into task with given ID * * @param task task to be checked * @return */ private boolean assignMeToTask(Task task) { String previous = getAssignee(task); if (!projectSrv.canEdit(task.getProject())) { return false; } Account assignee = Utils.getCurrentAccount(); if (!assignee.equals(task.getAssignee())) { task.setAssignee(assignee); task.setLastUpdate(new Date()); wlSrv.addActivityLog(task, Utils.changedFromTo(previous, assignee.toString()), LogType.ASSIGNED); task = taskSrv.save(task); watchSrv.startWatching(task); return true; } return false; } }