package com.qprogramming.tasq.agile; import com.qprogramming.tasq.account.Account; import com.qprogramming.tasq.account.Roles; import com.qprogramming.tasq.error.TasqAuthException; 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.SprintSorter; import com.qprogramming.tasq.support.sorters.TaskSorter; import com.qprogramming.tasq.support.web.MessageHelper; import com.qprogramming.tasq.task.DisplayTask; import com.qprogramming.tasq.task.Task; import com.qprogramming.tasq.task.TaskService; import com.qprogramming.tasq.task.TaskState; 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.Days; import org.joda.time.LocalDate; import org.joda.time.Period; 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.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.util.StringUtils; 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 java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; @Controller public class SprintController { private static final Logger LOG = LoggerFactory .getLogger(SprintController.class); private static final String SPACE = " "; private static final String NEW_LINE = "\n"; private ProjectService projSrv; private TaskService taskSrv; private AgileService agileSrv; private WorkLogService wrkLogSrv; private MessageSource msg; private DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"); @Autowired public SprintController(ProjectService prjSrv, TaskService taskSrv, AgileService agileSrv, WorkLogService wrkLogSrv, MessageSource msg) { this.projSrv = prjSrv; this.taskSrv = taskSrv; this.agileSrv = agileSrv; this.wrkLogSrv = wrkLogSrv; this.msg = msg; } @Transactional(readOnly = true) @RequestMapping(value = "{id}/scrum/board", method = RequestMethod.GET) public String showBoard(@PathVariable String id, Model model, HttpServletRequest request, RedirectAttributes ra) { Project project = projSrv.findByProjectId(id); if (project != null) { if (!projSrv.canEdit(project)) { throw new TasqAuthException(msg); } model.addAttribute("project", project); Sprint sprint = agileSrv.findByProjectIdAndActiveTrue(project .getId()); if (sprint == null) { MessageHelper.addWarningAttribute( ra, msg.getMessage("agile.sprint.noActive", null, Utils.getCurrentLocale())); return "redirect:/" + project.getProjectId() + "/scrum/backlog"; } List<Task> taskList = taskSrv.findAllBySprint(sprint); Collections.sort(taskList, new TaskSorter(TaskSorter.SORTBY.ORDER, true)); Set<String> tags = new HashSet<>(); List<DisplayTask> resultList = taskSrv.convertToDisplay(taskList, true); for (DisplayTask displayTask : resultList) { tags.addAll(displayTask.getTags()); } model.addAttribute("tags", tags); model.addAttribute("sprint", sprint); model.addAttribute("tasks", resultList); return "/scrum/board"; } return ""; } @Transactional(readOnly = true) @RequestMapping(value = "/{id}/scrum/backlog", method = RequestMethod.GET) public String showBacklog(@PathVariable String id, Model model, HttpServletRequest request) { Project project = projSrv.findByProjectId(id); if (project != null) { if (!projSrv.canEdit(project)) { throw new TasqAuthException(msg); } model.addAttribute("project", project); List<Task> taskList = taskSrv.findAllByProject(project); //init sprints data taskList.stream().parallel().forEach(task -> Hibernate.initialize(task.getSprints())); Collections.sort(taskList, new TaskSorter(TaskSorter.SORTBY.ORDER, true)); Set<String> tags = new HashSet<>(); List<DisplayTask> resultList = taskSrv.convertToDisplay(taskList, true); Map<Sprint, List<DisplayTask>> sprint_result = new LinkedHashMap<>(); List<Sprint> sprintList = agileSrv.findByProjectIdAndFinished( project.getProjectId(), false); Collections.sort(sprintList, new SprintSorter()); // Assign tasks to sprints in order to display them for (Sprint sprint : sprintList) { List<DisplayTask> sprint_tasks = new LinkedList<>(); taskList.stream().parallel().filter(task -> task.getSprints().contains(sprint)).forEach(task -> { DisplayTask displayTask = new DisplayTask(task); displayTask.setTagsFromTask(task.getTags()); tags.addAll(displayTask.getTags()); sprint_tasks.add(displayTask); }); sprint_result.put(sprint, sprint_tasks); } resultList.stream() .filter(displayTask -> !TaskState.CLOSED.equals(displayTask.getState())) .forEach(displayTask -> tags.addAll(displayTask.getTags())); model.addAttribute("tags", tags); model.addAttribute("sprint_result", sprint_result); model.addAttribute("tasks", resultList); model.addAttribute("sprints", sprintList); } return "/scrum/backlog"; } @RequestMapping(value = "/{id}/scrum/create", method = RequestMethod.POST) public String createSprint(@PathVariable String id, Model model, HttpServletRequest request, RedirectAttributes ra) { Project project = projSrv.findByProjectId(id); if (!projSrv.canAdminister(project)) { throw new TasqAuthException(msg); } List<Sprint> sprints = agileSrv.findByProjectId(project.getId()); Sprint sprint = new Sprint(); sprint.setProject(project); sprint.setSprint_no((long) sprints.size() + 1); agileSrv.save(sprint); MessageHelper.addSuccessAttribute( ra, msg.getMessage("agile.createdSprint", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader("Referer"); } @Transactional @RequestMapping(value = "/task/sprintAssign", method = RequestMethod.POST) @ResponseBody public ResponseEntity<ResultData> assignSprint( @RequestParam(value = "taskID") String taskID, @RequestParam(value = "sprintID") Long sprintID, HttpServletRequest request, RedirectAttributes ra) { ResultData result = new ResultData(); Sprint sprint = agileSrv.findById(sprintID); Task task = taskSrv.findById(taskID); Project project = task.getProject(); if (!projSrv.canAdminister(project)) { throw new TasqAuthException(msg); } Hibernate.initialize(task.getSprints()); if (sprint.isActive()) { if (checkIfNotEstimated(task)) { result.code = ResultData.Code.WARNING; result.message = msg.getMessage( "agile.task2Sprint.Notestimated", new Object[]{task.getId(), sprint.getSprintNo()}, Utils.getCurrentLocale()); return ResponseEntity.ok(result); } String message = ""; wrkLogSrv.addActivityLog(task, message, LogType.TASKSPRINTADD); } task.addSprint(sprint); taskSrv.save(task); List<Task> subtasks = taskSrv.findSubtasks(task); for (Task subtask : subtasks) { subtask.addSprint(sprint); taskSrv.save(subtask); } result.code = ResultData.Code.OK; result.message = msg.getMessage("agile.task2Sprint", new Object[]{ task.getId(), sprint.getSprintNo()}, Utils.getCurrentLocale()); return ResponseEntity.ok(result); } @Transactional @RequestMapping(value = "/task/sprintRemove", method = RequestMethod.POST) @ResponseBody public ResponseEntity<ResultData> removeFromSprint( @RequestParam(value = "taskID") String taskID, @RequestParam(value = "sprintID") Long sprintID, Model model, HttpServletRequest request, RedirectAttributes ra) { Task task = taskSrv.findById(taskID); Project project = task.getProject(); if (!projSrv.canAdminister(project)) { throw new TasqAuthException(msg); } ResultData result = new ResultData(); Sprint sprint = agileSrv.findById(sprintID); if (sprint.isActive()) { wrkLogSrv.addActivityLog(task, null, LogType.TASKSPRINTREMOVE); } Hibernate.initialize(task.getSprints()); task.removeSprint(sprint); taskSrv.save(task); List<Task> subtasks = taskSrv.findSubtasks(task); for (Task subtask : subtasks) { subtask.removeSprint(sprint); taskSrv.save(subtask); } result.code = ResultData.Code.OK; result.message = msg.getMessage("agile.taskRemoved", new Object[]{task.getId()}, Utils.getCurrentLocale()); return ResponseEntity.ok(result); } @Transactional @RequestMapping(value = "/scrum/delete", method = RequestMethod.GET) public String deleteSprint(@RequestParam(value = "id") Long id, Model model, HttpServletRequest request, RedirectAttributes ra) { Sprint sprint = agileSrv.findById(id); Project project = sprint.getProject(); if (!projSrv.canAdminister(project)) { throw new TasqAuthException(msg); } // consider checking if is active? if (sprint != null) { if (canEdit(sprint.getProject())) { List<Task> taskList = taskSrv.findAllBySprint(sprint); for (Task task : taskList) { Hibernate.initialize(task.getSprints()); if (task.getSprints().contains(sprint)) { task.removeSprint(sprint); taskSrv.save(task); } } } } agileSrv.delete(sprint); MessageHelper.addSuccessAttribute( ra, msg.getMessage("agile.sprint.removed", null, Utils.getCurrentLocale())); return "redirect:" + request.getHeader("Referer"); } @Transactional @ResponseBody @RequestMapping(value = "/scrum/start", method = RequestMethod.POST) public ResultData startSprint(@RequestParam(value = "sprintID") Long id, @RequestParam(value = "projectID") Long projectId, @RequestParam(value = "sprintStart") String sprintStart, @RequestParam(value = "sprintEnd") String sprintEnd, @RequestParam(value = "sprintStartTime") String sprintStartTime, @RequestParam(value = "sprintEndTime") String sprintEndTime) { Project project = projSrv.findById(projectId); if (!projSrv.canAdminister(project)) { throw new TasqAuthException(msg); } // check if other sprints are not ending when this new is starting List<Sprint> allSprints = agileSrv.findByProjectId(projectId); sprintStart += " " + sprintStartTime; sprintEnd += " " + sprintEndTime; Date startDate = Utils.convertStringToDateAndTime(sprintStart); Date endDate = Utils.convertStringToDateAndTime(sprintEnd); ResultData validateStartStop = validateStartStop(sprintStart, sprintEnd); if (ResultData.Code.WARNING.equals(validateStartStop.code)) { return validateStartStop; } DateTime startTime = new DateTime(startDate); DateTime endTime = new DateTime(endDate); List<LocalDate> freeDays = projSrv.getFreeDays(project, startTime, endTime); String freeDaysString = String.join(", ", freeDays.stream().map(LocalDate::toString).collect(Collectors.toList())); if (freeDays.contains(new LocalDate(startTime))) { return new ResultData(ResultData.Code.WARNING, msg.getMessage( "agile.sprint.start.freeday", new Object[]{sprintStart, freeDaysString}, Utils.getCurrentLocale())); } if (freeDays.contains(new LocalDate(endTime))) { return new ResultData(ResultData.Code.WARNING, msg.getMessage( "agile.sprint.end.freeday", new Object[]{sprintEnd, freeDaysString}, Utils.getCurrentLocale())); } for (Sprint sprint : allSprints) { if (sprint.getRawEnd_date() != null) { DateTime sprintEndDate = new DateTime(sprint.getRawEnd_date()); DateTime sprintStartDate = new DateTime(startDate); if (sprintEndDate.equals(sprintStartDate) || sprintStartDate.isBefore(sprintEndDate)) { return new ResultData(ResultData.Code.WARNING, msg.getMessage( "agile.sprint.startOnEnd", new Object[]{sprint.getSprintNo(), sprintStart}, Utils.getCurrentLocale())); } } } Sprint sprint = agileSrv.findById(id); Sprint active = agileSrv.findByProjectIdAndActiveTrue(projectId); if (sprint != null && !sprint.isActive() && active == null) { if (canEdit(sprint.getProject()) || Roles.isAdmin()) { Period total_estimate = new Period(); int totalStoryPoints = 0; StringBuilder warnings = new StringBuilder(); List<Task> taskList = taskSrv.findAllBySprint(sprint); for (Task task : taskList) { if (task.getState().equals(TaskState.ONGOING) || task.getState().equals(TaskState.BLOCKED)) { total_estimate = PeriodHelper.plusPeriods( total_estimate, task.getRawRemaining()); } else { total_estimate = PeriodHelper.plusPeriods( total_estimate, task.getRawEstimate()); } if (!task.isSubtask() && task.getStory_points() == 0 && task.isEstimated()) { warnings.append(task.getId()); warnings.append(" "); } totalStoryPoints += task.getStory_points(); } if (warnings.length() > 0) { return new ResultData(ResultData.Code.WARNING, msg.getMessage( "agile.sprint.notEstimated.sp", new Object[]{warnings.toString()}, Utils.getCurrentLocale())); } sprint.setTotalEstimate(total_estimate); sprint.setTotalStoryPoints(totalStoryPoints); sprint.setStart_date(startDate); sprint.setEnd_date(endDate); sprint.setActive(true); agileSrv.save(sprint); wrkLogSrv.addWorkLogNoTask(null, project, LogType.SPRINT_START); return new ResultData(ResultData.Code.OK, msg.getMessage( "agile.sprint.started", new Object[]{sprint.getSprintNo()}, Utils.getCurrentLocale())); } } return new ResultData(ResultData.Code.ERROR, msg.getMessage("error.unknown", null, Utils.getCurrentLocale())); } private boolean overlappingFreeDay(Project project, Date startDate, Date endDate) { DateTime startTime = new DateTime(startDate); DateTime endTime = new DateTime(endDate); List<LocalDate> freeDays = projSrv.getFreeDays(project, startTime, endTime); return freeDays.contains(startTime) || freeDays.contains(endTime); } @Transactional @RequestMapping(value = "/scrum/stop", method = RequestMethod.GET) public String finishSprint(@RequestParam(value = "id") Long id, HttpServletRequest request, RedirectAttributes ra) { Sprint sprint = agileSrv.findById(id); if (sprint != null) { Project project = projSrv.findById(sprint.getProject().getId()); if (!projSrv.canAdminister(project)) { throw new TasqAuthException(msg); } if (sprint.isActive() && (canEdit(sprint.getProject()) || Roles.isAdmin())) { sprint.setActive(false); sprint.finish(); sprint.setEnd_date(new Date()); List<Task> taskList = taskSrv.findAllBySprint(sprint); Map<TaskState, Integer> state_count = new HashMap<TaskState, Integer>(); for (Task task : taskList) { task.setInSprint(false); Integer value = state_count.get(task.getState()); value = value == null ? 0 : value; value++; state_count.put((TaskState) task.getState(), value); taskSrv.save(task); } StringBuilder message = new StringBuilder(msg.getMessage( "agile.sprint.finished", new Object[]{sprint.getSprintNo()}, Utils.getCurrentLocale())); for (Entry<TaskState, Integer> entry : state_count.entrySet()) { message.append(NEW_LINE); message.append(msg.getMessage(entry.getKey().getCode(), null, Utils.getCurrentLocale())); message.append(SPACE); message.append(entry.getValue()); } MessageHelper.addSuccessAttribute(ra, message.toString()); wrkLogSrv.addWorkLogNoTask(null, project, LogType.SPRINT_STOP); agileSrv.save(sprint); } } return "redirect:" + request.getHeader("Referer"); } @Transactional @RequestMapping(value = "/{id}/scrum/reports", method = RequestMethod.GET, produces = "application/json") public String showBurndown(@PathVariable String id, @RequestParam(value = "sprint", required = false) Long sprintNo, Model model, RedirectAttributes ra) { Project project = projSrv.findByProjectId(id); if (project != null) { if (sprintNo != null) { Sprint sprint = agileSrv.findByProjectIdAndSprintNo( project.getId(), sprintNo); if (sprint.getRawEnd_date() == null & !sprint.isActive()) { MessageHelper.addWarningAttribute(ra, msg.getMessage("agile.sprint.notStarted", new Object[]{sprintNo}, Utils.getCurrentLocale())); return "redirect:/" + project.getProjectId() + "/scrum/backlog"; } } Sprint lastSprint = agileSrv.findByProjectIdAndActiveTrue(project .getId()); if (lastSprint == null) { List<Sprint> sprints = agileSrv .findByProjectId(project.getId()); if (sprints.isEmpty()) { MessageHelper.addWarningAttribute(ra, msg.getMessage( "agile.sprint.noSprints", null, Utils.getCurrentLocale())); return "redirect:/" + project.getProjectId() + "/scrum/backlog"; } int counter = 1; Collections.sort(sprints, new SprintSorter()); lastSprint = sprints.get(sprints.size() - counter); while (StringUtils.isEmpty(lastSprint.getStart_date())) { counter++; if (counter > sprints.size()) { MessageHelper.addWarningAttribute(ra, msg.getMessage( "agile.sprint.noSprints", null, Utils.getCurrentLocale())); return "redirect:/" + project.getProjectId() + "/scrum/backlog"; } lastSprint = sprints.get(sprints.size() - counter); } } model.addAttribute("lastSprint", lastSprint); model.addAttribute("project", project); } return "/scrum/reports"; } /** * Retrieves burndown map for sprint. No extra checking, only if exists and * if is started; * * @param id * @param sprintNo * @return */ @RequestMapping(value = "/{id}/sprint-data", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Transactional public SprintData showBurndownChart(@PathVariable String id, @RequestParam(value = "sprint") Long sprintNo) { SprintData result = new SprintData(); Project project = projSrv.findByProjectId(id); if (project != null) { Hibernate.initialize(project.getHolidays()); Sprint sprint = agileSrv.findByProjectIdAndSprintNo( project.getId(), sprintNo); if (sprint == null || (sprint.getRawEnd_date() == null & !sprint.isActive())) { String message = msg.getMessage("agile.sprint.notStarted", new Object[]{sprintNo}, Utils.getCurrentLocale()); result.setMessage(message); return result; } DateTime startTime = new DateTime(sprint.getRawStart_date()); DateTime endTime = new DateTime(sprint.getRawEnd_date()); result.setStart(fmt.print(startTime)); result.setStop(fmt.print(endTime)); List<Task> sprintTasks = taskSrv.findAllBySprint(sprint); agileSrv.setTasksByStatus(result, endTime, sprintTasks); // Fill maps based on time or story point driven board List<WorkLog> wrkList = wrkLogSrv.getAllSprintEvents(sprint); result.setWorklogs(DisplayWorkLog.convertToDisplayWorkLogs(wrkList)); result.setTimeBurned(agileSrv.fillTimeBurndownMap(wrkList, startTime, endTime)); Period totalTime = new Period(); for (Map.Entry<String, Float> entry : result.getTimeBurned() .entrySet()) { totalTime = PeriodHelper.plusPeriods(totalTime, Utils.getPeriodValue(entry.getValue())); } result.setTotalTime(String.valueOf(Utils.round( Utils.getFloatValue(totalTime), 2))); Float remainingEstimate = getRemainingEstimate(sprint); if (!sprint.getFinished() && new DateTime().isAfter(endTime)) { endTime = new DateTime(); } fillIdeal(result, project, startTime, endTime, remainingEstimate); return fillLeftAndBurned(result, sprint, wrkList); } else { return result; } } private void fillIdeal(SprintData result, Project project, DateTime startTime, DateTime endTime, Float remainingEstimate) { Hibernate.initialize(project.getHolidays()); Map<String, Float> left = new LinkedHashMap<>(); Map<String, Float> burned = new LinkedHashMap<>(); Map<String, Float> ideal = new LinkedHashMap<>(); left.put(fmt.print(startTime), remainingEstimate); burned.put(fmt.print(startTime), 0f); List<LocalDate> freeDays = projSrv.getFreeDays(project, startTime, endTime); if (!freeDays.isEmpty()) { populateIdealWithFreeDays(startTime, endTime, remainingEstimate, ideal, freeDays); } else { ideal.put(fmt.print(startTime), remainingEstimate); } ideal.put(fmt.print(endTime), 0f); result.setIdeal(ideal); result.setBurned(burned); result.setLeft(left); } /** * Populates ideal data based on project FreeDays.Starting point is set to midnight of first day. For each free day, chart values are calculated based on formula: * (-remainingEstimate/sprintWorkDays) * counter + (remainingEstimate + freeDaysSoFar) * After it freeDaysSoFar is increased.. If there are consequentive freedays , value from previous day is taken instaedn of formula value * * @param startTime * @param endTime * @param remainingEstimate * @param ideal * @param freeDays */ private void populateIdealWithFreeDays(DateTime startTime, DateTime endTime, Float remainingEstimate, Map<String, Float> ideal, List<LocalDate> freeDays) { int sprintWorkDays = Days.daysBetween(startTime, endTime).getDays(); DateTime dateCounter = startTime.withHourOfDay(0).withMinuteOfHour(0); ideal.put(fmt.print(dateCounter), remainingEstimate); ideal.put(fmt.print(startTime), remainingEstimate); dateCounter = dateCounter.plusDays(1); int counter = 1; int freeDaysSoFar = 0; sprintWorkDays -= freeDays.size(); boolean wasFree = false; while (dateCounter.isBefore(endTime)) { if (freeDays.contains(new LocalDate(dateCounter))) { DateTime nearMidnight = dateCounter.withHourOfDay(23).withMinuteOfHour(59); if (wasFree) { ideal.put(fmt.print(dateCounter), ideal.get(fmt.print(dateCounter.minusDays(1)))); ideal.put(fmt.print(nearMidnight), ideal.get(fmt.print(dateCounter.minusDays(1)))); } else { ideal.put(fmt.print(dateCounter), (-remainingEstimate / sprintWorkDays) * counter + (remainingEstimate + freeDaysSoFar)); ideal.put(fmt.print(dateCounter.withHourOfDay(23).withMinuteOfHour(59)), (-(remainingEstimate) / (sprintWorkDays)) * counter + (remainingEstimate + freeDaysSoFar)); } freeDaysSoFar++; wasFree = true; } else { wasFree = false; } dateCounter = dateCounter.plusDays(1); counter++; } } @RequestMapping(value = "/getSprints", method = RequestMethod.GET) @ResponseBody public List<DisplaySprint> showProjectSprints( @RequestParam String projectID, HttpServletResponse response) { response.setContentType("application/json"); List<Sprint> projectSprints = agileSrv.findByProjectIdAndFinished( projectID, false); List<DisplaySprint> result = agileSrv.convertToDisplay(projectSprints); Collections.sort(result); return result; } @RequestMapping(value = "/validateSprint", method = RequestMethod.GET) @ResponseBody public ResultData validateSprint( @RequestParam String startDate, @RequestParam String endDate, HttpServletResponse response) { response.setContentType("application/json"); return validateStartStop(startDate, endDate); } private ResultData validateStartStop(@RequestParam String startDate, @RequestParam String endDate) { Date dateStart = Utils.convertStringToDate(startDate); Date dateEnd = Utils.convertStringToDate(endDate); ResultData result = new ResultData(ResultData.Code.OK, null); if (dateStart == null) { result.code = ResultData.Code.WARNING; result.message = msg.getMessage("agile.sprint.start.wrong", null, Utils.getCurrentLocale()); } if (dateEnd == null) { result.code = ResultData.Code.WARNING; result.message = result.message + " " + msg.getMessage("agile.sprint.end.wrong", null, Utils.getCurrentLocale()); ; } if (ResultData.Code.ERROR.equals(result.code)) { return result; } Days days = Days.daysBetween(new DateTime(dateStart), new DateTime(dateEnd)); if (days.getDays() > 28) { result.code = ResultData.Code.WARNING; result.message = msg.getMessage("agile.sprint.range.4weeks", null, Utils.getCurrentLocale()); } return result; } /** * Checks if sprint with given id is active or not * * @param sprintID * @param response * @return */ @RequestMapping(value = "/scrum/isActive", method = RequestMethod.GET) @ResponseBody public boolean checkIfActive( @RequestParam(value = "id") Long sprintID, HttpServletResponse response) { Sprint sprint = agileSrv.findById(sprintID); return sprint.isActive(); } /** * Checks if task is properly estimated based on project settings ( * Estimated time not 0m for time based or story points not 0 for story * points driven * * @param task * @return */ private boolean checkIfNotEstimated(Task task) { return task.getStory_points() == 0 && task.isEstimated(); } /** * Fills Left and burndown charts data * * @param result - Previously filled SpringData * @param sprint - sprint for which data will be filled * @param wrkList - list of all worklogs from this sprint * story point * @return */ private SprintData fillLeftAndBurned(SprintData result, Sprint sprint, List<WorkLog> wrkList) { List<WorkLog> tasksOnly = wrkList.stream().filter(w -> !w.getTask().isSubtask()).collect(Collectors.toList()); Map<DateTime, Float> leftMap = fillLeftMap(tasksOnly); Map<DateTime, Float> burnedMap = fillBurnedMap(tasksOnly); DateTime endTime = new DateTime(sprint.getRawEnd_date()); SprintData data = result; Float burned = 0f; Float remaining_estimate = getRemainingEstimate(sprint); // Iterate over sprint days Integer totalPoints; if (burnedMap.size() > leftMap.size()) { totalPoints = iterateOverLongerMap(burnedMap, leftMap, burnedMap, data, remaining_estimate, burned); } else { totalPoints = iterateOverLongerMap(leftMap, leftMap, burnedMap, data, remaining_estimate, burned); } //ensure left and burned final values are filled if (endTime.isBefore(DateTime.now())) { data.fillEnds(fmt.print(endTime)); } data.setTotalPoints(totalPoints); return data; } //TODO eliminate private Float getRemainingEstimate(Sprint sprint) { return new Float(sprint.getTotalStoryPoints()); } private Integer iterateOverLongerMap(Map<DateTime, Float> longerMap, Map<DateTime, Float> leftMap, Map<DateTime, Float> burnedMap, SprintData data, Float remaining_estimate, Float burned) { Integer totalPoints = new Integer(0); for (Entry<DateTime, Float> entry : longerMap.entrySet()) { DateTime date = entry.getKey(); Float value = leftMap.get(date); Float valueBurned = burnedMap.get(date); value = value == null ? 0 : value; valueBurned = valueBurned == null ? 0 : valueBurned; remaining_estimate -= value; burned += valueBurned; if (date.isAfter(DateTime.now())) { data.putToLeft(fmt.print(date), null); data.getBurned().put(fmt.print(date), null); } else { data.putToLeft(fmt.print(date), remaining_estimate); data.getBurned().put(fmt.print(date), burned); totalPoints = burned.intValue(); } } return totalPoints; } /** * Fils burned story points map based on worklogs * * @param worklogList list of events with task closed event * @return */ private Map<DateTime, Float> fillLeftMap(List<WorkLog> worklogList) { Map<DateTime, Float> leftMap = new LinkedHashMap<>(); for (WorkLog workLog : worklogList) { DateTime dateLogged = new DateTime(workLog.getRawTime()); pointsUpdate(leftMap, workLog, dateLogged); } return leftMap; } /** * Fill burned map based on worklog list * * @param worklogList * @return */ private Map<DateTime, Float> fillBurnedMap(List<WorkLog> worklogList) { Map<DateTime, Float> burnedMap = new LinkedHashMap<>(); for (WorkLog workLog : worklogList) { if (!LogType.ESTIMATE.equals(workLog.getType()) && !LogType.TASKSPRINTADD.equals(workLog.getType()) && !LogType.TASKSPRINTREMOVE.equals(workLog.getType())) { DateTime dateLogged = new DateTime(workLog.getRawTime()); pointsUpdate(burnedMap, workLog, dateLogged); } } return burnedMap; } private void pointsUpdate(Map<DateTime, Float> map, WorkLog workLog, DateTime dateLogged) { if (workLog.getActivity() == null) { Float value = map.get(dateLogged); value = addOrSubstractPoints(workLog, value); map.put(dateLogged, value); } } /** * Based on event type either add or subtract value for time tracked * projects * * @param workLog * @param value * @return */ private Float addOrSubstractTime(WorkLog workLog, Float value) { Float result = value; Float taskLogged = Utils.getFloatValue(workLog.getActivity()); if (LogType.ESTIMATE.equals(workLog.getType())) { taskLogged = Utils.getFloatValue(PeriodHelper.inFormat(workLog .getMessage())); taskLogged *= -1; } if (value == null) { result = taskLogged; } else { result += taskLogged; } return result; } private Float addOrSubstractPoints(WorkLog workLog, Float value) { Float result = value; Integer taskStoryPoints = workLog.getTask().getStory_points(); if (LogType.REOPEN.equals(workLog.getType()) || LogType.TASKSPRINTADD.equals(workLog.getType())) { taskStoryPoints *= -1; } if (LogType.ESTIMATE.equals(workLog.getType())) { try { taskStoryPoints = -1 * Integer.valueOf(workLog.getMessage()); } catch (NumberFormatException e) { LOG.debug(workLog.toString() + ": No story points in estimate change " + workLog.getTask()); } } if (value == null) { result = new Float(taskStoryPoints); } else { result += new Float(taskStoryPoints); } return result; } /** * Checks if currently logged in user have privileges to change anything in * project * * @param project * @return */ private boolean canEdit(Project project) { Project repo_project = projSrv.findById(project.getId()); if (repo_project == null) { return false; } Account currentAccount = Utils.getCurrentAccount(); return (repo_project.getAdministrators().contains(currentAccount) || repo_project.getParticipants().contains(currentAccount) || Roles .isAdmin()); } }