doortts doortts 2017-02-02
issue: Support issue template feature
If you create ISSUE_TEMPLATE.md file at your repository root,
then it will be used for issue template for the project.

Also, user can edit issue template at project setting page
without pushing ISSUE_TEMPLATE.md to the repository

This feature was inspired by Github's same functionality.

See:
- https://help.github.com/articles/creating-an-issue-template-for-your-repository
- Yona Github issue #136
@753f72250b081fd3facd1262f5c4cd3245e5e686
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -3442,7 +3442,7 @@
         width: 99%; /*946px;/*866px;*/
     }
     textarea.content{
-      height: 250px;
+      height: 300px;
     }
     textarea.text {
         resize:vertical;
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -17,6 +17,7 @@
 import models.enumeration.ResourceType;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
+import play.api.data.validation.ValidationError;
 import play.data.Form;
 import play.db.ebean.Transactional;
 import play.libs.Json;
@@ -167,6 +168,10 @@
             preparedBodyText = BareRepository.readREADME(project);
         }
 
+        if(issueTemplateEditRequested()){
+            preparedBodyText = StringUtils.defaultIfBlank(project.getIssueTemplate(), "");
+        }
+
         return ok(create.render("post.new", new Form<>(Posting.class), project, isAllowedToNotice, preparedBodyText));
     }
 
@@ -176,6 +181,10 @@
 
     private static boolean readmeEditRequested() {
         return request().getQueryString("readme") != null;
+    }
+
+    private static boolean issueTemplateEditRequested() {
+        return request().getQueryString("issueTemplate") != null;
     }
 
     @Transactional
@@ -209,6 +218,12 @@
                 commitReadmeFile(project, post);
             }
         }
+
+        if (post.issueTemplate != null) {
+            commitIssueTemplateFile(project, post);
+            return redirect(routes.ProjectApp.project(project.owner, projectName));
+        }
+
         post.save();
         attachUploadFilesToPost(post.asResource());
         NotificationEvent.afterNewPost(post);
@@ -229,6 +244,16 @@
         }
     }
 
+    private static void commitIssueTemplateFile(Project project, Posting post){
+        BareCommit bare = new BareCommit(project, UserApp.currentUser());
+        try{
+            bare.commitTextFile("ISSUE_TEMPLATE.md", post.body, post.title);
+        } catch (IOException e) {
+            e.printStackTrace();
+            play.Logger.error(e.getMessage());
+        }
+    }
+
     @IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST)
     public static Result post(String userName, String projectName, Long number) {
         Project project = Project.findByOwnerAndProjectName(userName, projectName);
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -1,23 +1,9 @@
 /**
- * Yobi, Project Hosting SW
- *
- * Copyright 2012 NAVER Corp.
- * http://yobi.io
- *
- * @author Tae
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp.
+ * https://yona.io
+ **/
 package controllers;
 
 import actions.NullProjectCheckAction;
@@ -290,7 +276,8 @@
     @IsCreatable(ResourceType.ISSUE_POST)
     public static Result newIssueForm(String ownerName, String projectName) {
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
-        return ok(create.render("title.newIssue", new Form<>(Issue.class), project));
+        String issueTemplate = StringUtils.defaultIfBlank(project.getIssueTemplate(), "");
+        return ok(create.render("title.newIssue", new Form<>(Issue.class), project, issueTemplate));
     }
 
     @Transactional
@@ -418,7 +405,8 @@
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
 
         if (issueForm.hasErrors()) {
-            return badRequest(create.render("error.validation", issueForm, project));
+            String issueTemplate = StringUtils.defaultIfBlank(project.getIssueTemplate(), "");
+            return badRequest(create.render("error.validation", issueForm, project, issueTemplate));
         }
 
         final Issue newIssue = issueForm.get();
app/models/Posting.java
--- app/models/Posting.java
+++ app/models/Posting.java
@@ -26,6 +26,9 @@
     public boolean notice;
     public boolean readme;
 
+    @Transient
+    public String issueTemplate;
+
     @OneToMany(cascade = CascadeType.ALL)
     public List<PostingComment> comments;
 
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -1,23 +1,9 @@
 /**
- * Yobi, Project Hosting SW
- *
- * Copyright 2012 NAVER Corp.
- * http://yobi.io
- *
- * @author Hwi Ahn
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp.
+ * https://yona.io
+ **/
 package models;
 
 import com.avaje.ebean.Ebean;
@@ -315,6 +301,16 @@
         }
     }
 
+    public String getIssueTemplate() {
+        try {
+            byte[] bytes = RepositoryService.getRepository(this)
+                    .getRawFile("HEAD", "ISSUE_TEMPLATE.md");
+            return new String(bytes, FileUtil.detectCharset(bytes));
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     /**
      * @return the readme file name or {@code null} if the file does not exist
      * @throws IOException Signals that an I/O exception has occurred.
app/views/board/create.scala.html
--- app/views/board/create.scala.html
+++ app/views/board/create.scala.html
@@ -1,24 +1,12 @@
 @**
-* Yobi, Project Hosting SW
+* Yona, 21st Century Project Hosting SW
 *
-* Copyright 2012 NAVER Corp.
-* http://yobi.io
-*
-* @author Ahn Hyeok Jun
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
 **@
-@(title:String, form: play.data.Form[Posting], project:Project, isAllowedToNotice:Boolean, preparedPostBody:String = "")
+@import play.data.Form
+@import org.apache.commons.lang3.StringUtils
+@(title:String, form: Form[Posting], project:Project, isAllowedToNotice:Boolean, preparedPostBody:String = "")
 
 @import utils.AccessControl._
 @import utils.TemplateHelper._
@@ -26,9 +14,11 @@
 @import models.enumeration._
 @implicitField = @{ helper.FieldConstructor(simpleForm) }
 
-@readmeUpdateMessage = @{
+@titleMessage = @{
   if( !requestHeader.getQueryString("readme").equals(None) ) {
       "Update README.md"
+  } else if( !requestHeader.getQueryString("issueTemplate").equals(None) ) {
+      "ISSUE_TEMPLATE.md: Project Issue Template"
   }
 }
 
@@ -44,7 +34,7 @@
           </dt>
           <dd>
             @defining(form.errors().get("title")) { errors =>
-              <input type="text" id="title" name="title" class="zen-mode text title @if(errors != null) {error}" maxlength="250" tabindex="1" value="@readmeUpdateMessage"/>
+              <input type="text" id="title" name="title" class="zen-mode text title @if(errors != null) {error}" maxlength="250" tabindex="1" value="@titleMessage"/>
               @if(errors != null) {
                 <div class="message">
                 @for(error <- errors) {
@@ -54,24 +44,34 @@
               }
             }
           </dd>
+            <dd>
+            @if(!requestHeader.getQueryString("issueTemplate").equals(None)){
+              <div class="attach-wrap">
+                <span class="help help-droppable">@Messages("issue.template.no.attachment.allow")</span>
+              </div>
+            }
+            </dd>
           <dd style="position: relative;">
             @common.editor("body", preparedPostBody, "tabindex=2")
           </dd>
         </dl>
           @** fileUploader **@
-        @if(!UserApp.currentUser.isAnonymous) {
+        @if(requestHeader.getQueryString("issueTemplate").equals(None)) {
+          @if(!UserApp.currentUser.isAnonymous) {
             @common.fileUploader(ResourceType.BOARD_POST, null)
+          }
         }
         @** end of fileUploader **@
 
         <div class="right-txt mt10 mb10">
-          @if(isAllowedToNotice ){
+          @if(isAllowedToNotice && requestHeader.getQueryString("issueTemplate").equals(None)){
           <label class="checkbox">
             <input type="checkbox" id="notice" name="notice">
             @Messages("post.notice.label")
           </label>
           }
 
+          <input type="hidden" id="issueTemplate" name="issueTemplate" @boolToCheckedString(!requestHeader.getQueryString("issueTemplate").equals(None))>
           @if(isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.COMMIT)){
             @if(project.isGit && !requestHeader.getQueryString("readme").equals(None)){
             <label class="checkbox">
app/views/issue/create.scala.html
--- app/views/issue/create.scala.html
+++ app/views/issue/create.scala.html
@@ -1,24 +1,11 @@
 @**
-* Yobi, Project Hosting SW
+* Yona, 21st Century Project Hosting SW
 *
-* Copyright 2012 NAVER Corp.
-* http://yobi.io
-*
-* @author Ahn Hyeok Jun
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
 **@
-@(title:String, issueForm: play.data.Form[Issue], project:Project)
+@import play.data.Form
+@(title:String, issueForm: Form[Issue], project:Project, issueTemplate:String = "")
 @import helper._
 @import scala.collection.mutable.Map
 @import models.enumeration.ResourceType
@@ -58,7 +45,7 @@
                 <div class="span9 span-left-pane">
                     <dl>
                         <dd style="position: relative;">
-                            @common.editor("body", "", "tabindex=2", "content-body")
+                            @common.editor("body", issueTemplate, "tabindex=2", "content-body")
                         </dd>
                     </dl>
 
app/views/project/setting.scala.html
--- app/views/project/setting.scala.html
+++ app/views/project/setting.scala.html
@@ -1,24 +1,11 @@
 @**
-* Yobi, Project Hosting SW
+* Yona, 21st Century Project Hosting SW
 *
-* Copyright 2012 NAVER Corp.
-* http://yobi.io
-*
-* @author Sangcheol Hwang
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*   http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
+* Copyright Yona & Yobi Authors & NAVER Corp.
+* https://yona.io
 **@
-@(message: String)(projectForm: play.data.Form[Project], project: Project, branches: List[String])
+@import play.data.Form
+@(message: String)(projectForm: Form[Project], project: Project, branches: List[String])
 
 @import helper._
 @import utils.TemplateHelper._
@@ -88,6 +75,12 @@
                     </div>
                 </div>
                 <div class="box-wrap middle">
+                    <div class="cu-label">@Messages("issue.template")</div>
+                    <div class="cu-desc">
+                        <a href="@routes.BoardApp.newPostForm(project.owner, project.name)?issueTemplate=true" class="ybtn" target="_blank">@Messages("issue.template.edit")</a>
+                    </div>
+                </div>
+                <div class="box-wrap middle">
                     <div class="cu-label">@Messages("project.codeAccessible")</div>
                     <div class="cu-desc">
                         <input name="isCodeAccessibleMemberOnly" type="radio" id="codeAccessibleMemberOnly" class="radio-btn" value="true" @if(project.isCodeAccessibleMemberOnly){checked="checked"}><label for="codeAccessibleMemberOnly" class="bg-radiobtn label-public">@Messages("button.yes")</label>
conf/messages
--- conf/messages
+++ conf/messages
@@ -286,6 +286,9 @@
 issue.state.closed = Closed
 issue.state.enrolled = Status entered
 issue.state.open = Open
+issue.template = Issue Template
+issue.template.edit = Edit
+issue.template.no.attachment.allow = Issue templates do not support attachments.
 issue.unvote.description = Click here if you no longer agree with this issue.
 issue.unwatch = Unsubscribe this issue
 issue.unwatch.start = You will no longer get notifications about this issue
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -285,6 +285,9 @@
 issue.state.closed = 닫힘
 issue.state.enrolled = 등록
 issue.state.open = 열림
+issue.template = 이슈 템플릿
+issue.template.edit = 편집
+issue.template.no.attachment.allow = 이슈 템플릿 작성시에는 첨부파일  기능을 지원하지 않습니다.
 issue.unvote.description = 공감을 취소하려면 버튼을 누릅니다.
 issue.unwatch = 이슈 그만지켜보기
 issue.unwatch.start = 이제 이 이슈에 관한 알림을 받지 않습니다
Add a comment
List