[Notice] Announcing the End of Demo Server [Read me]
Yi EungJun 2013-04-26
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
+++ app/controllers/ProjectApp.java
@@ -1,6 +1,7 @@
 package controllers;
 
 import com.avaje.ebean.Page;
+import com.avaje.ebean.ExpressionList;
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.RoleType;
@@ -32,12 +33,15 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.ArrayList;
 
 import static play.data.Form.form;
 import static play.libs.Json.toJson;
+import static com.avaje.ebean.Expr.contains;
 
 public class ProjectApp extends Controller {
 	public static final String[] LOGO_TYPE = {"jpg", "png", "gif", "bmp"};
+    private static final int MAX_FETCH_PROJECTS = 1000;
 
     public static Project getProject(String userName, String projectName) {
         return Project.findByNameAndOwner(userName, projectName);
@@ -247,6 +251,30 @@
     }
 
     public static Result projects(String filter, String state, int pageNum) {
+        final String HTML = "text/html";
+        final String JSON = "application/json";
+        String prefer = HttpUtil.getPreferType(request(), JSON, HTML);
+
+        if (prefer == null) {
+            return status(Http.Status.NOT_ACCEPTABLE);
+        }
+
+        if (prefer.equals(JSON)) {
+            String query = request().getQueryString("query");
+            List<String> projectNames = new ArrayList<String>();
+            ExpressionList<Project> el = Project.find.where().or(contains("name", query), contains("owner", query));
+            int total = el.findRowCount();
+            if (total > MAX_FETCH_PROJECTS) {
+                el.setMaxRows(MAX_FETCH_PROJECTS);
+                response().setHeader("Content-Range", "items " + MAX_FETCH_PROJECTS + "/" + total);
+            }
+            for (Project project: el.findList()) {
+                projectNames.add(project.owner + "/" + project.name);
+            }
+
+            return ok(toJson(projectNames));
+        }
+
         OrderParams orderParams = new OrderParams();
         SearchParams searchParams = new SearchParams();
 
app/controllers/SiteApp.java
--- app/controllers/SiteApp.java
+++ app/controllers/SiteApp.java
@@ -3,25 +3,34 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
 
+import models.ProjectUser;
 import org.apache.commons.mail.*;
 
 import models.Project;
 import models.User;
 import org.apache.commons.mail.SimpleEmail;
+import org.codehaus.jackson.JsonNode;
 import play.Configuration;
 import play.Logger;
 import play.mvc.Controller;
+import play.mvc.Http;
 import play.mvc.Result;
 import utils.Constants;
 
 import views.html.site.setting;
 import views.html.site.mail;
+import views.html.site.massMail;
 import views.html.site.userList;
 import views.html.site.projectList;
 
 import com.avaje.ebean.Page;
 import static play.data.Form.form;
+import static play.libs.Json.toJson;
+import play.i18n.Messages;
+
 import info.schleichardt.play2.mailplugin.Mailer;
 
 public class SiteApp extends Controller {
@@ -57,6 +66,21 @@
         String sender = config.getString("smtp.user") + "@" + config.getString("smtp.domain");
 
         return ok(mail.render("title.sendMail", notConfiguredItems, sender, errorMessage, sended));
+    }
+
+    public static Result massMail() {
+        Configuration config = play.Play.application().configuration();
+        List<String> notConfiguredItems = new ArrayList<String>();
+        String[] requiredItems = {"smtp.host", "smtp.user", "smtp.password"};
+        for(String key : requiredItems) {
+            if (config.getString(key) == null) {
+                notConfiguredItems.add(key);
+            }
+        }
+
+        String sender = config.getString("smtp.user") + "@" + config.getString("smtp.domain");
+
+        return ok(massMail.render("title.massMail", notConfiguredItems, sender));
     }
 
     public static Result setting() {
@@ -117,4 +141,43 @@
         flash(Constants.WARNING, "auth.unauthorized.waringMessage");
         return redirect(routes.Application.index());
     }
+
+    public static Result mailList() {
+        Set<String> emails = new HashSet<String>();
+        Map<String, String[]> projects = request().body().asFormUrlEncoded();
+
+        if(!UserApp.currentUser().isSiteManager()) {
+            return forbidden(Messages.get("auth.unauthorized.waringMessage"));
+        }
+
+        if (!request().accepts("application/json")) {
+            return status(Http.Status.NOT_ACCEPTABLE);
+        }
+
+        if (projects == null) {
+            return ok(toJson(new HashSet<String>()));
+        }
+
+        if (projects.containsKey("all")) {
+            if (projects.get("all")[0].equals("true")) {
+                for(User user : User.find.findList()) {
+                    emails.add(user.email);
+                }
+            }
+        } else {
+            for(String[] projectNames : projects.values()) {
+                String projectName = projectNames[0];
+                String[] parts = projectName.split("/");
+                String owner = parts[0];
+                String name = parts[1];
+                Project project = Project.findByNameAndOwner(owner, name);
+                for (ProjectUser projectUser : ProjectUser.findMemberListByProject(project.id)) {
+                    Logger.debug(projectUser.user.email);
+                    emails.add(projectUser.user.email);
+                }
+            }
+        }
+
+        return ok(toJson(emails));
+    }
 }
app/utils/HttpUtil.java
--- app/utils/HttpUtil.java
+++ app/utils/HttpUtil.java
@@ -1,7 +1,12 @@
 package utils;
 
+import play.api.http.MediaRange;
+import play.mvc.Http;
+import scala.Option;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.List;
 import java.util.Map;
 
 public class HttpUtil {
@@ -25,4 +30,16 @@
         filename = "filename*=UTF-8''" + filename;
         return filename;
     }
+
+    public static String getPreferType(Http.Request request, String ... types) {
+        // acceptedTypes is sorted by preference.
+        for(MediaRange range : request.acceptedTypes()) {
+            for(String type : types) {
+                if (range.accepts(type)) {
+                    return type;
+                }
+            }
+        }
+        return null;
+    }
 }
 
app/views/site/massMail.scala.html (added)
+++ app/views/site/massMail.scala.html
@@ -0,0 +1,42 @@
+@(message: String, notConfiguredItems: List[String], sender: String)
+
+@siteMngMain(message) {
+<div class="page-header">
+  <h3>@Messages(message)</h3>
+</div>
+
+<div>
+  <p>
+  <label class="radio">
+    <input type="radio" name="mailingType" value="all">
+    @Messages("site.massMail.toAll")
+  </label>
+  </p>
+  <p>
+  <label class="radio">
+    <input type="radio" name="mailingType" value="projects" checked>@Messages("site.massMail.toProjects"):
+    <span id="selected-projects"><!-- project names are added here by hive.site.MassMail.js --></span>
+    <input type="text" class="text uname"
+            id="input-project"
+            data-provider="typeahead" autocomplete="off"
+            placeholder="@Messages("project.name")"/>
+    <button type="submit" class="btn btn-primary" id="select-project" data-loading-text="@Messages("site.massMail.loading")">
+      <strong>@Messages("button.add")</strong>
+    </button>
+  </label>
+  </p>
+</div>
+
+<button type="submit" class="btn btn-primary" id="write-email">
+  <strong>@Messages("mail.write")</strong>
+</button>
+
+<script type="text/javascript">
+  $(document).ready(function(){
+    $hive.loadModule("site.MassMail", {
+        "sURLMailList": "@routes.SiteApp.mailList()",
+        "sURLProjects": "@routes.ProjectApp.projects()"
+    });
+  });
+</script>
+}
app/views/site/sidebar.scala.html
--- app/views/site/sidebar.scala.html
+++ app/views/site/sidebar.scala.html
@@ -27,5 +27,8 @@
         <li class="@isActiveMenu(routes.Application.index())">
             <a href="@routes.SiteApp.writeMail()"><i class="icon-envelope"></i>@Messages("site.sidebar.mailSend")</a>
         </li>
+        <li class="@isActiveMenu(routes.Application.index())">
+            <a href="@routes.SiteApp.massMail()"><i class="icon-envelope"></i>@Messages("site.sidebar.massMail")</a>
+        </li>
     </ul>
 </div> <!-- /well -->
conf/messages.en
--- conf/messages.en
+++ conf/messages.en
@@ -281,6 +281,7 @@
 site.sidebar.projectList = Projects
 site.sidebar.softwareMap = Software Map
 site.sidebar.mailSend = Send a Email
+site.sidebar.massMail = Send Mass Emails
 site.project.filter = Find Project by Project Name
 site.userList.search = Find a user by his/her login ID...
 site.userList.isActive = Active Status
@@ -291,6 +292,9 @@
 site.project.deleteConfirm = Do you want to delete this project?
 site.userlist.unlocked = Active User List
 site.userlist.locked = Account Locked User List
+site.massMail.toAll = To All
+site.massMail.toProjects = To members of specific projects
+site.massMail.loading = Loading...
 
 #User
 user.loginId = Login ID
@@ -384,6 +388,7 @@
 mail.subject = Subject
 mail.body = Body
 mail.send = Send
+mail.write = Write
 mail.fail = Failed to send a mail.
 mail.sended = Mail is sent.
 
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -22,6 +22,7 @@
 title.commitHistory = 커밋 히스토리
 title.mailSetting = 메일 설정
 title.sendMail = 메일 발송
+title.massMail = 대량 메일 발송
 title.projectList = 프로젝트 목록
 title.help = 도움말
 title.search = 검색
@@ -287,6 +288,7 @@
 site.sidebar.projectList = 프로젝트 설정
 site.sidebar.softwareMap = 소프트웨어 맵
 site.sidebar.mailSend = 메일 발송
+site.sidebar.massMail = 대량 메일 발송
 site.project.filter = 찾으려는 프로젝트 이름을 입력하세요
 site.userList.search = 찾으려는 사용자의 ID를 입력하세요
 site.userList.isActive = 활성 상태
@@ -297,6 +299,10 @@
 site.project.deleteConfirm = 정말로 해당 프로젝트를 사이트에서 삭제하겠습니까?
 site.userlist.unlocked = 활성화된 유저 목록
 site.userlist.locked = 계정이 잠긴 유저 목록
+site.massMail.toAll = 모두에게
+site.massMail.toProjects = 특정 프로젝트의 멤버들에게
+site.massMail.loading = 불러오는중...
+
 
 #User
 user.loginId = 아이디
@@ -390,6 +396,7 @@
 mail.subject = 제목
 mail.body = 본문
 mail.send = 발송
+mail.write = 메일 쓰기
 mail.fail = 메일 발송에 실패했습니다.
 mail.sended = 메일을 발송하였습니다.
 
conf/routes
--- conf/routes
+++ conf/routes
@@ -34,6 +34,8 @@
 GET     /sites/setting                                  controllers.SiteApp.setting()
 GET     /sites/mail                                     controllers.SiteApp.writeMail(errorMessage:String ?= "", sended:Boolean ?= false)
 POST    /sites/mail                                     controllers.SiteApp.sendMail()
+GET     /sites/massmail                                 controllers.SiteApp.massMail()
+POST    /sites/mailList                                 controllers.SiteApp.mailList()
 GET     /sites/userList                                 controllers.SiteApp.userList(pageNum: Int ?= 0, loginId: String ?= null)
 POST    /sites/userList                                 controllers.SiteApp.searchUser()
 GET     /sites/user/delete                              controllers.SiteApp.deleteUser(userId: Long)
public/javascripts/common/hive.Common.js
--- public/javascripts/common/hive.Common.js
+++ public/javascripts/common/hive.Common.js
@@ -69,7 +69,7 @@
 		// 그렇지 않으면 스크립트 파일 불러온 뒤 초기화 시도
 		if(registerModule(sName, htOptions) === false){
 			htVar.htTryLoad = htVar.htTryLoad || {};
-			htVar.htTryLoad[sName] = (typeof htVar.htTryLoad[sName] == "undefined") ? 1 : (htVar.htTryLoad[sName]++);
+			htVar.htTryLoad[sName] = (typeof htVar.htTryLoad[sName] == "undefined") ? 1 : (++htVar.htTryLoad[sName]);
 			
 			if(htVar.htTryLoad[sName] > 3){
 				console.log("[HIVE] fail to load module " + sName);
@@ -226,7 +226,7 @@
 		welForm.ajaxForm({
 			"success" : htOptions.fOnLoad  || function(){},
 			"error"   : htOptions.fOnError || function(){},
-			"datatype": htOptions.sDataType || null
+			"dataType": htOptions.sDataType || null
 		});
 		welForm.submit();
 		
@@ -286,4 +286,4 @@
 		
 		$hive.loadModule(sModuleName, htOptions);
 	}
-};
(No newline at end of file)
+};
public/javascripts/common/hive.ui.Typeahead.js
--- public/javascripts/common/hive.ui.Typeahead.js
+++ public/javascripts/common/hive.ui.Typeahead.js
@@ -60,6 +60,7 @@
             		"sURL"		: htVar.sActionURL,
             		"htOptForm"	: {"method":"get"},
             		"htData"	: {"query": sQuery},
+                    "sDataType" : "json",
             		"fOnLoad"	: function(oData, oStatus, oXHR){
             			var sContentRange = oXHR.getResponseHeader('Content-Range');
             			
@@ -78,7 +79,7 @@
          * Return whether the given content range is an entire range for items.
          * e.g) "items 10/10"
          *
-         * @param {String} sContentRange the vaule of Content-Range header from response
+         * @param {String} sContentRange the value of Content-Range header from response
          * @return {Boolean}
          */
          function _isEntireRange(sContentRange){
@@ -89,4 +90,4 @@
 		_init(sQuery, htOptions || {});
 	};
 	
-})("hive.ui.Typeahead");
(No newline at end of file)
+})("hive.ui.Typeahead");
 
public/javascripts/service/hive.site.MassMail.js (added)
+++ public/javascripts/service/hive.site.MassMail.js
@@ -0,0 +1,154 @@
+/**
+ * @(#)hive.project.Setting.js 2013.03.18
+ *
+ * Copyright NHN Corporation.
+ * Released under the MIT license
+ *
+ * http://hive.dev.naver.com/license
+ */
+
+(function(ns){
+
+	var oNS = $hive.createNamespace(ns);
+	oNS.container[oNS.name] = function(htOptions){
+
+		var htVar = {};
+		var htElement = {};
+
+		/**
+		 * initialize
+		 */
+		function _init(htOptions){
+			var htOpt = htOptions || {};
+			_initVar(htOpt);
+			_initElement(htOpt);
+			_attachEvent();
+		}
+
+		/**
+		 * initialize variables
+		 * 정규식 변수는 한번만 선언하는게 성능 향상에 도움이 됩니다
+		 */
+		function _initVar(htOptions){
+            htVar.sURLProjects = htOptions.sURLProjects;
+            htVar.sURLMailList = htOptions.sURLMailList;
+		}
+
+		/**
+		 * initialize element variables
+		 */
+		function _initElement(htOptions){
+            // projects
+            htElement.welInputProject = $('#input-project');
+            htElement.welSelectedProjects = $('#selected-projects');
+            htElement.welBtnSelectProject = $('#select-project');
+            htElement.welBtnWriteEmail = $('#write-email');
+		}
+
+        /**
+		 * attach event handlers
+		 */
+		function _attachEvent(){
+            htElement.welInputProject.keypress(_onKeyPressInputProject);
+            htElement.welBtnSelectProject.click(_onClickSelectProject);
+            htElement.welBtnWriteEmail.click(_onClickWriteEmail);
+            new hive.ui.Typeahead(htElement.welInputProject, {
+		"sActionURL": htVar.sURLProjects
+            });
+		}
+
+        /**
+        * Launch a mail client to write an email.
+        */
+        function _onClickWriteEmail() {
+            // Get project names from labels in #selected-projects div.
+            var sMailingType = $('[name=mailingType]:checked').val();
+            var waProjectSpan, aProjects;
+            if (sMailingType == 'all') {
+                aProjects = {'all': 'true'}
+            } else {
+                waProjectSpan = $('#selected-projects span');
+                aProjects = [];
+                for (var i = 0; i < waProjectSpan.length; i++) {
+                    aProjects.push(waProjectSpan[i].childNodes[0].nodeValue.trim());
+                }
+            }
+
+            // Send a request contains project names to get email addresses and
+            // launch user's mail client with them using mailto scheme.
+            htElement.welBtnWriteEmail.button('loading');
+            $hive.sendForm({
+                "sURL"      : htVar.sURLMailList,
+                "htOptForm": {"method":"POST"},
+                "htData"    : aProjects,
+                "sDataType" : "json",
+                "fOnLoad"   : function(data) {
+                    var form = $('<form>');
+                    var mailto = 'mailto:';
+                    for (var i = 0; i < data.length; i++) {
+                        mailto += data[i] + ',';
+                    }
+                    form.attr('method', 'POST');
+                    form.attr('action', mailto);
+                    form.attr('enctype', 'text/plain');
+                    form.submit();
+                    htElement.welBtnWriteEmail.button('reset');
+                }
+            });
+        }
+
+        /**
+        * Add a project, which user types in #input-project element, into
+        * #selected-projects div.
+        */
+        function _onClickSelectProject() {
+            _appendProjectLabel([htElement.welInputProject.val()]);
+            htElement.welInputProject.val("");
+            return false;
+        }
+
+        /**
+        * Same as _onClickSelectProject but triggered by pressing enter.
+        *
+        * @param {Object} oEvent
+        */
+        function _onKeyPressInputProject(oEvent) {
+            if (oEvent.keyCode == 13) {
+                _appendProjectLabel([htElement.welInputProject.val()]);
+                htElement.welInputProject.val("");
+                return false;
+            }
+        }
+
+        /**
+        * Make a project label by given name.
+        *
+        * @param {String} sName
+        */
+        function _createProjectLabel(sName) {
+            var fOnClickUnselect = function() {
+                welProject.remove();
+            };
+
+            var welProject = $('<span class="label label-info">' + sName + " </span>")
+		.append($('<a href="javascript:void(0)">x</a>')
+		.click(fOnClickUnselect));
+
+            return welProject;
+        }
+
+        /**
+        * Append the given projects on #selected-projects div to show them.
+        *
+        * @param {Object} htProjects
+        */
+        function _appendProjectLabel(htProjects) {
+            for(var sId in htProjects) {
+                htElement.welSelectedProjects.append(_createProjectLabel(htProjects[sId]));
+            }
+        }
+
+		_init(htOptions);
+	};
+
+})("hive.site.MassMail");
Add a comment
List