[Notice] Announcing the End of Demo Server [Read me]

siteAdmin: Add Mass Mail feature.
This feature allows Site Administrator to send emails to many users.
@979020403a4e9aeb70a3da3e2f586d847d0222e5
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
... | ... | @@ -1,6 +1,7 @@ |
1 | 1 |
package controllers; |
2 | 2 |
|
3 | 3 |
import com.avaje.ebean.Page; |
4 |
+import com.avaje.ebean.ExpressionList; |
|
4 | 5 |
import models.*; |
5 | 6 |
import models.enumeration.Operation; |
6 | 7 |
import models.enumeration.RoleType; |
... | ... | @@ -32,12 +33,15 @@ |
32 | 33 |
import java.util.HashMap; |
33 | 34 |
import java.util.List; |
34 | 35 |
import java.util.Map; |
36 |
+import java.util.ArrayList; |
|
35 | 37 |
|
36 | 38 |
import static play.data.Form.form; |
37 | 39 |
import static play.libs.Json.toJson; |
40 |
+import static com.avaje.ebean.Expr.contains; |
|
38 | 41 |
|
39 | 42 |
public class ProjectApp extends Controller { |
40 | 43 |
public static final String[] LOGO_TYPE = {"jpg", "png", "gif", "bmp"}; |
44 |
+ private static final int MAX_FETCH_PROJECTS = 1000; |
|
41 | 45 |
|
42 | 46 |
public static Project getProject(String userName, String projectName) { |
43 | 47 |
return Project.findByNameAndOwner(userName, projectName); |
... | ... | @@ -247,6 +251,30 @@ |
247 | 251 |
} |
248 | 252 |
|
249 | 253 |
public static Result projects(String filter, String state, int pageNum) { |
254 |
+ final String HTML = "text/html"; |
|
255 |
+ final String JSON = "application/json"; |
|
256 |
+ String prefer = HttpUtil.getPreferType(request(), JSON, HTML); |
|
257 |
+ |
|
258 |
+ if (prefer == null) { |
|
259 |
+ return status(Http.Status.NOT_ACCEPTABLE); |
|
260 |
+ } |
|
261 |
+ |
|
262 |
+ if (prefer.equals(JSON)) { |
|
263 |
+ String query = request().getQueryString("query"); |
|
264 |
+ List<String> projectNames = new ArrayList<String>(); |
|
265 |
+ ExpressionList<Project> el = Project.find.where().or(contains("name", query), contains("owner", query)); |
|
266 |
+ int total = el.findRowCount(); |
|
267 |
+ if (total > MAX_FETCH_PROJECTS) { |
|
268 |
+ el.setMaxRows(MAX_FETCH_PROJECTS); |
|
269 |
+ response().setHeader("Content-Range", "items " + MAX_FETCH_PROJECTS + "/" + total); |
|
270 |
+ } |
|
271 |
+ for (Project project: el.findList()) { |
|
272 |
+ projectNames.add(project.owner + "/" + project.name); |
|
273 |
+ } |
|
274 |
+ |
|
275 |
+ return ok(toJson(projectNames)); |
|
276 |
+ } |
|
277 |
+ |
|
250 | 278 |
OrderParams orderParams = new OrderParams(); |
251 | 279 |
SearchParams searchParams = new SearchParams(); |
252 | 280 |
|
--- app/controllers/SiteApp.java
+++ app/controllers/SiteApp.java
... | ... | @@ -3,25 +3,34 @@ |
3 | 3 |
import java.util.ArrayList; |
4 | 4 |
import java.util.List; |
5 | 5 |
import java.util.Map; |
6 |
+import java.util.Set; |
|
7 |
+import java.util.HashSet; |
|
6 | 8 |
|
9 |
+import models.ProjectUser; |
|
7 | 10 |
import org.apache.commons.mail.*; |
8 | 11 |
|
9 | 12 |
import models.Project; |
10 | 13 |
import models.User; |
11 | 14 |
import org.apache.commons.mail.SimpleEmail; |
15 |
+import org.codehaus.jackson.JsonNode; |
|
12 | 16 |
import play.Configuration; |
13 | 17 |
import play.Logger; |
14 | 18 |
import play.mvc.Controller; |
19 |
+import play.mvc.Http; |
|
15 | 20 |
import play.mvc.Result; |
16 | 21 |
import utils.Constants; |
17 | 22 |
|
18 | 23 |
import views.html.site.setting; |
19 | 24 |
import views.html.site.mail; |
25 |
+import views.html.site.massMail; |
|
20 | 26 |
import views.html.site.userList; |
21 | 27 |
import views.html.site.projectList; |
22 | 28 |
|
23 | 29 |
import com.avaje.ebean.Page; |
24 | 30 |
import static play.data.Form.form; |
31 |
+import static play.libs.Json.toJson; |
|
32 |
+import play.i18n.Messages; |
|
33 |
+ |
|
25 | 34 |
import info.schleichardt.play2.mailplugin.Mailer; |
26 | 35 |
|
27 | 36 |
public class SiteApp extends Controller { |
... | ... | @@ -57,6 +66,21 @@ |
57 | 66 |
String sender = config.getString("smtp.user") + "@" + config.getString("smtp.domain"); |
58 | 67 |
|
59 | 68 |
return ok(mail.render("title.sendMail", notConfiguredItems, sender, errorMessage, sended)); |
69 |
+ } |
|
70 |
+ |
|
71 |
+ public static Result massMail() { |
|
72 |
+ Configuration config = play.Play.application().configuration(); |
|
73 |
+ List<String> notConfiguredItems = new ArrayList<String>(); |
|
74 |
+ String[] requiredItems = {"smtp.host", "smtp.user", "smtp.password"}; |
|
75 |
+ for(String key : requiredItems) { |
|
76 |
+ if (config.getString(key) == null) { |
|
77 |
+ notConfiguredItems.add(key); |
|
78 |
+ } |
|
79 |
+ } |
|
80 |
+ |
|
81 |
+ String sender = config.getString("smtp.user") + "@" + config.getString("smtp.domain"); |
|
82 |
+ |
|
83 |
+ return ok(massMail.render("title.massMail", notConfiguredItems, sender)); |
|
60 | 84 |
} |
61 | 85 |
|
62 | 86 |
public static Result setting() { |
... | ... | @@ -117,4 +141,43 @@ |
117 | 141 |
flash(Constants.WARNING, "auth.unauthorized.waringMessage"); |
118 | 142 |
return redirect(routes.Application.index()); |
119 | 143 |
} |
144 |
+ |
|
145 |
+ public static Result mailList() { |
|
146 |
+ Set<String> emails = new HashSet<String>(); |
|
147 |
+ Map<String, String[]> projects = request().body().asFormUrlEncoded(); |
|
148 |
+ |
|
149 |
+ if(!UserApp.currentUser().isSiteManager()) { |
|
150 |
+ return forbidden(Messages.get("auth.unauthorized.waringMessage")); |
|
151 |
+ } |
|
152 |
+ |
|
153 |
+ if (!request().accepts("application/json")) { |
|
154 |
+ return status(Http.Status.NOT_ACCEPTABLE); |
|
155 |
+ } |
|
156 |
+ |
|
157 |
+ if (projects == null) { |
|
158 |
+ return ok(toJson(new HashSet<String>())); |
|
159 |
+ } |
|
160 |
+ |
|
161 |
+ if (projects.containsKey("all")) { |
|
162 |
+ if (projects.get("all")[0].equals("true")) { |
|
163 |
+ for(User user : User.find.findList()) { |
|
164 |
+ emails.add(user.email); |
|
165 |
+ } |
|
166 |
+ } |
|
167 |
+ } else { |
|
168 |
+ for(String[] projectNames : projects.values()) { |
|
169 |
+ String projectName = projectNames[0]; |
|
170 |
+ String[] parts = projectName.split("/"); |
|
171 |
+ String owner = parts[0]; |
|
172 |
+ String name = parts[1]; |
|
173 |
+ Project project = Project.findByNameAndOwner(owner, name); |
|
174 |
+ for (ProjectUser projectUser : ProjectUser.findMemberListByProject(project.id)) { |
|
175 |
+ Logger.debug(projectUser.user.email); |
|
176 |
+ emails.add(projectUser.user.email); |
|
177 |
+ } |
|
178 |
+ } |
|
179 |
+ } |
|
180 |
+ |
|
181 |
+ return ok(toJson(emails)); |
|
182 |
+ } |
|
120 | 183 |
} |
--- app/utils/HttpUtil.java
+++ app/utils/HttpUtil.java
... | ... | @@ -1,7 +1,12 @@ |
1 | 1 |
package utils; |
2 | 2 |
|
3 |
+import play.api.http.MediaRange; |
|
4 |
+import play.mvc.Http; |
|
5 |
+import scala.Option; |
|
6 |
+ |
|
3 | 7 |
import java.io.UnsupportedEncodingException; |
4 | 8 |
import java.net.URLEncoder; |
9 |
+import java.util.List; |
|
5 | 10 |
import java.util.Map; |
6 | 11 |
|
7 | 12 |
public class HttpUtil { |
... | ... | @@ -25,4 +30,16 @@ |
25 | 30 |
filename = "filename*=UTF-8''" + filename; |
26 | 31 |
return filename; |
27 | 32 |
} |
33 |
+ |
|
34 |
+ public static String getPreferType(Http.Request request, String ... types) { |
|
35 |
+ // acceptedTypes is sorted by preference. |
|
36 |
+ for(MediaRange range : request.acceptedTypes()) { |
|
37 |
+ for(String type : types) { |
|
38 |
+ if (range.accepts(type)) { |
|
39 |
+ return type; |
|
40 |
+ } |
|
41 |
+ } |
|
42 |
+ } |
|
43 |
+ return null; |
|
44 |
+ } |
|
28 | 45 |
} |
+++ app/views/site/massMail.scala.html
... | ... | @@ -0,0 +1,42 @@ |
1 | +@(message: String, notConfiguredItems: List[String], sender: String) | |
2 | + | |
3 | +@siteMngMain(message) { | |
4 | +<div class="page-header"> | |
5 | + <h3>@Messages(message)</h3> | |
6 | +</div> | |
7 | + | |
8 | +<div> | |
9 | + <p> | |
10 | + <label class="radio"> | |
11 | + <input type="radio" name="mailingType" value="all"> | |
12 | + @Messages("site.massMail.toAll") | |
13 | + </label> | |
14 | + </p> | |
15 | + <p> | |
16 | + <label class="radio"> | |
17 | + <input type="radio" name="mailingType" value="projects" checked>@Messages("site.massMail.toProjects"): | |
18 | + <span id="selected-projects"><!-- project names are added here by hive.site.MassMail.js --></span> | |
19 | + <input type="text" class="text uname" | |
20 | + id="input-project" | |
21 | + data-provider="typeahead" autocomplete="off" | |
22 | + placeholder="@Messages("project.name")"/> | |
23 | + <button type="submit" class="btn btn-primary" id="select-project" data-loading-text="@Messages("site.massMail.loading")"> | |
24 | + <strong>@Messages("button.add")</strong> | |
25 | + </button> | |
26 | + </label> | |
27 | + </p> | |
28 | +</div> | |
29 | + | |
30 | +<button type="submit" class="btn btn-primary" id="write-email"> | |
31 | + <strong>@Messages("mail.write")</strong> | |
32 | +</button> | |
33 | + | |
34 | +<script type="text/javascript"> | |
35 | + $(document).ready(function(){ | |
36 | + $hive.loadModule("site.MassMail", { | |
37 | + "sURLMailList": "@routes.SiteApp.mailList()", | |
38 | + "sURLProjects": "@routes.ProjectApp.projects()" | |
39 | + }); | |
40 | + }); | |
41 | +</script> | |
42 | +} |
--- conf/messages.en
+++ conf/messages.en
... | ... | @@ -281,6 +281,7 @@ |
281 | 281 |
site.sidebar.projectList = Projects |
282 | 282 |
site.sidebar.softwareMap = Software Map |
283 | 283 |
site.sidebar.mailSend = Send a Email |
284 |
+site.sidebar.massMail = Send Mass Emails |
|
284 | 285 |
site.project.filter = Find Project by Project Name |
285 | 286 |
site.userList.search = Find a user by his/her login ID... |
286 | 287 |
site.userList.isActive = Active Status |
... | ... | @@ -291,6 +292,9 @@ |
291 | 292 |
site.project.deleteConfirm = Do you want to delete this project? |
292 | 293 |
site.userlist.unlocked = Active User List |
293 | 294 |
site.userlist.locked = Account Locked User List |
295 |
+site.massMail.toAll = To All |
|
296 |
+site.massMail.toProjects = To members of specific projects |
|
297 |
+site.massMail.loading = Loading... |
|
294 | 298 |
|
295 | 299 |
#User |
296 | 300 |
user.loginId = Login ID |
... | ... | @@ -384,6 +388,7 @@ |
384 | 388 |
mail.subject = Subject |
385 | 389 |
mail.body = Body |
386 | 390 |
mail.send = Send |
391 |
+mail.write = Write |
|
387 | 392 |
mail.fail = Failed to send a mail. |
388 | 393 |
mail.sended = Mail is sent. |
389 | 394 |
|
--- conf/messages.ko
+++ conf/messages.ko
... | ... | @@ -22,6 +22,7 @@ |
22 | 22 |
title.commitHistory = 커밋 히스토리 |
23 | 23 |
title.mailSetting = 메일 설정 |
24 | 24 |
title.sendMail = 메일 발송 |
25 |
+title.massMail = 대량 메일 발송 |
|
25 | 26 |
title.projectList = 프로젝트 목록 |
26 | 27 |
title.help = 도움말 |
27 | 28 |
title.search = 검색 |
... | ... | @@ -287,6 +288,7 @@ |
287 | 288 |
site.sidebar.projectList = 프로젝트 설정 |
288 | 289 |
site.sidebar.softwareMap = 소프트웨어 맵 |
289 | 290 |
site.sidebar.mailSend = 메일 발송 |
291 |
+site.sidebar.massMail = 대량 메일 발송 |
|
290 | 292 |
site.project.filter = 찾으려는 프로젝트 이름을 입력하세요 |
291 | 293 |
site.userList.search = 찾으려는 사용자의 ID를 입력하세요 |
292 | 294 |
site.userList.isActive = 활성 상태 |
... | ... | @@ -297,6 +299,10 @@ |
297 | 299 |
site.project.deleteConfirm = 정말로 해당 프로젝트를 사이트에서 삭제하겠습니까? |
298 | 300 |
site.userlist.unlocked = 활성화된 유저 목록 |
299 | 301 |
site.userlist.locked = 계정이 잠긴 유저 목록 |
302 |
+site.massMail.toAll = 모두에게 |
|
303 |
+site.massMail.toProjects = 특정 프로젝트의 멤버들에게 |
|
304 |
+site.massMail.loading = 불러오는중... |
|
305 |
+ |
|
300 | 306 |
|
301 | 307 |
#User |
302 | 308 |
user.loginId = 아이디 |
... | ... | @@ -390,6 +396,7 @@ |
390 | 396 |
mail.subject = 제목 |
391 | 397 |
mail.body = 본문 |
392 | 398 |
mail.send = 발송 |
399 |
+mail.write = 메일 쓰기 |
|
393 | 400 |
mail.fail = 메일 발송에 실패했습니다. |
394 | 401 |
mail.sended = 메일을 발송하였습니다. |
395 | 402 |
|
--- conf/routes
+++ conf/routes
... | ... | @@ -34,6 +34,8 @@ |
34 | 34 |
GET /sites/setting controllers.SiteApp.setting() |
35 | 35 |
GET /sites/mail controllers.SiteApp.writeMail(errorMessage:String ?= "", sended:Boolean ?= false) |
36 | 36 |
POST /sites/mail controllers.SiteApp.sendMail() |
37 |
+GET /sites/massmail controllers.SiteApp.massMail() |
|
38 |
+POST /sites/mailList controllers.SiteApp.mailList() |
|
37 | 39 |
GET /sites/userList controllers.SiteApp.userList(pageNum: Int ?= 0, loginId: String ?= null) |
38 | 40 |
POST /sites/userList controllers.SiteApp.searchUser() |
39 | 41 |
GET /sites/user/delete controllers.SiteApp.deleteUser(userId: Long) |
--- public/javascripts/common/hive.Common.js
+++ public/javascripts/common/hive.Common.js
... | ... | @@ -69,7 +69,7 @@ |
69 | 69 |
// 그렇지 않으면 스크립트 파일 불러온 뒤 초기화 시도 |
70 | 70 |
if(registerModule(sName, htOptions) === false){ |
71 | 71 |
htVar.htTryLoad = htVar.htTryLoad || {}; |
72 |
- htVar.htTryLoad[sName] = (typeof htVar.htTryLoad[sName] == "undefined") ? 1 : (htVar.htTryLoad[sName]++); |
|
72 |
+ htVar.htTryLoad[sName] = (typeof htVar.htTryLoad[sName] == "undefined") ? 1 : (++htVar.htTryLoad[sName]); |
|
73 | 73 |
|
74 | 74 |
if(htVar.htTryLoad[sName] > 3){ |
75 | 75 |
console.log("[HIVE] fail to load module " + sName); |
... | ... | @@ -226,7 +226,7 @@ |
226 | 226 |
welForm.ajaxForm({ |
227 | 227 |
"success" : htOptions.fOnLoad || function(){}, |
228 | 228 |
"error" : htOptions.fOnError || function(){}, |
229 |
- "datatype": htOptions.sDataType || null |
|
229 |
+ "dataType": htOptions.sDataType || null |
|
230 | 230 |
}); |
231 | 231 |
welForm.submit(); |
232 | 232 |
|
... | ... | @@ -286,4 +286,4 @@ |
286 | 286 |
|
287 | 287 |
$hive.loadModule(sModuleName, htOptions); |
288 | 288 |
} |
289 |
-};(No newline at end of file) |
|
289 |
+}; |
--- public/javascripts/common/hive.ui.Typeahead.js
+++ public/javascripts/common/hive.ui.Typeahead.js
... | ... | @@ -60,6 +60,7 @@ |
60 | 60 |
"sURL" : htVar.sActionURL, |
61 | 61 |
"htOptForm" : {"method":"get"}, |
62 | 62 |
"htData" : {"query": sQuery}, |
63 |
+ "sDataType" : "json", |
|
63 | 64 |
"fOnLoad" : function(oData, oStatus, oXHR){ |
64 | 65 |
var sContentRange = oXHR.getResponseHeader('Content-Range'); |
65 | 66 |
|
... | ... | @@ -78,7 +79,7 @@ |
78 | 79 |
* Return whether the given content range is an entire range for items. |
79 | 80 |
* e.g) "items 10/10" |
80 | 81 |
* |
81 |
- * @param {String} sContentRange the vaule of Content-Range header from response |
|
82 |
+ * @param {String} sContentRange the value of Content-Range header from response |
|
82 | 83 |
* @return {Boolean} |
83 | 84 |
*/ |
84 | 85 |
function _isEntireRange(sContentRange){ |
... | ... | @@ -89,4 +90,4 @@ |
89 | 90 |
_init(sQuery, htOptions || {}); |
90 | 91 |
}; |
91 | 92 |
|
92 |
-})("hive.ui.Typeahead");(No newline at end of file) |
|
93 |
+})("hive.ui.Typeahead"); |
+++ public/javascripts/service/hive.site.MassMail.js
... | ... | @@ -0,0 +1,154 @@ |
1 | +/** | |
2 | + * @(#)hive.project.Setting.js 2013.03.18 | |
3 | + * | |
4 | + * Copyright NHN Corporation. | |
5 | + * Released under the MIT license | |
6 | + * | |
7 | + * http://hive.dev.naver.com/license | |
8 | + */ | |
9 | + | |
10 | +(function(ns){ | |
11 | + | |
12 | + var oNS = $hive.createNamespace(ns); | |
13 | + oNS.container[oNS.name] = function(htOptions){ | |
14 | + | |
15 | + var htVar = {}; | |
16 | + var htElement = {}; | |
17 | + | |
18 | + /** | |
19 | + * initialize | |
20 | + */ | |
21 | + function _init(htOptions){ | |
22 | + var htOpt = htOptions || {}; | |
23 | + _initVar(htOpt); | |
24 | + _initElement(htOpt); | |
25 | + _attachEvent(); | |
26 | + } | |
27 | + | |
28 | + /** | |
29 | + * initialize variables | |
30 | + * 정규식 변수는 한번만 선언하는게 성능 향상에 도움이 됩니다 | |
31 | + */ | |
32 | + function _initVar(htOptions){ | |
33 | + htVar.sURLProjects = htOptions.sURLProjects; | |
34 | + htVar.sURLMailList = htOptions.sURLMailList; | |
35 | + } | |
36 | + | |
37 | + /** | |
38 | + * initialize element variables | |
39 | + */ | |
40 | + function _initElement(htOptions){ | |
41 | + // projects | |
42 | + htElement.welInputProject = $('#input-project'); | |
43 | + htElement.welSelectedProjects = $('#selected-projects'); | |
44 | + htElement.welBtnSelectProject = $('#select-project'); | |
45 | + htElement.welBtnWriteEmail = $('#write-email'); | |
46 | + } | |
47 | + | |
48 | + /** | |
49 | + * attach event handlers | |
50 | + */ | |
51 | + function _attachEvent(){ | |
52 | + htElement.welInputProject.keypress(_onKeyPressInputProject); | |
53 | + htElement.welBtnSelectProject.click(_onClickSelectProject); | |
54 | + htElement.welBtnWriteEmail.click(_onClickWriteEmail); | |
55 | + new hive.ui.Typeahead(htElement.welInputProject, { | |
56 | + "sActionURL": htVar.sURLProjects | |
57 | + }); | |
58 | + } | |
59 | + | |
60 | + /** | |
61 | + * Launch a mail client to write an email. | |
62 | + */ | |
63 | + function _onClickWriteEmail() { | |
64 | + // Get project names from labels in #selected-projects div. | |
65 | + var sMailingType = $('[name=mailingType]:checked').val(); | |
66 | + var waProjectSpan, aProjects; | |
67 | + if (sMailingType == 'all') { | |
68 | + aProjects = {'all': 'true'} | |
69 | + } else { | |
70 | + waProjectSpan = $('#selected-projects span'); | |
71 | + aProjects = []; | |
72 | + for (var i = 0; i < waProjectSpan.length; i++) { | |
73 | + aProjects.push(waProjectSpan[i].childNodes[0].nodeValue.trim()); | |
74 | + } | |
75 | + } | |
76 | + | |
77 | + // Send a request contains project names to get email addresses and | |
78 | + // launch user's mail client with them using mailto scheme. | |
79 | + htElement.welBtnWriteEmail.button('loading'); | |
80 | + $hive.sendForm({ | |
81 | + "sURL" : htVar.sURLMailList, | |
82 | + "htOptForm": {"method":"POST"}, | |
83 | + "htData" : aProjects, | |
84 | + "sDataType" : "json", | |
85 | + "fOnLoad" : function(data) { | |
86 | + var form = $('<form>'); | |
87 | + var mailto = 'mailto:'; | |
88 | + for (var i = 0; i < data.length; i++) { | |
89 | + mailto += data[i] + ','; | |
90 | + } | |
91 | + form.attr('method', 'POST'); | |
92 | + form.attr('action', mailto); | |
93 | + form.attr('enctype', 'text/plain'); | |
94 | + form.submit(); | |
95 | + htElement.welBtnWriteEmail.button('reset'); | |
96 | + } | |
97 | + }); | |
98 | + } | |
99 | + | |
100 | + /** | |
101 | + * Add a project, which user types in #input-project element, into | |
102 | + * #selected-projects div. | |
103 | + */ | |
104 | + function _onClickSelectProject() { | |
105 | + _appendProjectLabel([htElement.welInputProject.val()]); | |
106 | + htElement.welInputProject.val(""); | |
107 | + return false; | |
108 | + } | |
109 | + | |
110 | + /** | |
111 | + * Same as _onClickSelectProject but triggered by pressing enter. | |
112 | + * | |
113 | + * @param {Object} oEvent | |
114 | + */ | |
115 | + function _onKeyPressInputProject(oEvent) { | |
116 | + if (oEvent.keyCode == 13) { | |
117 | + _appendProjectLabel([htElement.welInputProject.val()]); | |
118 | + htElement.welInputProject.val(""); | |
119 | + return false; | |
120 | + } | |
121 | + } | |
122 | + | |
123 | + /** | |
124 | + * Make a project label by given name. | |
125 | + * | |
126 | + * @param {String} sName | |
127 | + */ | |
128 | + function _createProjectLabel(sName) { | |
129 | + var fOnClickUnselect = function() { | |
130 | + welProject.remove(); | |
131 | + }; | |
132 | + | |
133 | + var welProject = $('<span class="label label-info">' + sName + " </span>") | |
134 | + .append($('<a href="javascript:void(0)">x</a>') | |
135 | + .click(fOnClickUnselect)); | |
136 | + | |
137 | + return welProject; | |
138 | + } | |
139 | + | |
140 | + /** | |
141 | + * Append the given projects on #selected-projects div to show them. | |
142 | + * | |
143 | + * @param {Object} htProjects | |
144 | + */ | |
145 | + function _appendProjectLabel(htProjects) { | |
146 | + for(var sId in htProjects) { | |
147 | + htElement.welSelectedProjects.append(_createProjectLabel(htProjects[sId])); | |
148 | + } | |
149 | + } | |
150 | + | |
151 | + _init(htOptions); | |
152 | + }; | |
153 | + | |
154 | +})("hive.site.MassMail"); |
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?