Keesun 2014-02-20
Organization: user can make organization
@d2bb653eae200cc0af4177dd6910f0390b41dc49
 
app/controllers/OrganizationApp.java (added)
+++ app/controllers/OrganizationApp.java
@@ -0,0 +1,96 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package controllers;
+
+import actions.AnonymousCheckAction;
+import models.Organization;
+import models.User;
+import play.data.Form;
+import play.data.validation.Validation;
+import play.mvc.Controller;
+import play.mvc.Result;
+import play.mvc.With;
+import utils.Constants;
+import utils.ErrorViews;
+import views.html.organization.create;
+import views.html.organization.view;
+
+import javax.validation.ConstraintViolation;
+import java.util.Date;
+import java.util.Set;
+
+import static play.data.Form.form;
+
+/**
+ * @author Keeun Baik
+ */
+public class OrganizationApp extends Controller {
+
+    @With(AnonymousCheckAction.class)
+    public static Result newForm() {
+        return ok(create.render("title.newOrganization", new Form<>(Organization.class)));
+    }
+
+    @With(AnonymousCheckAction.class)
+    public static Result newOrganization() throws Exception {
+        Form<Organization> newOrgForm = form(Organization.class).bindFromRequest();
+        validate(newOrgForm);
+        if (newOrgForm.hasErrors()) {
+            flash(Constants.WARNING, newOrgForm.error("name").message());
+            return badRequest(create.render("title.newOrganization", newOrgForm));
+        } else {
+            Organization org = newOrgForm.get();
+            org.created = new Date();
+            org.save();
+
+            UserApp.currentUser().createOrganization(org);
+            return redirect(routes.OrganizationApp.organization(org.name));
+        }
+    }
+
+    public static Result organization(String name) {
+        Organization org = Organization.findByName(name);
+        if(org == null) {
+            return notFound(ErrorViews.NotFound.render("error.notfound.organization"));
+        }
+        return ok(view.render(org));
+    }
+
+    private static void validate(Form<Organization> newOrgForm) {
+        // 조직 이름 패턴을 검사한다.
+        Set<ConstraintViolation<Organization>> results = Validation.getValidator().validate(newOrgForm.get());
+        if(!results.isEmpty()) {
+            newOrgForm.reject("name", "organization.name.alert");
+        }
+
+        String name = newOrgForm.field("name").value();
+        // 중복된 loginId로 가입할 수 없다.
+        if (User.isLoginIdExist(name)) {
+            newOrgForm.reject("name", "organization.name.duplicate");
+        }
+
+        // 같은 이름의 조직을 만들 수 없다.
+        if (Organization.isNameExist(name)) {
+            newOrgForm.reject("name", "organization.name.duplicate");
+        }
+    }
+
+}
app/models/Organization.java
--- app/models/Organization.java
+++ app/models/Organization.java
@@ -1,23 +1,47 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik, Wansoon Park, ChangSung Kim
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package models;
 
 import play.data.format.Formats;
+import play.data.validation.Constraints;
 import play.db.ebean.Model;
+import utils.ReservedWordsValidator;
 
 import javax.persistence.*;
 import java.util.Date;
 import java.util.List;
 
-/**
- * @author Keeun Baik
- */
 @Entity
 public class Organization extends Model {
 
     private static final long serialVersionUID = -1L;
 
+    public static final Finder<Long, Organization> find = new Finder<>(Long.class, Organization.class);
+
     @Id
     public Long id;
 
+    @Constraints.Pattern(value = "^" + User.LOGIN_ID_PATTERN + "$", message = "user.wrongloginId.alert")
+    @Constraints.Required
+    @Constraints.ValidateWith(ReservedWordsValidator.class)
     public String name;
 
     @Formats.DateTime(pattern = "yyyy-MM-dd")
@@ -31,5 +55,16 @@
 
     public String descr;
 
+    public void add(OrganizationUser ou) {
+        this.users.add(ou);
+    }
 
+    public static Organization findByName(String name) {
+        return find.where().eq("name", name).findUnique();
+    }
+
+    public static boolean isNameExist(String name) {
+        int findRowCount = find.where().ieq("name", name).findRowCount();
+        return (findRowCount != 0);
+    }
 }
app/models/OrganizationUser.java
--- app/models/OrganizationUser.java
+++ app/models/OrganizationUser.java
@@ -5,6 +5,7 @@
 import javax.persistence.Entity;
 import javax.persistence.Id;
 import javax.persistence.ManyToOne;
+import java.util.List;
 
 /**
  * @author Keeun Baik
@@ -13,6 +14,8 @@
 public class OrganizationUser extends Model {
 
     private static final long serialVersionUID = -1L;
+
+    public static final Finder<Long, OrganizationUser> find = new Finder<>(Long.class, OrganizationUser.class);
 
     @Id
     public Long id;
@@ -26,4 +29,10 @@
     @ManyToOne
     public Role role;
 
+    public static List<OrganizationUser> findAdminsOf(Organization organization) {
+        return find.where()
+                .eq("organization", organization)
+                .eq("role", Role.findByName("org_admin"))
+                .findList();
+    }
 }
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -577,4 +577,20 @@
 
         return this.recentlyVisitedProjects.findRecentlyVisitedProjects(size);
     }
+
+    public void createOrganization(Organization organization) {
+        OrganizationUser ou = new OrganizationUser();
+        ou.user = this;
+        ou.organization = organization;
+        ou.role = Role.findByRoleType(RoleType.ORG_ADMIN);
+        ou.save();
+
+        this.add(ou);
+        organization.add(ou);
+        this.update();
+    }
+
+    private void add(OrganizationUser ou) {
+        this.organizationUsers.add(ou);
+    }
 }
app/models/enumeration/RoleType.java
--- app/models/enumeration/RoleType.java
+++ app/models/enumeration/RoleType.java
@@ -1,7 +1,7 @@
 package models.enumeration;
 
 public enum RoleType {
-    MANAGER(1l), MEMBER(2l), SITEMANAGER(3l), ANONYMOUS(4l), GUEST(5l);
+    MANAGER(1l), MEMBER(2l), SITEMANAGER(3l), ANONYMOUS(4l), GUEST(5l), ORG_ADMIN(6l), ORG_MEMBER(7l);
 
     private Long roleType;
 
 
app/views/organization/create.scala.html (added)
+++ app/views/organization/create.scala.html
@@ -0,0 +1,49 @@
+@(title:String, form: Form[Organization])
+
+@siteLayout("app.name", utils.MenuType.NONE) {
+<div class="page-wrap-outer">
+    <div class="project-page-wrap">
+        <div class="form-wrap new-project">
+            <form action="@routes.OrganizationApp.newOrganization()" method="post" name="new-org" class="frm-wrap">
+                <legend>
+                    @Messages("title.newOrganization")
+                </legend>
+                <dl>
+                    <dt>
+                        <div class="n-alert" data-errType="name">
+                            <div class="orange-txt">
+                                @if(flash.get("warning") != null) { <span class="warning">@Messages(flash.get("warning"))</span> }
+                                <span class="msg wrongName" style="display: none;">@Messages("project.wrongName")</span>
+                            </div>
+                        </div>
+                        <label for="name">@Messages("organization.name.placeholder")</label>
+                    </dt>
+                    <dd>
+                        <input id="name" type="text" name="name" class="text" placeholder="" maxlength="250" value="@form.field("name").value()" />
+                    </dd>
+
+                    <dt>
+                        <label for="descr">@Messages("organization.description.placeholder")</label>
+                    </dt>
+                    <dd>
+                        <textarea id="descr" name="descr" class="text textarea.span4" style="resize: vertical;" >@form.field("descr").value()</textarea>
+                    </dd>
+                </dl>
+                <div class="actions">
+                    <button class="ybtn ybtn-success">
+                        <i class="yobicon-friends"></i> @Messages("organization.create")
+                    </button>
+                    <a href="/" class="ybtn">@Messages("button.cancel")</a>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+<script type="text/javascript">
+    $(document).ready(function() {
+        $yobi.loadModule("organization.New", {
+            "sFormName" : "new-org"
+        });
+    });
+</script>
+}
 
app/views/organization/view.scala.html (added)
+++ app/views/organization/view.scala.html
@@ -0,0 +1,9 @@
+@(org:Organization)
+
+@siteLayout(org.name, utils.MenuType.NONE) {
+<div class="site-breadcrumb-outer">
+    <div class="site-breadcrumb-inner">
+        <h3>@org.name <small>@org.descr</small></h3>
+    </div>
+</div>
+}
conf/evolutions/default/63.sql
--- conf/evolutions/default/63.sql
+++ conf/evolutions/default/63.sql
@@ -31,6 +31,9 @@
 
 update project set project_scope = 'PUBLIC' where is_public = true;
 update project set project_scope = 'PRIVATE' where is_public = false;
+
+insert into role (id, name, active) values (6, 'org_admin', true);
+insert into role (id, name, active) values (7, 'org_member', true);
 # --- !Downs
 alter table project drop constraint if exists ck_project_project_scope;
 alter table project drop column project_scope;
@@ -47,3 +50,6 @@
 
 drop sequence if exists organization_seq;
 drop table if exists organization;
+
+delete from role where id = 6;
+delete from role where id = 7;
conf/initial-data.yml
--- conf/initial-data.yml
+++ conf/initial-data.yml
@@ -16,15 +16,31 @@
 # Role
 roles:
     - !!models.Role
+        id:             1
         name:           manager
         active:         true
     - !!models.Role
+        id:             2
         name:           member
         active:         true
     - !!models.Role
+        id:             3
+        name:           sitemanager
+        active:         true
+    - !!models.Role
+        id:             4
+        name:           anonymous
+        active:         true
+    - !!models.Role
+        id:             5
+        name:           guest
+        active:         true
+    - !!models.Role
+        id:             6
         name:           org_admin
         active:         true
     - !!models.Role
+        id:             7
         name:           org_participant
         active:         true
 
conf/messages
--- conf/messages
+++ conf/messages
@@ -172,6 +172,7 @@
 error.notfound.issue_post = Issue not found
 error.notfound.milestone = Milestone not found
 error.notfound.project = Project not found
+error.notfound.organization = Organization not found
 error.notfound.watch = Watch not found
 error.required = Mandatory field is empty
 error.tooLargeText.admin = You can modify "parsers.text.maxLength" in the Play configuration to loose the limit.
@@ -320,6 +321,11 @@
 notification.type.pullrequest.unreviewed = PullRequest Unreviewed
 notification.watch = Watch
 notification.will.help = You can receive next notifications.
+organization.create = Create Organization
+organization.description.placeholder = input organization's description
+organization.name.alert = Wrong organization name. (Characters which can be used in URL are allowed)
+organization.name.duplicate = Already existing user's login id or existing organization's name.
+organization.name.placeholder = input organization's name
 post.author = Author
 post.comment.empty = you have to write contents.
 post.createdDate = Created Date
@@ -614,6 +620,7 @@
 title.newMilestone = New Milestone
 title.newProject = Create a New Project
 title.newPullRequest = Pull Request
+title.newOrganization = New Organization
 title.post.notExistingPage = Page not found
 title.privateProject = Private Repositories
 title.projectDelete = Delete Project
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -172,6 +172,7 @@
 error.notfound.issue_post = 존재하지 않는 이슈입니다
 error.notfound.milestone = 존재하지 않는 마일스톤입니다.
 error.notfound.project = 존재하지 않는 프로젝트입니다.
+error.notfound.organization = 존재하지 않는 조직입니다.
 error.notfound.watch = 존재하지 않는 알림 정보입니다.
 error.required = 필수 입력란입니다.
 error.tooLargeText.admin = Play 설정에서 "parsers.text.maxLength"를 고쳐서 최대 허용치를 조절할 수 있습니다.
@@ -321,6 +322,11 @@
 notification.type.pullrequest.unreviewed = 코드보내기 리뷰 완료 취소
 notification.watch = 지켜보기
 notification.will.help = 프로젝트를 지켜보면 다음 이벤트가 발생할 때 알림 메시지를 받습니다.
+organization.create = 조직 만들기
+organization.description.placeholder = 조직 설명을 입력해주세요
+organization.name.alert = 조직 이름은 URL로 사용할 수 있는 글자(영문자,숫자,-'하이픈')만 허용합니다
+organization.name.duplicate = 같은 이름을 가진 조직 또는 사용자가 있습니다. 다른 이름을 사용하세요.
+organization.name.placeholder = 조직 이름을 입력해주세요.
 post.author = 글쓴이
 post.comment.empty = 댓글 내용은 반드시 입력해야 합니다.
 post.createdDate = 작성일
@@ -614,6 +620,7 @@
 title.newMilestone = 새 마일스톤
 title.newProject = 새 프로젝트 시작
 title.newPullRequest = 코드 보내기
+title.newOrganization = 새 조직 만들기
 title.post.notExistingPage = 페이지를 찾지 못했습니다.
 title.privateProject = 비공개 프로젝트
 title.projectDelete = 프로젝트 삭제
conf/routes
--- conf/routes
+++ conf/routes
@@ -22,6 +22,11 @@
 GET            /import                                                                controllers.ImportApp.importForm()
 POST           /import                                                                controllers.ImportApp.newProject()
 
+# Organization
+GET            /organizations/new                                                     controllers.OrganizationApp.newForm()
+POST           /organizations/new                                                     controllers.OrganizationApp.newOrganization()
+GET            /organizations/:name                                                   controllers.OrganizationApp.organization(name: String)
+
 # Notification
 POST           /noti/toggle/:projectId/:notiType                                      controllers.WatchProjectApp.toggle(projectId: Long, notiType)
 
@@ -243,5 +248,6 @@
 # Compare
 GET            /:user/:project/compare/:revA..:revB                                             controllers.CompareApp.compare(user, project, revA, revB)
 
+
 # remove trailing slash - must be the bottom of this file
 GET            /*paths                                                                controllers.Application.removeTrailer(paths)
 
public/javascripts/service/yobi.organization.New.js (added)
+++ public/javascripts/service/yobi.organization.New.js
@@ -0,0 +1,116 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function(ns){
+
+    var oNS = $yobi.createNamespace(ns);
+    oNS.container[oNS.name] = function(htOptions){
+
+        var htVar = {};
+        var htElement = {};
+
+        function _init(htOptions){
+            _initVar(htOptions);
+            _initElement(htOptions);
+            _attachEvent();
+
+            _initFormValidator();
+        }
+
+        /**
+         * initialize variables
+         */
+        function _initVar(htOptions){
+            htVar.sFormName = htOptions.sFormName || "new-org";
+            htVar.rxOrgName = /^[a-zA-Z0-9-]+([_.][a-zA-Z0-9-]+)*$/;
+        }
+
+        /**
+         * initialize element
+         */
+        function _initElement(htOptions){
+            htElement.welInputOrgName = $("#name");
+            htElement.welInputOrgName.focus();
+        }
+
+        /**
+         * attach event handler
+         */
+        function _attachEvent(){
+
+        }
+
+        /**
+         * initialize formValidator
+         * @require validate.js
+         */
+        function _initFormValidator(){
+            // name : name of input element
+            // rules: rules to apply to the input element.
+            var aRules = [];
+
+            htVar.oValidator = new FormValidator(htVar.sFormName, aRules, function(aErrors){
+                var oForm = $(document.forms[htVar.sFormName]);
+                var oElement = oForm.find("input[name=name]");
+                var sOrgName = oElement.val();
+                if(!htVar.rxOrgName.test(sOrgName)){
+                    aErrors.push({
+                        id: oElement.attr("id"),
+                        name: oElement.attr("name"),
+                        message: Messages("organization.name.alert")
+                    });
+                }
+                _onFormValidate(aErrors);
+            });
+        }
+
+        /**
+         * handler for validation errors.
+         */
+        function _onFormValidate(aErrors){
+            if(aErrors.length > 0){
+                $('span.warning').hide();
+                $('span.msg').html(aErrors[0].message).show();
+            } else {
+                new Spinner({
+                    lines: 13, // The number of lines to draw
+                    length: 10, // The length of each line
+                    width: 5, // The line thickness
+                    radius: 10, // The radius of the inner circle
+                    corners: 1, // Corner roundness (0..1)
+                    rotate: 0, // The rotation offset
+                    direction: 1, // 1: clockwise, -1: counterclockwise
+                    color: '#000', // #rgb or #rrggbb
+                    speed: 1, // Rounds per second
+                    trail: 60, // Afterglow percentage
+                    shadow: false, // Whether to render a shadow
+                    hwaccel: false, // Whether to use hardware acceleration
+                    className: 'spinner', // The CSS class to assign to the spinner
+                    zIndex: 2e9, // The z-index (defaults to 2000000000)
+                    top: 'auto', // Top position relative to parent in px
+                    left: 'auto' // Left position relative to parent in px
+                }).spin(document.forms[htVar.sFormName]);
+            }
+        }
+
+        _init(htOptions || {});
+    };
+
+})("yobi.organization.New");
 
test/models/OrganizationTest.java (added)
+++ test/models/OrganizationTest.java
@@ -0,0 +1,97 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package models;
+
+import org.junit.Test;
+import play.data.validation.Validation;
+
+import java.util.List;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+/**
+ * @author Keeun Baik
+ */
+public class OrganizationTest extends ModelTest<Organization> {
+
+    /**
+     * 조직을 생성하면 생성한 사용자가 조직의 org_admin 권한을 가진다.
+     */
+    @Test
+    public void create() {
+        // Given
+        User doortts = User.findByLoginId("doortts");
+        assertThat(doortts).isNotNull();
+
+        Organization weblabs = new Organization();
+        weblabs.name = "weblabs";
+        weblabs.descr = "weblab < labs";
+        weblabs.save();
+
+        // When
+        doortts.createOrganization(weblabs);
+
+        // Then
+        assertThat(Organization.findByName("weblabs")).isNotNull();
+        List<OrganizationUser> ous = OrganizationUser.findAdminsOf(weblabs);
+        assertThat(ous.size()).isEqualTo(1);
+        assertThat(ous.get(0).user).isEqualTo(doortts);
+    }
+
+    /**
+     * 조직의 이름은 사용자 이름과 동일한 패턴을 사용한다.
+     */
+    @Test
+    public void validateName() {
+        Organization org = new Organization();
+        org.name="foo";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo' should be accepted.").isEqualTo(0);
+
+        org.name=".foo";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'.foo' should NOT be accepted.").isGreaterThan(0);
+
+        org.name="foo.bar";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo.bar' should be accepted.").isEqualTo(0);
+
+        org.name="foo.";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo.' should NOT be accepted.").isGreaterThan(0);
+
+        org.name="_foo";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'_foo' should NOT be accepted.").isGreaterThan(0);
+
+        org.name="foo_bar";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo_bar' should be accepted.").isEqualTo(0);
+
+        org.name="foo_";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo_' should NOT be accepted.").isGreaterThan(0);
+
+        org.name="-foo";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'-foo' should be accepted.").isEqualTo(0);
+
+        org.name="foo-";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo-' should be accepted.").isEqualTo(0);
+
+        org.name="foo bar";
+        assertThat(Validation.getValidator().validate(org).size()).describedAs("'foo bar' should NOT be accepted.").isGreaterThan(0);
+    }
+
+}
Add a comment
List