package com.qprogramming.tasq.agile;
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.Utils;
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.tag.Tag;
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.apache.commons.lang3.StringUtils;
import org.hibernate.Hibernate;
import org.joda.time.*;
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.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 KanbanController {
private static final Logger LOG = LoggerFactory
.getLogger(KanbanController.class);
private static final String TODO_ONGOING = "<strike>To do</strike> ➧ Ongoing";
private static final String COMPLETED_ONGOING = "<strike>Complete</strike> ➧ Ongoing";
// TODO depreciated
private static final String TODO_ONGOING_DEPR = "To do -> Ongoing";
private static final String ONGOING = "ongoing";
private TaskService taskSrv;
private ProjectService projSrv;
private WorkLogService wrkLogSrv;
private MessageSource msg;
private AgileService agileSrv;
private DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
// private ReleaseRepository releaseRepo;
@Autowired
public KanbanController(TaskService taskSrv, ProjectService projSrv,
WorkLogService wlSrv, MessageSource msg, AgileService agileSrv) {
this.taskSrv = taskSrv;
this.projSrv = projSrv;
this.wrkLogSrv = wlSrv;
this.msg = msg;
this.agileSrv = agileSrv;
}
@Transactional(readOnly = true)
@RequestMapping(value = "{id}/kanban/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);
List<Task> taskList = taskSrv.findAllWithoutRelease(project);
Collections.sort(taskList, new TaskSorter(TaskSorter.SORTBY.ORDER,
true));
Set<Tag> tags = new HashSet<Tag>();
for (Task task : taskList) {
Hibernate.initialize(task.getTags());
tags.addAll(task.getTags());
}
List<DisplayTask> resultList = taskSrv.convertToDisplay(taskList, true);
model.addAttribute("tags", tags);
model.addAttribute("tasks", resultList);
return "/kanban/board";
}
return "";
}
@Transactional
@RequestMapping(value = "/kanban/release", method = RequestMethod.POST)
public String newRelease(@RequestParam(value = "id") String id,
@RequestParam(value = "release") String releaseNo,
@RequestParam(value = "comment", required = false) String comment,
HttpServletRequest request, RedirectAttributes ra) {
Project project = projSrv.findByProjectId(id);
if (project != null) {
if (!projSrv.canAdminister(project)) {
throw new TasqAuthException(msg);
}
// search if name unique for project
Release unique = agileSrv.findByProjectIdAndRelease(
project.getId(), releaseNo);
if (unique != null) {
StringBuilder projectName = new StringBuilder("[");
projectName.append(project.getProjectId());
projectName.append("] ");
projectName.append(project.getName());
MessageHelper.addWarningAttribute(
ra,
msg.getMessage("agile.release.exists", new Object[]{
releaseNo, projectName.toString()},
Utils.getCurrentLocale()));
return "redirect:" + request.getHeader("Referer");
}
List<Task> taskList = taskSrv.findAllToRelease(project);
if (taskList.isEmpty()) {
MessageHelper.addWarningAttribute(
ra,
msg.getMessage("agile.newRelease.noTasks", null,
Utils.getCurrentLocale()));
return "redirect:" + request.getHeader("Referer");
}
Release release = new Release(project, releaseNo, comment);
List<Release> releases = agileSrv
.findReleaseByProjectIdOrderByDateDesc(project.getId());
if (!releases.isEmpty()) {
release.setStartDate(releases.get(releases.size() - 1)
.getEndDate());
}
release = agileSrv.save(release);
int count = 0;
for (Task task : taskList) {
task.setRelease(release);
count++;
}
MessageHelper.addSuccessAttribute(
ra,
msg.getMessage("agile.newRelease.success", new Object[]{
releaseNo, count}, Utils.getCurrentLocale()));
}
return "redirect:" + request.getHeader("Referer");
}
@RequestMapping(value = "{id}/kanban/reports", method = RequestMethod.GET)
public String showReport(
@PathVariable String id,
@RequestParam(value = "release", required = false) String releaseNo,
Model model, HttpServletRequest request, RedirectAttributes ra) {
Project project = projSrv.findByProjectId(id);
if (project != null) {
List<Release> releases = agileSrv
.findReleaseByProjectIdOrderByDateDesc(project.getId());
model.addAttribute("project", project);
model.addAttribute("releases", releases);
}
return "/kanban/reports";
}
@RequestMapping(value = "/getReleases", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<List<Release>> showProjectReleases(@RequestParam Long projectID,
HttpServletResponse response) {
response.setContentType("application/json");
Project project = projSrv.findById(projectID);
return ResponseEntity.ok(agileSrv
.findReleaseByProjectIdOrderByDateDesc(project.getId()));
}
@Transactional
@RequestMapping(value = "/{id}/release-data", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public ResponseEntity<KanbanData> showBurndownChart(@PathVariable String id,
@RequestParam(value = "release", required = false) String releaseNo) {
KanbanData result = new KanbanData();
Project project = projSrv.findByProjectId(id);
if (project != null) {
Release release;
DateTime startTime;
DateTime endTime;
List<Task> releaseTasks;
if (releaseNo == null || "".equals(releaseNo)) {
release = agileSrv.findLastReleaseByProjectId(project.getId());
if (release == null) {
startTime = new DateTime(project.getRawStartDate());
} else {
startTime = release.getEndDate();
release = null;
}
releaseTasks = taskSrv.findAllByRelease(project, release);
endTime = new DateTime();
release = new Release();
release.setProject(project);
release.setStartDate(startTime);
release.setActive(true);
} else {
release = agileSrv.findByProjectIdAndRelease(project.getId(),
releaseNo);
startTime = release.getStartDate();
endTime = release.getEndDate();
releaseTasks = taskSrv.findAllByRelease(release);
}
List<LocalDate> freeDays = projSrv.getFreeDays(project, startTime, endTime);
LocalTime nearMidnight = new LocalTime(23, 59);
result.setFreeDays(freeDays.stream()
.map(localDate -> new StartStop(fmt.print(localDate.minusDays(1).toDateTime(nearMidnight)), fmt.print(localDate.toDateTime(nearMidnight))))
.collect(Collectors.toList()));
List<WorkLog> wrkList = wrkLogSrv.getAllReleaseEvents(release);
agileSrv.setTasksByStatus(result,endTime,releaseTasks);
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)));
return ResponseEntity.ok(fillOpenAndClosed(result, release, wrkList));
} else {
return ResponseEntity.ok(result);
}
}
private KanbanData fillOpenAndClosed(KanbanData result, Release release,
List<WorkLog> wrkList) {
KanbanData data = result;
Map<LocalDate, Integer> openMap = new LinkedHashMap<>();
Map<LocalDate, Integer> closedMap = new LinkedHashMap<>();
Map<LocalDate, Integer> progressMap = new LinkedHashMap<>();
for (WorkLog workLog : wrkList) {
LocalDate dateLogged = new LocalDate(workLog.getRawTime());
if (LogType.CREATE.equals(workLog.getType())) {
increaseMap(openMap, dateLogged);
} else if (LogType.CLOSED.equals(workLog.getType())) {
increaseMap(closedMap, dateLogged);
decreaseMap(openMap, dateLogged);
decreaseMap(progressMap, dateLogged);
} else if (LogType.REOPEN.equals(workLog.getType())) {
decreaseMap(closedMap, dateLogged);
increaseMap(progressMap, dateLogged);
} else if (LogType.STATUS.equals(workLog.getType()) && isOngoing(workLog)) {
increaseMap(progressMap, dateLogged);
decreaseMap(openMap, dateLogged);
}
}
LocalDate startTime = new LocalDate(release.getStartDate());
LocalDate endTime = new LocalDate(release.getEndDate());
data.setStart(startTime.toString());
data.setStop(endTime.toString());
fillChartData(data, openMap, closedMap, progressMap, startTime, endTime);
normalizeProgressLabels(data, progressMap);
return data;
}
private boolean isOngoing(WorkLog workLog) {
String message = workLog.getMessage();
if (StringUtils.isNotBlank(message)) {
return message.contains(TODO_ONGOING) || message.contains(COMPLETED_ONGOING);
}
return false;
}
private void fillChartData(KanbanData data,
Map<LocalDate, Integer> openMap, Map<LocalDate, Integer> closedMap,
Map<LocalDate, Integer> progressMap, LocalDate startTime,
LocalDate endTime) {
Integer open = 0;
Integer closed = 0;
Integer progress = 0;
int releaseDays = Days.daysBetween(startTime, endTime).getDays() + 1;
for (int i = 0; i < releaseDays; i++) {
LocalDate date = startTime.plusDays(i);
Integer openValue = openMap.get(date);
Integer closedValue = closedMap.get(date);
Integer progressValue = progressMap.get(date);
openValue = openValue == null ? 0 : openValue;
closedValue = closedValue == null ? 0 : closedValue;
progressValue = progressValue == null ? 0 : progressValue;
open += openValue;
open = open < 0 ? 0 : open;
closed += closedValue;
progress += progressValue;
progress = progress < 0 ? 0 : progress;
data.putToClosed(date.toString(), closed);
data.putToInProgress(date.toString(), closed + progress);
data.putToOpen(date.toString(), closed + progress + open);
data.putToInProgressLabel(date.toString(), progress);
data.putToOpenLabel(date.toString(), open);
}
}
private void normalizeProgressLabels(KanbanData data,
Map<LocalDate, Integer> progressMap) {
for (Entry<LocalDate, Integer> entry : progressMap.entrySet()) {
Integer value = entry.getValue();
if (value < 0) {
value = 0;
data.putToInProgressLabel(entry.getKey().toString(), value);
}
}
}
private void increaseMap(Map<LocalDate, Integer> map, LocalDate dateLogged) {
Integer value = map.get(dateLogged);
value = value == null ? 0 : value;
value++;
map.put(dateLogged, value);
}
private void decreaseMap(Map<LocalDate, Integer> map, LocalDate dateLogged) {
Integer value = map.get(dateLogged);
value = value == null ? 0 : value;
value--;
map.put(dateLogged, value);
}
@Deprecated
private String getToDoOngoing() {
return TODO_ONGOING_DEPR;
}
}