diff --git a/src/org/april/agirstatool/charts/DateCount.java b/src/org/april/agirstatool/charts/DateCount.java new file mode 100644 index 0000000..429584a --- /dev/null +++ b/src/org/april/agirstatool/charts/DateCount.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020 Christian Pierre MOMON + * + * This file is part of AgirStatool, simple key value database. + * + * AgirStatool is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * AgirStatool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with AgirStatool. If not, see . + */ +package org.april.agirstatool.charts; + +import org.apache.commons.lang3.StringUtils; + +/** + * The Class Projects. + */ +public class DateCount +{ + private String date; + private long count; + + /** + * Instantiates a new date count. + * + * @param date + * the date + * @param count + * the count + */ + public DateCount(final String date, final long count) + { + setDate(date); + this.count = count; + } + + public long getCount() + { + return this.count; + } + + public String getDate() + { + return this.date; + } + + public void setCount(final long count) + { + this.count = count; + } + + public void setDate(final String date) + { + if (StringUtils.isBlank(date)) + { + throw new IllegalArgumentException("Null parameter."); + } + else + { + this.date = date; + } + } + +} diff --git a/src/org/april/agirstatool/charts/DateCountList.java b/src/org/april/agirstatool/charts/DateCountList.java new file mode 100644 index 0000000..92ec357 --- /dev/null +++ b/src/org/april/agirstatool/charts/DateCountList.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Christian Pierre MOMON + * + * This file is part of AgirStatool, simple key value database. + * + * AgirStatool is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * AgirStatool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with AgirStatool. If not, see . + */ +package org.april.agirstatool.charts; + +import java.util.ArrayList; + +import fr.devinsy.strings.StringList; + +/** + * The Class Projects. + */ +public class DateCountList extends ArrayList +{ + private static final long serialVersionUID = -5526492552751712533L; + + /** + * Instantiates a new date count map. + */ + public DateCountList() + { + super(); + } + + /** + * To value list. + * + * @return the string list + */ + public StringList toValueList() + { + StringList result; + + result = new StringList(); + + for (DateCount item : this) + { + result.append(item.getCount()); + } + + // + return result; + } +} \ No newline at end of file diff --git a/src/org/april/agirstatool/charts/DateCountMap.java b/src/org/april/agirstatool/charts/DateCountMap.java new file mode 100644 index 0000000..fbae265 --- /dev/null +++ b/src/org/april/agirstatool/charts/DateCountMap.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 Christian Pierre MOMON + * + * This file is part of AgirStatool, simple key value database. + * + * AgirStatool is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * AgirStatool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with AgirStatool. If not, see . + */ +package org.april.agirstatool.charts; + +import java.util.HashMap; + +/** + * The Class Projects. + */ +public class DateCountMap extends HashMap +{ + private static final long serialVersionUID = -5526492552751712533L; + + /** + * Instantiates a new date count map. + */ + public DateCountMap() + { + super(); + } +} diff --git a/src/org/april/agirstatool/core/AgirStatool.java b/src/org/april/agirstatool/core/AgirStatool.java index e563dd3..cbed5d9 100644 --- a/src/org/april/agirstatool/core/AgirStatool.java +++ b/src/org/april/agirstatool/core/AgirStatool.java @@ -25,8 +25,13 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import org.apache.commons.io.FileUtils; +import org.april.agirstatool.charts.DateCount; +import org.april.agirstatool.charts.DateCountList; +import org.april.agirstatool.charts.DateCountMap; import org.april.agirstatool.cli.SQLUtils; import org.april.agirstatool.core.pages.ProjectPage; import org.slf4j.Logger; @@ -194,7 +199,8 @@ public class AgirStatool Project root = listProjectsAsTree(); // Create welcome page. - refreshWelcomePage(root); + refreshPage(root); + FileUtils.copyFile(new File(this.targetDirectory, "all.xhtml"), new File(this.targetDirectory, "index.xhtml")); // Create one page per project. for (Project project : root.subProjects()) @@ -213,6 +219,154 @@ public class AgirStatool } } + /** + * Fetch week concluded count. + * + * In Redmine database: closed_on when resolved, rejected or closed. + * + * @param project + * the project + * @param consolidated + * the consolidated + * @return the date count map + * @throws AgirStatoolException + * the agir statool exception + */ + public DateCountMap fetchWeekConcludedCount(final Project project, final ProjectMode mode) throws AgirStatoolException + { + DateCountMap result; + + result = new DateCountMap(); + + // + PreparedStatement statement = null; + ResultSet resultSet = null; + try + { + StringList subSql = new StringList(); + if (mode == ProjectMode.CONSOLIDATED) + { + subSql.append("select "); + subSql.append(" id "); + subSql.append("from "); + subSql.append(" projects as childProject "); + subSql.append("where "); + subSql.append(" (childProject.id=" + project.getId() + " or childProject.parent_id=" + project.getId() + ")"); + subSql.append(" and childProject.status=1 and childProject.is_public=1"); + } + else + { + subSql.append(project.getId()); + } + + StringList sql = new StringList(); + sql.append("SELECT"); + sql.append(" concat_ws('-', year(closed_on), weekofyear(closed_on)) as date, "); + sql.append(" count(*) as count "); + sql.append("FROM "); + sql.append(" issues "); + sql.append("WHERE "); + sql.append(" project_id in (" + subSql.toString() + ") and closed_on is not null "); + sql.append("GROUP BY "); + sql.append(" date;"); + + // System.out.println(sql.toStringSeparatedBy("\n")); + + this.connection.setAutoCommit(true); + statement = this.connection.prepareStatement(sql.toString()); + resultSet = statement.executeQuery(); + + while (resultSet.next()) + { + result.put(resultSet.getString(1), new DateCount(resultSet.getString(1), resultSet.getLong(2))); + } + } + catch (SQLException exception) + { + throw new AgirStatoolException("Error fetching day closed count: " + exception.getMessage(), exception); + } + finally + { + SQLUtils.closeQuietly(statement, resultSet); + } + + // + return result; + } + + /** + * Fetch week created count. + * + * @param project + * the project + * @param consolidated + * the consolidated + * @return the date count map + * @throws AgirStatoolException + * the agir statool exception + */ + public DateCountMap fetchWeekCreatedCount(final Project project, final ProjectMode mode) throws AgirStatoolException + { + DateCountMap result; + + result = new DateCountMap(); + + // + PreparedStatement statement = null; + ResultSet resultSet = null; + try + { + StringList subSql = new StringList(); + if (mode == ProjectMode.CONSOLIDATED) + { + subSql.append("select "); + subSql.append(" id "); + subSql.append("from "); + subSql.append(" projects as childProject "); + subSql.append("where "); + subSql.append(" (childProject.id=" + project.getId() + " or childProject.parent_id=" + project.getId() + ")"); + subSql.append(" and childProject.status=1 and childProject.is_public=1"); + } + else + { + subSql.append(project.getId()); + } + + StringList sql = new StringList(); + sql.append("SELECT "); + sql.append(" concat_ws('-', year(created_on), weekofyear(created_on)) as date, "); + sql.append(" count(*) as count "); + sql.append("FROM "); + sql.append(" issues "); + sql.append("WHERE "); + sql.append(" project_id in (" + subSql.toString() + ") "); + sql.append("GROUP BY "); + sql.append(" date;"); + + // logger.debug(sql.toStringSeparatedBy("\n")); + + this.connection.setAutoCommit(true); + statement = this.connection.prepareStatement(sql.toString()); + resultSet = statement.executeQuery(); + + while (resultSet.next()) + { + result.put(resultSet.getString(1), new DateCount(resultSet.getString(1), resultSet.getLong(2))); + } + } + catch (SQLException exception) + { + throw new AgirStatoolException("Error fetching day created count: " + exception.getMessage(), exception); + } + finally + { + SQLUtils.closeQuietly(statement, resultSet); + } + + // + return result; + } + /** * Gets the project all. * @@ -240,10 +394,10 @@ public class AgirStatool // StringList sql = new StringList(); sql.append("SELECT"); - sql.append(" 0,"); - sql.append(" 'all',"); - sql.append(" '*',"); - sql.append(" null,"); + sql.append(" 0 as root_project_id,"); + sql.append(" 'all' as root_project_identifier,"); + sql.append(" '*' as root_project_name,"); + sql.append(" null as root_parent_id,"); sql.append(" (select count(*) from projects where status = 1 and is_public = 1) as child_count, "); sql.append(" (select max(updated_on) from projects where status = 1 and is_public = 1) as last_update,"); sql.append(" (select count(*) from issues where issues.project_id in (" + subSql.toString() + ")) as issue_count,"); @@ -402,6 +556,36 @@ public class AgirStatool } } + // Fill created and concluded issues history. + for (Project project : projects) + { + logger.info("Fetching Created/Closed history for " + project.getName()); + if (project.hasIssue()) + { + ProjectMode mode; + if (project.getName().startsWith("@")) + { + mode = ProjectMode.ALONE; + } + else + { + mode = ProjectMode.CONSOLIDATED; + } + + { + DateCountMap map = fetchWeekCreatedCount(project, mode); + DateCountList counts = normalizedWeekCountList(map, project.issueStats().getFirstCreate().toLocalDate()); + project.issueStats().setWeekCreatedIssueCounts(counts); + } + { + DateCountMap map = fetchWeekConcludedCount(project, mode); + DateCountList counts = normalizedWeekCountList(map, project.issueStats().getFirstCreate().toLocalDate()); + project.issueStats().setWeekClosedIssueCounts(counts); + } + } + } + logger.info("Fetching Created/Closed history done."); + // Transform as tree. for (Project project : projects) { @@ -432,7 +616,7 @@ public class AgirStatool { Projects result; - result = listProjectsWithStats(false); + result = listProjectsWithStats(ProjectMode.ALONE); // return result; } @@ -446,7 +630,7 @@ public class AgirStatool * @throws AgirStatoolException * the agir statool exception */ - public Projects listProjectsWithStats(final boolean consolidated) throws AgirStatoolException + public Projects listProjectsWithStats(final ProjectMode mode) throws AgirStatoolException { Projects result; @@ -458,9 +642,8 @@ public class AgirStatool try { StringList subSql = new StringList(); - if (consolidated) + if (mode == ProjectMode.CONSOLIDATED) { - subSql.append("select "); subSql.append(" id "); subSql.append("from "); @@ -585,7 +768,41 @@ public class AgirStatool { Projects result; - result = listProjectsWithStats(true); + result = listProjectsWithStats(ProjectMode.CONSOLIDATED); + + // + return result; + } + + /** + * Builds the week created count list. + * + * @param source + * the source + * @param start + * the start + * @return the date count list + */ + public DateCountList normalizedWeekCountList(final DateCountMap source, final LocalDate start) + { + DateCountList result; + + result = new DateCountList(); + + LocalDate end = LocalDate.now(); + LocalDate date = start; + long count = 0; + while (date.isBefore(end) || date.isEqual(end)) + { + String dateToken = date.format(DateTimeFormatter.ofPattern("yyyy-w")); + DateCount current = source.get(dateToken); + if (current != null) + { + count += current.getCount(); + } + result.add(new DateCount(dateToken, count)); + date = date.plusDays(7); + } // return result; @@ -638,28 +855,4 @@ public class AgirStatool throw new AgirStatoolException("Error refreshing page: " + exception.getMessage(), exception); } } - - /** - * Refresh welcomme page. - * - * @param project - * the project - * @throws AgirStatoolException - * the agir statool exception - */ - public void refreshWelcomePage(final Project root) throws AgirStatoolException - { - try - { - if (root != null) - { - String page = ProjectPage.build(root); - FileUtils.write(new File(this.targetDirectory, "index.xhtml"), page, StandardCharsets.UTF_8); - } - } - catch (IOException exception) - { - throw new AgirStatoolException("Error refreshing welcome page: " + exception.getMessage(), exception); - } - } } diff --git a/src/org/april/agirstatool/core/IssueStats.java b/src/org/april/agirstatool/core/IssueStats.java index b91f992..373da6d 100644 --- a/src/org/april/agirstatool/core/IssueStats.java +++ b/src/org/april/agirstatool/core/IssueStats.java @@ -20,6 +20,8 @@ package org.april.agirstatool.core; import java.time.LocalDateTime; +import org.april.agirstatool.charts.DateCountList; + /** * The Class Projects. */ @@ -48,6 +50,9 @@ public class IssueStats private LocalDateTime firstCreate; private LocalDateTime lastUpdate; + private DateCountList weekCreatedIssueCounts; + private DateCountList weekClosedIssueCounts; + /** * Instantiates a new issue stats. */ @@ -225,6 +230,16 @@ public class IssueStats return this.waitingCount; } + public DateCountList getWeekClosedIssueCounts() + { + return this.weekClosedIssueCounts; + } + + public DateCountList getWeekCreatedIssueCounts() + { + return this.weekCreatedIssueCounts; + } + public void setClosedCount(final long closedCount) { this.closedCount = closedCount; @@ -324,4 +339,14 @@ public class IssueStats { this.waitingCount = waitingCount; } + + public void setWeekClosedIssueCounts(final DateCountList weekClosedIssueCounts) + { + this.weekClosedIssueCounts = weekClosedIssueCounts; + } + + public void setWeekCreatedIssueCounts(final DateCountList weekCreatedIssueCounts) + { + this.weekCreatedIssueCounts = weekCreatedIssueCounts; + } } diff --git a/src/org/april/agirstatool/core/Project.java b/src/org/april/agirstatool/core/Project.java index 3f091f6..5db3488 100644 --- a/src/org/april/agirstatool/core/Project.java +++ b/src/org/april/agirstatool/core/Project.java @@ -107,6 +107,26 @@ public class Project return result; } + /** + * @return + */ + public boolean hasIssue() + { + boolean result; + + if (this.issueStats().getCount() == 0) + { + result = false; + } + else + { + result = true; + } + + // + return result; + } + public IssueStats issueStats() { return this.stats; diff --git a/src/org/april/agirstatool/core/ProjectMode.java b/src/org/april/agirstatool/core/ProjectMode.java new file mode 100644 index 0000000..4d712c1 --- /dev/null +++ b/src/org/april/agirstatool/core/ProjectMode.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 Christian Pierre MOMON + * + * This file is part of AgirStatool, simple key value database. + * + * AgirStatool is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * AgirStatool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with AgirStatool. If not, see . + */ +package org.april.agirstatool.core; + +/** + * The Enum ProjectMode. + */ +public enum ProjectMode +{ + ALONE, + CONSOLIDATED +} \ No newline at end of file diff --git a/src/org/april/agirstatool/core/pages/CreatedConcludedCountChartView.java b/src/org/april/agirstatool/core/pages/CreatedConcludedCountChartView.java new file mode 100644 index 0000000..33059e3 --- /dev/null +++ b/src/org/april/agirstatool/core/pages/CreatedConcludedCountChartView.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 Christian Pierre MOMON + * + * This file is part of AgirStatool, simple key value database. + * + * AgirStatool is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * AgirStatool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with AgirStatool. If not, see . + */ +package org.april.agirstatool.core.pages; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.apache.commons.codec.digest.DigestUtils; +import org.april.agirstatool.core.AgirStatool; +import org.april.agirstatool.core.AgirStatoolException; +import org.april.agirstatool.core.AgirStatoolUtils; +import org.april.agirstatool.core.Project; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fr.devinsy.strings.StringList; +import fr.devinsy.xidyn.utils.XidynUtils; + +/** + * The Class projectsRawPageBuilder. + */ +public class CreatedConcludedCountChartView +{ + private static Logger logger = LoggerFactory.getLogger(CreatedConcludedCountChartView.class); + + /** + * Builds the. + * + * @param projects + * the projects + * @return the string + * @throws AgirStatoolException + * the agir statool exception + */ + public static String build(final String title, final Project project) throws AgirStatoolException + { + String result; + + try + { + logger.info("Building chartBar view…"); + + if (project.hasIssue() && !project.getName().equals("*")) + { + String source = XidynUtils.load(AgirStatool.class.getResource("/org/april/agirstatool/core/pages/chartLineView.xhtml")); + String code = XidynUtils.extractBodyContent(source); + + code = code.replaceAll("myChart", "myChart_" + DigestUtils.md5Hex(title + "lineBar")); + + StringList labels = buildWeekLabels(project.issueStats().getFirstCreate()); + code = code.replaceAll("labels: \\[.*\\]", "labels: " + AgirStatoolUtils.toJSonStrings(labels)); + + StringList values = project.issueStats().getWeekCreatedIssueCounts().toValueList(); + code = code.replaceAll("data: \\[.*\\]", "data: " + AgirStatoolUtils.toJSonNumbers(values)); + + values = project.issueStats().getWeekClosedIssueCounts().toValueList(); + code = code.replaceAll("data: \\[.*\\] ", "data: " + AgirStatoolUtils.toJSonNumbers(values)); + + result = code.toString(); + } + else + { + result = "No issue."; + } + } + catch (IOException exception) + { + throw new AgirStatoolException("Error building ProjectsRaw view: " + exception.getMessage(), exception); + } + + // + return result; + } + + /** + * Builds the 3 months. + * + * @param title + * the title + * @param project + * the project + * @return the string + * @throws AgirStatoolException + * the agir statool exception + */ + public static String build3months(final String title, final Project project) throws AgirStatoolException + { + String result; + + try + { + logger.info("Building created/closed 3 months chart view…"); + + if (project.hasIssue() && !project.getName().equals("*")) + { + String source = XidynUtils.load(AgirStatool.class.getResource("/org/april/agirstatool/core/pages/chartLineView.xhtml")); + String code = XidynUtils.extractBodyContent(source); + + code = code.replaceAll("myChart", "myChart_" + DigestUtils.md5Hex(title + "lineChart3months")); + + StringList labels = buildWeekLabels(project.issueStats().getFirstCreate()); + code = code.replaceAll("labels: \\[.*\\]", "labels: " + AgirStatoolUtils.toJSonStrings(labels)); + + StringList values = project.issueStats().getWeekCreatedIssueCounts().toValueList(); + code = code.replaceAll("data: \\[.*\\]", "data: " + AgirStatoolUtils.toJSonNumbers(values)); + + values = project.issueStats().getWeekClosedIssueCounts().toValueList(); + code = code.replaceAll("data: \\[.*\\] ", "data: " + AgirStatoolUtils.toJSonNumbers(values)); + + result = code.toString(); + } + else + { + result = "No issue."; + } + } + catch (IOException exception) + { + throw new AgirStatoolException("Error building ProjectsRaw view: " + exception.getMessage(), exception); + } + + // + return result; + } + + private static StringList buildWeekLabels(final LocalDateTime start) + { + StringList result; + + result = new StringList(); + + if (start != null) + { + LocalDate end = LocalDate.now(); + LocalDate date = start.toLocalDate(); + while (date.isBefore(end) || date.isEqual(end)) + { + // if + // (date.get(WeekFields.of(Locale.getDefault()).weekOfMonth()) + // == 1) + // { + String label = date.format(DateTimeFormatter.ofPattern("yyyy-MMM")); + result.add(label); + // } + // else + // { + // result.add(""); + // } + date = date.plusWeeks(1); + } + } + + // + return result; + } +} diff --git a/src/org/april/agirstatool/core/pages/ProjectPage.java b/src/org/april/agirstatool/core/pages/ProjectPage.java index 50e38a1..83261d1 100644 --- a/src/org/april/agirstatool/core/pages/ProjectPage.java +++ b/src/org/april/agirstatool/core/pages/ProjectPage.java @@ -55,9 +55,11 @@ public class ProjectPage TagDataManager data = new TagDataManager(); - data.setContent("projectName", project.getName()); + data.setContent("agirLink", project.getName()); data.setAttribute("agirLink", "href", "https://agir.april.org/projects/" + project.getIdentifier() + "/issues"); + data.setContent("issueCreatedClosedChart", CreatedConcludedCountChartView.build("Created/closed Count", project)); + data.setContent("issueRawChart", IssueStatChartView.build("Issue Raw Count", project)); data.setContent("issueGroupedChart", IssueStatChartView.buildGrouped("Issue Grouped Count", project)); data.setContent("unassignedRawChart", UnassignedPolarChartView.build("Unassigned Raw Count", project)); diff --git a/src/org/april/agirstatool/core/pages/chartLineView.xhtml b/src/org/april/agirstatool/core/pages/chartLineView.xhtml index ef3685b..fdf4f4b 100644 --- a/src/org/april/agirstatool/core/pages/chartLineView.xhtml +++ b/src/org/april/agirstatool/core/pages/chartLineView.xhtml @@ -11,7 +11,7 @@ -
+