
Merge branch 'feature/update-notification' of dlab/hive
from pull request 1171
@3bfc204d75472798889856c11136d8a9e2c25958
--- app/Global.java
+++ app/Global.java
... | ... | @@ -69,6 +69,7 @@ |
69 | 69 |
NotificationMail.onStart(); |
70 | 70 |
NotificationEvent.onStart(); |
71 | 71 |
Attachment.onStart(); |
72 |
+ YobiUpdate.onStart(); |
|
72 | 73 |
} |
73 | 74 |
|
74 | 75 |
private boolean equalsDefaultSecret() { |
--- app/assets/stylesheets/less/_common.less
+++ app/assets/stylesheets/less/_common.less
... | ... | @@ -218,3 +218,18 @@ |
218 | 218 |
.font-blue { |
219 | 219 |
color: #0088cc; |
220 | 220 |
} |
221 |
+ |
|
222 |
+.notification-badge { |
|
223 |
+ font-size:12px; |
|
224 |
+ border:2px solid #FFF; |
|
225 |
+ line-height:20px; |
|
226 |
+ padding:0 5px; |
|
227 |
+ background-color:@yobi-primary; |
|
228 |
+ color:#ECF0F1; |
|
229 |
+ .border-radius(10px); |
|
230 |
+ |
|
231 |
+ -webkit-box-shadow: 0 1px 1px rgba(0,0,0,0.2), inset 0 1px 1px rgba(0,0,0,0.1); |
|
232 |
+ -moz-box-shadow: 0 1px 1px rgba(0,0,0,0.2), inset 0 1px 1px rgba(0,0,0,0.1); |
|
233 |
+ -o-box-shadow: 0 1px 1px rgba(0,0,0,0.2), inset 0 1px 1px rgba(0,0,0,0.1); |
|
234 |
+ box-shadow: 0 1px 1px rgba(0,0,0,0.2), inset 0 1px 1px rgba(0,0,0,0.1); |
|
235 |
+} |
--- app/controllers/SiteApp.java
+++ app/controllers/SiteApp.java
... | ... | @@ -31,6 +31,7 @@ |
31 | 31 |
import org.apache.commons.mail.EmailException; |
32 | 32 |
import org.apache.commons.mail.SimpleEmail; |
33 | 33 |
import org.codehaus.jackson.node.ObjectNode; |
34 |
+import org.eclipse.jgit.api.errors.GitAPIException; |
|
34 | 35 |
import play.Configuration; |
35 | 36 |
import play.Logger; |
36 | 37 |
import play.db.ebean.Transactional; |
... | ... | @@ -48,6 +49,8 @@ |
48 | 49 |
import java.util.*; |
49 | 50 |
|
50 | 51 |
import static play.libs.Json.toJson; |
52 |
+ |
|
53 |
+import utils.Config; |
|
51 | 54 |
|
52 | 55 |
/** |
53 | 56 |
* The Class SiteApp. |
... | ... | @@ -247,4 +250,30 @@ |
247 | 250 |
|
248 | 251 |
return ok(toJson(emails)); |
249 | 252 |
} |
253 |
+ |
|
254 |
+ /** |
|
255 |
+ * Hide the notification for Yobi updates. |
|
256 |
+ */ |
|
257 |
+ public static Result unwatchUpdate() { |
|
258 |
+ YobiUpdate.isWatched = false; |
|
259 |
+ return ok(); |
|
260 |
+ } |
|
261 |
+ |
|
262 |
+ /** |
|
263 |
+ * Show the page to update Yobi. |
|
264 |
+ */ |
|
265 |
+ public static Result update() throws GitAPIException { |
|
266 |
+ String currentVersion = null; |
|
267 |
+ Exception exception = null; |
|
268 |
+ |
|
269 |
+ try { |
|
270 |
+ currentVersion = Config.getCurrentVersion(); |
|
271 |
+ YobiUpdate.refreshVersionToUpdate(); |
|
272 |
+ } catch (Exception e) { |
|
273 |
+ exception = e; |
|
274 |
+ } |
|
275 |
+ |
|
276 |
+ return ok(update.render("title.siteSetting", currentVersion, |
|
277 |
+ YobiUpdate.versionToUpdate, exception)); |
|
278 |
+ } |
|
250 | 279 |
} |
+++ app/models/YobiUpdate.java
... | ... | @@ -0,0 +1,131 @@ |
1 | +/** | |
2 | + * Yobi, Project Hosting SW | |
3 | + * | |
4 | + * Copyright 2014 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. | |
20 | + */ | |
21 | + | |
22 | +package models; | |
23 | + | |
24 | +import com.github.zafarkhaja.semver.Version; | |
25 | +import com.github.zafarkhaja.semver.util.UnexpectedElementTypeException; | |
26 | +import com.typesafe.config.*; | |
27 | +import org.eclipse.jgit.api.Git; | |
28 | +import org.eclipse.jgit.api.errors.GitAPIException; | |
29 | +import org.eclipse.jgit.api.errors.InvalidRemoteException; | |
30 | +import org.eclipse.jgit.lib.Ref; | |
31 | +import play.Configuration; | |
32 | +import play.Logger; | |
33 | +import play.libs.Akka; | |
34 | +import scala.concurrent.duration.Duration; | |
35 | +import utils.Config; | |
36 | + | |
37 | +import java.util.*; | |
38 | +import java.util.concurrent.TimeUnit; | |
39 | + | |
40 | +public class YobiUpdate { | |
41 | + private static final Long UPDATE_NOTIFICATION_INITDELAY_IN_MILLIS = Configuration.root() | |
42 | + .getMilliseconds("application.update.notification.initdelay", 5 * 1000L); | |
43 | + private static final Long UPDATE_NOTIFICATION_INTERVAL_IN_MILLIS = Configuration.root() | |
44 | + .getMilliseconds("application.update.notification.interval", 60 * 60 * 1000L); | |
45 | + private static final String UPDATE_REPOSITORY_URL = Configuration.root() | |
46 | + .getString("application.update.repositoryUrl", "http://demo.yobi.io/naver/Yobi"); | |
47 | + private static final String RELEASE_URL_FORMAT = Configuration.root() | |
48 | + .getString("application.update.releaseUrlFormat", | |
49 | + "https://github.com/naver/yobi/releases/tag/v%s"); | |
50 | + | |
51 | + public static String versionToUpdate = null; | |
52 | + | |
53 | + public static boolean isWatched = true; | |
54 | + | |
55 | + public static void onStart() { | |
56 | + if (UPDATE_NOTIFICATION_INTERVAL_IN_MILLIS <= 0) { | |
57 | + return; | |
58 | + } | |
59 | + | |
60 | + Akka.system().scheduler().schedule( | |
61 | + Duration.create(UPDATE_NOTIFICATION_INITDELAY_IN_MILLIS, TimeUnit.MILLISECONDS), | |
62 | + Duration.create(UPDATE_NOTIFICATION_INTERVAL_IN_MILLIS, TimeUnit.MILLISECONDS), | |
63 | + new Runnable() { | |
64 | + public void run() { | |
65 | + try { | |
66 | + refreshVersionToUpdate(); | |
67 | + } catch (Exception e) { | |
68 | + play.Logger.warn("Failed to fetch the latest Yobi version to update", e); | |
69 | + } | |
70 | + } | |
71 | + }, | |
72 | + Akka.system().dispatcher() | |
73 | + ); | |
74 | + } | |
75 | + | |
76 | + public static String getReleaseUrl() throws GitAPIException { | |
77 | + return getReleaseUrl(versionToUpdate); | |
78 | + } | |
79 | + | |
80 | + public static String getReleaseUrl(String version) throws GitAPIException { | |
81 | + return String.format(RELEASE_URL_FORMAT, version); | |
82 | + } | |
83 | + | |
84 | + public static void refreshVersionToUpdate() throws GitAPIException { | |
85 | + versionToUpdate = fetchVersionToUpdate(); | |
86 | + } | |
87 | + | |
88 | + /** | |
89 | + * Fetch the latest version to update. | |
90 | + * | |
91 | + * @Return a string to represent the version to update; null if there is | |
92 | + * no version to update | |
93 | + */ | |
94 | + public static String fetchVersionToUpdate() throws GitAPIException { | |
95 | + Version current = Version.valueOf(Config.semverize(Config.getCurrentVersion())); | |
96 | + Version latest = current; | |
97 | + boolean isUpdateRequired = false; | |
98 | + | |
99 | + Collection<Ref> refs; | |
100 | + | |
101 | + refs = Git.lsRemoteRepository() | |
102 | + .setRemote(UPDATE_REPOSITORY_URL) | |
103 | + .setHeads(false) | |
104 | + .setTags(true) | |
105 | + .call(); | |
106 | + | |
107 | + for(Ref ref : refs) { | |
108 | + String tag = ref.getName().replaceFirst("^refs/tags/", ""); | |
109 | + if (tag.charAt(0) == 'v') { | |
110 | + String versionString = Config.semverize(tag); | |
111 | + | |
112 | + try { | |
113 | + Version ver = Version.valueOf(versionString); | |
114 | + if (ver.greaterThan(latest)) { | |
115 | + isUpdateRequired = true; | |
116 | + latest = ver; | |
117 | + } | |
118 | + } catch (UnexpectedElementTypeException e) { | |
119 | + play.Logger.warn("Failed to parse a version: " + | |
120 | + versionString); | |
121 | + } | |
122 | + } | |
123 | + } | |
124 | + | |
125 | + if (isUpdateRequired) { | |
126 | + return latest.toString(); | |
127 | + } else { | |
128 | + return null; | |
129 | + } | |
130 | + } | |
131 | +} |
--- app/utils/Config.java
+++ app/utils/Config.java
... | ... | @@ -20,11 +20,14 @@ |
20 | 20 |
*/ |
21 | 21 |
package utils; |
22 | 22 |
|
23 |
+import com.typesafe.config.*; |
|
23 | 24 |
import org.apache.commons.lang3.ObjectUtils; |
24 | 25 |
import play.Configuration; |
25 | 26 |
import play.mvc.Http; |
26 | 27 |
|
28 |
+import java.io.File; |
|
27 | 29 |
import java.net.*; |
30 |
+import java.nio.file.Paths; |
|
28 | 31 |
import java.util.Enumeration; |
29 | 32 |
|
30 | 33 |
public class Config { |
... | ... | @@ -208,4 +211,41 @@ |
208 | 211 |
public static URI createFullURI(String uri) throws URISyntaxException { |
209 | 212 |
return createFullURI(new URI(uri)); |
210 | 213 |
} |
214 |
+ |
|
215 |
+ /** |
|
216 |
+ * Convert the given version to be compatible with |
|
217 |
+ * <a href="http://semver.org/">semver</a>. |
|
218 |
+ * |
|
219 |
+ * Examples: |
|
220 |
+ * v0.5.7 -> 0.5.7 |
|
221 |
+ * 0.5 -> 0.5.0 |
|
222 |
+ * 0.4.0.pre -> 0.4.0-pre |
|
223 |
+ * |
|
224 |
+ * @param ver a version string to be semverized |
|
225 |
+ * @return the semverized string |
|
226 |
+ */ |
|
227 |
+ public static String semverize(String ver) { |
|
228 |
+ // v0.5.7 -> 0.5.7 |
|
229 |
+ ver = ver.replaceFirst("^v", ""); |
|
230 |
+ |
|
231 |
+ // 0.4.0.pre -> 0.4.0-pre |
|
232 |
+ ver = ver.replaceAll("\\.([^\\d]+)","-$1"); |
|
233 |
+ |
|
234 |
+ // 0.5 -> 0.5.0 |
|
235 |
+ // 0.5-alpha -> 0.5.0-alpha |
|
236 |
+ ver = ver.replaceFirst("^(\\d+\\.\\d+)($|-)", "$1.0$2"); |
|
237 |
+ |
|
238 |
+ return ver; |
|
239 |
+ } |
|
240 |
+ |
|
241 |
+ /** |
|
242 |
+ * Return the version of Yobi installed currently. |
|
243 |
+ * |
|
244 |
+ * @return the current version |
|
245 |
+ */ |
|
246 |
+ public static String getCurrentVersion() { |
|
247 |
+ File versionFile = Paths.get("conf", "version.conf").toFile(); |
|
248 |
+ |
|
249 |
+ return ConfigFactory.parseFile(versionFile).resolve().getString("app.version"); |
|
250 |
+ } |
|
211 | 251 |
} |
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
... | ... | @@ -40,6 +40,7 @@ |
40 | 40 |
</head> |
41 | 41 |
|
42 | 42 |
<body class="@theme"> |
43 |
+@partial_update_notification() |
|
43 | 44 |
@content |
44 | 45 |
@common.scripts() |
45 | 46 |
</body> |
+++ app/views/partial_update_notification.scala.html
... | ... | @@ -0,0 +1,29 @@ |
1 | +@** | |
2 | +* Yobi, Project Hosting SW | |
3 | +* | |
4 | +* Copyright 2014 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. | |
20 | +**@ | |
21 | + | |
22 | +@import models.YobiUpdate | |
23 | + | |
24 | +@if(UserApp.currentUser().isSiteManager() && YobiUpdate.isWatched && YobiUpdate.versionToUpdate != null){ | |
25 | +<p class="center-txt"> | |
26 | +<a href="@YobiUpdate.getReleaseUrl()">@Messages("site.update.notification", YobiUpdate.versionToUpdate)</a> | |
27 | +<button type="button" data-request-method="post" data-request-uri="@routes.SiteApp.unwatchUpdate()" class="ybtn ybtn-small">@Messages("site.update.notification.hide", YobiUpdate.versionToUpdate)</button> | |
28 | +</p> | |
29 | +} |
--- app/views/site/siteMngLayout.scala.html
+++ app/views/site/siteMngLayout.scala.html
... | ... | @@ -59,6 +59,11 @@ |
59 | 59 |
<li class="@isActiveMenu(routes.SiteApp.massMail())"> |
60 | 60 |
<a href="@routes.SiteApp.massMail()">@Messages("site.sidebar.massMail")</a> |
61 | 61 |
</li> |
62 |
+ <li class="@isActiveMenu(routes.SiteApp.update())"> |
|
63 |
+ <a href="@routes.SiteApp.update()">@Messages("site.sidebar.update") |
|
64 |
+ @if(YobiUpdate.versionToUpdate != null) { <span class="notification-badge">1</span> } |
|
65 |
+ </a> |
|
66 |
+ </li> |
|
62 | 67 |
</ul> |
63 | 68 |
</div> |
64 | 69 |
<div class="span10"> |
+++ app/views/site/update.scala.html
... | ... | @@ -0,0 +1,48 @@ |
1 | +@** | |
2 | +* Yobi, Project Hosting SW | |
3 | +* | |
4 | +* Copyright 2014 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. | |
20 | +**@ | |
21 | +@(message: String, currentVersion: String, versionToUpdate: String, exception: Exception = null) | |
22 | + | |
23 | +@import org.apache.commons.lang.exception.ExceptionUtils | |
24 | +@import utils.TemplateHelper._ | |
25 | + | |
26 | +@siteMngLayout(message) { | |
27 | + <div class="title_area"> | |
28 | + <h2 class="pull-left">@Messages("site.sidebar.update")</h2> | |
29 | + </div> | |
30 | + | |
31 | + @if(versionToUpdate != null){ | |
32 | + <p><strong>@Messages("site.update.isAvailable", versionToUpdate)</strong> | |
33 | + <a href="@YobiUpdate.getReleaseUrl(versionToUpdate)" class="ybtn ybtn-success">@Messages("site.update.download")</a></p> | |
34 | + } | |
35 | + | |
36 | + @if(currentVersion != null){ | |
37 | + <p>@Messages("site.update.currentVersion", currentVersion)</p> | |
38 | + } | |
39 | + | |
40 | + @if(versionToUpdate == null && exception == null) { | |
41 | + <p>@Messages("site.update.isNotNecessary", currentVersion)</p> | |
42 | + } | |
43 | + | |
44 | + @if(exception != null){ | |
45 | + <p>@Messages("site.update.error")</p> | |
46 | + <pre>@ExceptionUtils.getStackTrace(exception)</pre> | |
47 | + } | |
48 | +} |
--- conf/application.conf.default
+++ conf/application.conf.default
... | ... | @@ -127,6 +127,16 @@ |
127 | 127 |
# If this value is undefined or not positive number, notifications will remain forever. |
128 | 128 |
# application.notification.keep-time = 60 |
129 | 129 |
|
130 |
+# Software Update |
|
131 |
+# ~~~~~~~~~~~~~~~ |
|
132 |
+# Check for updates of Yobi at this interval if it is grater than 0. |
|
133 |
+application.update.notification.interval = 1d |
|
134 |
+# A url to the git repository for Yobi releases. |
|
135 |
+application.update.repositoryUrl = "https://github.com/naver/yobi" |
|
136 |
+# A format to construct the url to latest Yobi release. "%s" is a format |
|
137 |
+# specifier for Yobi version to download like "0.5.7". |
|
138 |
+application.update.releaesUrlFormat = "https://github.com/naver/yobi/releases/tag/v%s" |
|
139 |
+ |
|
130 | 140 |
#customize play default thread pool size |
131 | 141 |
play { |
132 | 142 |
akka { |
--- conf/messages
+++ conf/messages
... | ... | @@ -679,6 +679,14 @@ |
679 | 679 |
site.sidebar.postList = Posts |
680 | 680 |
site.sidebar.projectList = Projects |
681 | 681 |
site.sidebar.userList = Users |
682 |
+site.sidebar.update = Software Update |
|
683 |
+site.update.currentVersion = Current version is Yobi {0} |
|
684 |
+site.update.download = Download |
|
685 |
+site.update.isAvailable = Yobi {0} is available |
|
686 |
+site.update.isNotNecessary = You are using the latest version |
|
687 |
+site.update.error = Failed to check for updates because of the following error: |
|
688 |
+site.update.notification = Software update: Yobi {0} is available |
|
689 |
+site.update.notification.hide = Hide |
|
682 | 690 |
site.user.delete = Delete user |
683 | 691 |
site.user.deleteConfirm = Are you sure you want this user to leave? |
684 | 692 |
site.userList.deleteAlert = This user cannot be deleted because this user is the only manager of the project. |
--- conf/messages.ko
+++ conf/messages.ko
... | ... | @@ -679,6 +679,14 @@ |
679 | 679 |
site.sidebar.postList = 게시물 |
680 | 680 |
site.sidebar.projectList = 프로젝트 |
681 | 681 |
site.sidebar.userList = 사용자 |
682 |
+site.sidebar.update = 업데이트 |
|
683 |
+site.update.currentVersion = 현재 버전은 {0} 입니다 |
|
684 |
+site.update.download = 다운로드 |
|
685 |
+site.update.error = 다음과 같이 에러가 발생하여 업데이트 할 버전을 확인하지 못했습니다. |
|
686 |
+site.update.isAvailable = Yobi {0} 버전으로 업데이트 할 수 있습니다 |
|
687 |
+site.update.isNotNecessary = 현재 최신 버전을 사용중입니다 |
|
688 |
+site.update.notification = 업데이트 알림: Yobi {0} 버전이 나왔습니다 |
|
689 |
+site.update.notification.hide = 숨기기 |
|
682 | 690 |
site.user.delete = 사용자 삭제 |
683 | 691 |
site.user.deleteConfirm = 정말로 해당 사용자를 사이트에서 탈퇴시키겠습니까? |
684 | 692 |
site.userList.deleteAlert = 프로젝트의 유일한 관리자이므로 사이트에서 삭제할 수 없습니다. |
--- conf/routes
+++ conf/routes
... | ... | @@ -71,6 +71,8 @@ |
71 | 71 |
GET /sites/projectList controllers.SiteApp.projectList(filter:String ?= "", pageNum: Int ?=0) |
72 | 72 |
DELETE /sites/project/delete/:projectId controllers.SiteApp.deleteProject(projectId:Long) |
73 | 73 |
POST /sites/toggleAccountLock controllers.SiteApp.toggleAccountLock(loginId: String, state: String ?= null, query: String ?= null) |
74 |
+GET /sites/update controllers.SiteApp.update() |
|
75 |
+POST /sites/unwatchUpdate controllers.SiteApp.unwatchUpdate() |
|
74 | 76 |
GET /lostPassword controllers.PasswordResetApp.lostPassword |
75 | 77 |
POST /lostPassword controllers.PasswordResetApp.requestResetPasswordEmail() |
76 | 78 |
GET /resetPassword controllers.PasswordResetApp.resetPasswordForm(s:String) |
--- project/Build.scala
+++ project/Build.scala
... | ... | @@ -52,7 +52,8 @@ |
52 | 52 |
"commons-collections" % "commons-collections" % "3.2.1", |
53 | 53 |
"org.jsoup" % "jsoup" % "1.7.2", |
54 | 54 |
"com.googlecode.juniversalchardet" % "juniversalchardet" % "1.0.3", |
55 |
- "org.mockito" % "mockito-all" % "1.9.0" % "test" |
|
55 |
+ "org.mockito" % "mockito-all" % "1.9.0" % "test", |
|
56 |
+ "com.github.zafarkhaja" % "java-semver" % "0.7.2" |
|
56 | 57 |
) |
57 | 58 |
|
58 | 59 |
val projectSettings = Seq( |
--- test/utils/ConfigTest.java
+++ test/utils/ConfigTest.java
... | ... | @@ -100,4 +100,13 @@ |
100 | 100 |
assertThat(Config.getSiteName()).isEqualTo("Yobi"); |
101 | 101 |
Helpers.stop(app); |
102 | 102 |
} |
103 |
+ |
|
104 |
+ @Test |
|
105 |
+ public void semverize() { |
|
106 |
+ assertThat(Config.semverize("0.5.7")).isEqualTo("0.5.7"); |
|
107 |
+ assertThat(Config.semverize("v0.5.7")).isEqualTo("0.5.7"); |
|
108 |
+ assertThat(Config.semverize("0.5")).isEqualTo("0.5.0"); |
|
109 |
+ assertThat(Config.semverize("0.4.0.pre")).isEqualTo("0.4.0-pre"); |
|
110 |
+ assertThat(Config.semverize("0.5-alpha")).isEqualTo("0.5.0-alpha"); |
|
111 |
+ } |
|
103 | 112 |
} |
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?