[Notice] Announcing the End of Demo Server [Read me]
Yi EungJun 2013-05-03
project: Improve Project Tagging.
* Allow to edit tags on Overview page.
* Remove the tag editor on Project Setting page.
* Make tag has its own category.
* Add defeault tags.
* Add a document for Tag Typeahead API.
@e320a5577dfd77ce8db50654022916dfe1f3a178
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -1865,8 +1865,16 @@
                         padding-bottom: 0;
                         border-bottom: 0 none;
                     }
+                    span:not(.label) {
+                        &:not(:last-child):after {
+                            content: ", ";
+                        }
+                    }
                 }
             }
+            button#edit-toggle {
+                float: right;
+            }
         }
         
         .member-wrap {
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -390,7 +390,7 @@
 
         Map<Long, String> tags = new HashMap<Long, String>();
         for (Tag tag: project.tags) {
-            tags.put(tag.id, tag.name);
+            tags.put(tag.id, tag.toString());
         }
 
         return ok(toJson(tags));
@@ -404,27 +404,53 @@
      */
     public static Result tag(String ownerName, String projectName) {
         Project project = Project.findByNameAndOwner(ownerName, projectName);
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
+        if (!AccessControl.isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)) {
             return forbidden();
         }
 
-        // Get tag name from the request. Return empty map if the name is not given.
+        // Get category and name from the request. Return 400 Bad Request if name is not given.
         Map<String, String[]> data = request().body().asFormUrlEncoded();
+        String category = HttpUtil.getFirstValueFromQuery(data, "category");
         String name = HttpUtil.getFirstValueFromQuery(data, "name");
         if (name == null || name.length() == 0) {
-            return ok(toJson(new HashMap<Long, String>()));
+            // A tag must have its name.
+            return badRequest("Tag name is missing.");
         }
 
-        Tag tag = project.tag(name);
+        Tag tag = Tag.find
+            .where().eq("category", category).eq("name", name).findUnique();
 
+        boolean isCreated = false;
         if (tag == null) {
-            // Return empty map if the tag has been already attached.
-            return ok(toJson(new HashMap<Long, String>()));
-        } else {
-            // Return the tag.
+            // Create new tag if there is no tag which has the given name.
+            tag = new Tag(category, name);
+            tag.save();
+            isCreated = true;
+        }
+
+        Boolean isAttached = project.tag(tag);
+
+        if (!isCreated && !isAttached) {
+            // Something is wrong. This case is not possible.
+            play.Logger.warn(
+                    "A tag '" + tag + "' is created but failed to attach to project '"
+                    + project + "'.");
+        }
+
+        if (isAttached) {
+            // Return the attached tag. The return type is Map<Long, String>
+            // even if there is only one tag, to unify the return type with
+            // ProjectApp.tags().
             Map<Long, String> tags = new HashMap<Long, String>();
-            tags.put(tag.id, tag.name);
-            return ok(toJson(tags));
+            tags.put(tag.id, tag.toString());
+            if (isCreated) {
+                return created(toJson(tags));
+            } else {
+                return ok(toJson(tags));
+            }
+        } else {
+            // Return 204 No Content if the tag has been attached already.
+            return status(Http.Status.NO_CONTENT);
         }
     }
 
@@ -437,7 +463,7 @@
      */
     public static Result untag(String ownerName, String projectName, Long id) {
         Project project = Project.findByNameAndOwner(ownerName, projectName);
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
+        if (!AccessControl.isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)) {
             return forbidden();
         }
 
app/controllers/TagApp.java
--- app/controllers/TagApp.java
+++ app/controllers/TagApp.java
@@ -3,40 +3,137 @@
 import com.avaje.ebean.ExpressionList;
 import models.Project;
 import models.Tag;
-import models.enumeration.Operation;
 import play.mvc.Controller;
 import play.mvc.Http;
 import play.mvc.Result;
-import utils.AccessControl;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
+import static com.avaje.ebean.Expr.*;
 import static play.libs.Json.toJson;
 
 public class TagApp extends Controller {
     private static final int MAX_FETCH_TAGS = 1000;
 
-    public static Result tags(String query) {
+    public enum Context {
+        PROJECT_TAGGING_TYPEAHEAD, DEFAULT
+    }
+
+    public static Result tags(String query, String contextAsString, Integer limit) {
         if (!request().accepts("application/json")) {
             return status(Http.Status.NOT_ACCEPTABLE);
         }
 
-        ExpressionList<Tag> el = Tag.find.where().contains("name", query);
-        int total = el.findRowCount();
-        if (total > MAX_FETCH_TAGS) {
-            el.setMaxRows(MAX_FETCH_TAGS);
-            response().setHeader("Content-Range", "items " + MAX_FETCH_TAGS + "/" + total);
+        List<String> tags;
+
+        Context context = null;
+
+        if (contextAsString == null || contextAsString.isEmpty()) {
+            context = Context.DEFAULT;
+        } else {
+            try {
+                context = Context.valueOf(contextAsString);
+            } catch (IllegalArgumentException e) {
+                return badRequest("Invalid context '" + contextAsString + "'");
+            }
         }
 
-        List<String> tags = new ArrayList<String>();
-        for (Tag tag: el.findList()) {
-            tags.add(tag.name);
+        if (limit == null || limit > MAX_FETCH_TAGS) {
+            limit = MAX_FETCH_TAGS;
+        }
+
+        switch(context) {
+            case PROJECT_TAGGING_TYPEAHEAD:
+                try {
+                    Long projectId  = Long.valueOf(request().getQueryString("project_id"));
+                    Project project = Project.find.byId(projectId);
+                    if (project == null) {
+                        return badRequest("No project matches given project_id '" + projectId + "'");
+                    }
+                    tags = tagsForProjectTagging(query, limit, project);
+                } catch (IllegalArgumentException e) {
+                    return badRequest("In " + Context.PROJECT_TAGGING_TYPEAHEAD + " context, " +
+                            "the query string should have a project_id field in integer type.");
+                }
+                break;
+            default:
+                tags = tagsForDefault(query, limit);
+                break;
         }
 
         return ok(toJson(tags));
     }
 
+    private static List<String> tags(ExpressionList<Tag> el) {
+        ArrayList<String> tags = new ArrayList<String>();
+
+        for (Tag tag: el.findList()) {
+            tags.add(tag.toString());
+        }
+
+        return tags;
+    }
+
+    private static List<String> tagsForDefault(String query, int limit) {
+        ExpressionList<Tag> el =
+                Tag.find.where().or(icontains("category", query), icontains("name", query));
+
+        int total = el.findRowCount();
+
+        if (total > limit) {
+            el.setMaxRows(limit);
+            response().setHeader("Content-Range", "items " + limit + "/" + total);
+        }
+
+        return tags(el);
+    }
+
+    private static List<String> tagsForProjectTagging(String query, int limit, Project project) {
+        ExpressionList<Tag> el =
+                Tag.find.where().or(icontains("category", query), icontains("name", query));
+
+        int total = el.findRowCount();
+
+        // If the limit is bigger than the total number of resulting tags, juts return all of them.
+        if (limit > total) {
+            return tagsForDefault(query, limit);
+        }
+
+        // If the project has no License tag, list License tags first to
+        // recommend add one of them.
+        boolean hasLicenseTags =
+                Tag.find.where().eq("projects.id", project.id).eq("category", "License")
+                        .findRowCount() > 0;
+
+        if (hasLicenseTags) {
+            return tagsForDefault(query, limit);
+        }
+
+        ExpressionList<Tag> elLicense =
+                Tag.find.where().and(eq("category", "License"), icontains("name", query));
+        elLicense.setMaxRows(limit);
+        List<String> tags = tags(elLicense);
+
+        // If every license tags are listed but quota still remains, then add
+        // any other tags not in License category to the list.
+        if (elLicense.findRowCount() < limit) {
+            ExpressionList<Tag> elExceptLicense =
+                    Tag.find.where().and(
+                            ne("category", "License"),
+                            or(icontains("category", query), icontains("name", query)));
+
+            elExceptLicense.setMaxRows(limit - elLicense.findRowCount());
+
+            for (Tag tag: elExceptLicense.findList()) {
+                tags.add(tag.toString());
+            }
+        }
+
+        if (tags.size() < total) {
+            response().setHeader("Content-Range", "items " + tags.size() + "/" + total);
+        }
+
+        return tags;
+    }
 }
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -9,6 +9,7 @@
 import javax.validation.constraints.NotNull;
 
 import com.avaje.ebean.Ebean;
+import com.avaje.ebean.ExpressionList;
 import models.enumeration.ResourceType;
 import models.enumeration.RoleType;
 import models.resource.Resource;
@@ -308,6 +309,27 @@
         }
     }
 
+	public Resource tagsAsResource() {
+	    return new Resource() {
+
+            @Override
+            public Long getId() {
+                return id;
+            }
+
+            @Override
+            public Project getProject() {
+                return Project.this;
+            }
+
+            @Override
+            public ResourceType getType() {
+                return ResourceType.PROJECT_TAGS;
+            }
+
+	    };
+	}
+
 	public Resource asResource() {
 	    return new Resource() {
 
@@ -333,25 +355,17 @@
         return User.findByLoginId(userId);
     }
 
-    public Tag tag(String tagName) {
-        // Find a tag by the given name.
-        Tag tag = Tag.find.where().eq("name", tagName).findUnique();
-
-        if (tag == null) {
-            // Create new tag if there is no tag which has the given name.
-            tag = new Tag();
-            tag.name = tagName;
-            tag.save();
-        } else if (tag.projects.contains(this)) {
-            // Return empty map if the tag has been already attached.
-            return null;
+    public Boolean tag(Tag tag) {
+       if (tags.contains(tag)) {
+            // Return false if the tag has been already attached.
+            return false;
         }
 
         // Attach new tag.
-        tag.projects.add(this);
-        tag.update();
+        tags.add(tag);
+        update();
 
-        return tag;
+        return true;
     }
 
     public void untag(Tag tag) {
@@ -366,4 +380,8 @@
     public boolean isOwner(User user) {
         return owner.toLowerCase().equals(user.loginId.toLowerCase());
     }
+
+    public String toString() {
+        return owner + "/" + name;
+    }
 }
app/models/Tag.java
--- app/models/Tag.java
+++ app/models/Tag.java
@@ -10,6 +10,7 @@
 import java.util.Set;
 
 @Entity
+@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"category", "name"}))
 public class Tag extends Model {
 
     /**
@@ -22,23 +23,26 @@
     public Long id;
 
     @Required
-    @Column(unique=true)
+    public String category;
+
+    @Required
     public String name;
 
     @ManyToMany(mappedBy="tags")
     public Set<Project> projects;
 
-    public static List<Tag> findByProjectId(Long projectId) {
-        return find.where().eq("project.id", projectId).findList();
-    }
-
-    public static Tag findById(Long id) {
-        return find.byId(id);
+    public Tag(String category, String name) {
+        if (category == null) {
+            category = "Tag";
+        }
+        this.category = category;
+        this.name = name;
     }
 
     @Transient
     public boolean exists() {
-        return find.where().eq("name", name).findRowCount() > 0;
+        return find.where().eq("category", category).eq("name", name)
+            .findRowCount() > 0;
     }
 
     @Override
@@ -48,6 +52,11 @@
             project.save();
         }
         super.delete();
+    }
+
+    @Override
+    public String toString() {
+        return category + " - " + name;
     }
 
     public Resource asResource() {
@@ -68,4 +77,4 @@
             }
         };
     }
-}
(파일 끝에 줄바꿈 문자 없음)
+}
app/models/enumeration/ResourceType.java
--- app/models/enumeration/ResourceType.java
+++ app/models/enumeration/ResourceType.java
@@ -22,7 +22,8 @@
     ATTACHMENT("attachment"),
     ISSUE_COMMENT("issue_comment"),
     NONISSUE_COMMENT("nonissue_comment"),
-    TAG("tag");
+    TAG("tag"),
+    PROJECT_TAGS("project_tags");
 
     private String resource;
 
app/views/layout.scala.html
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
@@ -25,6 +25,7 @@
 <script type="text/javascript" src="@getJSLink("lib/jquery/jquery.zclip.min")"></script>
 <script type="text/javascript" src="@getJSLink("lib/jquery/jquery.placeholder.min")"></script>
 <script type="text/javascript" src="@getJSLink("lib/bootstrap")"></script>
+<script type="text/javascript" src="@getJSLink("lib/bootstrap-better-typeahead")"></script>
 <script type="text/javascript" src="@getJSLink("lib/rgbcolor")"></script>
 <script type="text/javascript" src="@getJSLink("lib/humanize")"></script>
 <script type="text/javascript" src="@getJSLink("lib/validate")"></script>
@@ -79,4 +80,4 @@
 @googleAnalytics("UA-40528193-1")
 
 </body>
-</html>
(파일 끝에 줄바꿈 문자 없음)
+</html>
app/views/project/overview.scala.html
--- app/views/project/overview.scala.html
+++ app/views/project/overview.scala.html
@@ -4,6 +4,7 @@
 @import utils.JodaDateUtil._
 @import utils.TemplateHelper._
 @import models.enumeration._
+@import utils.AccessControl._
 
 @projectLogoImage = @{
 	defining(Attachment.findByContainer(project.asResource)) { files =>
@@ -40,29 +41,16 @@
         <div class="inner project-info">
             <header>
                 <h3>@Messages("project.info")</h3>
-                <!--<div class="project-status">
-                    <i class="ico ico-like"></i>
-                    <span class="num">100</span>
-                    <span class="sp">|</span>
-                    <i class="ico ico-activity high"></i>
-                </div>-->
+                @if(isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)){
+                <button type="button" class="btn btn-small" data-toggle="button" id="tag-editor-toggle">@Messages("button.edit")</button>
+                }
             </header>
-            <ul class="infos">
-                <li class="info">
-                    <strong>@Messages("project.license") :</strong> GPL v2
-                </li>
-                <li class="info">
-                    <strong>@Messages("project.tags") :</strong>
-                    @for(tag <- project.tags) {
-                    <span class="label">@tag.name</span>
-                    }
-                </li>
-                <li class="info">
-                    <strong>@Messages("project.codeLanguage") :</strong> Java, JavaScript
-                </li>
-                <li class="info">
-                    <strong>@Messages("project.vcs") :</strong> @project.vcs
-                </li>
+            <ul class="infos" id="tags">
+                <!-- tags are added here by hive.project.Home.js -->
+            </ul>
+            <ul>
+                <input name="newTag" type="text" class="text hidden" style="margin-bottom:0px;" data-provider="typeahead" autocomplete="off"/>
+                <button id="addTag" type="button" class="btn-transparent n-btn med gray hidden">@Messages("button.add")</button>
             </ul>
         </div>
         <div class="inner member-info">
@@ -126,7 +114,11 @@
 
 <script type="text/javascript">
 $(document).ready(function(){
-	$hive.loadModule("project.Home");
+    $hive.loadModule("project.Home", {
+        "sURLProjectTags": "@routes.ProjectApp.tags(project.owner, project.name)",
+        "sURLTags"       : "@routes.TagApp.tags()",
+        "nProjectId"     : @project.id
+    });
 });
 </script>
-}
(파일 끝에 줄바꿈 문자 없음)
+}
app/views/project/setting.scala.html
--- app/views/project/setting.scala.html
+++ app/views/project/setting.scala.html
@@ -58,16 +58,6 @@
                 </dd>
             </dl>
         </div>
-        <div class="box-wrap middle">
-            <div class="cu-label" style="line-height:30px;">@Messages("project.tags")</div>
-            <div class="cu-desc">
-                <div id="tags">
-                    <!-- tags will be added here by hive.project.Settings.js -->
-                </div>
-                <input name="newTag" type="text" class="text" style="margin-bottom:0px" data-provider="typeahead" autocomplete="off"/>
-                <button id="addTag" type="button" class="nbtn medium">@Messages("button.add")</button>
-            </div>
-        </div>
         
         <div class="box-wrap middle">
             <div class="cu-label">@Messages("project.shareOption")</div>
@@ -92,10 +82,7 @@
 
 <script type="text/javascript">
 	$(document).ready(function(){
-        $hive.loadModule("project.Setting", {
-			"sURLProjectTags": "@routes.ProjectApp.tags(project.owner, project.name)",
-            "sURLTags"       : "@routes.TagApp.tags()"
-        });
+        $hive.loadModule("project.Setting");
 	});
 </script>
 
 
conf/evolutions/default/11.sql (added)
+++ conf/evolutions/default/11.sql
@@ -0,0 +1,322 @@
+# --- !Ups
+
+ALTER TABLE tag ADD COLUMN category VARCHAR(255) NOT NULL;
+UPDATE tag SET category='Tag';
+ALTER TABLE tag DROP CONSTRAINT uq_tag_name;
+ALTER TABLE tag ADD CONSTRAINT uq_tag_category_name UNIQUE (category, name);
+
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ALGOL 58');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ALGOL 60');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ALGOL 68');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'APL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ASP.NET');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'AWK');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ActionScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Ada');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'AppleScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'AspectJ');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Assembly language');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'AutoLISP / Visual LISP');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'B');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'BASIC');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'BCPL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'BREW');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Bash');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Batch (Windows/Dos)');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Bourne shell');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'C#');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'C');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'C++');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'COBOL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Clipper');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Clojure');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'CobolScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'CoffeeScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ColdFusion');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Common Lisp');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Component Pascal');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Curl');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'D');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Dart');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Delphi');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ECMAScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Eiffel');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Emacs Lisp');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Erlang');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'F#');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'F');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Forth');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Fortran');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'FoxBase');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'FoxPro');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Go!');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Go');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Groovy');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Haskell');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Io');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'J');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'JScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Java');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'JavaFX Script');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'JavaScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'LaTeX');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Lisp');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Logo');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Lua');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'MATLAB');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'MDL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ML');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Machine code');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Mathematica');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Maya');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Microcode');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Microsoft Visual C++');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Modula');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Modula-2');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Modula-3');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'OCaml');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Object Lisp');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Object Pascal');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Objective-C');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Opa');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Orc');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PHP');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL-11');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/0');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/B');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/C');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/I');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/M');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/P');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PL/SQL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'POP-11');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Pascal');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Perl');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PostScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PowerBuilder');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'PowerShell');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Processing.js');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Prolog');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Python');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'QBasic');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'QuakeC');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'R');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'R++');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'REXX');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Ruby');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Rust');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Scala');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Scheme');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Script.NET');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Sed');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Self');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Simula');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Simulink');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Small Basic');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Smalltalk');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Snowball');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Squeak');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'TEX');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Tcl');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'TeX');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'UNITY');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Unix shell');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'UnrealScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'VBA');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'VBScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'VHDL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Vala');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Verilog');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual Basic .NET');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual Basic');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual C#');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual DataFlex');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual DialogScript');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual Fortran');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual FoxPro');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual J#');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual J++');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual LISP');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Visual Prolog');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'Windows PowerShell');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'XQuery');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'XSLT');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'bc');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'csh');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'dBase');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'ksh');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'Language ', 'make');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'Apache');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'BSD');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'EPL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'GPL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'ISC');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'LGPL');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'MIT');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'MPL v1.1');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'MPL v2.0');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'License', 'Public Domain');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'OS', 'Linux');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'OS', 'OSX');
+INSERT INTO tag (id, category, name) VALUES (nextval('tag_seq'), 'OS', 'Windows');
+
+# --- !Downs
+
+ALTER TABLE tag DROP COLUMN category;
+ALTER TABLE tag ADD CONSTRAINT uq_tag_name UNIQUE (name);
+ALTER TABLE tag DROP CONSTRAINT uq_tag_category_name;
+
+DELETE FROM tag WHERE category='Language' AND name='ALGOL 58'
+DELETE FROM tag WHERE category='Language' AND name='ALGOL 60'
+DELETE FROM tag WHERE category='Language' AND name='ALGOL 68'
+DELETE FROM tag WHERE category='Language' AND name='APL'
+DELETE FROM tag WHERE category='Language' AND name='ASP.NET'
+DELETE FROM tag WHERE category='Language' AND name='AWK'
+DELETE FROM tag WHERE category='Language' AND name='ActionScript'
+DELETE FROM tag WHERE category='Language' AND name='Ada'
+DELETE FROM tag WHERE category='Language' AND name='AppleScript'
+DELETE FROM tag WHERE category='Language' AND name='AspectJ'
+DELETE FROM tag WHERE category='Language' AND name='Assembly language'
+DELETE FROM tag WHERE category='Language' AND name='AutoLISP / Visual LISP'
+DELETE FROM tag WHERE category='Language' AND name='B'
+DELETE FROM tag WHERE category='Language' AND name='BASIC'
+DELETE FROM tag WHERE category='Language' AND name='BCPL'
+DELETE FROM tag WHERE category='Language' AND name='BREW'
+DELETE FROM tag WHERE category='Language' AND name='Bash'
+DELETE FROM tag WHERE category='Language' AND name='Batch (Windows/Dos)'
+DELETE FROM tag WHERE category='Language' AND name='Bourne shell'
+DELETE FROM tag WHERE category='Language' AND name='C#'
+DELETE FROM tag WHERE category='Language' AND name='C'
+DELETE FROM tag WHERE category='Language' AND name='C++'
+DELETE FROM tag WHERE category='Language' AND name='COBOL'
+DELETE FROM tag WHERE category='Language' AND name='Clipper'
+DELETE FROM tag WHERE category='Language' AND name='Clojure'
+DELETE FROM tag WHERE category='Language' AND name='CobolScript'
+DELETE FROM tag WHERE category='Language' AND name='CoffeeScript'
+DELETE FROM tag WHERE category='Language' AND name='ColdFusion'
+DELETE FROM tag WHERE category='Language' AND name='Common Lisp'
+DELETE FROM tag WHERE category='Language' AND name='Component Pascal'
+DELETE FROM tag WHERE category='Language' AND name='Curl'
+DELETE FROM tag WHERE category='Language' AND name='D'
+DELETE FROM tag WHERE category='Language' AND name='Dart'
+DELETE FROM tag WHERE category='Language' AND name='Delphi'
+DELETE FROM tag WHERE category='Language' AND name='ECMAScript'
+DELETE FROM tag WHERE category='Language' AND name='Eiffel'
+DELETE FROM tag WHERE category='Language' AND name='Emacs Lisp'
+DELETE FROM tag WHERE category='Language' AND name='Erlang'
+DELETE FROM tag WHERE category='Language' AND name='F#'
+DELETE FROM tag WHERE category='Language' AND name='F'
+DELETE FROM tag WHERE category='Language' AND name='Forth'
+DELETE FROM tag WHERE category='Language' AND name='Fortran'
+DELETE FROM tag WHERE category='Language' AND name='FoxBase'
+DELETE FROM tag WHERE category='Language' AND name='FoxPro'
+DELETE FROM tag WHERE category='Language' AND name='Go!'
+DELETE FROM tag WHERE category='Language' AND name='Go'
+DELETE FROM tag WHERE category='Language' AND name='Groovy'
+DELETE FROM tag WHERE category='Language' AND name='Haskell'
+DELETE FROM tag WHERE category='Language' AND name='Io'
+DELETE FROM tag WHERE category='Language' AND name='J'
+DELETE FROM tag WHERE category='Language' AND name='JScript'
+DELETE FROM tag WHERE category='Language' AND name='Java'
+DELETE FROM tag WHERE category='Language' AND name='JavaFX Script'
+DELETE FROM tag WHERE category='Language' AND name='JavaScript'
+DELETE FROM tag WHERE category='Language' AND name='LaTeX'
+DELETE FROM tag WHERE category='Language' AND name='Lisp'
+DELETE FROM tag WHERE category='Language' AND name='Logo'
+DELETE FROM tag WHERE category='Language' AND name='Lua'
+DELETE FROM tag WHERE category='Language' AND name='MATLAB'
+DELETE FROM tag WHERE category='Language' AND name='MDL'
+DELETE FROM tag WHERE category='Language' AND name='ML'
+DELETE FROM tag WHERE category='Language' AND name='Machine code'
+DELETE FROM tag WHERE category='Language' AND name='Mathematica'
+DELETE FROM tag WHERE category='Language' AND name='Maya'
+DELETE FROM tag WHERE category='Language' AND name='Microcode'
+DELETE FROM tag WHERE category='Language' AND name='Microsoft Visual C++'
+DELETE FROM tag WHERE category='Language' AND name='Modula'
+DELETE FROM tag WHERE category='Language' AND name='Modula-2'
+DELETE FROM tag WHERE category='Language' AND name='Modula-3'
+DELETE FROM tag WHERE category='Language' AND name='OCaml'
+DELETE FROM tag WHERE category='Language' AND name='Object Lisp'
+DELETE FROM tag WHERE category='Language' AND name='Object Pascal'
+DELETE FROM tag WHERE category='Language' AND name='Objective-C'
+DELETE FROM tag WHERE category='Language' AND name='Opa'
+DELETE FROM tag WHERE category='Language' AND name='Orc'
+DELETE FROM tag WHERE category='Language' AND name='PHP'
+DELETE FROM tag WHERE category='Language' AND name='PL-11'
+DELETE FROM tag WHERE category='Language' AND name='PL/0'
+DELETE FROM tag WHERE category='Language' AND name='PL/B'
+DELETE FROM tag WHERE category='Language' AND name='PL/C'
+DELETE FROM tag WHERE category='Language' AND name='PL/I'
+DELETE FROM tag WHERE category='Language' AND name='PL/M'
+DELETE FROM tag WHERE category='Language' AND name='PL/P'
+DELETE FROM tag WHERE category='Language' AND name='PL/SQL'
+DELETE FROM tag WHERE category='Language' AND name='POP-11'
+DELETE FROM tag WHERE category='Language' AND name='Pascal'
+DELETE FROM tag WHERE category='Language' AND name='Perl'
+DELETE FROM tag WHERE category='Language' AND name='PostScript'
+DELETE FROM tag WHERE category='Language' AND name='PowerBuilder'
+DELETE FROM tag WHERE category='Language' AND name='PowerShell'
+DELETE FROM tag WHERE category='Language' AND name='Processing.js'
+DELETE FROM tag WHERE category='Language' AND name='Prolog'
+DELETE FROM tag WHERE category='Language' AND name='Python'
+DELETE FROM tag WHERE category='Language' AND name='QBasic'
+DELETE FROM tag WHERE category='Language' AND name='QuakeC'
+DELETE FROM tag WHERE category='Language' AND name='R'
+DELETE FROM tag WHERE category='Language' AND name='R++'
+DELETE FROM tag WHERE category='Language' AND name='REXX'
+DELETE FROM tag WHERE category='Language' AND name='Ruby'
+DELETE FROM tag WHERE category='Language' AND name='Rust'
+DELETE FROM tag WHERE category='Language' AND name='Scala'
+DELETE FROM tag WHERE category='Language' AND name='Scheme'
+DELETE FROM tag WHERE category='Language' AND name='Script.NET'
+DELETE FROM tag WHERE category='Language' AND name='Sed'
+DELETE FROM tag WHERE category='Language' AND name='Self'
+DELETE FROM tag WHERE category='Language' AND name='Simula'
+DELETE FROM tag WHERE category='Language' AND name='Simulink'
+DELETE FROM tag WHERE category='Language' AND name='Small Basic'
+DELETE FROM tag WHERE category='Language' AND name='Smalltalk'
+DELETE FROM tag WHERE category='Language' AND name='Snowball'
+DELETE FROM tag WHERE category='Language' AND name='Squeak'
+DELETE FROM tag WHERE category='Language' AND name='TEX'
+DELETE FROM tag WHERE category='Language' AND name='Tcl'
+DELETE FROM tag WHERE category='Language' AND name='TeX'
+DELETE FROM tag WHERE category='Language' AND name='UNITY'
+DELETE FROM tag WHERE category='Language' AND name='Unix shell'
+DELETE FROM tag WHERE category='Language' AND name='UnrealScript'
+DELETE FROM tag WHERE category='Language' AND name='VBA'
+DELETE FROM tag WHERE category='Language' AND name='VBScript'
+DELETE FROM tag WHERE category='Language' AND name='VHDL'
+DELETE FROM tag WHERE category='Language' AND name='Vala'
+DELETE FROM tag WHERE category='Language' AND name='Verilog'
+DELETE FROM tag WHERE category='Language' AND name='Visual Basic .NET'
+DELETE FROM tag WHERE category='Language' AND name='Visual Basic'
+DELETE FROM tag WHERE category='Language' AND name='Visual C#'
+DELETE FROM tag WHERE category='Language' AND name='Visual DataFlex'
+DELETE FROM tag WHERE category='Language' AND name='Visual DialogScript'
+DELETE FROM tag WHERE category='Language' AND name='Visual Fortran'
+DELETE FROM tag WHERE category='Language' AND name='Visual FoxPro'
+DELETE FROM tag WHERE category='Language' AND name='Visual J#'
+DELETE FROM tag WHERE category='Language' AND name='Visual J++'
+DELETE FROM tag WHERE category='Language' AND name='Visual LISP'
+DELETE FROM tag WHERE category='Language' AND name='Visual Prolog'
+DELETE FROM tag WHERE category='Language' AND name='Windows PowerShell'
+DELETE FROM tag WHERE category='Language' AND name='XQuery'
+DELETE FROM tag WHERE category='Language' AND name='XSLT'
+DELETE FROM tag WHERE category='Language' AND name='bc'
+DELETE FROM tag WHERE category='Language' AND name='csh'
+DELETE FROM tag WHERE category='Language' AND name='dBase'
+DELETE FROM tag WHERE category='Language' AND name='ksh'
+DELETE FROM tag WHERE category='Language' AND name='make'
+DELETE FROM tag WHERE category='License' AND name='Apache'
+DELETE FROM tag WHERE category='License' AND name='BSD'
+DELETE FROM tag WHERE category='License' AND name='EPL'
+DELETE FROM tag WHERE category='License' AND name='GPL'
+DELETE FROM tag WHERE category='License' AND name='ISC'
+DELETE FROM tag WHERE category='License' AND name='LGPL'
+DELETE FROM tag WHERE category='License' AND name='MIT'
+DELETE FROM tag WHERE category='License' AND name='MPL v1.1'
+DELETE FROM tag WHERE category='License' AND name='MPL v2.0'
+DELETE FROM tag WHERE category='License' AND name='Public Domain'
+DELETE FROM tag WHERE category='OS' AND name='Linux'
+DELETE FROM tag WHERE category='OS' AND name='OSX'
+DELETE FROM tag WHERE category='OS' AND name='Windows'
conf/initial-data.yml
--- conf/initial-data.yml
+++ conf/initial-data.yml
@@ -22,3 +22,471 @@
     - !!models.Role
         name:           member
         active:         true
+
+# Tags
+tags:
+    - !!models.Tag
+        category:       OS
+        name:           Windows
+    - !!models.Tag
+        category:       OS
+        name:           OSX
+    - !!models.Tag
+        category:       OS
+        name:           Linux
+    - !!models.Tag
+        category:       Language
+        name:           ActionScript
+    - !!models.Tag
+        category:       Language
+        name:           Ada
+    - !!models.Tag
+        category:       Language
+        name:           ALGOL 58
+    - !!models.Tag
+        category:       Language
+        name:           ALGOL 60
+    - !!models.Tag
+        category:       Language
+        name:           ALGOL 68
+    - !!models.Tag
+        category:       Language
+        name:           APL
+    - !!models.Tag
+        category:       Language
+        name:           AppleScript
+    - !!models.Tag
+        category:       Language
+        name:           AspectJ
+    - !!models.Tag
+        category:       Language
+        name:           ASP.NET
+    - !!models.Tag
+        category:       Language
+        name:           Assembly language
+    - !!models.Tag
+        category:       Language
+        name:           AutoLISP / Visual LISP
+    - !!models.Tag
+        category:       Language
+        name:           Visual LISP
+    - !!models.Tag
+        category:       Language
+        name:           AWK
+    - !!models.Tag
+        category:       Language
+        name:           B
+    - !!models.Tag
+        category:       Language
+        name:           Bash
+    - !!models.Tag
+        category:       Language
+        name:           BASIC
+    - !!models.Tag
+        category:       Language
+        name:           bc
+    - !!models.Tag
+        category:       Language
+        name:           BCPL
+    - !!models.Tag
+        category:       Language
+        name:           Batch (Windows/Dos)
+    - !!models.Tag
+        category:       Language
+        name:           Bourne shell
+    - !!models.Tag
+        category:       Language
+        name:           BREW
+    - !!models.Tag
+        category:       Language
+        name:           C
+    - !!models.Tag
+        category:       Language
+        name:           C++
+    - !!models.Tag
+        category:       Language
+        name:           C#
+    - !!models.Tag
+        category:       Language
+        name:           Clipper
+    - !!models.Tag
+        category:       Language
+        name:           Clojure
+    - !!models.Tag
+        category:       Language
+        name:           COBOL
+    - !!models.Tag
+        category:       Language
+        name:           CobolScript
+    - !!models.Tag
+        category:       Language
+        name:           CoffeeScript
+    - !!models.Tag
+        category:       Language
+        name:           ColdFusion
+    - !!models.Tag
+        category:       Language
+        name:           Common Lisp
+    - !!models.Tag
+        category:       Language
+        name:           Component Pascal
+    - !!models.Tag
+        category:       Language
+        name:           csh
+    - !!models.Tag
+        category:       Language
+        name:           Curl
+    - !!models.Tag
+        category:       Language
+        name:           D
+    - !!models.Tag
+        category:       Language
+        name:           Dart
+    - !!models.Tag
+        category:       Language
+        name:           dBase
+    - !!models.Tag
+        category:       Language
+        name:           Delphi
+    - !!models.Tag
+        category:       Language
+        name:           ECMAScript
+    - !!models.Tag
+        category:       Language
+        name:           Eiffel
+    - !!models.Tag
+        category:       Language
+        name:           Emacs Lisp
+    - !!models.Tag
+        category:       Language
+        name:           Erlang
+    - !!models.Tag
+        category:       Language
+        name:           F
+    - !!models.Tag
+        category:       Language
+        name:           F#
+    - !!models.Tag
+        category:       Language
+        name:           Forth
+    - !!models.Tag
+        category:       Language
+        name:           Fortran
+    - !!models.Tag
+        category:       Language
+        name:           FoxBase
+    - !!models.Tag
+        category:       Language
+        name:           FoxPro
+    - !!models.Tag
+        category:       Language
+        name:           Go
+    - !!models.Tag
+        category:       Language
+        name:           Go!
+    - !!models.Tag
+        category:       Language
+        name:           Groovy
+    - !!models.Tag
+        category:       Language
+        name:           Haskell
+    - !!models.Tag
+        category:       Language
+        name:           Io
+    - !!models.Tag
+        category:       Language
+        name:           J
+    - !!models.Tag
+        category:       Language
+        name:           Java
+    - !!models.Tag
+        category:       Language
+        name:           JavaScript
+    - !!models.Tag
+        category:       Language
+        name:           JScript
+    - !!models.Tag
+        category:       Language
+        name:           JavaFX Script
+    - !!models.Tag
+        category:       Language
+        name:           ksh
+    - !!models.Tag
+        category:       Language
+        name:           LaTeX
+    - !!models.Tag
+        category:       Language
+        name:           Lisp
+    - !!models.Tag
+        category:       Language
+        name:           Logo
+    - !!models.Tag
+        category:       Language
+        name:           Lua
+    - !!models.Tag
+        category:       Language
+        name:           Machine code
+    - !!models.Tag
+        category:       Language
+        name:           make
+    - !!models.Tag
+        category:       Language
+        name:           Mathematica
+    - !!models.Tag
+        category:       Language
+        name:           MATLAB
+    - !!models.Tag
+        category:       Language
+        name:           Maya
+    - !!models.Tag
+        category:       Language
+        name:           MDL
+    - !!models.Tag
+        category:       Language
+        name:           Microcode
+    - !!models.Tag
+        category:       Language
+        name:           ML
+    - !!models.Tag
+        category:       Language
+        name:           Modula
+    - !!models.Tag
+        category:       Language
+        name:           Modula-2
+    - !!models.Tag
+        category:       Language
+        name:           Modula-3
+    - !!models.Tag
+        category:       Language
+        name:           Object Lisp
+    - !!models.Tag
+        category:       Language
+        name:           Object Pascal
+    - !!models.Tag
+        category:       Language
+        name:           Objective-C
+    - !!models.Tag
+        category:       Language
+        name:           OCaml
+    - !!models.Tag
+        category:       Language
+        name:           Opa
+    - !!models.Tag
+        category:       Language
+        name:           Orc
+    - !!models.Tag
+        category:       Language
+        name:           Pascal
+    - !!models.Tag
+        category:       Language
+        name:           Perl
+    - !!models.Tag
+        category:       Language
+        name:           PHP
+    - !!models.Tag
+        category:       Language
+        name:           PL-11
+    - !!models.Tag
+        category:       Language
+        name:           PL/0
+    - !!models.Tag
+        category:       Language
+        name:           PL/B
+    - !!models.Tag
+        category:       Language
+        name:           PL/C
+    - !!models.Tag
+        category:       Language
+        name:           PL/I
+    - !!models.Tag
+        category:       Language
+        name:           PL/M
+    - !!models.Tag
+        category:       Language
+        name:           PL/P
+    - !!models.Tag
+        category:       Language
+        name:           PL/SQL
+    - !!models.Tag
+        category:       Language
+        name:           POP-11
+    - !!models.Tag
+        category:       Language
+        name:           PostScript
+    - !!models.Tag
+        category:       Language
+        name:           PowerBuilder
+    - !!models.Tag
+        category:       Language
+        name:           PowerShell
+    - !!models.Tag
+        category:       Language
+        name:           Processing.js
+    - !!models.Tag
+        category:       Language
+        name:           Prolog
+    - !!models.Tag
+        category:       Language
+        name:           Visual Prolog
+    - !!models.Tag
+        category:       Language
+        name:           Python
+    - !!models.Tag
+        category:       Language
+        name:           QBasic
+    - !!models.Tag
+        category:       Language
+        name:           QuakeC
+    - !!models.Tag
+        category:       Language
+        name:           R
+    - !!models.Tag
+        category:       Language
+        name:           R++
+    - !!models.Tag
+        category:       Language
+        name:           REXX
+    - !!models.Tag
+        category:       Language
+        name:           Ruby
+    - !!models.Tag
+        category:       Language
+        name:           Rust
+    - !!models.Tag
+        category:       Language
+        name:           Scala
+    - !!models.Tag
+        category:       Language
+        name:           Scheme
+    - !!models.Tag
+        category:       Language
+        name:           Script.NET
+    - !!models.Tag
+        category:       Language
+        name:           Sed
+    - !!models.Tag
+        category:       Language
+        name:           Self
+    - !!models.Tag
+        category:       Language
+        name:           Simula
+    - !!models.Tag
+        category:       Language
+        name:           Simulink
+    - !!models.Tag
+        category:       Language
+        name:           Smalltalk
+    - !!models.Tag
+        category:       Language
+        name:           Small Basic
+    - !!models.Tag
+        category:       Language
+        name:           Snowball
+    - !!models.Tag
+        category:       Language
+        name:           Squeak
+    - !!models.Tag
+        category:       Language
+        name:           Tcl
+    - !!models.Tag
+        category:       Language
+        name:           TeX
+    - !!models.Tag
+        category:       Language
+        name:           TEX
+    - !!models.Tag
+        category:       Language
+        name:           UNITY
+    - !!models.Tag
+        category:       Language
+        name:           Unix shell
+    - !!models.Tag
+        category:       Language
+        name:           UnrealScript
+    - !!models.Tag
+        category:       Language
+        name:           Vala
+    - !!models.Tag
+        category:       Language
+        name:           VBA
+    - !!models.Tag
+        category:       Language
+        name:           VBScript
+    - !!models.Tag
+        category:       Language
+        name:           Verilog
+    - !!models.Tag
+        category:       Language
+        name:           VHDL
+    - !!models.Tag
+        category:       Language
+        name:           Visual Basic
+    - !!models.Tag
+        category:       Language
+        name:           Visual Basic .NET
+    - !!models.Tag
+        category:       Language
+        name:           Microsoft Visual C++
+    - !!models.Tag
+        category:       Language
+        name:           Visual C#
+    - !!models.Tag
+        category:       Language
+        name:           Visual DataFlex
+    - !!models.Tag
+        category:       Language
+        name:           Visual DialogScript
+    - !!models.Tag
+        category:       Language
+        name:           Visual Fortran
+    - !!models.Tag
+        category:       Language
+        name:           Visual FoxPro
+    - !!models.Tag
+        category:       Language
+        name:           Visual J++
+    - !!models.Tag
+        category:       Language
+        name:           Visual J#
+    - !!models.Tag
+        category:       Language
+        name:           Windows PowerShell
+    - !!models.Tag
+        category:       Language
+        name:           XQuery
+    - !!models.Tag
+        category:       Language
+        name:           XSLT
+    - !!models.Tag
+        category:       License
+        name:           Apache
+    - !!models.Tag
+        category:       License
+        name:           BSD
+    - !!models.Tag
+        category:       License
+        name:           GPL
+    - !!models.Tag
+        category:       License
+        name:           ISC
+    - !!models.Tag
+        category:       License
+        name:           LGPL
+    - !!models.Tag
+        category:       License
+        name:           MIT
+    - !!models.Tag
+        category:       License
+        name:           MPL v2.0
+    - !!models.Tag
+        category:       License
+        name:           Public Domain
+    - !!models.Tag
+        category:       License
+        name:           Apache
+    - !!models.Tag
+        category:       License
+        name:           EPL
+    - !!models.Tag
+        category:       License
+        name:           MPL v1.1
conf/routes
--- conf/routes
+++ conf/routes
@@ -72,7 +72,7 @@
 GET     /:user/:project/post/:id/comment/:commentId/delete     controllers.BoardApp.deleteComment(user, project, id:Long, commentId:Long)
 
 # Tags
-GET    /tags                                            controllers.TagApp.tags(query: String ?= "")
+GET    /tags                                            controllers.TagApp.tags(query: String ?= "", context: String ?= "", limit: Integer ?= null)
 GET    /:user/:project/tags                             controllers.ProjectApp.tags(user, project)
 POST   /:user/:project/tags                             controllers.ProjectApp.tag(user, project)
 POST   /:user/:project/tags/:id                         controllers.ProjectApp.untag(user, project, id: Long)
 
docs/technical/tag-typeahead.md (added)
+++ docs/technical/tag-typeahead.md
@@ -0,0 +1,90 @@
+소개
+----
+
+HIVE는 태그에 대한 자동완성 API를 제공한다. URL `/tags`로 요청하면 json 포맷으로 된 태그들의 목록을 돌려받는다.
+
+요청
+----
+
+### 메소드
+
+    GET
+
+### URL
+
+    /tags
+
+### 파라메터
+
+태그를 요청할 때 쿼리 스트링에 다음의 필드를 포함시킬 수 있다. 쿼리 스트링은 [application/x-www-form-urlencoded](http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type)에 따라 인코딩된다.
+
+#### query
+
+태그에 대한 검색어. 서버는 태그의 카테고리나 이름에 이 `query`의 값이 포함된 태그를 반환한다. 대소문자를 구분하지 않는다.
+
+#### context
+
+태그를 요청하는 상황에 대한 문맥. 예를 들어 `PROJECT_TAGGING_TYPEAHEAD` 라는 값을 넘겨준 경우, 서버는 현재 상황이 Project Overview 페이지에서 태그를 추가시 자동완성을 위해 태그의 목록을 요청한 것으로 이해하고, 상황에 알맞게(현재 프로젝트에 라이선스 태그가 하나도 없다면 라이선스 태그를 우선한다거나) 태그를 정렬하여 돌려준다.
+
+#### project_id
+
+위 `context`에 따라 `project_id` 필드가 필요한 경우가 있다.
+
+#### limit
+
+돌려받을 항목의 갯수를 제한한다. 그러나 서버 역시 별도로 넘겨줄 수 있는 항목 갯수의 상한값을 가지고 있을 수 있다. 그러한 경우에는 경우에는 작은 쪽이 선택된다.
+
+응답
+----
+
+응답은 json 형식으로, 문자열의 배열로 반환된다.
+
+배열의 각 요소는 태그의 카테고리와 이름이 ` - `을 구분자로 하여 조합된 문자열이다. 예를 들어 License 카테고리의 Public Domain이라면 다음과 같다.
+
+    License - Public Domain
+
+ABNF로 표현하면 다음과 같다.
+
+    tag = tag-category SP "-" SP tag-name
+
+`-`의 좌우에 SP가 있음에 주의하라. 이것은 보통 엄격하게 검사된다.
+
+### Content-Range 헤더
+
+서버는 항상 자동완성 후보의 목록을 남김없이 모두 둘려주는 것은 아니다. 다음의 경우 일부분만을 돌려줄 수 있다.
+
+1. 클라이언트가 일부분만을 요청한 경우 (쿼리 스트링의 `limit` 필드를 이용했을 것이다)
+2. 서버가 항목의 최대 갯수를 제한하고 있는 경우
+
+이러한 경우 서버는 다음과 같은 형식의 Content-Range 헤더를 포함시켜서, 몇 개의 항목 중에서 몇 개의 항목을 돌려주었는지 알려줄 수 있다. (HTTP/1.1의 bytes-range-spec과 차이가 있음에 유의하라)
+
+    Content-Range     = items-unit SP number-of-items "/" complete-length
+    items-unit        = "items"
+    number-of-items   = 1*DIGIT
+    complete-length   = 1*DIGIT
+    SP                = <US-ASCII SP, space (32)>
+
+예를 들어 총 10개 중 8개만을 돌려주었다는 의미의 Content-Range 헤더는 다음과 같다.
+
+    Content-Range: items 8/10
+
+`complete-length`는, 서버의 제약이나 클라이언트의 제한 요청이 없었을 경우 돌려주었을 항목의 갯수와 같다.
+
+주의: 클라이언트의 요청이 Range 요청이 아님에도 Content-Range 헤더를 포함하여 응답하는 것이므로, 응답의 상태 코드는 206 Partial Content 이어서는 안된다. 206으로 응답할 때는 반드시 요청에 Range 헤더가 포함되어 있어야 하기(MUST) 때문이다.
+
+예외처리
+--------
+
+* 오로지 json으로만 응답이 가능하기 때문에, `application/json`을 받아들일 수 없는 요청이라면 406 Not Acceptable로 응답한다.
+* 필요한 파라메터가 없거나, 잘못된 파라메터가 주어진 경우에는 400 Bad Request로 응답한다.
+
+요청과 응답의 예
+----------------
+
+요청
+
+    GET /tags?context=PROJECT_TAGGING_TYPEAHEAD&limit=8&project_id=1&query=a
+
+응답
+
+    ["License - Public Domain","Language - @Formula","Language - A# (Axiom)","Language - A# .NET","Language - A+","Language - A++","Language - A-0 System","Language - ABAP"]
public/javascripts/common/hive.ui.Typeahead.js
--- public/javascripts/common/hive.ui.Typeahead.js
+++ public/javascripts/common/hive.ui.Typeahead.js
@@ -30,6 +30,7 @@
 		function _initVar(htOptions){
 			htVar.sActionURL = htOptions.sActionURL || "/users";
 			htVar.rxContentRange = /items\s+([0-9]+)\/([0-9]+)/;
+            htVar.htData = htOptions.htData;
 		}
 		
 		/**
@@ -39,8 +40,11 @@
 		 */
 		function _initElement(sQuery){
 			htElement.welInput = $(sQuery);
-			htElement.welInput.typeahead();
-			htElement.welInput.data("typeahead").source = _onTypeAhead;
+			htElement.welInput.typeahead({
+                source: _onTypeAhead,
+                minLength: 0,
+                items: htVar.htData.limit || 8
+            });
 		}
 		
         /**
@@ -56,10 +60,11 @@
             if (sQuery.match(htVar.sLastQuery) && htVar.bIsLastRangeEntire) {
             	fProcess(htVar.htCachedUsers);
             } else {
+                htVar.htData.query = sQuery;
             	$hive.sendForm({
             		"sURL"		: htVar.sActionURL,
             		"htOptForm"	: {"method":"get"},
-            		"htData"	: {"query": sQuery},
+			"htData"	: htVar.htData,
                     "sDataType" : "json",
             		"fOnLoad"	: function(oData, oStatus, oXHR){
             			var sContentRange = oXHR.getResponseHeader('Content-Range');
 
public/javascripts/lib/bootstrap-better-typeahead.js (added)
+++ public/javascripts/lib/bootstrap-better-typeahead.js
@@ -0,0 +1,136 @@
+/* =============================================================
+ * bootstrap-better-typeahead.js v1.0.0 by Philipp Nolte
+ * https://github.com/ptnplanet/Bootstrap-Better-Typeahead
+ * =============================================================
+ * This plugin makes use of twitter bootstrap typeahead
+ * http://twitter.github.com/bootstrap/javascript.html#typeahead
+ *
+ * Bootstrap is licensed under the Apache License, Version 2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * ============================================================ */
+
+// **************************************************
+// Hive modifications by Yi EungJun
+// http://github.github.com/nforge/hive/
+//
+// Modifications are tagged with "hive"
+// **************************************************
+
+!function($) {
+
+    "use strict";
+
+    /**
+     * The better typeahead plugin will extend the bootstrap typeahead plugin and provide the abbility to set the
+     * maxLength option to zero. The tab keyup event handler had to be moved to the keydown event handler, so that
+     * the full list of available items is shown on tab-focus and the original behaviour is preserved as best as
+     * possible.
+     *
+     * @type {object}
+     */
+    var BetterTypeahead = {
+
+        lookup: function(event) {
+            var items;
+
+            // Now supports empty queries (eg. with a length of 0).
+            this.query = this.$element.val() || '';
+
+            if (this.query.length < this.options.minLength) {
+                return this.shown ? this.hide() : this;
+            }
+
+            items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source;
+
+            return items ? this.process(items) : this;
+        }
+
+        , process: function (items) {
+            var that = this;
+
+            items = $.grep(items, function (item) {
+                return that.matcher(item);
+            });
+
+            items = this.sorter(items);
+
+            if (!items.length) {
+                return this.shown ? this.hide() : this;
+            }
+
+            // hive: If-clause to make this work only if query's length is more
+            // than 0, has been removed from the original version.
+            items = items.slice(0, this.options.items);
+
+            return this.render(items).show();
+        }
+
+        , render: function (items) {
+            var that = this
+
+            items = $(items).map(function (i, item) {
+                i = $(that.options.item).attr('data-value', item);
+                i.find('a').html(that.highlighter(item));
+                return i[0];
+            });
+
+            if (this.query.length > 0) {
+                items.first().addClass('active');
+            }
+
+            this.$menu.html(items);
+            return this;
+        }
+
+        , keydown: function (e) {
+            this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]);
+
+            // Added tab handler. Tabbing out of the input (thus blurring).
+            if (e.keyCode === 9) { // tab
+                if (!this.shown) return;
+                this.select();
+            } else {
+                this.move(e);
+            }
+        }
+
+        , keyup: function (e) {
+            switch(e.keyCode) {
+                case 40: // down arrow
+                case 38: // up arrow
+                case 16: // shift
+                case 17: // ctrl
+                case 18: // alt
+                    break;
+
+                // Moved tab handler to keydown.
+                case 13: // enter
+                    if (!this.shown) return;
+                    this.select();
+                    break;
+
+                case 27: // escape;
+                    if (!this.shown) return;
+                    this.hide();
+                    break;
+
+                default:
+                    this.lookup();
+            }
+
+            e.stopPropagation();
+            e.preventDefault();
+        }
+
+        , focus: function(e) {
+            this.focused = true;
+
+            if (!this.mousedover) {
+                this.lookup(e);
+            }
+        }
+    };
+
+    $.extend($.fn.typeahead.Constructor.prototype, BetterTypeahead);
+
+}(window.jQuery);
public/javascripts/service/hive.project.Home.js
--- public/javascripts/service/hive.project.Home.js
+++ public/javascripts/service/hive.project.Home.js
@@ -16,15 +16,31 @@
 		var htElement = {};
 	
 		function _init(htOptions){
+			var htOpt = htOptions || {};
+			_initVar(htOpt);
 			_initElement(htOptions);
 			_attachEvent();			
+            _initTags();
 		}
-		
+
+        function _initVar(htOptions){
+            htVar.sURLProjectTags = htOptions.sURLProjectTags;
+            htVar.sURLTags = htOptions.sURLTags;
+            htVar.nProjectId = htOptions.nProjectId;
+		}
+
 		/**
 		 * initialize element
 		 */
 		function _initElement(htOptions){
 			htElement.welRepoURL = $("#repositoryURL");
+
+            // tags
+            htElement.welInputAddTag = $('input[name="newTag"]');
+            htElement.welTags = $('#tags');
+            htElement.welBtnAddTag = $('#addTag');
+            htElement.welTagEditorToggle = $('#tag-editor-toggle');
+            htElement.waTag = $();
 		}
 		
 		/**
@@ -32,12 +48,203 @@
 		 */
 		function _attachEvent(){
 			htElement.welRepoURL.click(_onClickRepoURL);
+            htElement.welInputAddTag.keypress(_onKeyPressNewTag);
+            htElement.welBtnAddTag.click(_submitTag);
+            htElement.welTagEditorToggle.on('click', function() {
+                if ($(this).hasClass('active')) {
+                    // Now inactive
+                    _hideTagEditor();
+                } else {
+                    // Now active
+                    _showTagEditor();
+                }
+            });
+            new hive.ui.Typeahead(htElement.welInputAddTag, {
+		"sActionURL": htVar.sURLTags,
+                "htData": {
+                    "context": "PROJECT_TAGGING_TYPEAHEAD",
+                    "project_id": htVar.nProjectId,
+                    "limit": 8
+                }
+            });
 		}
-		
+
 		function _onClickRepoURL(){
 			htElement.welRepoURL.select();
 		}
-		
+
+        /**
+        * Add a tag, which user types in htElement.welInputAddTag, into #tags div.
+        *
+        * @param {Object} oEvent
+        */
+        function _onKeyPressNewTag(oEvent) {
+            if (oEvent.keyCode == 13) {
+                _submitTag();
+                htElement.welInputAddTag.val("");
+                return false;
+            }
+        }
+
+        function _parseTag(sTag) {
+            var sSeparator = ' - ';
+            var aPart =
+                jQuery.map(sTag.split(sSeparator), function(s) { return s.trim(); });
+            var htTag;
+
+            if (aPart.length > 2) {
+                aPart = [aPart.shift(), aPart.join(sSeparator)];
+            }
+
+            if (aPart.length > 1) {
+                htTag = {"category": aPart[0], "name": aPart[1]};
+            } else if (aPart.length == 1) {
+                htTag = {"category": "Tag", "name": aPart[0]};
+            } else {
+                return null;
+            }
+
+            return htTag;
+        }
+
+        /**
+        * Submit new tag to add that.
+        */
+        function _submitTag () {
+            var htTag = _parseTag(htElement.welInputAddTag.val());
+
+            if (htTag == null) {
+                return;
+            }
+
+		$hive.sendForm({
+			"sURL"   : htVar.sURLProjectTags,
+			"htData" : htTag,
+			"fOnLoad": _appendTags
+		});
+        }
+
+        /**
+        * Get list of tags from the server and show them in #tags div.
+        */
+        function _initTags() {
+		$hive.sendForm({
+			"sURL"     : htVar.sURLProjectTags,
+			"htOptForm": {"method":"get"},
+			"fOnLoad"  : function(data) {
+                    _appendTags(data);
+                    _hideTagEditor();
+                }
+		});
+        }
+
+        /**
+        * Make a tag element by given instance id and name.
+        *
+        * @param {String} sInstanceId
+        * @param {String} sName
+        */
+        function _createTag(sInstanceId, sName) {
+            // If someone clicks a delete button, remove the tag which contains
+            // the button, and also hide its category in .project-info div if
+            // the category becomes to have no tag.
+            var fOnClickDelete = function() {
+		$hive.sendForm({
+			"sURL"   : htVar.sURLProjectTags + '/' + sInstanceId,
+			"htData" : {"_method":"DELETE"},
+			"fOnLoad": function(){
+                        var welCategory = welTag.parent();
+				welTag.remove();
+                        if (welCategory.children('span').length == 0) {
+                            welCategory.remove();
+                        }
+			}
+		});
+            };
+
+            var welDeleteButton = $('<a>')
+                .attr('href', 'javascript:void(0)')
+                .text(' x')
+                .click(fOnClickDelete);
+
+            var welTag = $('<span>')
+                .text(sName)
+                .addClass('label')
+		.append(welDeleteButton);
+
+            welTag.setRemovability = function(bFlag) {
+                if (bFlag === true) {
+                    welTag.addClass('label');
+                    welDeleteButton.show();
+                } else {
+                    welTag.removeClass('label');
+                    welDeleteButton.hide();
+                }
+            }
+
+            htElement.waTag.push(welTag);
+
+            return welTag;
+        }
+
+        /**
+        * Append the given tags on #tags div to show them.
+        *
+        * @param {Object} htTags
+        */
+        function _appendTags(htTags) {
+            for(var sInstanceId in htTags) {
+                var waChildren, newCategory;
+                var htTag = _parseTag(htTags[sInstanceId]);
+
+                waChildren =
+                    htElement.welTags.children("[category=" + htTag.category + "]");
+
+                if (waChildren.length > 0) {
+                    waChildren.append(_createTag(sInstanceId, htTag.name));
+                } else {
+                    newCategory = $('<li>')
+                        .addClass('info')
+                        .attr('category', htTag.category)
+                        .append($('<strong>').text(htTag.category + ' : '))
+                        .append(_createTag(sInstanceId, htTag.name));
+                    htElement.welTags.append(newCategory);
+                }
+            }
+        }
+
+        /**
+        * Show all delete buttons for all tags if bFlag is true, and hide if
+        * bFlag is false.
+        *
+        * @param {Boolean} bFlag
+        */
+        function _setTagsRemovability(bFlag) {
+            jQuery.map(htElement.waTag, function(tag) { tag.setRemovability(bFlag); });
+        }
+
+        /**
+        * Make .project-info div editable.
+        *
+        * @param {Boolean} bFlag
+        */
+        function _hideTagEditor() {
+            _setTagsRemovability(false);
+            htElement.welInputAddTag.addClass('hidden');
+            htElement.welBtnAddTag.addClass('hidden');
+        }
+
+        /**
+        * Make .project-info div uneditable.
+        *
+        * @param {Boolean} bFlag
+        */
+        function _showTagEditor() {
+            _setTagsRemovability(true);
+            htElement.welInputAddTag.removeClass('hidden');
+            htElement.welBtnAddTag.removeClass('hidden');
+        }
+
 		_init(htOptions || {});
 	};
 	
public/javascripts/service/hive.project.Setting.js
--- public/javascripts/service/hive.project.Setting.js
+++ public/javascripts/service/hive.project.Setting.js
@@ -23,7 +23,6 @@
 			_initVar(htOpt);
 			_initElement(htOpt);
 			_attachEvent();
-            _updateTags();
 			
 			htVar.waPopOvers.popover();
 		}
@@ -35,8 +34,6 @@
 		function _initVar(htOptions){
 			htVar.rxLogoExt = /\.(gif|bmp|jpg|jpeg|png)$/i;
 			htVar.rxPrjName = /^[a-zA-Z0-9_][-a-zA-Z0-9_]+[^-]$/;
-            htVar.sURLProjectTags = htOptions.sURLProjectTags;
-            htVar.sURLTags = htOptions.sURLTags;
 		}
 
 		/**
@@ -60,27 +57,15 @@
 			
 			// popovers
 			htVar.waPopOvers = $([$("#project_name"), $("#share_option_explanation"), $("#terms")]);
-
-            // tags
-            htElement.welInputAddTag = $('input[name="newTag"]');
-            htElement.welTags = $('#tags');
-            htElement.welBtnAddTag = $('#addTag');
-            
-            htVar.oTagInput = new hive.ui.Typeahead(htElement.welInputAddTag, {
-            	"sActionURL": htVar.sURLTags
-            });
 		}
 
-        /**
+		/**
 		 * attach event handlers
 		 */
 		function _attachEvent(){
 			htElement.welInputLogo.change(_onChangeLogoPath);
 			htElement.welBtnDeletePrj.click(_onClickBtnDeletePrj);
 			htElement.welBtnSave.click(_onClickBtnSave);
-            htElement.welInputAddTag.keypress(_onKeyPressNewTag);
-//                .typeahead().data('typeahead').source = _tagTypeaheadSource;
-            htElement.welBtnAddTag.click(_submitTag);
 		}
 		
 		/**
@@ -125,76 +110,6 @@
 			return true;
 		}
 
-        /**
-        * Submit new tag to add that.
-        */
-        function _submitTag () {
-        	$hive.sendForm({
-        		"sURL"   : htVar.sURLProjectTags,
-        		"htData" : {"name": htElement.welInputAddTag.val()},
-        		"fOnLoad": _appendTags
-        	});
-        }
-
-        /**
-        * Add a tag, which user types in htElement.welInputAddTag, into #tags div.
-        *
-        * @param {Object} oEvent
-        */
-        function _onKeyPressNewTag(oEvent) {
-            if (oEvent.keyCode == 13) {
-                _submitTag();
-                htElement.welInputAddTag.val("");
-                return false;
-            }
-        }
-
-        /**
-        * Get list of tags from the server and show them in #tags div.
-        */
-        function _updateTags() {
-        	$hive.sendForm({
-        		"sURL"     : htVar.sURLProjectTags,
-        		"htOptForm": {"method":"get"},
-        		"fOnLoad"  : _appendTags
-        	});
-        }
-
-        /**
-        * Make a tag element by given id and name.
-
-        * @param {String} sId
-        * @param {String} sName
-        */
-        function _createTag(sId, sName) {
-            var fOnClickDelete = function() {
-            	$hive.sendForm({
-            		"sURL"   : htVar.sURLProjectTags + '/' + sId,
-            		"htData" : {"_method":"DELETE"},
-            		"fOnLoad": function(){
-            			welTag.remove();
-            		}
-            	});            	
-            };
-
-            var welTag = $('<span class="label label-info">' + sName + " </span>")
-            	.append($('<a href="javascript:void(0)">x</a>')
-            	.click(fOnClickDelete));
-
-            return welTag;
-        }
-
-        /**
-        * Append the given tags on #tags div to show them.
-        *
-        * @param {Object} htTags
-        */
-        function _appendTags(htTags) {
-            for(var sId in htTags) {
-                htElement.welTags.append(_createTag(sId, htTags[sId]));
-            }
-        }
-
 		_init(htOptions);
 	};
 	
test/controllers/ProjectAppTest.java
--- test/controllers/ProjectAppTest.java
+++ test/controllers/ProjectAppTest.java
@@ -31,7 +31,8 @@
             public void run() {
                 //Given
                 Map<String,String> data = new HashMap<String,String>();
-                data.put("name", "foo");
+                data.put("category", "OS");
+                data.put("name", "linux");
                 User admin = User.findByLoginId("admin");
 
                 //When
@@ -44,13 +45,13 @@
                 );
 
                 //Then
-                assertThat(status(result)).isEqualTo(OK);
+                assertThat(status(result)).isEqualTo(CREATED);
                 Iterator<Map.Entry<String, JsonNode>> fields = Json.parse(contentAsString(result)).getFields();
                 Map.Entry<String, JsonNode> field = fields.next();
                 Tag expected = new Tag();
                 expected.id = Long.valueOf(field.getKey());
                 expected.name = field.getValue().asText();
-                assertThat(expected.name).isEqualTo("foo");
+                assertThat(expected.name).isEqualTo("OS - linux");
                 assertThat(Project.findByNameAndOwner("hobi", "nForge4java").tags.contains(expected)).isTrue();
             }
         });
@@ -63,14 +64,13 @@
                 //Given
                 Project project = Project.findByNameAndOwner("hobi", "nForge4java");
 
-                Tag tag1 = new Tag();
-                tag1.name = "foo";
+                Tag tag1 = new Tag("OS", "linux");
                 tag1.save();
                 project.tags.add(tag1);
                 project.update();
 
-                Tag tag2 = new Tag();
-                tag2.name = "bar";
+                // If null is given as the first parameter, "Tag" is chosen as the category.
+                Tag tag2 = new Tag(null, "foo");
                 tag2.save();
                 project.tags.add(tag2);
                 project.update();
@@ -87,8 +87,8 @@
                 JsonNode json = Json.parse(contentAsString(result));
                 assertThat(json.has(tag1.id.toString())).isTrue();
                 assertThat(json.has(tag2.id.toString())).isTrue();
-                assertThat(json.get(tag1.id.toString()).asText()).isEqualTo("foo");
-                assertThat(json.get(tag2.id.toString()).asText()).isEqualTo("bar");
+                assertThat(json.get(tag1.id.toString()).asText()).isEqualTo("OS - linux");
+                assertThat(json.get(tag2.id.toString()).asText()).isEqualTo("Tag - foo");
             }
         });
     }
@@ -100,8 +100,7 @@
                 //Given
                 Project project = Project.findByNameAndOwner("hobi", "nForge4java");
 
-                Tag tag1 = new Tag();
-                tag1.name = "foo";
+                Tag tag1 = new Tag("OS", "linux");
                 tag1.save();
                 project.tags.add(tag1);
                 project.update();
Add a comment
List