migraion: Support yona to github migration
Suport yona to github migration. It can be processed at /migration url path. eg. https://repo.yona.io/migration
@b3ba0c4b20c14c33e038fa58f3c17255024b2885
--- .gitignore
+++ .gitignore
... | ... | @@ -30,3 +30,4 @@ |
30 | 30 |
conf/generated.keystore |
31 | 31 |
conf/application-logger.xml |
32 | 32 |
.java-version |
33 |
+migration-client |
+++ app/assets/stylesheets/less/_migration.less
... | ... | @@ -0,0 +1,288 @@ |
1 | +.yobi-migration { | |
2 | + .title-text-bg{ | |
3 | + background: #333333; | |
4 | + border-top: 1px solid rgba(85, 85, 85, 0.53); | |
5 | + border-bottom: 1px solid #ccc; | |
6 | + } | |
7 | + .title-text { | |
8 | + font-family: 'Montserrat', sans-serif; | |
9 | + font-size: 20px; | |
10 | + } | |
11 | + .left-border { | |
12 | + border-left: 1px solid #ccc; | |
13 | + } | |
14 | + .comeback-text{ | |
15 | + background: #333333; | |
16 | + color: whitesmoke; | |
17 | + font-family: 'Muli', sans-serif; | |
18 | + font-size: 30px; | |
19 | + text-align: right; | |
20 | + .midium-font { | |
21 | + font-size: 14px; | |
22 | + } | |
23 | + margin-right: 20px; | |
24 | + margin-top: 25px; | |
25 | + } | |
26 | + .head-title { | |
27 | + background-color: white; | |
28 | + padding-bottom: 0 !important; | |
29 | + word-wrap: break-word; | |
30 | + height: 60px; | |
31 | + .source-title { | |
32 | + text-align: right; | |
33 | + } | |
34 | + .destination-title { | |
35 | + text-align: left; | |
36 | + padding-left: 8px; | |
37 | + } | |
38 | + .project-name { | |
39 | + margin-top: 10px; | |
40 | + &.warn { | |
41 | + color: white; | |
42 | + font-size: 20px; | |
43 | + margin-top: 4px; | |
44 | + padding: 5px 10px; | |
45 | + font-weight: bold; | |
46 | + background-color: #b94a48; | |
47 | + } | |
48 | + font-size: 30px; | |
49 | + font-family: Consolas, monospace, Menlo; | |
50 | + line-height: normal; | |
51 | + } | |
52 | + .arrow { | |
53 | + text-align: center; | |
54 | + color: #b94a48; | |
55 | + } | |
56 | + i { | |
57 | + font-size: 20px; | |
58 | + padding: 10px; | |
59 | + } | |
60 | + | |
61 | + } | |
62 | + .header-pannel { | |
63 | + .board { | |
64 | + font-size: 14px; | |
65 | + max-height: 60px; | |
66 | + overflow: hidden; | |
67 | + background-color: #333333 !important; | |
68 | + color: #fff; | |
69 | + border-radius: 0 !important; | |
70 | + margin-bottom: 0 !important; | |
71 | + border: none !important; | |
72 | + padding-left: 40px; | |
73 | + } | |
74 | + .buttons { | |
75 | + margin-left: 20px; | |
76 | + } | |
77 | + .error-data { | |
78 | + color: #ff4840; | |
79 | + } | |
80 | + .progress { | |
81 | + border-radius: 0 !important; | |
82 | + margin-bottom: 0 !important; | |
83 | + } | |
84 | + } | |
85 | + | |
86 | + .search { | |
87 | + border-right: 1px solid #ccc; | |
88 | + border-bottom: none; | |
89 | + height: 40px; | |
90 | + } | |
91 | + | |
92 | + .project-list{ | |
93 | + padding: 5px; | |
94 | + margin: 5px; | |
95 | + &:hover { | |
96 | + background-color: #EEE; | |
97 | + cursor: pointer; | |
98 | + } | |
99 | + } | |
100 | +} | |
101 | + | |
102 | +.source-destination { | |
103 | + margin-left: 20px; | |
104 | + .header { | |
105 | + padding: 10px; | |
106 | + background-color: #e36b23 ; | |
107 | + font-weight: bold; | |
108 | + font-size: 16px; | |
109 | + color: white; | |
110 | + } | |
111 | + | |
112 | + .owner { | |
113 | + font-weight: bold; | |
114 | + color: black; | |
115 | + padding: 5px; | |
116 | + font-size: 14px; | |
117 | + margin-left: -10px; | |
118 | + } | |
119 | + | |
120 | + .search { | |
121 | + input { | |
122 | + border: none; | |
123 | + height: 30px; | |
124 | + font-size: 18px; | |
125 | + } | |
126 | + } | |
127 | + | |
128 | + .status{ | |
129 | + .progress { | |
130 | + margin-left: 0; | |
131 | + .bar { | |
132 | + margin-left: 0; | |
133 | + } | |
134 | + } | |
135 | + .left-title { | |
136 | + font-size: 14px; | |
137 | + font-weight: bold; | |
138 | + min-width: 30px; | |
139 | + } | |
140 | + .yobicon-check-circle { | |
141 | + display: inline-block; | |
142 | + margin-left: -20px; | |
143 | + vertical-align: top; | |
144 | + margin-top: 8px; | |
145 | + color: green; | |
146 | + font-size: 16px; | |
147 | + } | |
148 | + .yobicon-delete-circle-alt { | |
149 | + display: inline-block; | |
150 | + margin-left: -20px; | |
151 | + vertical-align: top; | |
152 | + margin-top: 8px; | |
153 | + color: red; | |
154 | + font-size: 16px; | |
155 | + } | |
156 | + .caution{ | |
157 | + border-radius: 3px; | |
158 | + padding: 5px; | |
159 | + background-color: #eee; | |
160 | + color: #333; | |
161 | + margin-bottom: 7px; | |
162 | + margin-top: 0; | |
163 | + a { | |
164 | + color: #1fb0ff; | |
165 | + font-size: 14px; | |
166 | + } | |
167 | + } | |
168 | + .btn-group { | |
169 | + width: 300px; | |
170 | + } | |
171 | + .btn{ | |
172 | + font-weight: bold !important; | |
173 | + width: 100%; | |
174 | + } | |
175 | + } | |
176 | + | |
177 | + .project-name { | |
178 | + font-size: 18px; | |
179 | + font-weight: bold; | |
180 | + a { | |
181 | + color: #0088cc; | |
182 | + } | |
183 | + margin-bottom: 5px; | |
184 | + } | |
185 | + .metainfo-sm { | |
186 | + font-size: 13px; | |
187 | + color: #999; | |
188 | + } | |
189 | + .metainfo { | |
190 | + font-size: 14px; | |
191 | + color: #999; | |
192 | + } | |
193 | + | |
194 | + .project-list { | |
195 | + margin: 5px 10px; | |
196 | + } | |
197 | + | |
198 | + .private { | |
199 | + font-size: 12px; | |
200 | + font-weight: normal !important; | |
201 | + padding: 0 2px !important; | |
202 | + } | |
203 | + | |
204 | + td { | |
205 | + vertical-align: middle !important; | |
206 | + text-align: center; | |
207 | + | |
208 | + } | |
209 | + .selected { | |
210 | + border-left: 3px solid #e36b23; | |
211 | + padding-left: 11px; | |
212 | + background-color: #eee; | |
213 | + margin-left: 1px; | |
214 | + } | |
215 | + | |
216 | + .left-project-list { | |
217 | + height:60vh; | |
218 | + overflow: auto; | |
219 | + border: 1px solid #ccc; | |
220 | + } | |
221 | + .destination-project { | |
222 | + margin-left: 0 !important; | |
223 | + } | |
224 | + .destination-project-list { | |
225 | + height:60vh; | |
226 | + overflow: auto; | |
227 | + border: 1px solid #ccc; | |
228 | + border-left: none !important; | |
229 | + } | |
230 | + | |
231 | + .dl-horizontal { | |
232 | + font-size: 12px !important; | |
233 | + dd { | |
234 | + margin-left: 120px !important; | |
235 | + width: 100% !important; | |
236 | + } | |
237 | + dt { | |
238 | + width: 100px !important; | |
239 | + } | |
240 | + } | |
241 | + | |
242 | + .text-align-left { | |
243 | + text-align: left !important; | |
244 | + } | |
245 | + | |
246 | + .td-title { | |
247 | + width: 100px !important; | |
248 | + vertical-align: top !important; | |
249 | + } | |
250 | + | |
251 | + .alert-bg { | |
252 | + background-color: #333; | |
253 | + } | |
254 | + | |
255 | + .alert-icon { | |
256 | + font-size: 20px; | |
257 | + color: #FD6956; | |
258 | + padding: 5px; | |
259 | + margin-bottom: 5px; | |
260 | + .description { | |
261 | + font-size: 12px !important; | |
262 | + color: #fafafa; | |
263 | + } | |
264 | + } | |
265 | + | |
266 | + .alert-icon-text { | |
267 | + font-size: 16px !important; | |
268 | + } | |
269 | + | |
270 | + .assignee { | |
271 | + font-size: 14px; | |
272 | + input { | |
273 | + font-size: 14px; | |
274 | + line-height: 30px; | |
275 | + height: 30px; | |
276 | + padding-bottom: 0; | |
277 | + margin-bottom: 0; | |
278 | + width: 90%; | |
279 | + border: 0; | |
280 | + } | |
281 | + } | |
282 | + .table-bordered { | |
283 | + width: 100%; | |
284 | + } | |
285 | + .warn-no-worker, .warn-user-project { | |
286 | + color: red; | |
287 | + } | |
288 | +} |
--- app/assets/stylesheets/yobi.less
+++ app/assets/stylesheets/yobi.less
... | ... | @@ -7,4 +7,5 @@ |
7 | 7 |
@import "less/_yobiUI.less"; |
8 | 8 |
@import "less/_temporary.less"; |
9 | 9 |
@import "less/_markdown.less"; |
10 |
+@import "less/_migration.less"; |
|
10 | 11 |
@import "less/_override.less"; |
--- app/controllers/BoardApi.java
+++ app/controllers/BoardApi.java
... | ... | @@ -9,8 +9,10 @@ |
9 | 9 |
|
10 | 10 |
import com.fasterxml.jackson.databind.JsonNode; |
11 | 11 |
import com.fasterxml.jackson.databind.node.ObjectNode; |
12 |
+import controllers.annotation.IsAllowed; |
|
12 | 13 |
import controllers.annotation.IsCreatable; |
13 | 14 |
import models.*; |
15 |
+import models.enumeration.Operation; |
|
14 | 16 |
import models.enumeration.ResourceType; |
15 | 17 |
import org.joda.time.DateTime; |
16 | 18 |
import play.db.ebean.Transactional; |
... | ... | @@ -50,6 +52,18 @@ |
50 | 52 |
return ok(result); |
51 | 53 |
} |
52 | 54 |
|
55 |
+ @IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST) |
|
56 |
+ public static Result getPosts(String owner, String projectName, Long number) { |
|
57 |
+ Project project = Project.findByOwnerAndProjectName(owner, projectName); |
|
58 |
+ Posting post = Posting.findByNumber(project, number); |
|
59 |
+ |
|
60 |
+ ObjectNode json = Json.newObject(); |
|
61 |
+ json.put("title", post.title); |
|
62 |
+ json.put("body", post.body); |
|
63 |
+ json.put("author", post.authorLoginId); |
|
64 |
+ return ok(json); |
|
65 |
+ } |
|
66 |
+ |
|
53 | 67 |
@Transactional |
54 | 68 |
@IsCreatable(ResourceType.BOARD_POST) |
55 | 69 |
public static Result newPostByJson(String owner, String projectName) { |
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
... | ... | @@ -1,22 +1,8 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
2 |
+ * Yobire, Project Hosting SW |
|
3 | 3 |
* |
4 |
- * Copyright 2012 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Sangcheol Hwang |
|
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. |
|
4 |
+ * @author Suwon Chae |
|
5 |
+ * Copyright 2016 the original author or authors. |
|
20 | 6 |
*/ |
21 | 7 |
package controllers; |
22 | 8 |
|
... | ... | @@ -36,6 +22,7 @@ |
36 | 22 |
import play.db.ebean.Transactional; |
37 | 23 |
import play.libs.Json; |
38 | 24 |
import play.mvc.Call; |
25 |
+import play.mvc.Http; |
|
39 | 26 |
import play.mvc.Result; |
40 | 27 |
import play.mvc.With; |
41 | 28 |
import playRepository.BareCommit; |
... | ... | @@ -53,6 +40,10 @@ |
53 | 40 |
import java.util.*; |
54 | 41 |
|
55 | 42 |
import static com.avaje.ebean.Expr.icontains; |
43 |
+import static controllers.MigrationApp.composeCommentsJson; |
|
44 |
+import static controllers.MigrationApp.composePlainCommentsJson; |
|
45 |
+import static controllers.MigrationApp.exportPosts; |
|
46 |
+import static play.libs.Json.toJson; |
|
56 | 47 |
|
57 | 48 |
public class BoardApp extends AbstractPostingApp { |
58 | 49 |
public static class SearchCondition extends AbstractPostingApp.SearchCondition { |
... | ... | @@ -254,8 +245,12 @@ |
254 | 245 |
if (request().getHeader("Accept").contains("application/json")) { |
255 | 246 |
ObjectNode json = Json.newObject(); |
256 | 247 |
json.put("title", post.title); |
248 |
+ json.put("created_at", post.createdDate.getTime()); |
|
257 | 249 |
json.put("body", post.body); |
258 | 250 |
json.put("author", post.authorLoginId); |
251 |
+ json.put("authorName", post.authorName); |
|
252 |
+ json.put("attachments", toJson(Attachment.findByContainer(post.asResource()))); |
|
253 |
+ json.put("comments", toJson(composePlainCommentsJson(post, ResourceType.NONISSUE_COMMENT))); |
|
259 | 254 |
return ok(json); |
260 | 255 |
} |
261 | 256 |
|
+++ app/controllers/MigrationApp.java
... | ... | @@ -0,0 +1,426 @@ |
1 | +/** | |
2 | + * Yobire, Project Hosting SW | |
3 | + * | |
4 | + * @author Suwon Chae | |
5 | + * Copyright 2016 the original author or authors. | |
6 | + */ | |
7 | +package controllers; | |
8 | + | |
9 | +import com.avaje.ebean.Ebean; | |
10 | +import com.avaje.ebean.Query; | |
11 | +import com.avaje.ebean.RawSql; | |
12 | +import com.avaje.ebean.RawSqlBuilder; | |
13 | +import com.fasterxml.jackson.databind.node.ObjectNode; | |
14 | +import controllers.annotation.AnonymousCheck; | |
15 | +import models.*; | |
16 | +import models.enumeration.ResourceType; | |
17 | +import models.support.IssueLabelAggregate; | |
18 | +import org.apache.commons.lang.StringUtils; | |
19 | +import play.libs.F; | |
20 | +import play.libs.F.Promise; | |
21 | +import play.libs.Json; | |
22 | +import play.libs.ws.WS; | |
23 | +import play.mvc.Result; | |
24 | +import views.html.migration.home; | |
25 | + | |
26 | +import javax.validation.constraints.NotNull; | |
27 | +import java.time.LocalDateTime; | |
28 | +import java.time.ZoneId; | |
29 | +import java.time.format.DateTimeFormatter; | |
30 | +import java.util.*; | |
31 | +import java.util.regex.Matcher; | |
32 | +import java.util.regex.Pattern; | |
33 | +import java.util.regex.PatternSyntaxException; | |
34 | +import java.util.stream.Collectors; | |
35 | + | |
36 | +import static play.libs.Json.toJson; | |
37 | +import static play.mvc.Http.Context.Implicit.request; | |
38 | +import static play.mvc.Results.ok; | |
39 | + | |
40 | +@AnonymousCheck | |
41 | +public class MigrationApp { | |
42 | + | |
43 | + static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); | |
44 | + private static final String YONA_SERVER = "/"; | |
45 | + | |
46 | + | |
47 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
48 | + public static Promise<Result> migration() { | |
49 | + String authProcessingCode = request().getQueryString("code"); | |
50 | + | |
51 | + if(StringUtils.isNotBlank(authProcessingCode)){ | |
52 | + return getOAuthToken(authProcessingCode).map((F.Function<String, Result>) token | |
53 | + -> ok(home.render("Migration", authProcessingCode, token))); | |
54 | + } else { | |
55 | + return Promise.promise((F.Function0<Result>) () | |
56 | + -> ok(home.render("Migration", null, null))); | |
57 | + } | |
58 | + } | |
59 | + | |
60 | + private static Promise<String> getOAuthToken(String code) { | |
61 | + final String CLIENT_ID = "e7f9ad76a3a4ba19b2a5"; | |
62 | + final String CLIENT_SECRET = "32e7fb33ee5c42501cb2aac9a6f6c485bf285cf5"; | |
63 | + final String ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; | |
64 | + | |
65 | + return WS.url(ACCESS_TOKEN_URL) | |
66 | + .setContentType("application/x-www-form-urlencoded") | |
67 | + .setHeader("Accept", "application/json,application/x-www-form-urlencoded,text/html,*/*") | |
68 | + .post("client_id=" + CLIENT_ID + "&client_secret=" + CLIENT_SECRET + "&code=" + code) | |
69 | + .map(response -> { | |
70 | + play.Logger.debug(response.getBody()); | |
71 | + String accessToken = ""; | |
72 | + try { | |
73 | + Pattern p = Pattern.compile("access_token=([^&]+)"); | |
74 | + Matcher m = p.matcher(response.getBody()); | |
75 | + if(m.find() ){ | |
76 | + accessToken = m.group(1); | |
77 | + } | |
78 | + } catch (PatternSyntaxException ex) { | |
79 | + play.Logger.error("Couldn't find access_token"); | |
80 | + } | |
81 | + play.Logger.error("token=" + accessToken); | |
82 | + return accessToken; | |
83 | + }); | |
84 | + } | |
85 | + | |
86 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
87 | + public static Result projects(){ | |
88 | + Set<Project> sourceProjects = new HashSet<>(); | |
89 | + | |
90 | + getheringOrgProjects(sourceProjects); | |
91 | + gatheringUserProjects(sourceProjects); | |
92 | + | |
93 | + List<ObjectNode> projects = new ArrayList<>(); | |
94 | + for(Project project: sortProjectsByOwnerAndName(sourceProjects)){ | |
95 | + ObjectNode projectNode = Json.newObject(); | |
96 | + projectNode.put("owner", project.owner); | |
97 | + projectNode.put("projectName", project.name); | |
98 | + projectNode.put("private", project.isPrivate()); | |
99 | + projectNode.put("members", project.members().size()); | |
100 | + projectNode.put("full_name", project.owner + "/" + project.name); | |
101 | + projects.add(projectNode); | |
102 | + } | |
103 | + return ok(toJson(projects)); | |
104 | + } | |
105 | + | |
106 | + private static List<Project> sortProjectsByOwnerAndName(Set<Project> projects) { | |
107 | + Comparator<Project> comparator = Comparator.comparing(project -> project.owner); | |
108 | + comparator = comparator.thenComparing(Comparator.comparing(project -> project.name)); | |
109 | + List<Project> list = new ArrayList<>(projects); | |
110 | + Collections.sort(list, comparator); | |
111 | + return list; | |
112 | + } | |
113 | + | |
114 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
115 | + public static Result project(String owner, String projectName){ | |
116 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
117 | + | |
118 | + ObjectNode result = Json.newObject(); | |
119 | + result.put("owner", project.owner); | |
120 | + result.put("projectName", project.name); | |
121 | + result.put("full_name", project.owner + "/" + project.name); | |
122 | + result.put("assignees", toJson(getAssginees(project).toArray())); | |
123 | + result.put("memberCount", project.members().size()); | |
124 | + result.put("issueCount", project.issues.size()); | |
125 | + result.put("postCount", project.posts.size()); | |
126 | + result.put("milestoneCount", project.milestones.size()); | |
127 | + return ok(result); | |
128 | + } | |
129 | + | |
130 | + private static List<ObjectNode> getAssginees(Project project) { | |
131 | + List<ObjectNode> members = new ArrayList<>(); | |
132 | + for(Assignee assignee: project.assignees){ | |
133 | + ObjectNode member = Json.newObject(); | |
134 | + member.put("name", assignee.user.name); | |
135 | + member.put("login", assignee.user.loginId); | |
136 | + members.add(member); | |
137 | + } | |
138 | + return members; | |
139 | + } | |
140 | + | |
141 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
142 | + public static Result exportIssueLabelPairs(String owner, String projectName){ | |
143 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
144 | + | |
145 | + Query<IssueLabelAggregate> query = Ebean.find(IssueLabelAggregate.class); | |
146 | + String sql = "select issue_id, issue_label_id \n" + | |
147 | + "from issue i, issue_issue_label iil \n" + | |
148 | + "where project_id = " + project.id + "\n" + | |
149 | + "and i.id = iil.issue_id"; | |
150 | + RawSql rawSql = RawSqlBuilder.parse(sql).create(); | |
151 | + query.setRawSql(rawSql); | |
152 | + List<IssueLabelAggregate> results = query.findList(); | |
153 | + | |
154 | + ObjectNode issueLabelPairs = Json.newObject(); | |
155 | + issueLabelPairs.put("issueLabelPairs", toJson(results)); | |
156 | + return ok(issueLabelPairs); | |
157 | + } | |
158 | + | |
159 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
160 | + public static Result exportLabels(String owner, String projectName){ | |
161 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
162 | + | |
163 | + ObjectNode labels = Json.newObject(); | |
164 | + for (IssueLabel label : IssueLabel.findByProject(project)) { | |
165 | + ObjectNode node = Json.newObject(); | |
166 | + node.put("id", label.id); | |
167 | + node.put("name", label.name); | |
168 | + node.put("categoryId", label.category.id); | |
169 | + node.put("categoryName", label.category.name); | |
170 | + labels.put(String.valueOf(label.id), node); | |
171 | + } | |
172 | + | |
173 | + ObjectNode exportData = Json.newObject(); | |
174 | + exportData.put("labels", toJson(labels)); | |
175 | + return ok(exportData); | |
176 | + } | |
177 | + | |
178 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
179 | + public static Result exportMilestones(String owner, String projectName){ | |
180 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
181 | + | |
182 | + List<ObjectNode> milestones = project.milestones.stream() | |
183 | + .map(MigrationApp::composeMilestoneJson).collect(Collectors.toList()); | |
184 | + | |
185 | + ObjectNode exportData = Json.newObject(); | |
186 | + exportData.put("milestones", toJson(milestones)); | |
187 | + return ok(exportData); | |
188 | + } | |
189 | + | |
190 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
191 | + public static Result exportPosts(String owner, String projectName){ | |
192 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
193 | + | |
194 | + List<ObjectNode> issues = project.posts.stream() | |
195 | + .map(MigrationApp::composePostJson).collect(Collectors.toList()); | |
196 | + | |
197 | + ObjectNode exportData = Json.newObject(); | |
198 | + exportData.put("issues", toJson(issues)); | |
199 | + return ok(exportData); | |
200 | + } | |
201 | + | |
202 | + @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true) | |
203 | + public static Result exportIssues(String owner, String projectName){ | |
204 | + Project project = Project.findByOwnerAndProjectName(owner, projectName); | |
205 | + | |
206 | + List<ObjectNode> issues = project.issues.stream() | |
207 | + .map(MigrationApp::composeIssueJson).collect(Collectors.toList()); | |
208 | + | |
209 | + ObjectNode exportData = Json.newObject(); | |
210 | + exportData.put("issues", toJson(issues)); | |
211 | + return ok(exportData); | |
212 | + } | |
213 | + | |
214 | + private static ObjectNode composeMilestoneJson(Milestone m) { | |
215 | + ObjectNode node = Json.newObject(); | |
216 | + node.put("id", m.id); | |
217 | + node.put("title", m.title); | |
218 | + node.put("state", m.state.state()); | |
219 | + node.put("description", m.contents); | |
220 | + Optional.ofNullable(m.dueDate).ifPresent(dueDate -> node.put("due_on", | |
221 | + LocalDateTime.ofInstant(m.dueDate.toInstant(), ZoneId.systemDefault()).format(formatter))); | |
222 | + | |
223 | + ObjectNode milestoneJson = Json.newObject(); | |
224 | + milestoneJson.put("milestone", node); | |
225 | + return milestoneJson; | |
226 | + } | |
227 | + | |
228 | + private static String addOriginalAuthorName(String bodyText, String authorLoginId, | |
229 | + String authorName, String type, String link){ | |
230 | + return String.format("@%s (%s) 님이 작성한 [%s](%s)입니다. \n\\---\n\n%s", | |
231 | + authorLoginId, authorName, type, link, bodyText); | |
232 | + } | |
233 | + | |
234 | + private static String relativeLinksToAbsolutePath(String text){ | |
235 | + // replace relative img tag src to absolute path | |
236 | + // and replace relative markdown link path to absolute path | |
237 | + return text.replaceAll("(<img src=[\"\'])/(?<link>.*)([\"\']>)", "$1" + YONA_SERVER + "$2$3") | |
238 | + .replaceAll("\\[(?<text>[^\\]]*)\\]\\(/(?<link>[^\\)]*)\\)", "[$1](" + YONA_SERVER + "$2)"); | |
239 | + } | |
240 | + | |
241 | + private static String relativeLinksToWikiCommitPath(String text){ | |
242 | + // replace relative img tag src to absolute path | |
243 | + // and replace relative markdown link path to wiki commit file path | |
244 | + return text.replaceAll("(<img src=[\"\'])/(?<link>.*)([\"\']>)", "$1" + YONA_SERVER + "$2$3") | |
245 | + .replaceAll("\\[(?<text>[^\\]]*)\\]\\(/(?<link>[^\\)]*)\\)", "[$1](../wiki/$2/$1)"); | |
246 | + } | |
247 | + | |
248 | + private static StringBuilder addAttachmentsString(@NotNull StringBuilder sb, ResourceType type, String id){ | |
249 | + try { | |
250 | + List<Map<String, String>> attachments = AttachmentApp.getFileList(type.toString(), id).get("attachments"); | |
251 | + if(attachments.size()>0){ | |
252 | + addListHeader(sb); | |
253 | + } | |
254 | + for(Map<String, String> attachment: attachments){ | |
255 | + sb.append(String.format("\n[%s](%s)", attachment.get("name"), YONA_SERVER + attachment.get("url"))); | |
256 | + } | |
257 | + } catch (Exception e) { | |
258 | + e.printStackTrace(); | |
259 | + } | |
260 | + return sb; | |
261 | + } | |
262 | + | |
263 | + private static void addListHeader(@NotNull StringBuilder sb) { | |
264 | + sb.append("\n\n--- attachments ---"); | |
265 | + } | |
266 | + | |
267 | + private static StringBuilder addAttachmentsStringUsingWikiCommit(@NotNull StringBuilder sb, ResourceType type, String id){ | |
268 | + try { | |
269 | + List<Map<String, String>> attachments = AttachmentApp.getFileList(type.toString(), id).get("attachments"); | |
270 | + if(attachments.size()>0){ | |
271 | + addListHeader(sb); | |
272 | + } | |
273 | + for(Map<String, String> attachment: attachments){ | |
274 | + sb.append(String.format("\n[%s](../wiki/files/%s/%s)", attachment.get("name"), attachment.get("id"), | |
275 | + attachment.get("name").replaceAll("#", "%23"))); | |
276 | + } | |
277 | + } catch (Exception e) { | |
278 | + e.printStackTrace(); | |
279 | + } | |
280 | + return sb; | |
281 | + } | |
282 | + | |
283 | + private static StringBuilder addAttachmentsStringWithLocalDir(@NotNull StringBuilder sb, ResourceType type, String id){ | |
284 | + try { | |
285 | + List<Map<String, String>> attachments = AttachmentApp.getFileList(type.toString(), id).get("attachments"); | |
286 | + if(attachments.size()>0){ | |
287 | + addListHeader(sb); | |
288 | + } | |
289 | + for(Map<String, String> attachment: attachments){ | |
290 | + sb.append(String.format("\n[%s](./attachments/%s/%s)", attachment.get("name"), attachment.get("id"), | |
291 | + attachment.get("name").replaceAll("#", "%23"))); | |
292 | + } | |
293 | + } catch (Exception e) { | |
294 | + e.printStackTrace(); | |
295 | + } | |
296 | + return sb; | |
297 | + } | |
298 | + | |
299 | + private static ObjectNode composePostJson(Posting posting) { | |
300 | + String originalPostingLink = String.format("%s/%s/post/%s", | |
301 | + YONA_SERVER + posting.project.owner, posting.project.name, posting.getNumber()); | |
302 | + | |
303 | + ObjectNode node = Json.newObject(); | |
304 | + node.put("title", posting.title); | |
305 | + | |
306 | + // body 작성 | |
307 | + StringBuilder sb = new StringBuilder(); | |
308 | + | |
309 | + if(usingWikiCommitForAttachment()){ | |
310 | + sb.append(addOriginalAuthorName( | |
311 | + relativeLinksToWikiCommitPath(posting.body), posting.authorLoginId, | |
312 | + posting.authorName, "게시글", originalPostingLink)); | |
313 | + sb = addAttachmentsStringUsingWikiCommit(sb, ResourceType.BOARD_POST, posting.id.toString()); | |
314 | + } else { | |
315 | + sb.append(addOriginalAuthorName( | |
316 | + relativeLinksToAbsolutePath(posting.body), posting.authorLoginId, posting.authorName, | |
317 | + "게시글", originalPostingLink)); | |
318 | + sb = addAttachmentsString(sb, ResourceType.BOARD_POST, posting.id.toString()); | |
319 | + } | |
320 | + node.put("body", sb.toString()); | |
321 | + node.put("created_at", LocalDateTime.ofInstant(posting.createdDate.toInstant(), | |
322 | + ZoneId.systemDefault()).format(formatter)); | |
323 | + | |
324 | + ObjectNode postingJson = Json.newObject(); | |
325 | + postingJson.put("issue", node); // intentionally 'issue' key name is used for Github api compatibility | |
326 | + postingJson.put("comments", toJson(composeCommentsJson(posting, originalPostingLink, ResourceType.NONISSUE_COMMENT))); | |
327 | + return postingJson; | |
328 | + } | |
329 | + | |
330 | + private static boolean usingWikiCommitForAttachment() { | |
331 | + String withWikiCommit = request().getQueryString("withWikiCommit"); | |
332 | + boolean usingWikiCommit = StringUtils.isNotBlank(withWikiCommit) && withWikiCommit.endsWith("true"); | |
333 | + return usingWikiCommit; | |
334 | + } | |
335 | + | |
336 | + private static ObjectNode composeIssueJson(Issue issue) { | |
337 | + String originalIssueLink = String.format("%s/%s/issue/%s", | |
338 | + YONA_SERVER + issue.project.owner, issue.project.name, issue.getNumber()); | |
339 | + | |
340 | + ObjectNode node = Json.newObject(); | |
341 | + node.put("id", issue.id); | |
342 | + node.put("title", issue.title); | |
343 | + | |
344 | + // body 작성 | |
345 | + StringBuilder sb = new StringBuilder(); | |
346 | + | |
347 | + if(usingWikiCommitForAttachment()){ | |
348 | + sb.append(addOriginalAuthorName( | |
349 | + relativeLinksToWikiCommitPath(issue.body), issue.authorLoginId, issue.authorName, "이슈", originalIssueLink)); | |
350 | + sb = addAttachmentsStringUsingWikiCommit(sb, ResourceType.ISSUE_POST, issue.id.toString()); | |
351 | + } else { | |
352 | + sb.append(addOriginalAuthorName( | |
353 | + relativeLinksToAbsolutePath(issue.body), issue.authorLoginId, issue.authorName, "이슈", originalIssueLink)); | |
354 | + sb = addAttachmentsString(sb, ResourceType.ISSUE_POST, issue.id.toString()); | |
355 | + } | |
356 | + node.put("body", sb.toString()); | |
357 | + | |
358 | + node.put("created_at", LocalDateTime.ofInstant(issue.createdDate.toInstant(), | |
359 | + ZoneId.systemDefault()).format(formatter)); | |
360 | + Optional.ofNullable(issue.assignee).ifPresent(assignee -> node.put("assignee", assignee.user.loginId)); | |
361 | + Optional.ofNullable(issue.milestone).ifPresent(milestone -> node.put("milestone", milestone.title)); | |
362 | + Optional.ofNullable(issue.milestone).ifPresent(milestone -> node.put("milestoneId", milestone.id)); | |
363 | + | |
364 | + node.put("closed", issue.isClosed()); | |
365 | + | |
366 | + ObjectNode issueJson = Json.newObject(); | |
367 | + issueJson.put("issue", node); | |
368 | + issueJson.put("comments", toJson(composeCommentsJson(issue, originalIssueLink, ResourceType.ISSUE_COMMENT))); | |
369 | + return issueJson; | |
370 | + } | |
371 | + | |
372 | + public static List<ObjectNode> composeCommentsJson(AbstractPosting posting, String orgLink, ResourceType type) { | |
373 | + List<ObjectNode> comments = new ArrayList<>(); | |
374 | + for (Comment comment : posting.getComments()) { | |
375 | + StringBuilder sb = new StringBuilder(); | |
376 | + ObjectNode commentNode = Json.newObject(); | |
377 | + commentNode.put("created_at", LocalDateTime.ofInstant(comment.createdDate.toInstant(), | |
378 | + ZoneId.systemDefault()).format(formatter)); | |
379 | + | |
380 | + if(usingWikiCommitForAttachment()){ | |
381 | + sb.append(addOriginalAuthorName( | |
382 | + relativeLinksToWikiCommitPath(comment.contents), comment.authorLoginId, comment.authorName, | |
383 | + "코멘트", orgLink + "#comment-" + comment.id)); | |
384 | + sb = addAttachmentsStringUsingWikiCommit(sb, type, comment.id.toString()); | |
385 | + } else { | |
386 | + sb.append(addOriginalAuthorName( | |
387 | + relativeLinksToAbsolutePath(comment.contents), comment.authorLoginId, comment.authorName, | |
388 | + "코멘트", orgLink + "#comment-" + comment.id)); | |
389 | + sb = addAttachmentsString(sb, type, comment.id.toString()); | |
390 | + } | |
391 | + commentNode.put("body", sb.toString()); | |
392 | + comments.add(commentNode); | |
393 | + } | |
394 | + return comments; | |
395 | + } | |
396 | + | |
397 | + public static List<ObjectNode> composePlainCommentsJson(AbstractPosting posting, ResourceType type) { | |
398 | + List<ObjectNode> comments = new ArrayList<>(); | |
399 | + for (Comment comment : posting.getComments()) { | |
400 | + StringBuilder sb = new StringBuilder(); | |
401 | + ObjectNode commentNode = Json.newObject(); | |
402 | + commentNode.put("created_at",comment.createdDate.getTime()); | |
403 | + sb = addAttachmentsStringWithLocalDir(sb, type, comment.id.toString()); | |
404 | + commentNode.put("authorId", comment.authorLoginId); | |
405 | + commentNode.put("authorName", comment.authorName); | |
406 | + commentNode.put("body", sb.toString()); | |
407 | + commentNode.put("attachments", toJson(Attachment.findByContainer(comment.asResource()))); | |
408 | + comments.add(commentNode); | |
409 | + } | |
410 | + return comments; | |
411 | + } | |
412 | + | |
413 | + private static void gatheringUserProjects(Set<Project> targetProjects) { | |
414 | + User worker = UserApp.currentUser(); | |
415 | + targetProjects.addAll(worker.projectUser.stream(). | |
416 | + filter(projectUser -> ProjectUser.isAllowedToSettings(worker.loginId, projectUser.project)) | |
417 | + .map(projectUser -> projectUser.project).collect(Collectors.toList())); | |
418 | + } | |
419 | + | |
420 | + private static void getheringOrgProjects(Set<Project> targetProjects) { | |
421 | + User worker = UserApp.currentUser(); | |
422 | + for (OrganizationUser organizationUser : OrganizationUser.findByAdmin(worker.id)) { | |
423 | + targetProjects.addAll(organizationUser.organization.projects); | |
424 | + } | |
425 | + } | |
426 | +} |
--- app/models/Attachment.java
+++ app/models/Attachment.java
... | ... | @@ -1,25 +1,12 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
2 |
+ * Yobire, Project Hosting SW |
|
3 | 3 |
* |
4 |
- * Copyright 2012 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Yi EungJun |
|
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. |
|
4 |
+ * @author Suwon Chae |
|
5 |
+ * Copyright 2016 the original author or authors. |
|
20 | 6 |
*/ |
21 | 7 |
package models; |
22 | 8 |
|
9 |
+import com.fasterxml.jackson.annotation.JsonIgnore; |
|
23 | 10 |
import controllers.AttachmentApp; |
24 | 11 |
import models.enumeration.ResourceType; |
25 | 12 |
import models.resource.GlobalResource; |
... | ... | @@ -279,6 +266,7 @@ |
279 | 266 |
* |
280 | 267 |
* @return the file |
281 | 268 |
*/ |
269 |
+ @JsonIgnore |
|
282 | 270 |
public File getFile() { |
283 | 271 |
return new File(getUploadDirectory(), this.hash); |
284 | 272 |
} |
+++ app/models/support/IssueLabelAggregate.java
... | ... | @@ -0,0 +1,20 @@ |
1 | +/** | |
2 | + * Yona, 21c Project Hosting SW | |
3 | + * <p> | |
4 | + * Copyright Yona & Yobi Authors & NAVER Corp. | |
5 | + * https://yona.io | |
6 | + **/ | |
7 | +package models.support; | |
8 | + | |
9 | +import com.avaje.ebean.annotation.Sql; | |
10 | +import play.db.ebean.Model; | |
11 | + | |
12 | +import javax.persistence.Entity; | |
13 | + | |
14 | +@Entity | |
15 | +@Sql | |
16 | +public class IssueLabelAggregate extends Model { | |
17 | + private static final long serialVersionUID = -8843323869004757091L; | |
18 | + public Long issueId; | |
19 | + public Long issueLabelId; | |
20 | +} |
+++ app/views/migration/home.scala.html
... | ... | @@ -0,0 +1,151 @@ |
1 | +@import org.apache.commons.lang.StringUtils | |
2 | +@(title: String, code:String, token:String) | |
3 | + | |
4 | +@migrationPageLayout(utils.Config.getSiteName)("") { | |
5 | + <div ng-app="yona.migration" ng-controller="MigrationController as vm" class="yobi-migration" ng-init="vm.setUserToken('@token');vm.yobiUser = '@UserApp.currentUser().loginId';vm.yobiUserEmail = '@UserApp.currentUser().email'"> | |
6 | + <div class="header-pannel" ng-if="'@token'"> | |
7 | + <div class="comeback-text pull-right">Yona to Github<span class="midium-font"></span></div> | |
8 | + <div class="row title-text-bg"> | |
9 | + <div id="system-msg" class="well board" ng-cloak> | |
10 | + <div class="messages" ng-repeat="msg in vm.systemMessages track by $index"> | |
11 | + {{::msg}}</div> | |
12 | + <div class="error-data" ng-if="vm.importResult.errorData.length > 0">{{vm.importResult.errorData}}</div> | |
13 | + </div> | |
14 | + </div> | |
15 | + <div class="status"> | |
16 | + <div class="row"> | |
17 | + <div class="head-title row-fluid" ng-cloak> | |
18 | + <div class="source-title span5"> | |
19 | + <div class="project-name warn" ng-if="!vm.source.owner">Source 프로젝트를 선택해 주세요</div> | |
20 | + <div class="project-name" ng-if="vm.source.owner">{{vm.source.owner}}/{{vm.source.projectName}}</div> | |
21 | + </div> | |
22 | + <div class="arrow span1"><i class="yobicon-arrow-right-alt"></i></div> | |
23 | + <div class="destination-title span6"> | |
24 | + <div class="project-name warn" ng-if="!vm.destination.owner">Destination 프로젝트를 선택해 주세요</div> | |
25 | + <div class="project-name" ng-if="vm.destination.owner">{{vm.destination.owner}}/{{vm.destination.projectName}}</div> | |
26 | + </div> | |
27 | + </div> | |
28 | + </div> | |
29 | + </div> | |
30 | + <div class="row source-destination" > | |
31 | + <div class="source-project span4"> | |
32 | + <div class="header" ng-cloak>Source {{vm.sourceProjects.length}} 개</div> | |
33 | + <div class="search left-border"><input tabindex="1" type="text" class="search-query" name="target-filter" ng-model="source.full_name" placeholder="Search.." autofocus></div> | |
34 | + <div class="left-project-list"> | |
35 | + <div class="project-list" ng-repeat="repo in vm.sourceProjects | filter:source:strict" ng-click="vm.getSourceProject(repo.owner, repo.projectName);vm.selectSourceProject(repo.full_name)" ng-class="{'selected': repo.full_name == vm.selectedSourceName }" ng-cloak> | |
36 | + <div class="owner" ng-if="vm.isNewOwner(repo)">{{repo.owner}}</div> | |
37 | + <div class="project-name"><a href="http://yobi.navercorp.com/{{repo.full_name}}" target="_blank"> | |
38 | + {{repo.projectName}}</a></div> | |
39 | + <div class="metainfo-sm"> | |
40 | + {{repo.owner}} {{repo.members}}명 <i class="yobicon-lock yobicon-small" ng-if="repo.private"></i></div> | |
41 | + </div> | |
42 | + </div> | |
43 | + </div> | |
44 | + <div class="destination-project span4" @if(StringUtils.isNotBlank(token)) { | |
45 | + ng-init="vm.getDestinationProjects();vm.getCurrentGithubUser()"}> | |
46 | + <div class="header" ng-cloak>Destination {{vm.destinationProjects.length}} 개</div> | |
47 | + <div class="search"><input type="text" tabindex="2" class="search-query" name="target-filter" ng-model="destination.full_name" placeholder="Search.."></div> | |
48 | + <div class="destination-project-list"> | |
49 | + <div class="project-list" ng-repeat="repo in vm.destinationProjects | filter:destination:strict" ng-click="vm.setDestination(repo);vm.selectDestinationProject(repo.full_name)" ng-class="{'selected': repo.full_name == vm.selectedDestinationName }" ng-cloak> | |
50 | + <div class="owner" ng-if="vm.isNewOwner(repo)">{{::repo.owner.login}}</div> | |
51 | + <div class="project-name"><a href="{{repo.html_url}}" target="_blank">{{repo.name}}</a></div> | |
52 | + <div class="metainfo-sm"> | |
53 | + {{repo.owner.login}} <i class="yobicon-lock yobicon-small" ng-if="repo.private"></i> | |
54 | + </div> | |
55 | + <div class="warn-no-worker" ng-show="repo.full_name == vm.destination.full_name && vm.showWorkerWarning" ng-cloak> | |
56 | + <div class="label label-important">Admin에 {{vm.CONFIG.DEFAULT_WORKER}} 유저가 없음</div> | |
57 | + <div>{{vm.CONFIG.DEFAULT_WORKER}} 유저가 대상 프로젝트/그룹의 admin으로 추가되어 있어야 합니다. 그렇지 않을 경우 {{vm.CONFIG.DEFAULT_WORKER}} 대신 사용자 아이디가 작성자로 표시됩니다. | |
58 | + </div> | |
59 | + <div ng-if="repo.owner.type === 'Organization' && vm.showNotAdminWarning"> | |
60 | + <div class="label label-warning">사용자가 Admin인 프로젝트가 아닙니다.</div> | |
61 | + <div>{{vm.CONFIG.DEFAULT_WORKER}}, {{vm.currentGithubUser.login}} 둘 다 Admin이 아닌 프로젝트로는 마이그레이션을 진행할 수 없습니다!</div> | |
62 | + </div> | |
63 | + </div> | |
64 | + <div class="warn-user-project" ng-show="repo.full_name == vm.destination.full_name && vm.showUserProjectWarning" ng-cloak> | |
65 | + <div class="label label-important">User Project</div> | |
66 | + <div>Organization 소속의 프로젝트가 아닌 경우에는 마이그레이션시에 {{vm.CONFIG.DEFAULT_WORKER}} 대신 사용자 계정이 사용됩니다.</div> | |
67 | + </div> | |
68 | + <div ng-if="repo.owner.type === 'User' && repo.owner.login !== vm.currentGithubUser.login"> | |
69 | + <div class="label label-warning">사용자가 Admin인 프로젝트가 아닙니다.</div> | |
70 | + <div>Admin이 아닌 프로젝트로는 마이그레이션을 진행할 수 없습니다!</div> | |
71 | + </div> | |
72 | + </div> | |
73 | + </div> | |
74 | + </div> | |
75 | + <div class="span6 status"> | |
76 | + <div class="progress row"> | |
77 | + <div class="bar span10" ng-cloak ng-class="vm.importResult.count/vm.expectedImportCount*100<100 ? 'bar-danger' : 'bar-success'" style="width: {{vm.importResult.count/vm.expectedImportCount*100 || 0 }}%"> | |
78 | + {{vm.importResult.count}}/{{vm.expectedImportCount}}</div> | |
79 | + </div> | |
80 | + <table class="table"> | |
81 | + <thead> | |
82 | + <tr> | |
83 | + <th colspan="2">Migration 대상</th> | |
84 | + <th></th> | |
85 | + </tr> | |
86 | + </thead> | |
87 | + <tbody ng-cloak> | |
88 | + <tr> | |
89 | + <td class="left-title">마일스톤</td><td class="left-title">{{vm.source.milestoneCount}}</td> | |
90 | + <td ng-class="vm.destination.milestones.length > 0?'alert-bg':''"> | |
91 | + <import-warning type="마일스톤" data="vm.destination.milestones"></import-Warning> | |
92 | + <div class="btn-group"><button class="btn btn-danger" ng-click="vm.importMilestones()" ng-if="vm.destination.projectName" ng-disabled="!vm.source.milestoneCount || vm.importBtnDisabled" ng-cloak> | |
93 | + 마일스톤 옮기기</button></div> | |
94 | + </td> | |
95 | + </tr> | |
96 | + <tr> | |
97 | + <td class="left-title">이슈</td><td class="left-title"><span>{{vm.source.issueCount}}</span></td> | |
98 | + <td ng-class="vm.destination.issues.length > 0?'alert-bg':''"> | |
99 | + <div class="text-align-left caution" ng-if="vm.destination.projectName"> | |
100 | + 마일스톤이 존재할 경우 마일스톤을 먼저 옮겨 놓지 않으면 마일스톤이 지정되지 않은 상태로 이슈가 이동됩니다. | |
101 | + </div> | |
102 | + <import-warning type="이슈" data="vm.destination.issues"></import-Warning> | |
103 | + <div class="btn-group"><button class="btn btn-danger" ng-click="vm.importIssues(vm.source)" ng-if="vm.destination.projectName" ng-disabled="!vm.source.issueCount || vm.importBtnDisabled" ng-cloak> | |
104 | + 이슈 옮기기</button></div> | |
105 | + </td> | |
106 | + </tr> | |
107 | + <tr> | |
108 | + <td class="left-title">게시글</td><td class="left-title"><span>{{vm.source.postCount}}</span></td> | |
109 | + <td ng-class="vm.destination.posts.length > 0?'alert-bg':''"> | |
110 | + <div class="text-align-left caution" ng-if="vm.destination.projectName"> | |
111 | + 기존 게시글은 '게시글'라벨을 붙여 이슈로 옮겨집니다. | |
112 | + </div> | |
113 | + <div class="text-align-left caution" ng-if="vm.showMoreDesc">(만약 먼저 마이그레이션 작업을 진행한 이슈의 개수가 많을 경우 대상 프로젝트에 게시글이 보이는 시점까지는 상당한 시간이 소요될 수 있습니다. 진행바가 정상적으로 완료되었으면 차분히 기다려 주세요.)</div> | |
114 | + <import-warning type="게시글" data="vm.destination.posts"></import-Warning> | |
115 | + <div class="btn-group"><button class="btn btn-danger" ng-click="vm.showMoreDesc=!vm.showMoreDesc;vm.importPosts()" ng-if="vm.destination.projectName" ng-disabled="!vm.source.postCount || vm.importBtnDisabled" ng-cloak> | |
116 | + 게시글 옮기기</button></div> | |
117 | + </td> | |
118 | + </tr> | |
119 | + <tr> | |
120 | + <td class="td-title left-title">첨부파일</td> | |
121 | + <td colspan="2" class="text-align-left"> | |
122 | + <div class="label label-warning"> | |
123 | + 단일 파일기준 50M 미만 크기의 첨부파일만 이동됩니다! | |
124 | + </div> | |
125 | + <div class="caution"> | |
126 | + Yona to Githbub 마이그레이션 가이드를 꼭 읽어주세요. | |
127 | + </div> | |
128 | + <div class="btn-group pull-right"><button class="btn btn-danger" data-toggle="tooltip" data-placement="top" title="사전 wiki 생성을 잊었을 경우" ng-click="vm.showMoreDesc=!vm.showMoreDesc;vm.delegateAttachmentsMigration(true)" ng-if="vm.destination.projectName" ng-disabled="!vm.source.postCount && !vm.source.issueCount || vm.importBtnDisabled" ng-cloak>파일이동만 다시 한번 더 요청하기</button></div> | |
129 | + </td> | |
130 | + </tr> | |
131 | + <tbody> | |
132 | + </table> | |
133 | + <div class="left-title" ng-cloak> | |
134 | + 기존 이슈 담당자<span ng-if="vm.source.assignees"> ({{vm.source.assignees.length}})</span> | |
135 | + </div> | |
136 | + <div class="caution" ng-if="vm.destination.projectName"> | |
137 | + 대응되는 새 프로젝트 소속의 담당자 id를 입력해 주세요. 만약 지정하지 않으면 기존 담당자의 이슈는 담당자가 해제된 상태로 이전됩니다.</div> | |
138 | + <div> | |
139 | + <table class="table table-bordered" ng-if="vm.source.assignees.length > 0" ng-show="vm.destination.projectName" ng-cloak> | |
140 | + <tr ng-repeat="assignee in vm.source.assignees" class="assignee"> | |
141 | + <td>{{assignee.name}}<br/>@@{{assignee.login}}</td> | |
142 | + <td> | |
143 | + <input ng-keyup="vm.userExistAtDestinationProject(vm.destination, assignee.login)" type="text" placeholder="지정되지 않음" ng-model="vm.destination.assignees[assignee.login].login" ng-init="vm.getAssigneeFromLocal(assignee.login)"><i class="yobicon-check-circle" ng-if="vm.destination.assignees[assignee.login].login && vm.destination.assignees[assignee.login].confirmed"></i><i class="yobicon-delete-circle-alt" ng-if="vm.destination.assignees[assignee.login].login && vm.destination.assignees[assignee.login].confirmed === false"></i></td> | |
144 | + </tr> | |
145 | + </table> | |
146 | + </div> | |
147 | + </div> | |
148 | + </div> | |
149 | + </div> | |
150 | + </div> | |
151 | +} |
+++ app/views/migration/migrationPageLayout.scala.html
... | ... | @@ -0,0 +1,52 @@ |
1 | +@** | |
2 | +* Yona, 21c Project Hosting SW | |
3 | +* | |
4 | +* Copyright Yona & Yobi Authors & NAVER Corp. | |
5 | +* https://yona.io | |
6 | +**@ | |
7 | +@(title: String)(theme:String)(content: Html) | |
8 | +<!DOCTYPE html> | |
9 | +<html> | |
10 | + <head> | |
11 | + <meta charset="utf-8"> | |
12 | + <title>@title</title> | |
13 | + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | |
14 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
15 | + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
16 | + <meta http-equiv="cache-control" content="no-cache"> | |
17 | + <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.ico")"> | |
18 | + <link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet' type='text/css'> | |
19 | + <link href='https://fonts.googleapis.com/css?family=Indie+Flower' rel='stylesheet' type='text/css'> | |
20 | + <link href='https://fonts.googleapis.com/css?family=Muli' rel='stylesheet' type='text/css'> | |
21 | + <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("bootstrap/css/bootstrap.css")"> | |
22 | + <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("stylesheets/yobicon/style.css")"> | |
23 | + <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("javascripts/lib/select2/select2.css")"/> | |
24 | + <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("javascripts/lib/pikaday/pikaday.css")" /> | |
25 | + <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("stylesheets/yobi.css")"> | |
26 | + <link rel='stylesheet' href="@routes.Assets.at("javascripts/lib/nprogress/nprogress.css")"/> | |
27 | + | |
28 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/nprogress/nprogress.js")"></script> | |
29 | + <script type="text/javascript"> | |
30 | + NProgress.configure({ minimum: 0.6 }); | |
31 | + </script> | |
32 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/jquery/jquery-1.9.0.js")"></script> | |
33 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/jquery/jquery.browser.js")"></script> | |
34 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/jquery/jquery.pjax.js")"></script> | |
35 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/common/yobi.Common.js")"></script> | |
36 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/vendor.js")"></script> | |
37 | + <script type="text/javascript" src="@routes.Assets.at("javascripts/service/yona.Migration.js")"></script> | |
38 | + </head> | |
39 | + | |
40 | + <body class="@theme"> | |
41 | + @if(UserApp.isSiteAdminLoggedInSession){ | |
42 | + <div class="admin-logged-in-affix" data-spy="affix" data-offset-top="30">@Messages("user.siteAdminLoggedInAffix") <span class="small-font">@Messages("user.siteAdminLoggedInAffix.maxim")</span></div> | |
43 | + } | |
44 | + @partial_update_notification() | |
45 | + @common.navbar(utils.MenuType.SITE_HOME, null, null) | |
46 | + | |
47 | + @content | |
48 | + | |
49 | + @common.scripts() | |
50 | + </body> | |
51 | +</html> | |
52 | + |
--- conf/routes
+++ conf/routes
... | ... | @@ -9,6 +9,17 @@ |
9 | 9 |
|
10 | 10 |
# Home page |
11 | 11 |
GET / controllers.Application.index() |
12 |
+ |
|
13 |
+# Migration support page |
|
14 |
+GET /migration controllers.MigrationApp.migration() |
|
15 |
+GET /migration/projects controllers.MigrationApp.projects() |
|
16 |
+GET /migration/:owner/projects/:projectName controllers.MigrationApp.project(owner, projectName) |
|
17 |
+GET /migration/:owner/projects/:projectName/labels controllers.MigrationApp.exportLabels(owner, projectName) |
|
18 |
+GET /migration/:owner/projects/:projectName/issuelabel controllers.MigrationApp.exportIssueLabelPairs(owner, projectName) |
|
19 |
+GET /migration/:owner/projects/:projectName/milestones controllers.MigrationApp.exportMilestones(owner, projectName) |
|
20 |
+GET /migration/:owner/projects/:projectName/issues controllers.MigrationApp.exportIssues(owner, projectName) |
|
21 |
+GET /migration/:owner/projects/:projectName/posts controllers.MigrationApp.exportPosts(owner, projectName) |
|
22 |
+ |
|
12 | 23 |
# Map static resources from the /public folder to the /assets URL path |
13 | 24 |
GET /messages.js controllers.Application.jsMessages() |
14 | 25 |
GET /favicon.ico controllers.Assets.at(path="/public", file="images/favicon.ico") |
+++ public/javascripts/lib/vendor.js
This file is too big to display. |
+++ public/javascripts/service/yona.Migration.js
This diff is skipped because there are too many other diffs. |
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?