reviews: Support reviews items to download as xls file
See: Yona github issue #90
@bf37fd2f58b4c51aa59410bccc213080194f0105
--- app/controllers/ReviewThreadApp.java
+++ app/controllers/ReviewThreadApp.java
... | ... | @@ -1,43 +1,37 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2013 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Changsung Kim, Keesun Baik |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
21 | 7 |
package controllers; |
22 | 8 |
|
23 | 9 |
import com.avaje.ebean.ExpressionList; |
24 | 10 |
import com.avaje.ebean.Page; |
25 | 11 |
import controllers.annotation.AnonymousCheck; |
26 | 12 |
import controllers.annotation.IsAllowed; |
27 |
-import models.CommentThread; |
|
28 |
-import models.Project; |
|
13 |
+import jxl.Workbook; |
|
14 |
+import jxl.format.ScriptStyle; |
|
15 |
+import jxl.format.UnderlineStyle; |
|
16 |
+import jxl.write.*; |
|
17 |
+import models.*; |
|
29 | 18 |
import models.enumeration.Operation; |
30 | 19 |
import models.support.ReviewSearchCondition; |
20 |
+import org.apache.commons.lang3.StringUtils; |
|
21 |
+import org.apache.tika.Tika; |
|
31 | 22 |
import play.data.Form; |
32 | 23 |
import play.db.ebean.Transactional; |
33 | 24 |
import play.mvc.Controller; |
34 | 25 |
import play.mvc.Result; |
26 |
+import utils.HttpUtil; |
|
27 |
+import utils.JodaDateUtil; |
|
35 | 28 |
import views.html.reviewthread.list; |
36 | 29 |
|
37 |
-/** |
|
38 |
- * @author Changsung Kim |
|
39 |
- * @author Keesun Baik |
|
40 |
- */ |
|
30 |
+import java.io.ByteArrayOutputStream; |
|
31 |
+import java.io.IOException; |
|
32 |
+import java.util.Date; |
|
33 |
+import java.util.List; |
|
34 |
+ |
|
41 | 35 |
@AnonymousCheck |
42 | 36 |
public class ReviewThreadApp extends Controller { |
43 | 37 |
|
... | ... | @@ -50,8 +44,129 @@ |
50 | 44 |
Project project = Project.findByOwnerAndProjectName(ownerName, projectName); |
51 | 45 |
ReviewSearchCondition searchCondition = Form.form(ReviewSearchCondition.class).bindFromRequest().get(); |
52 | 46 |
ExpressionList<CommentThread> el = searchCondition.asExpressionList(project); |
47 |
+ if ("xls".equals(request().getQueryString("format"))) { |
|
48 |
+ return reviewThreadsDownload(project, el); |
|
49 |
+ } |
|
53 | 50 |
Page<CommentThread> commentThreads = el.findPagingList(REVIEWS_PER_PAGE).getPage(searchCondition.pageNum - 1); |
54 | 51 |
return ok(list.render(project, commentThreads, searchCondition)); |
55 | 52 |
} |
56 | 53 |
|
54 |
+ private static Result reviewThreadsDownload(Project project, ExpressionList<CommentThread> el) { |
|
55 |
+ List<CommentThread> commentThreads = el.findList(); |
|
56 |
+ |
|
57 |
+ String filename = null; |
|
58 |
+ byte[] excelData = null; |
|
59 |
+ try { |
|
60 |
+ excelData = excelFrom(commentThreads); |
|
61 |
+ filename = HttpUtil.encodeContentDisposition( |
|
62 |
+ project.name + "_reviews_" + JodaDateUtil.getDateStringWithoutSpace(new Date()) + ".xls"); |
|
63 |
+ } catch (WriteException | IOException e) { |
|
64 |
+ e.printStackTrace(); |
|
65 |
+ } |
|
66 |
+ |
|
67 |
+ response().setHeader("Content-Type", new Tika().detect(filename)); |
|
68 |
+ response().setHeader("Content-Disposition", "attachment; " + filename); |
|
69 |
+ |
|
70 |
+ assert excelData != null; |
|
71 |
+ return ok(excelData); |
|
72 |
+ } |
|
73 |
+ |
|
74 |
+ public static byte[] excelFrom(List<CommentThread> commentThreads) throws WriteException, IOException { |
|
75 |
+ WritableWorkbook workbook; |
|
76 |
+ WritableSheet sheet; |
|
77 |
+ |
|
78 |
+ WritableCellFormat headerCellFormat = getHeaderCellFormat(); |
|
79 |
+ WritableCellFormat bodyCellFormat = getBodyCellFormat(); |
|
80 |
+ WritableCellFormat dateCellFormat = getDateCellFormat(); |
|
81 |
+ |
|
82 |
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
|
83 |
+ workbook = Workbook.createWorkbook(bos); |
|
84 |
+ sheet = workbook.createSheet(String.valueOf(JodaDateUtil.today().getTime()), 0); |
|
85 |
+ |
|
86 |
+ String[] titles = {"No", "COMMIT ID", "REVIEW ID", "REVIEW TITLE", "Thread Author", "Response Text", "Response", "REVIEW STATE", "is PullRequest?", "Date"}; |
|
87 |
+ |
|
88 |
+ for (int i = 0; i < titles.length; i++) { |
|
89 |
+ sheet.addCell(new jxl.write.Label(i, 0, titles[i], headerCellFormat)); |
|
90 |
+ sheet.setColumnView(i, 20); |
|
91 |
+ } |
|
92 |
+ |
|
93 |
+ int rowNumber = 0; |
|
94 |
+ for (int idx = 0; idx < commentThreads.size(); idx++) { |
|
95 |
+ CommentThread commentThread = commentThreads.get(idx); |
|
96 |
+ String commitId = ""; |
|
97 |
+ if ( commentThread instanceof NonRangedCodeCommentThread){ |
|
98 |
+ commitId = ((NonRangedCodeCommentThread) commentThread).commitId; |
|
99 |
+ } else { |
|
100 |
+ commitId = ((CodeCommentThread) commentThread).commitId; |
|
101 |
+ } |
|
102 |
+ String threadFirstComment = commentThread.getFirstReviewComment().getContents(); |
|
103 |
+ for (int j = 0; j < commentThread.reviewComments.size(); j++) { |
|
104 |
+ ReviewComment comment = commentThread.reviewComments.get(j); |
|
105 |
+ int columnPos = 0; |
|
106 |
+ String reponseComment = threadFirstComment.equals(comment.getContents())? "":comment.getContents(); |
|
107 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, "" + (rowNumber + 1), bodyCellFormat)); |
|
108 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, commitId.substring(0, 7), bodyCellFormat)); |
|
109 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, commentThread.id.toString(), bodyCellFormat)); |
|
110 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, StringUtils.isEmpty(reponseComment)?threadFirstComment:"", bodyCellFormat)); |
|
111 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, StringUtils.isEmpty(reponseComment)?commentThread.author.name:"", bodyCellFormat)); |
|
112 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, reponseComment, bodyCellFormat)); |
|
113 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, StringUtils.isNotEmpty(reponseComment)?comment.author.name:"", bodyCellFormat)); |
|
114 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, commentThread.state.toString(), bodyCellFormat)); |
|
115 |
+ sheet.addCell(new jxl.write.Label(columnPos++, rowNumber + 1, "" + commentThread.isOnPullRequest(), bodyCellFormat)); |
|
116 |
+ sheet.addCell(new jxl.write.DateTime(columnPos++, rowNumber + 1, comment.createdDate, dateCellFormat)); |
|
117 |
+ rowNumber++; |
|
118 |
+ } |
|
119 |
+ } |
|
120 |
+ workbook.write(); |
|
121 |
+ |
|
122 |
+ try { |
|
123 |
+ workbook.close(); |
|
124 |
+ } catch (WriteException | IOException e) { |
|
125 |
+ e.printStackTrace(); |
|
126 |
+ } |
|
127 |
+ |
|
128 |
+ return bos.toByteArray(); |
|
129 |
+ } |
|
130 |
+ |
|
131 |
+ private static String getIssueLabels(Issue issue) { |
|
132 |
+ StringBuilder labels = new StringBuilder(); |
|
133 |
+ for(IssueLabel issueLabel: issue.getLabels()){ |
|
134 |
+ labels.append(issueLabel.name).append(", "); |
|
135 |
+ } |
|
136 |
+ return labels.toString().replaceAll(", $", ""); |
|
137 |
+ } |
|
138 |
+ |
|
139 |
+ private static WritableCellFormat getDateCellFormat() throws WriteException { |
|
140 |
+ WritableFont baseFont= new WritableFont(WritableFont.ARIAL, 12, WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE, jxl.format.Colour.BLACK, ScriptStyle.NORMAL_SCRIPT); |
|
141 |
+ DateFormat valueFormatDate = new DateFormat("yyyy-MM-dd HH:mm"); |
|
142 |
+ WritableCellFormat cellFormat = new WritableCellFormat(valueFormatDate); |
|
143 |
+ cellFormat.setFont(baseFont); |
|
144 |
+ cellFormat.setShrinkToFit(true); |
|
145 |
+ cellFormat.setBorder(jxl.format.Border.ALL, jxl.format.BorderLineStyle.THIN); |
|
146 |
+ cellFormat.setAlignment(jxl.format.Alignment.CENTRE); |
|
147 |
+ cellFormat.setVerticalAlignment(jxl.format.VerticalAlignment.TOP); |
|
148 |
+ return cellFormat; |
|
149 |
+ } |
|
150 |
+ |
|
151 |
+ private static WritableCellFormat getBodyCellFormat() throws WriteException { |
|
152 |
+ WritableFont baseFont = new WritableFont(WritableFont.ARIAL, 12, WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE, jxl.format.Colour.BLACK, ScriptStyle.NORMAL_SCRIPT); |
|
153 |
+ return getBodyCellFormat(baseFont); |
|
154 |
+ } |
|
155 |
+ |
|
156 |
+ private static WritableCellFormat getBodyCellFormat(WritableFont baseFont) throws WriteException { |
|
157 |
+ WritableCellFormat cellFormat = new WritableCellFormat(baseFont); |
|
158 |
+ cellFormat.setBorder(jxl.format.Border.ALL, jxl.format.BorderLineStyle.THIN); |
|
159 |
+ cellFormat.setWrap(true); |
|
160 |
+ cellFormat.setVerticalAlignment(jxl.format.VerticalAlignment.TOP); |
|
161 |
+ return cellFormat; |
|
162 |
+ } |
|
163 |
+ |
|
164 |
+ private static WritableCellFormat getHeaderCellFormat() throws WriteException { |
|
165 |
+ WritableFont headerFont = new WritableFont(WritableFont.ARIAL, 14, WritableFont.BOLD, false, |
|
166 |
+ UnderlineStyle.NO_UNDERLINE, jxl.format.Colour.BLACK, ScriptStyle.NORMAL_SCRIPT); |
|
167 |
+ WritableCellFormat headerCell = new WritableCellFormat(headerFont); |
|
168 |
+ headerCell.setBorder(jxl.format.Border.ALL, jxl.format.BorderLineStyle.DOUBLE); |
|
169 |
+ headerCell.setAlignment(jxl.format.Alignment.CENTRE); |
|
170 |
+ return headerCell; |
|
171 |
+ } |
|
57 | 172 |
} |
--- app/models/Issue.java
+++ app/models/Issue.java
... | ... | @@ -1,23 +1,9 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2012 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author yoon |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
21 | 7 |
package models; |
22 | 8 |
|
23 | 9 |
import com.avaje.ebean.Ebean; |
... | ... | @@ -36,6 +22,7 @@ |
36 | 22 |
import models.enumeration.State; |
37 | 23 |
import models.resource.Resource; |
38 | 24 |
import models.support.SearchCondition; |
25 |
+import org.apache.commons.lang3.StringUtils; |
|
39 | 26 |
import org.apache.commons.lang3.time.DateUtils; |
40 | 27 |
import org.apache.shiro.util.CollectionUtils; |
41 | 28 |
import play.data.Form; |
... | ... | @@ -55,9 +42,6 @@ |
55 | 42 |
@Entity |
56 | 43 |
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "number"})) |
57 | 44 |
public class Issue extends AbstractPosting implements LabelOwner { |
58 |
- /** |
|
59 |
- * @author Yobi TEAM |
|
60 |
- */ |
|
61 | 45 |
private static final long serialVersionUID = -2409072006294045262L; |
62 | 46 |
|
63 | 47 |
public static final Finder<Long, Issue> finder = new Finder<>(Long.class, Issue.class); |
... | ... | @@ -238,7 +222,7 @@ |
238 | 222 |
workbook = Workbook.createWorkbook(bos); |
239 | 223 |
sheet = workbook.createSheet(String.valueOf(JodaDateUtil.today().getTime()), 0); |
240 | 224 |
|
241 |
- String[] titles = {"No", Messages.get("issue.state"), Messages.get("title"), Messages.get("issue.assignee"), Messages.get("issue.label"), "Date", "Link"}; |
|
225 |
+ String[] titles = {"No", Messages.get("issue.state"), Messages.get("title"), Messages.get("issue.assignee"), Messages.get("issue.label"), "Created", "Due Date", "Source"}; |
|
242 | 226 |
|
243 | 227 |
for (int i = 0; i < titles.length; i++) { |
244 | 228 |
sheet.addCell(new jxl.write.Label(i, 0, titles[i], headerCellFormat)); |
... | ... | @@ -253,15 +237,14 @@ |
253 | 237 |
sheet.addCell(new jxl.write.Label(columnPos++, idx, getAssigneeName(issue.assignee), bodyCellFormat)); |
254 | 238 |
sheet.addCell(new jxl.write.Label(columnPos++, idx, getIssueLabels(issue), bodyCellFormat)); |
255 | 239 |
sheet.addCell(new jxl.write.DateTime(columnPos++, idx, issue.createdDate, dateCellFormat)); |
240 |
+ sheet.addCell(new jxl.write.Label(columnPos++, idx, JodaDateUtil.geYMDDate(issue.dueDate), bodyCellFormat)); |
|
256 | 241 |
sheet.addCell(new jxl.write.Label(columnPos++, idx, controllers.routes.IssueApp.issue(issue.project.owner, issue.project.name, issue.number).toString(), bodyCellFormat)); |
257 | 242 |
} |
258 | 243 |
workbook.write(); |
259 | 244 |
|
260 | 245 |
try { |
261 | 246 |
workbook.close(); |
262 |
- } catch (WriteException e) { |
|
263 |
- e.printStackTrace(); |
|
264 |
- } catch (IOException e) { |
|
247 |
+ } catch (WriteException | IOException e) { |
|
265 | 248 |
e.printStackTrace(); |
266 | 249 |
} |
267 | 250 |
|
... | ... | @@ -278,7 +261,7 @@ |
278 | 261 |
|
279 | 262 |
private static WritableCellFormat getDateCellFormat() throws WriteException { |
280 | 263 |
WritableFont baseFont= new WritableFont(WritableFont.ARIAL, 12, WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE, Colour.BLACK, ScriptStyle.NORMAL_SCRIPT); |
281 |
- DateFormat valueFormatDate = new DateFormat("yyyy-MM-dd HH:mm:ss"); |
|
264 |
+ DateFormat valueFormatDate = new DateFormat("yyyy-MM-dd HH:mm"); |
|
282 | 265 |
WritableCellFormat cellFormat = new WritableCellFormat(valueFormatDate); |
283 | 266 |
cellFormat.setFont(baseFont); |
284 | 267 |
cellFormat.setShrinkToFit(true); |
--- app/utils/JodaDateUtil.java
+++ app/utils/JodaDateUtil.java
... | ... | @@ -103,4 +103,21 @@ |
103 | 103 |
|
104 | 104 |
return momentFromNow(date, Locale.getDefault().getLanguage()); |
105 | 105 |
} |
106 |
+ |
|
107 |
+ public static String getDateStringWithoutSpace(Date date){ |
|
108 |
+ if (date == null) { |
|
109 |
+ date = new Date(); |
|
110 |
+ } |
|
111 |
+ DateTime dateTime = new DateTime(date); |
|
112 |
+ return dateTime.toString("yyyyMMddHHmm", Locale.getDefault()); |
|
113 |
+ } |
|
114 |
+ |
|
115 |
+ |
|
116 |
+ public static String geYMDDate(Date date){ |
|
117 |
+ if (date == null) { |
|
118 |
+ return ""; |
|
119 |
+ } |
|
120 |
+ DateTime dateTime = new DateTime(date); |
|
121 |
+ return dateTime.toString("yyyy-MM-dd", Locale.getDefault()); |
|
122 |
+ } |
|
106 | 123 |
} |
--- app/views/reviewthread/list.scala.html
+++ app/views/reviewthread/list.scala.html
... | ... | @@ -1,27 +1,16 @@ |
1 | 1 |
@** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2014 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Keesun Baik |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- **@ |
|
21 |
-@(project:Project, page:com.avaje.ebean.Page[CommentThread], param:models.support.ReviewSearchCondition) |
|
2 |
+* Yona, 21st Century Project Hosting SW |
|
3 |
+* |
|
4 |
+* Copyright Yona & Yobi Authors & NAVER Corp. |
|
5 |
+* https://yona.io |
|
6 |
+**@ |
|
7 |
+@import com.avaje.ebean.Page |
|
8 |
+@import models.support.ReviewSearchCondition |
|
9 |
+@(project:Project, page:Page[CommentThread], param:ReviewSearchCondition) |
|
22 | 10 |
@import views.html.helper.form |
23 | 11 |
@import utils.TemplateHelper._ |
24 | 12 |
@import utils.JodaDateUtil |
13 |
+@import utils.HttpUtil._ |
|
25 | 14 |
|
26 | 15 |
@makeSortLink(sortField:String, sortFieldString:String) = { |
27 | 16 |
@if(param.orderBy == sortField) { |
... | ... | @@ -97,6 +86,11 @@ |
97 | 86 |
<div class="review-list-wrap"> |
98 | 87 |
@partial_list(project, page, param) |
99 | 88 |
</div> |
89 |
+ <div class="pull-left" style="padding:10px;"> |
|
90 |
+ <a href="@addQueryString(removeQueryString(requestHeader.uri, "pageNum"), "format=xls")" class="ybtn small"> |
|
91 |
+ <i class="yobicon-file-excel"></i> @Messages("issue.downloadAsExcel") |
|
92 |
+ </a> |
|
93 |
+ </div> |
|
100 | 94 |
<div id="pagination"></div> |
101 | 95 |
</div> |
102 | 96 |
</div> |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?