Package controllers

Source Code of controllers.IssueApp

/**
* Yobi, Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @Author Tae
*
* 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.NullProjectCheckAction;
import actions.AnonymousCheckAction;
import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Page;
import controllers.annotation.IsAllowed;
import controllers.annotation.IsCreatable;
import jxl.write.WriteException;
import models.*;
import models.enumeration.Operation;
import models.enumeration.ResourceType;
import models.enumeration.State;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.Tika;
import org.codehaus.jackson.node.ObjectNode;
import play.api.templates.Html;
import play.data.Form;
import play.data.validation.ValidationError;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import play.libs.Json;
import play.mvc.Call;
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.With;
import utils.*;
import views.html.issue.*;

import java.io.IOException;
import java.util.*;

import utils.HttpUtil;

public class IssueApp extends AbstractPostingApp {
    private static final String EXCEL_EXT = "xls";
    private static final Integer ITEMS_PER_PAGE_MAX = 45;

    @With(AnonymousCheckAction.class)
    public static Result userIssues(String state, String format, int pageNum) throws WriteException, IOException {
        Project project = null;
        // SearchCondition from param
        Form<models.support.SearchCondition> issueParamForm = new Form<>(models.support.SearchCondition.class);
        models.support.SearchCondition searchCondition = issueParamForm.bindFromRequest().get();
        if (hasNotConditions(searchCondition)) {
            searchCondition.assigneeId = UserApp.currentUser().id;
        }
        searchCondition.pageNum = pageNum - 1;

        // determine pjax or json when requested with XHR
        if (HttpUtil.isRequestedWithXHR(request())) {
            format = HttpUtil.isPJAXRequest(request()) ? "pjax" : "json";
        }

        Integer itemsPerPage = getItemsPerPage();
        ExpressionList<Issue> el = searchCondition.asExpressionList();
        Page<Issue> issues = el.findPagingList(itemsPerPage).getPage(searchCondition.pageNum);

        switch(format){
            case EXCEL_EXT:
                return issuesAsExcel(project, el);

            case "pjax":
                return issuesAsPjax(project, issues, searchCondition);

            case "json":
                return issuesAsJson(project, issues);

            case "html":
            default:
                return issuesAsHTML(project, issues, searchCondition);
        }
    }

    private static boolean hasNotConditions(models.support.SearchCondition searchCondition) {
        return searchCondition.assigneeId == null && searchCondition.authorId == null && searchCondition.mentionId == null;
    }

    @IsAllowed(Operation.READ)
    public static Result issues(String ownerName, String projectName, String state, String format, int pageNum) throws WriteException, IOException {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);

        // SearchCondition from param
        Form<models.support.SearchCondition> issueParamForm = new Form<>(models.support.SearchCondition.class);
        models.support.SearchCondition searchCondition = issueParamForm.bindFromRequest().get();
        searchCondition.pageNum = pageNum - 1;
        searchCondition.labelIds.addAll(LabelSearchUtil.getLabelIds(request()));
        searchCondition.labelIds.remove(null);

        // determine pjax or json when requested with XHR
        if (HttpUtil.isRequestedWithXHR(request())) {
            format = HttpUtil.isPJAXRequest(request()) ? "pjax" : "json";
        }

        Integer itemsPerPage = getItemsPerPage();
        ExpressionList<Issue> el = searchCondition.asExpressionList(project);
        Page<Issue> issues = el.findPagingList(itemsPerPage).getPage(searchCondition.pageNum);

        switch(format){
            case EXCEL_EXT:
                return issuesAsExcel(project, el);

            case "pjax":
                return issuesAsPjax(project, issues, searchCondition);

            case "json":
                return issuesAsJson(project, issues);

            case "html":
            default:
                return issuesAsHTML(project, issues, searchCondition);
        }
    }

    private static Integer getItemsPerPage(){
        Integer itemsPerPage = ITEMS_PER_PAGE;
        String amountStr = request().getQueryString("itemsPerPage");

        if(amountStr != null){ // or amount from query string
            try {
                itemsPerPage = Integer.parseInt(amountStr);
            } catch (NumberFormatException ignored){}
        }

        return Math.min(itemsPerPage, ITEMS_PER_PAGE_MAX);
    }

    private static Result issuesAsHTML(Project project, Page<Issue> issues, models.support.SearchCondition searchCondition){
        if(project == null){
            return ok(my_list.render("title.issueList", issues, searchCondition, project));
        } else {
            return ok(list.render("title.issueList", issues, searchCondition, project));
        }

    }

    private static Result issuesAsExcel(Project project, ExpressionList<Issue> el) throws WriteException, IOException {
        byte[] excelData = Issue.excelFrom(el.findList());
        String filename = HttpUtil.encodeContentDisposition(
                project.name + "_issues_" + JodaDateUtil.today().getTime() + "." + EXCEL_EXT);

        response().setHeader("Content-Type", new Tika().detect(filename));
        response().setHeader("Content-Disposition", "attachment; " + filename);

        return ok(excelData);
    }

    private static Result issuesAsPjax(Project project, Page<Issue> issues, models.support.SearchCondition searchCondition) {
        response().setHeader("Cache-Control", "no-cache, no-store");
        if (project == null) {
            return ok(my_partial_search.render("title.issueList", issues, searchCondition, project));
        } else {
            return ok(partial_list_wrap.render("title.issueList", issues, searchCondition, project));
        }

    }

    private static Result issuesAsJson(Project project, Page<Issue> issues) {
        ObjectNode listData = Json.newObject();

        String exceptIdStr = request().getQueryString("exceptId");
        Long exceptId = -1L;

        if(!StringUtils.isEmpty(exceptIdStr)){
            try {
                exceptId = Long.parseLong(exceptIdStr);
            } catch(Exception e){
                return badRequest(listData);
            }
        }

        List<Issue> issueList = issues.getList();

        for (Issue issue : issueList){
            Long issueId = issue.getNumber();

            if(issueId.equals(exceptId)){
                continue;
            }

            ObjectNode result = Json.newObject();
            result.put("id", issueId);
            result.put("title", issue.title);
            result.put("state", issue.state.toString());
            result.put("createdDate", issue.createdDate.toString());
            result.put("link", routes.IssueApp.issue(project.owner, project.name, issueId).toString());
            listData.put(issue.id.toString(), result);
        }

        return ok(listData);
    }

    @With(NullProjectCheckAction.class)
    public static Result issue(String ownerName, String projectName, Long number) {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);

        Issue issueInfo = Issue.findByNumber(project, number);

        response().setHeader("Vary", "Accept");

        if (issueInfo == null) {
            if (HttpUtil.isJSONPreferred(request())){
                ObjectNode result = Json.newObject();
                result.put("title", number);
                result.put("body", Messages.get("error.notfound.issue_post"));
                return ok(result);
            } else {
                return notFound(ErrorViews.NotFound.render("error.notfound", project, ResourceType.ISSUE_POST.resource()));
            }
        }

        if (!AccessControl.isAllowed(UserApp.currentUser(), issueInfo.asResource(), Operation.READ)) {
            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
        }

        for (IssueLabel label: issueInfo.labels) {
            label.refresh();
        }

        Form<Comment> commentForm = new Form<>(Comment.class);
        Form<Issue> editForm = new Form<>(Issue.class).fill(Issue.findByNumber(project, number));
        UserApp.currentUser().visits(project);
        // Determine response type with Accept header
        if (HttpUtil.isJSONPreferred(request())){
            ObjectNode result = Json.newObject();
            result.put("id", issueInfo.getNumber());
            result.put("title", issueInfo.title);
            result.put("state", issueInfo.state.toString());
            result.put("body", StringUtils.abbreviate(issueInfo.body, 1000));
            result.put("createdDate", issueInfo.createdDate.toString());
            result.put("link", routes.IssueApp.issue(project.owner, project.name, issueInfo.getNumber()).toString());
            return ok(result);
        } else {
            return ok(view.render("title.issueDetail", issueInfo, editForm, commentForm, project));
        }
    }

    @IsAllowed(resourceType = ResourceType.ISSUE_POST, value = Operation.READ)
    public static Result timeline(String ownerName, String projectName, Long number) {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
        Issue issueInfo = Issue.findByNumber(project, number);

        for (IssueLabel label: issueInfo.labels) {
            label.refresh();
        }

        return ok(partial_comments.render(project, issueInfo));
    }

    @With(AnonymousCheckAction.class)
    @IsCreatable(ResourceType.ISSUE_POST)
    public static Result newIssueForm(String ownerName, String projectName) {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
        return ok(create.render("title.newIssue", new Form<>(Issue.class), project));
    }

    @Transactional
    @With(NullProjectCheckAction.class)
    public static Result massUpdate(String ownerName, String projectName) {
        Form<IssueMassUpdate> issueMassUpdateForm
                = new Form<>(IssueMassUpdate.class).bindFromRequest();
        if (issueMassUpdateForm.hasErrors()) {
            return badRequest(issueMassUpdateForm.errorsAsJson());
        }
        IssueMassUpdate issueMassUpdate = issueMassUpdateForm.get();

        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);

        int updatedItems = 0;
        int rejectedByPermission = 0;

        for (Issue issue : issueMassUpdate.issues) {
            issue.refresh();
            if (issueMassUpdate.delete) {
                if (AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(),
                        Operation.DELETE)) {
                    issue.delete();
                    continue;
                } else {
                    rejectedByPermission++;
                    continue;
                }
            }

            if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(),
                    Operation.UPDATE)) {
                rejectedByPermission++;
                continue;
            }

            boolean assigneeChanged = false;
            User oldAssignee = null;
            if (issueMassUpdate.assignee != null) {
                if(hasAssignee(issue)) {
                    oldAssignee = issue.assignee.user;
                }
                Assignee newAssignee = null;
                if (issueMassUpdate.assignee.isAnonymous()) {
                    newAssignee = null;
                } else {
                    newAssignee = Assignee.add(issueMassUpdate.assignee.id, project.id);
                }
                assigneeChanged = !issue.assignedUserEquals(newAssignee);
                issue.assignee = newAssignee;
            }

            boolean stateChanged = false;
            State oldState = null;
            if ((issueMassUpdate.state != null) && (issue.state != issueMassUpdate.state)) {
                stateChanged = true;
                oldState = issue.state;
                issue.state = issueMassUpdate.state;
            }

            if (issueMassUpdate.milestone != null) {
                if(issueMassUpdate.milestone.isNullMilestone()) {
                    issue.milestone = null;
                } else {
                    issue.milestone = issueMassUpdate.milestone;
                }
            }

            if (issueMassUpdate.attachingLabelIds != null) {
                for (Long labelId : issueMassUpdate.attachingLabelIds) {
                    issue.labels.add(IssueLabel.finder.byId(labelId));
                }
            }

            if (issueMassUpdate.detachingLabelIds != null) {
                for (Long labelId : issueMassUpdate.detachingLabelIds) {
                    issue.labels.remove(IssueLabel.finder.byId(labelId));
                }
            }

            if (issueMassUpdate.isDueDateChanged) {
                issue.dueDate = JodaDateUtil.lastSecondOfDay(issueMassUpdate.dueDate);
            }

            issue.updatedDate = JodaDateUtil.now();
            issue.update();
            updatedItems++;

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

        if (updatedItems == 0 && rejectedByPermission > 0) {
            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
        }

        // Determine type of response with Accept header
        if (HttpUtil.isJSONPreferred(request())){
            // jQuery treats as error if response text empty
            // on dataType is json
            return ok("{}");
        } else {
            return redirect(request().getHeader("Referer"));
        }
    }

    @Transactional
    @IsCreatable(ResourceType.ISSUE_POST)
    public static Result newIssue(String ownerName, String projectName) {
        Form<Issue> issueForm = new Form<>(Issue.class).bindFromRequest();
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);

        if (issueForm.hasErrors()) {
            return badRequest(create.render("error.validation", issueForm, project));
        }

        final Issue newIssue = issueForm.get();
        removeAnonymousAssignee(newIssue);

        if (newIssue.body == null) {
            return status(REQUEST_ENTITY_TOO_LARGE,
                    ErrorViews.RequestTextEntityTooLarge.render());
        }

        newIssue.createdDate = JodaDateUtil.now();
        newIssue.updatedDate = JodaDateUtil.now();
        newIssue.setAuthor(UserApp.currentUser());
        newIssue.project = project;

        newIssue.state = State.OPEN;

        addLabels(newIssue, request());
        setMilestone(issueForm, newIssue);

        newIssue.dueDate = JodaDateUtil.lastSecondOfDay(newIssue.dueDate);
        newIssue.save();

        attachUploadFilesToPost(newIssue.asResource());

        NotificationEvent.afterNewIssue(newIssue);

        return redirect(routes.IssueApp.issue(project.owner, project.name, newIssue.getNumber()));
    }

    private static void removeAnonymousAssignee(Issue issue) {
        if(hasAssignee(issue) && isAnonymousAssignee(issue)) {
            issue.assignee = null;
        }
    }

    private static boolean isAnonymousAssignee(Issue issue) {
        return issue.assignee.user != null && issue.assignee.user.isAnonymous();
    }

    private static boolean hasAssignee(Issue issue) {
        return issue.assignee != null;
    }

    @With(NullProjectCheckAction.class)
    public static Result editIssueForm(String ownerName, String projectName, Long number) {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
        Issue issue = Issue.findByNumber(project, number);

        if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
        }

        Form<Issue> editForm = new Form<>(Issue.class).fill(issue);

        return ok(edit.render("title.editIssue", editForm, issue, project));
    }

    @Transactional
    @IsAllowed(value = Operation.UPDATE, resourceType = ResourceType.ISSUE_POST)
    public static Result nextState(String ownerName, String projectName, Long number) {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);

        final Issue issue = Issue.findByNumber(project, number);

        Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number);
        issue.toNextState();
        NotificationEvent notiEvent = NotificationEvent.afterStateChanged(issue.previousState(), issue);
        IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
        return redirect(redirectTo);
    }

    private static void addAssigneeChangedNotification(Issue modifiedIssue, Issue originalIssue) {
        if(!originalIssue.assignedUserEquals(modifiedIssue.assignee)) {
            User oldAssignee = null;
            if(hasAssignee(originalIssue)) {
                oldAssignee = originalIssue.assignee.user;
            }
            NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, modifiedIssue);
            IssueEvent.addFromNotificationEvent(notiEvent, modifiedIssue, UserApp.currentUser().loginId);
        }
    }

    private static void addStateChangedNotification(Issue modifiedIssue, Issue originalIssue) {
        if(modifiedIssue.state != originalIssue.state) {
            NotificationEvent notiEvent = NotificationEvent.afterStateChanged(originalIssue.state, modifiedIssue);
            IssueEvent.addFromNotificationEvent(notiEvent, modifiedIssue, UserApp.currentUser().loginId);
        }
    }

    private static void addBodyChangedNotification(Issue modifiedIssue, Issue originalIssue) {
        if (!modifiedIssue.body.equals(originalIssue.body)) {
            NotificationEvent notiEvent = NotificationEvent.afterIssueBodyChanged(originalIssue.body, modifiedIssue);
            IssueEvent.addFromNotificationEvent(notiEvent, modifiedIssue, UserApp.currentUser().loginId);
        }
    }

    @With(NullProjectCheckAction.class)
    public static Result editIssue(String ownerName, String projectName, Long number) {
        Form<Issue> issueForm = new Form<>(Issue.class).bindFromRequest();

        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);

        if (issueForm.hasErrors()) {
            return badRequest(edit.render("error.validation", issueForm, Issue.findByNumber(project, number), project));
        }

        final Issue issue = issueForm.get();
        removeAnonymousAssignee(issue);
        setMilestone(issueForm, issue);
        issue.dueDate = JodaDateUtil.lastSecondOfDay(issue.dueDate);

        final Issue originalIssue = Issue.findByNumber(project, number);

        Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number);

        // preUpdateHook.run would be called just before this issue is updated.
        // It updates some properties only for issues, such as assignee or labels, but not for non-issues.
        Runnable preUpdateHook = new Runnable() {
            @Override
            public void run() {
                // Below addAll() method is needed to avoid the exception, 'Timeout trying to lock table ISSUE'.
                // This is just workaround and the cause of the exception is not figured out yet.
                // Do not replace it to 'issue.comments = originalIssue.comments;'
                issue.voters.addAll(originalIssue.voters);
                issue.comments = originalIssue.comments;
                addLabels(issue, request());

                addAssigneeChangedNotification(issue, originalIssue);
                addStateChangedNotification(issue, originalIssue);
                addBodyChangedNotification(issue, originalIssue);
            }
        };

        return editPosting(originalIssue, issue, issueForm, redirectTo, preUpdateHook);
    }

    private static void setMilestone(Form<Issue> issueForm, Issue issue) {
        String milestoneId = issueForm.data().get("milestoneId");
        if(milestoneId != null && !milestoneId.isEmpty()) {
            issue.milestone = Milestone.findById(Long.parseLong(milestoneId));
        } else {
            issue.milestone = null;
        }
    }

    /**
     * @ see {@link AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, Call)}
     */
    @Transactional
    @With(NullProjectCheckAction.class)
    public static Result deleteIssue(String ownerName, String projectName, Long number) {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
        Issue issue = Issue.findByNumber(project, number);
        if(!issue.canBeDeleted()) {
            return badRequest(ErrorViews.BadRequest.render());
        }
        Call redirectTo =
            routes.IssueApp.issues(project.owner, project.name, State.OPEN.state(), "html", 1);

        return delete(issue, issue.asResource(), redirectTo);
    }

    /**
     * @see {@link AbstractPostingApp#newComment(models.Comment, play.data.Form}
     */
    @Transactional
    @With(NullProjectCheckAction.class)
    public static Result newComment(String ownerName, String projectName, Long number) throws IOException {
        Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
        final Issue issue = Issue.findByNumber(project, number);
        Call redirectTo = routes.IssueApp.issue(project.owner, project.name, number);
        Form<IssueComment> commentForm = new Form<>(IssueComment.class).bindFromRequest();

        if (!AccessControl.isResourceCreatable(
                    UserApp.currentUser(), issue.asResource(), ResourceType.ISSUE_COMMENT)) {
            return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
        }

        if (commentForm.hasErrors()) {
            return badRequest(commentFormValidationResult(project, commentForm));
        }

        if( containsStateTransitionRequest() ){
            toNextState(number, project);
            IssueEvent.addFromNotificationEvent(
                    NotificationEvent.afterStateChanged(issue.previousState(), issue),
                    issue, UserApp.currentUser().loginId);
        }

        final IssueComment comment = commentForm.get();

        IssueComment existingComment = IssueComment.find.where().eq("id", comment.id).findUnique();
        if( existingComment != null){
            existingComment.contents = comment.contents;
            return saveComment(existingComment, commentForm, redirectTo, getContainerUpdater(issue, comment));
        } else {
            return saveComment(comment, commentForm, redirectTo, getContainerUpdater(issue, comment));
        }
    }

    private static Runnable getContainerUpdater(final Issue issue, final IssueComment comment) {
        return new Runnable() {
            @Override
            public void run() {
                comment.issue = issue;
            }
        };
    }

    private static void toNextState(Long number, Project project) {
        final Issue issue = Issue.findByNumber(project, number);
        issue.toNextState();
    }

    private static boolean containsStateTransitionRequest() {

        if (!isMultipartForm() || getStateTransitionFormValue() == null){
            return false;
        }

        return StringUtils.isNotBlank(getStateTransitionFormValue()[0]);
    }

    private static String[] getStateTransitionFormValue() {
        return request().body().asMultipartFormData().asFormUrlEncoded().get("withStateTransition");
    }

    private static boolean isMultipartForm() {
        return request().body().asMultipartFormData() != null;
    }

    private static Html commentFormValidationResult(Project project, Form<IssueComment> commentForm) {
        Map<String,List<ValidationError>> errors = commentForm.errors();
        if( errors.get("contents") != null ){
            return ErrorViews.BadRequest.render("post.comment.empty", project);
        } else {
            return ErrorViews.BadRequest.render("error.validation", project);
        }
    }


    /**
     * @see {@link AbstractPostingApp#delete(play.db.ebean.Model, models.resource.Resource, Call)}
     */
    @Transactional
    @With(NullProjectCheckAction.class)
    public static Result deleteComment(String ownerName, String projectName, Long issueNumber,
            Long commentId) {
        Comment comment = IssueComment.find.byId(commentId);
        Project project = comment.asResource().getProject();
        Call redirectTo =
            routes.IssueApp.issue(project.owner, project.name, issueNumber);

        return delete(comment, comment.asResource(), redirectTo);
    }

    private static void addLabels(Issue issue, Http.Request request) {
        if (issue.labels == null) {
            issue.labels = new HashSet<>();
        }

        Http.MultipartFormData multipart = request.body().asMultipartFormData();
        Map<String, String[]> form;
        if (multipart != null) {
            form = multipart.asFormUrlEncoded();
        } else {
            form = request.body().asFormUrlEncoded();
        }
        String[] labelIds = form.get("labelIds");
        if (labelIds != null) {
            for (String labelId : labelIds) {
                if(!StringUtils.isEmpty(labelId)) {
                    issue.labels.add(IssueLabel.finder.byId(Long.parseLong(labelId)));
                }
            }
        }
    }
}
TOP

Related Classes of controllers.IssueApp

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.