package com.qprogramming.tasq.projects; 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.agile.StartStop; import com.qprogramming.tasq.error.TasqAuthException; import com.qprogramming.tasq.events.EventsService; import com.qprogramming.tasq.projects.holiday.HolidayService; 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.*; import com.qprogramming.tasq.task.worklog.DisplayWorkLog; import com.qprogramming.tasq.task.worklog.LogType; import com.qprogramming.tasq.task.worklog.WorkLog; import com.qprogramming.tasq.task.worklog.WorkLogService; import org.hibernate.Hibernate; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.web.PageableDefault; 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.util.StringUtils; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.util.*; import java.util.stream.Collectors; @Controller public class ProjectController { public static final String APPLICATION_JSON = "application/json"; public static final String REFERER = "Referer"; private static final Logger LOG = LoggerFactory.getLogger(ProjectController.class); private ProjectService projSrv; private AccountService accSrv; private TaskService taskSrv; private AgileService sprintSrv; private WorkLogService wrkLogSrv; private MessageSource msg; private EventsService eventsSrv; private HolidayService holidayService; private LastVisitedService visitedSrv; private DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"); @Autowired public ProjectController(ProjectService projSrv, AccountService accSrv, TaskService taskSrv, AgileService sprintSrv, WorkLogService wrklSrv, MessageSource msg, EventsService eventsSrv, HolidayService holidayService, LastVisitedService visitedSrv) { this.projSrv = projSrv; this.accSrv = accSrv; this.taskSrv = taskSrv; this.sprintSrv = sprintSrv; this.wrkLogSrv = wrklSrv; this.msg = msg; this.eventsSrv = eventsSrv; this.holidayService = holidayService; this.visitedSrv = visitedSrv; } @Transactional @RequestMapping(value = "project/{id}", method = RequestMethod.GET) public String showDetails(@PathVariable String id, @RequestParam(value = "closed", required = false) String closed, Model model, RedirectAttributes ra) { Project project = projSrv.findByProjectId(id); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } if (!project.getParticipants().contains(Utils.getCurrentAccount()) && !Roles.isAdmin()) { throw new TasqAuthException(msg, "role.error.project.permission"); } Account account = Utils.getCurrentAccount(); visitedSrv.addLastVisited(account.getId(), project); // Check status of all projects List<Task> tasks = project.getTasks().stream().filter(task -> !task.isSubtask()).collect(Collectors.toList()); Map<TaskState, Integer> stateCount = new HashMap<>(); for (TaskState state : TaskState.values()) { stateCount.put(state, 0); } for (Task task : tasks) { Integer value = stateCount.get(task.getState()); value++; stateCount.put((TaskState) task.getState(), value); } model.addAttribute("TO_DO", stateCount.get(TaskState.TO_DO)); model.addAttribute("ONGOING", stateCount.get(TaskState.ONGOING)); model.addAttribute("COMPLETE", stateCount.get(TaskState.COMPLETE)); model.addAttribute("CLOSED", stateCount.get(TaskState.CLOSED)); model.addAttribute("BLOCKED", stateCount.get(TaskState.BLOCKED)); List<Task> taskList; if (closed == null) { taskList = taskSrv.findByProjectAndOpen(project); } else { taskList = taskSrv.findAllByProject(project); } Collections.sort(taskList, new TaskSorter(TaskSorter.SORTBY.ID, false)); model.addAttribute("tasks", taskList); model.addAttribute("project", project); return "project/details"; } @RequestMapping(value = "projectEvents", method = RequestMethod.GET) @ResponseBody public ResponseEntity<Page<DisplayWorkLog>> getProjectEvents(@RequestParam(value = "id") String id, @PageableDefault(size = 25, page = 0, sort = "time", direction = Direction.DESC) Pageable p) { Project project = projSrv.findByProjectId(id); if (project == null) { // NULL return null; } if (!project.getParticipants().contains(Utils.getCurrentAccount()) && !Roles.isAdmin()) { throw new TasqAuthException(msg, "role.error.project.permission"); } // Fetch events Page<WorkLog> page = wrkLogSrv.findByProjectId(project.getId(), p); List<DisplayWorkLog> list = new LinkedList<DisplayWorkLog>(); for (WorkLog workLog : page) { list.add(new DisplayWorkLog(workLog)); } return ResponseEntity.ok(new PageImpl<>(list, p, page.getTotalElements())); } @RequestMapping(value = "/usersProjectsEvents", method = RequestMethod.GET) @ResponseBody public ResponseEntity<Page<DisplayWorkLog>> getProjectsLogs( @PageableDefault(size = 25, page = 0, sort = "time", direction = Direction.DESC) Pageable p) { Account account = Utils.getCurrentAccount(); List<Project> usersProjects = projSrv.findAllByUser(account.getId()); if (!usersProjects.isEmpty()) { List<Long> ids = usersProjects.stream().map(Project::getId).collect(Collectors.toCollection(LinkedList::new)); Page<WorkLog> page = wrkLogSrv.findByProjectIdIn(ids, p); List<DisplayWorkLog> list = page.getContent().stream().map(DisplayWorkLog::new).collect(Collectors.toList()); return ResponseEntity.ok(new PageImpl<>(list, p, page.getTotalElements())); } return null; } @RequestMapping(value = "projects", method = RequestMethod.GET) public String listProjects(Model model) { List<Project> projects; if (Roles.isAdmin()) { projects = projSrv.findAll(); } else { projects = projSrv.findAllByUser(); } Collections.sort(projects, new ProjectSorter(ProjectSorter.SORTBY.LAST_VISIT, Utils.getCurrentAccount().getActiveProject(), true)); model.addAttribute("projects", projects); return "project/list"; } @RequestMapping(value = "project/activate/{id}", method = RequestMethod.GET) public String activate(@PathVariable(value = "id") String id, HttpServletRequest request, RedirectAttributes ra) { Project activatedProj = projSrv.activateForCurrentUser(id); if (activatedProj != null) { MessageHelper.addSuccessAttribute(ra, msg.getMessage("project.activated", new Object[]{activatedProj.getName()}, Utils.getCurrentLocale())); } return "redirect:" + request.getHeader(REFERER); } @RequestMapping(value = "project/create", method = RequestMethod.GET) public NewProjectForm startProjectcreate() { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } return new NewProjectForm(); } @RequestMapping(value = "project/create", method = RequestMethod.POST) public String createProject(@Valid @ModelAttribute("newProjectForm") NewProjectForm newProjectForm, Errors errors, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } if (newProjectForm.getProject_id().length() > 5) { errors.rejectValue("project_id", "project.idValid"); } if (newProjectForm.getProject_id().matches(".*\\d.*")) { errors.rejectValue("project_id", "project.idValid.letters"); } if (Utils.containsHTMLTags(newProjectForm.getName())) { errors.rejectValue("name", "error.name.html"); } Utils.setHttpRequest(request); String name = newProjectForm.getName(); if (null != projSrv.findByName(name)) { errors.rejectValue("name", "project.exists", new Object[]{name}, ""); } String projectId = newProjectForm.getProject_id(); if (null != projSrv.findByProjectId(projectId)) { errors.rejectValue("project_id", "project.idunique", new Object[]{projectId}, ""); } if (errors.hasErrors()) { return null; } Project newProject = newProjectForm.createProject(); newProject = projSrv.save(newProject); if (projSrv.findAllByUser().size() == 1) { Account account = Utils.getCurrentAccount(); account.setActiveProject(newProject.getProjectId()); accSrv.update(account); } // TODO Create first release if Kanban ? MessageHelper.addSuccessAttribute(ra, msg.getMessage("project.created", new Object[]{name}, Utils.getCurrentLocale())); return "redirect:/project/" + newProject.getProjectId(); } @Transactional @RequestMapping(value = "project/{id}/manage", method = RequestMethod.GET) public String manageProject(@PathVariable(value = "id") String id, Model model, RedirectAttributes ra) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Project project = projSrv.findByProjectId(id); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } if (project.getDefaultAssigneeID() != null) { DisplayAccount assignee = new DisplayAccount(accSrv.findById(project.getDefaultAssigneeID())); model.addAttribute("defaultAssignee", assignee); } Hibernate.initialize(project.getHolidays()); model.addAttribute("project", project); return "project/manage"; } @Transactional @RequestMapping(value = "project/{id}/delete", method = RequestMethod.POST) public String deleteProject(@PathVariable(value = "id") String id, @RequestParam(value = "projectId") String projectId, @RequestParam(value = "projectname") String name, RedirectAttributes ra, HttpServletRequest request) { Project project = projSrv.findByProjectId(id); ResultData result; if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } if (!Roles.isAdmin()) { throw new TasqAuthException(msg); } if (!projectId.equals(project.getProjectId()) || !name.equals(project.getName())) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.delete.confirm", null, Utils.getCurrentLocale())); return Utils.REDIRECT + request.getHeader(REFERER); } //add owners, assignees, participants, admins and active tasks ppl to list to notify about project removal List<Task> projectTasks = taskSrv.findAllByProject(project); Set<Account> notifyAccounts = new LinkedHashSet<>(); notifyAccounts.addAll(projectTasks.stream().map(Task::getOwner).collect(Collectors.toSet())); notifyAccounts.addAll(projectTasks.stream().map(Task::getAssignee).collect(Collectors.toSet())); //remove active project from project participants Set<Account> participantsAndAdmins = updateActiveProjects(project); notifyAccounts.addAll(participantsAndAdmins); for (Task task : projectTasks) { notifyAccounts.addAll(accSrv.findAllWithActiveTask(task.getId())); result = taskSrv.deleteTask(task, true); if (result.code.equals(ResultData.Code.ERROR)) { MessageHelper.addErrorAttribute(ra, result.message, Utils.getCurrentLocale()); rollBack(); return Utils.REDIRECT + request.getHeader(REFERER); } } notifyAccounts.remove(null); sendNotificationsAfterRemove(project, notifyAccounts); MessageHelper.addSuccessAttribute(ra, msg.getMessage("project.delete.success", new Object[]{project.toString()}, Utils.getCurrentLocale()), Utils.getCurrentLocale()); visitedSrv.delete(project); projSrv.delete(project); return "redirect:/projects"; } private void sendNotificationsAfterRemove(Project project, Set<Account> notifyAccounts) { Map<String, String[]> localeMap = new HashMap<>(); for (Account account : notifyAccounts) { Locale locale = new Locale(account.getLanguage()); String moreDetails; String message; if (!localeMap.containsKey(account.getLanguage())) { moreDetails = msg.getMessage("project.delete.event", new Object[]{Utils.getCurrentAccount(), project.toString()}, locale); message = msg.getMessage(LogType.PROJ_REMOVE.getCode(), null, locale); localeMap.put(account.getLanguage(), new String[]{message, moreDetails}); } if (!account.equals(Utils.getCurrentAccount())) { eventsSrv.addSystemEvent(account, LogType.PROJ_REMOVE, localeMap.get(account.getLanguage())[0], localeMap.get(account.getLanguage())[1]); } } } private Set<Account> updateActiveProjects(Project project) { Set<Account> participantsAndAdmins = new HashSet<>(); participantsAndAdmins.addAll(project.getParticipants()); participantsAndAdmins.addAll(project.getAdministrators()); Set<Account> removeActive = participantsAndAdmins.stream().filter(account -> project.getProjectId().equals(account.getActiveProject())).collect(Collectors.toSet()); removeActive.stream().forEach(account -> account.setActiveProject(null)); accSrv.update(new ArrayList<>(removeActive)); return participantsAndAdmins; } @RequestMapping(value = "project/useradd", method = RequestMethod.POST) public String addParticipant(@RequestParam(value = "id") String id, @RequestParam(value = "email") String email, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Account account = accSrv.findByEmail(email); if (account != null) { Project project = projSrv.findByProjectId(id); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } project.addParticipant(account); if (StringUtils.isEmpty(account.getActiveProject())) { account.setActiveProject(project.getProjectId()); accSrv.update(account); } eventsSrv.addProjectEvent(account, LogType.ASSIGN_TO_PROJ, project); projSrv.save(project); } return "redirect:" + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "project/{id}/workdays", method = RequestMethod.POST) public String saveWorkdays(@PathVariable(value = "id") String id, @RequestParam(value = "workingWeekends", required = false, defaultValue = "false") boolean workingWeekends, @RequestParam(value = "holiday", required = false) Set<String> holidays, RedirectAttributes ra, HttpServletRequest request) { Set<String> holidaysSet = holidays == null ? new HashSet<>() : holidays; if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Project project = projSrv.findByProjectId(id); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } project.setWorkingWeekends(workingWeekends); Hibernate.initialize(project.getHolidays()); project = holidayService.processProjectHolidays(holidaysSet, project); projSrv.save(project); return "redirect:" + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "project/userRemove", method = RequestMethod.POST) public String removeParticipant(@RequestParam(value = "project_id") String projectId, @RequestParam(value = "account_id") Long accountId, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Account account = accSrv.findById(accountId); if (account != null) { Project project = projSrv.findByProjectId(projectId); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader(REFERER); } Set<Account> admins = project.getAdministrators(); if (admins.contains(account)) { if (admins.size() == 1) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.lastAdmin", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader(REFERER); } else { project.removeAdministrator(account); } } project.removeParticipant(account); eventsSrv.addProjectEvent(account, LogType.REMOVE_FROM_PROJ, project); projSrv.save(project); } return "redirect:" + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "project/grantAdmin", method = RequestMethod.POST) public String grantAdmin(@RequestParam(value = "project_id") String projectId, @RequestParam(value = "account_id") Long accountId, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Account account = accSrv.findById(accountId); if (account != null) { Project project = projSrv.findByProjectId(projectId); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } project.addAdministrator(account); projSrv.save(project); } return "redirect:" + request.getHeader(REFERER); } @Transactional @RequestMapping(value = "project/removeAdmin", method = RequestMethod.POST) public String removeAdmin(@RequestParam(value = "project_id") String projectId, @RequestParam(value = "account_id") Long accountId, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Account account = accSrv.findById(accountId); if (account != null) { Project project = projSrv.findByProjectId(projectId); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } if (project.getAdministrators().size() == 1) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.lastAdmin", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader(REFERER); } project.removeAdministrator(account); projSrv.save(project); } return "redirect:" + request.getHeader(REFERER); } @RequestMapping(value = "/project/getParticipants", method = RequestMethod.GET) public @ResponseBody ResponseEntity<List<DisplayAccount>> listParticipants(@RequestParam String id, @RequestParam String term, @RequestParam(required = false) boolean userOnly, HttpServletResponse response) { response.setContentType(APPLICATION_JSON); List<Account> accounts = projSrv.getProjectAccounts(id, term); if (userOnly) { return ResponseEntity.ok(accounts.stream().filter(Account::getIsUser).map(DisplayAccount::new).collect(Collectors.toList())); } return ResponseEntity.ok(accounts.stream().map(DisplayAccount::new).collect(Collectors.toList())); } @Transactional @RequestMapping(value = "/project/getChart", method = RequestMethod.GET) @ResponseBody public ResponseEntity<ProjectChart> getProjectChart(@RequestParam String id, @RequestParam(required = false) boolean all, HttpServletResponse response) { response.setContentType(APPLICATION_JSON); Project project = projSrv.findByProjectId(id); Map<String, Integer> created = new HashMap<>(); Map<String, Integer> closed = new HashMap<>(); ProjectChart result = new ProjectChart(); List<WorkLog> events = wrkLogSrv.findProjectCreateCloseEvents(project, all); // Fill maps if (events.size() > 0) { for (WorkLog workLog : events) { // Don't calculate for subtask ( not important ) if (workLog.getTask() != null && !workLog.getTask().isSubtask()) { LocalDate date = new LocalDate(workLog.getRawTime()); if (LogType.CREATE.equals(workLog.getType())) { Integer value = created.get(date.toString()); if (value == null) { value = 0; } value++; created.put(date.toString(), value); } else if (LogType.REOPEN.equals(workLog.getType())) { Integer value = closed.get(date.toString()); if (value == null) { value = 0; } value--; closed.put(date.toString(), value); } else { Integer value = closed.get(date.toString()); if (value == null) { value = 0; } value++; closed.put(date.toString(), value); } } } // Look for the first event ever (they are sorted) LocalDate start = new LocalDate(events.get(0).getRawTime()); LocalDate end = new LocalDate().plusDays(1); LocalDate counter = start; Integer taskCreated = 0; Integer taskClosed = 0; LocalTime nearMidnight = new LocalTime(23, 59); DateTime startTime = start.toDateTime(new LocalTime(0, 0)); DateTime endTime = end.toDateTime(nearMidnight); List<LocalDate> freeDays = projSrv.getFreeDays(project, startTime, endTime); result.setFreeDays(freeDays.stream() .map(dateTime -> new StartStop(fmt.print(dateTime.minusDays(1).toDateTime(nearMidnight)), fmt.print(dateTime.toDateTime(nearMidnight)))) .collect(Collectors.toList())); while (counter.isBefore(end)) { Integer createValue = created.get(counter.toString()); if (createValue == null) { createValue = 0; } taskCreated += createValue; result.getCreated().put(counter.toString(), taskCreated); Integer closeValue = closed.get(counter.toString()); if (closeValue == null) { closeValue = 0; } taskClosed += closeValue; result.getClosed().put(counter.toString(), taskClosed); counter = counter.plusDays(1); } } return ResponseEntity.ok(result); } /** * Returns DisplayProject - minified version of project detials to get all * default values etc. * * @param id id of project * @param response * @return */ @RequestMapping(value = "/project/getDefaults", method = RequestMethod.GET) public @ResponseBody ResponseEntity<DisplayProject> getDefaults(@RequestParam String id, HttpServletResponse response) { response.setContentType(APPLICATION_JSON); Project project = projSrv.findByProjectId(id); if (project == null) { return ResponseEntity.badRequest().body(null); } DisplayProject result = new DisplayProject(project); Account account = accSrv.findById(project.getDefaultAssigneeID()); if (account != null) { result.setDefaultAssignee(new DisplayAccount(account)); } return ResponseEntity.ok(result); } @Transactional @RequestMapping(value = "project/{id}/update", method = RequestMethod.POST) public String updateProperties(@PathVariable String id, @RequestParam(value = "default_priority") TaskPriority priority, @RequestParam(value = "default_type") TaskType type, @RequestParam(value = "defaultAssignee") Long assigneId, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Project project = projSrv.findByProjectId(id); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } if (priority != null) { project.setDefault_priority(priority); } project.setDefault_type(type); Account account = accSrv.findById(assigneId); assigneId = account != null ? account.getId() : null; project.setDefaultAssigneeID(assigneId); projSrv.save(project); Sprint activeSprint = sprintSrv.findByProjectIdAndActiveTrue(project.getId()); return "redirect:" + request.getHeader(REFERER); } @RequestMapping(value = "project/{id}/editDescriptions", method = RequestMethod.POST) public String editDescriptions(@PathVariable String id, @RequestParam(value = "name") String name, @RequestParam(value = "description") String description, RedirectAttributes ra, HttpServletRequest request) { if (!Roles.isPowerUser()) { throw new TasqAuthException(msg); } Project project = projSrv.findByProjectId(id); if (project == null) { MessageHelper.addErrorAttribute(ra, msg.getMessage("project.notexists", null, Utils.getCurrentLocale())); return "redirect:/projects"; } if (Utils.containsHTMLTags(name)) { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.name.html", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader(REFERER); } if (!projSrv.canEdit(project.getId())) { MessageHelper.addErrorAttribute(ra, msg.getMessage("error.accesRights", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader(REFERER); } if (description != null) { project.setDescription(description); } if (name != null) { project.setName(name); visitedSrv.updateName(project); } projSrv.save(project); return "redirect:" + request.getHeader(REFERER); } @VisibleForTesting void rollBack() { TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); } }