Skip to content

Commit

Permalink
issue: Change assignee selection method
Browse files Browse the repository at this point in the history
- Search by async call
- In public project, it can be selected any user of site.
  • Loading branch information
doortts committed Aug 10, 2017
1 parent 10c05b1 commit 5170f1c
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 68 deletions.
202 changes: 195 additions & 7 deletions app/controllers/api/IssueApi.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
/**
* Yona, Project Hosting SW
*
* Copyright 2016 the original author or authors.
*/
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
* https://yona.io
**/

package controllers.api;

import com.avaje.ebean.ExpressionList;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import controllers.*;
import controllers.AbstractPostingApp;
import controllers.UserApp;
import controllers.annotation.IsAllowed;
import controllers.annotation.IsCreatable;
import controllers.routes;
import models.*;
import models.enumeration.Operation;
import models.enumeration.ResourceType;
import models.enumeration.State;
import org.joda.time.DateTime;
import models.enumeration.UserState;
import org.apache.commons.lang3.StringUtils;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import play.libs.Json;
import play.mvc.Http;
import play.mvc.Result;
import utils.*;
import utils.AccessControl;
import utils.ErrorViews;
import utils.JodaDateUtil;
import utils.RouteUtil;

import javax.annotation.Nonnull;
import java.io.IOException;
Expand All @@ -28,6 +38,7 @@
import java.text.SimpleDateFormat;
import java.util.*;

import static controllers.UserApp.MAX_FETCH_USERS;
import static controllers.api.UserApi.createUserNode;
import static play.libs.Json.toJson;

Expand Down Expand Up @@ -247,4 +258,181 @@ public static Date parseDateString(JsonNode dateStringNode){

return null;
}

@IsAllowed(Operation.READ)
public static Result findAssignableUsers(String ownerName, String projectName, Long number, String query) {
if (!request().accepts("application/json")) {
return status(Http.Status.NOT_ACCEPTABLE);
}

Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
Issue issue = Issue.findByNumber(project, number);

List<ObjectNode> users = new ArrayList<>();


if(StringUtils.isEmpty(query)){
User issueAuthor = issue.getAuthor();

if (issue.hasAssignee()) {
addMyself(issue, users);
addAuthorIfNotMeAndNotAssginee(issue, users, issueAuthor);
addUserToUsersWithCustomName(User.anonymous, users, Messages.get("issue.noAssignee"));
addUserToUsers(issue.assignee.user, users); // To positioned up rank of list
} else {
addUserToUsersWithCustomName(UserApp.currentUser(), users, Messages.get("issue.assignToMe"));
addAuthorIfNotMe(issue, users, issueAuthor);
}

for(User user: project.getAssignableUsersAndAssignee(issue)){
addUserToUsers(user, users);
}

return ok(toJson(users));
}

ExpressionList<User> el = getUserExpressionList(query, request().getQueryString("type"));

int total = el.findRowCount();
if (total > MAX_FETCH_USERS) {
el.setMaxRows(MAX_FETCH_USERS);
response().setHeader("Content-Range", "items " + MAX_FETCH_USERS + "/" + total);
}

for (User user : el.findList()) {
if (project.isPublic()) {
addUserToUsers(user, users);
} else {
if (user.isMemberOf(project)
|| project.hasGroup() && user.isMemberOf(project.organization)) {
addUserToUsers(user, users);
}
}
}

return ok(toJson(users));
}

private static ExpressionList<User> getUserExpressionList(String query, String searchType) {
ExpressionList<User> el = User.find.select("loginId, name").where()
.eq("state", UserState.ACTIVE).disjunction();
if( StringUtils.isNotBlank(searchType)){
el.eq(searchType, query);
} else {
el.icontains("loginId", query);
el.icontains("name", query);
el.endJunction();
}
return el;
}

private static void addAuthorIfNotMe(Issue issue, List<ObjectNode> users, User issueAuthor) {
if (!issue.getAuthor().loginId.equals(UserApp.currentUser().loginId)) {
addUserToUsersWithCustomName(issueAuthor, users, Messages.get("issue.assignToAuthor"));
}
}

private static void addAuthorIfNotMeAndNotAssginee(Issue issue, List<ObjectNode> users, User issueAuthor) {
if (!issue.getAuthor().loginId.equals(UserApp.currentUser().loginId)
&& !issue.getAuthor().loginId.equals(issue.assignee.user.loginId)) {
addUserToUsersWithCustomName(issueAuthor, users, Messages.get("issue.assignToAuthor"));
}
}

private static void addMyself(Issue issue, List<ObjectNode> users) {
if (!UserApp.currentUser().loginId.equals(issue.assignee.user.loginId)) {
addUserToUsersWithCustomName(UserApp.currentUser(), users, Messages.get("issue.assignToMe"));
}
}

private static void addUserToUsers(User user, List<ObjectNode> users) {
ObjectNode userNode = Json.newObject();
userNode.put("loginId", user.loginId);
userNode.put("name", user.name);
userNode.put("avatarUrl", user.avatarUrl());

if(!users.contains(userNode)) {
users.add(userNode);
}
}

private static void addUserToUsersWithCustomName(User user, List<ObjectNode> users, String name) {
ObjectNode userNode = Json.newObject();
userNode.put("loginId", user.loginId);
userNode.put("name", name);
userNode.put("avatarUrl", "");

if(!users.contains(userNode)) {
users.add(userNode);
}
}

public static Result updateAssginees(String owner, String projectName, Long number){
ObjectNode result = Json.newObject();
JsonNode json = request().body().asJson();
if (json == null) {
return badRequest(result.put("message", "Expecting Json data"));
}

Project project = Project.findByOwnerAndProjectName(owner, projectName);
Issue issue = Issue.findByNumber(project, number);

if (AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(),
Operation.UPDATE)) {

JsonNode assignees = json.findValue("assignees");
if(assignees == null || assignees.size() == 0){
return badRequest(result.put("message", "No assignee"));
}

boolean assigneeChanged = false;

for(JsonNode assgineeNode: assignees){
User assigneeUser = User.findByLoginId(assgineeNode.asText());

User oldAssignee = null;

if (issue.hasAssignee()) {
oldAssignee = issue.assignee.user;
}
Assignee newAssignee = getAssignee(project, assigneeUser);
assigneeChanged = !issue.assignedUserEquals(newAssignee);

issue.assignee = newAssignee;
issue.updatedDate = JodaDateUtil.now();
issue.update();

if(assigneeChanged) {
NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, issue);
IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
}

composeResultJson(result, assigneeUser);
}
}

result.put("issue", routes.IssueApp.issue(owner, projectName, number).url());
return ok(result);
}

private static void composeResultJson(ObjectNode result, User assigneeUser) {
ObjectNode node = Json.newObject();
node.put("loginId", assigneeUser.loginId);
if(assigneeUser.isAnonymous()){
node.put("name", Messages.get("common.none"));
} else {
node.put("name", assigneeUser.name);
}
result.put("assignee", node);
}

private static Assignee getAssignee(Project project, User assigneeUser) {
Assignee newAssignee;
if (assigneeUser.isAnonymous()) {
newAssignee = null;
} else {
newAssignee = Assignee.add(assigneeUser.id, project.id);
}
return newAssignee;
}
}
16 changes: 6 additions & 10 deletions app/controllers/api/ProjectApi.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/**
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright 2016 the original author or authors.
*/
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
* https://yona.io
**/

package controllers.api;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import controllers.*;
import controllers.UserApp;
import controllers.annotation.IsAllowed;
import controllers.annotation.IsCreatable;
import models.*;
Expand All @@ -17,7 +18,6 @@
import models.enumeration.ResourceType;
import models.enumeration.RoleType;
import org.apache.commons.lang3.StringUtils;
import play.data.Form;
import play.db.ebean.Model;
import play.db.ebean.Transactional;
import play.i18n.Messages;
Expand All @@ -29,15 +29,11 @@

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

import static models.AbstractPosting.findByProject;
import static models.enumeration.ProjectScope.PRIVATE;
import static play.data.Form.form;
import static play.libs.Json.toJson;
import static utils.CacheStore.getProjectCacheKey;
import static utils.CacheStore.projectMap;
Expand Down
12 changes: 8 additions & 4 deletions app/models/Issue.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
* https://yona.io
**/
package models;

Expand Down Expand Up @@ -132,6 +132,10 @@ public String assigneeName() {
return ((assignee != null && assignee.user != null) ? assignee.user.name : null);
}

public boolean hasAssignee() {
return (assignee != null && assignee.user != null);
}

/**
* @see Assignee#add(Long, Long)
*/
Expand Down
55 changes: 9 additions & 46 deletions app/views/issue/partial_assignee.scala.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
@**
* Yobi, Project Hosting SW
* Yona, 21st Century Project Hosting SW
*
* Copyright 2014 NAVER Corp.
* http://yobi.io
*
* @author Suwon Chae, 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.
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**@
@(project:Project, issue:Issue)

@import utils.AccessControl._
@(project:Project, issue:Issue)

@makeOptionTagWithFlag(user:User, users:List[User], flag:String) = {
<option value="@user.id"
Expand All @@ -32,32 +17,10 @@
</option>
}

<select id="assignee" name="assignee.user.id" data-field-name="assignee.id"
data-toggle="select2" data-format="user" data-container-css-class="fullsize">
<option value="@User.anonymous.id" @if(issue == null || issue.assignee == null){selected}>@Messages("issue.noAssignee")</option>
@if(isAllowed(UserApp.currentUser(), project.asResource(), Operation.ASSIGN_ISSUE)) {
<option value="@UserApp.currentUser().id" data-force-change="true">@Messages("issue.assignToMe")</option>
}
@if(issue != null){
@defining(issue.getAuthor) { issueAuthor =>
@if(!UserApp.currentUser.equals(issueAuthor) &&
isAllowed(issueAuthor, project.asResource(), Operation.ASSIGN_ISSUE)) {
<option value="@issueAuthor.id" data-force-change="true">@Messages("issue.assignToAuthor")</option>
}
}
}
@defining(project.getAssignableUsersAndAssignee(issue)) { users =>
@for(user <- users){
@if(issue != null && issue.assignee != null && issue.assignee.user.loginId == user.loginId) {
@makeOptionTagWithFlag(user, users, "selected")
} else {
@makeOptionTagWithFlag(user, users, "")
}
}

@if(issue != null && issue.assignee != null && issue.assignee.user.isSiteManager() && !UserApp.currentUser().isSiteManager()) {
@makeOptionTagWithFlag(issue.assignee.user, users, "selected")
}
@assignee = @{
if(issue != null && issue.assignee != null) {
issue.assignee.user.loginId
}
}

</select>
<input type="hidden" class="bigdrop" id="assignee" placeholder="@Messages("issue.noAssignee")" value="@assignee" style="width: 100%" title="">
Loading

0 comments on commit 5170f1c

Please sign in to comment.