IDEA-74652 Time tracking features. Integration with youtrack and jira.

This commit is contained in:
Evgeny Zakrevsky
2012-12-27 20:53:42 +04:00
parent 1d3c3ef6b5
commit 77fc4c5626
20 changed files with 293 additions and 92 deletions
@@ -362,4 +362,9 @@ public class GitHubRepository extends BaseRepositoryImpl {
if (getRepoName() != null ? !getRepoName().equals(that.getRepoName()) : that.getRepoName() != null) return false;
return true;
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -52,9 +52,4 @@ public class GitHubRepositoryType extends BaseRepositoryType<GitHubRepository> {
Consumer<GitHubRepository> changeListener) {
return new GitHubRepositoryEditor(project, repository, changeListener);
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -4,6 +4,7 @@ import com.atlassian.connector.commons.jira.soap.axis.JiraSoapService;
import com.atlassian.connector.commons.jira.soap.axis.JiraSoapServiceServiceLocator;
import com.atlassian.theplugin.jira.api.JIRAIssueBean;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.tasks.LocalTask;
import com.intellij.tasks.Task;
import com.intellij.tasks.TaskRepository;
import com.intellij.tasks.TaskState;
@@ -17,6 +18,7 @@ import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
@@ -211,4 +213,30 @@ public class JiraRepository extends BaseRepositoryImpl {
return null;
}
}
@Override
public void updateTimeSpent(final LocalTask task, final int timeSpent, final String comment) throws Exception {
final HttpClient client = login();
checkVersion(client);
PostMethod method = new PostMethod(getUrl() + "/rest/api/2/issue/" + task.getId() + "/worklog");
method.setRequestEntity(new StringRequestEntity("{\"timeSpentSeconds\" : " + String.valueOf(timeSpent * 60) +
", \"comment\" : " + comment + "}", "application/json", "UTF-8"));
client.executeMethod(method);
if (method.getStatusCode() != 201) {
throw new Exception(method.getResponseBodyAsString());
}
}
private void checkVersion(final HttpClient client) throws Exception {
GetMethod method = new GetMethod(getUrl() + "/rest/api/2/project");
client.executeMethod(method);
if (method.getStatusCode() != 200) {
throw new Exception("This version of JIRA doesn't have support REST API for working with worklog items.");
}
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION | TIME_MANAGEMENT;
}
}
@@ -34,11 +34,6 @@ public class JiraRepositoryType extends BaseRepositoryType<JiraRepository> {
public Class<JiraRepository> getRepositoryClass() {
return JiraRepository.class;
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
//
//@Override
//public EnumSet<TaskState> getPossibleTaskStates() {
@@ -36,6 +36,10 @@ import java.util.concurrent.Callable;
*/
@Tag("server")
public abstract class TaskRepository {
protected static final int NO_FEATURES = 0;
public static final int BASIC_HTTP_AUTHORIZATION = 0x0001;
public static final int LOGIN_ANONYMOUSLY = 0x0002;
public static final int TIME_MANAGEMENT = 0x0004;
@Attribute("url")
public String getUrl() {
@@ -190,6 +194,10 @@ public abstract class TaskRepository {
return "{id} (e.g. FOO-001), {summary}, {number} (e.g. 001), {project} (e.g. FOO)";
}
public void updateTimeSpent(final LocalTask task, final int timeSpent, final String comment) throws Exception {
throw new UnsupportedOperationException();
}
public abstract static class CancellableConnection implements Callable<Exception> {
@Nullable
@@ -209,4 +217,11 @@ public abstract class TaskRepository {
public abstract void cancel();
}
public boolean isSupported(int feature) {
return (getFeatures() & feature) != 0;
}
protected int getFeatures() {
return NO_FEATURES;
}
}
@@ -33,11 +33,6 @@ public abstract class TaskRepositoryType<T extends TaskRepository> {
public static final ExtensionPointName<TaskRepositoryType> EP_NAME = new ExtensionPointName<TaskRepositoryType>("com.intellij.tasks.repositoryType");
protected static final int NO_FEATURES = 0;
public static final int BASIC_HTTP_AUTHORIZATION = 0x0001;
public static final int LOGIN_ANONYMOUSLY = 0x0002;
@NotNull
public abstract String getName();
@@ -52,15 +47,7 @@ public abstract class TaskRepositoryType<T extends TaskRepository> {
public abstract Class<T> getRepositoryClass();
public boolean isSupported(int feature) {
return (getFeatures() & feature) != 0;
}
public EnumSet<TaskState> getPossibleTaskStates() {
return EnumSet.noneOf(TaskState.class);
}
protected int getFeatures() {
return NO_FEATURES;
}
}
@@ -21,7 +21,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.project.Project;
import com.intellij.tasks.TaskManager;
import com.intellij.tasks.TaskRepositoryType;
import com.intellij.tasks.TaskRepository;
import com.intellij.tasks.impl.BaseRepository;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.PanelWithAnchor;
@@ -99,9 +99,9 @@ public class BaseRepositoryEditor<T extends BaseRepository> extends TaskReposito
myUseProxy.setSelected(repository.isUseProxy());
myUseHttpAuthenticationCheckBox.setSelected(repository.isUseHttpAuthentication());
myUseHttpAuthenticationCheckBox.setVisible(repository.getRepositoryType().isSupported(TaskRepositoryType.BASIC_HTTP_AUTHORIZATION));
myUseHttpAuthenticationCheckBox.setVisible(repository.isSupported(TaskRepository.BASIC_HTTP_AUTHORIZATION));
myLoginAnonymouslyJBCheckBox.setVisible(repository.getRepositoryType().isSupported(TaskRepositoryType.LOGIN_ANONYMOUSLY));
myLoginAnonymouslyJBCheckBox.setVisible(repository.isSupported(TaskRepository.LOGIN_ANONYMOUSLY));
myLoginAnonymouslyJBCheckBox.setSelected(repository.isLoginAnonymously());
myLoginAnonymouslyJBCheckBox.addActionListener(new ActionListener() {
@Override
@@ -331,4 +331,9 @@ public class GenericRepository extends BaseRepositoryImpl {
protected String getTasksListURLDefault() {
return "";
}
@Override
protected int getFeatures() {
return LOGIN_ANONYMOUSLY | BASIC_HTTP_AUTHORIZATION;
}
}
@@ -45,9 +45,4 @@ public class GenericRepositoryType extends BaseRepositoryType<GenericRepository>
final Consumer<GenericRepository> changeListener) {
return new GenericRepositoryEditor<GenericRepository>(project, repository, changeListener);
}
@Override
protected int getFeatures() {
return LOGIN_ANONYMOUSLY | BASIC_HTTP_AUTHORIZATION;
}
}
@@ -79,4 +79,9 @@ public class AssemblaRepository extends GenericRepository {
public boolean isConfigured() {
return super.isConfigured() && StringUtil.isNotEmpty(getUsername()) && StringUtil.isNotEmpty(getPassword());
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -45,9 +45,4 @@ public class AssemblaRepositoryType extends TaskRepositoryType<AssemblaRepositor
public Class<AssemblaRepository> getRepositoryClass() {
return AssemblaRepository.class;
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -361,4 +361,9 @@ public class PivotalTrackerRepository extends BaseRepositoryImpl {
if (getCommitMessageFormat() != null ? !getCommitMessageFormat().equals(that.getCommitMessageFormat()) : that.getCommitMessageFormat() != null) return false;
return isShouldFormatCommitMessage() == that.isShouldFormatCommitMessage();
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -52,9 +52,4 @@ public class PivotalTrackerRepositoryType extends BaseRepositoryType<PivotalTrac
Consumer<PivotalTrackerRepository> changeListener) {
return new PivotalTrackerRepositoryEditor(project, repository, changeListener);
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -306,4 +306,9 @@ public class RedmineRepository extends BaseRepositoryImpl {
"/projects" +
(!StringUtil.isEmpty(getProjectId()) ? "/" + getProjectId() : "");
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -45,9 +45,4 @@ public class RedmineRepositoryType extends BaseRepositoryType<RedmineRepository>
Consumer<RedmineRepository> changeListener) {
return new RedmineRepositoryEditor(project, repository, changeListener);
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -258,4 +258,9 @@ public class TracRepository extends BaseRepositoryImpl {
public boolean equals(Object o) {
return super.equals(o) && Comparing.equal(((TracRepository)o).getDefaultSearch(), getDefaultSearch());
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
}
@@ -53,11 +53,6 @@ public class TracRepositoryType extends BaseRepositoryType<TracRepository> {
return TracRepository.class;
}
@Override
protected int getFeatures() {
return BASIC_HTTP_AUTHORIZATION;
}
@NotNull
@Override
public TaskRepositoryEditor createEditor(TracRepository repository,
@@ -8,6 +8,7 @@ import com.intellij.tasks.impl.BaseRepository;
import com.intellij.tasks.impl.BaseRepositoryImpl;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.VersionComparatorUtil;
import com.intellij.util.xmlb.annotations.Tag;
import icons.TasksIcons;
import org.apache.axis.utils.XMLChar;
@@ -37,7 +38,9 @@ public class YouTrackRepository extends BaseRepositoryImpl {
private String myDefaultSearch = "for: me sort by: updated #Unresolved";
/** for serialization */
/**
* for serialization
*/
@SuppressWarnings({"UnusedDeclaration"})
public YouTrackRepository() {
}
@@ -277,4 +280,30 @@ public class YouTrackRepository extends BaseRepositoryImpl {
}
private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.youtrack.YouTrackRepository");
@Override
public void updateTimeSpent(final LocalTask task, final int timeSpent, final String comment) throws Exception {
checkVersion();
final HttpMethod method = doREST("/rest/issue/execute/" + task.getId() + "?command=work+Today+" + timeSpent + "m+" + comment, true);
if (method.getStatusCode() != 200) {
InputStream stream = method.getResponseBodyAsStream();
String message = new SAXBuilder(false).build(stream).getRootElement().getText();
throw new Exception(message);
}
}
private void checkVersion() throws Exception {
HttpMethod method = doREST("/rest/workflow/version", false);
InputStream stream = method.getResponseBodyAsStream();
Element element = new SAXBuilder(false).build(stream).getRootElement();
final boolean timeTrackingAvailable = element.getName().equals("version") && VersionComparatorUtil.compare(element.getChildText("version"), "4.1") >= 0;
if (!timeTrackingAvailable) {
throw new Exception("This version of Youtrack the time tracking is not supported");
}
}
@Override
protected int getFeatures() {
return TIME_MANAGEMENT;
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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 com.intellij.tasks.timeTracking;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.tasks.LocalTask;
import com.intellij.tasks.TaskRepository;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* User: evgeny.zakrevsky
* Date: 12/26/12
*/
public class SendTimeTrackingInformationDialog extends DialogWrapper {
private final static Logger LOG = Logger.getInstance("#com.intellij.tasks.timeTracking.TasksToolWindowPanel");
public static final Pattern PATTERN = Pattern.compile("([0-9]+)d ([0-9]+)h ([0-9]+)m");
@Nullable private final Project myProject;
private final LocalTask myTask;
private JTextField myTimeSpentField;
private JTextArea myCommentField;
protected SendTimeTrackingInformationDialog(@Nullable final Project project, final LocalTask localTask) {
super(project);
myProject = project;
myTask = localTask;
setTitle("Time Tracking");
init();
}
@Nullable
@Override
protected JComponent createCenterPanel() {
myTimeSpentField = new JTextField(String.valueOf(formatDuration(myTask.getTimeSpent())));
myCommentField = new JTextArea();
return FormBuilder.createFormBuilder()
.addComponent(new JLabel("Send information about activity on " + myTask.getPresentableName()))
.addLabeledComponent("Time spent:", myTimeSpentField, UIUtil.LARGE_VGAP)
.addLabeledComponent("Comment", ScrollPaneFactory.createScrollPane(myCommentField)).getPanel();
}
private static String formatDuration(final long milliseconds) {
final int second = 1000;
final int minute = 60 * second;
final int hour = 60 * minute;
final int day = 24 * hour;
final int days = (int)(milliseconds / day);
final int hours = (int)(milliseconds % day / hour);
final int minutes = (int)(milliseconds % hour / minute);
String daysString = days + "d ";
String hoursString = hours + "h ";
String minutesString = minutes + "m";
return daysString + hoursString + minutesString;
}
@Override
protected void doOKAction() {
final Matcher matcher = PATTERN.matcher(myTimeSpentField.getText());
if (matcher.matches()) {
final int timeSpent = Integer.valueOf(matcher.group(1)) * 24 * 60 + Integer.valueOf(matcher.group(2)) * 60 + Integer.valueOf(
matcher.group(3));
final TaskRepository repository = myTask.getRepository();
if (repository != null &&
repository.isSupported(TaskRepository.TIME_MANAGEMENT)) {
try {
repository.updateTimeSpent(myTask, timeSpent, myCommentField.getText());
}
catch (Exception e1) {
Messages
.showErrorDialog(myProject, "<html>Could not send information for " + myTask.getPresentableName() + "<br/>" + e1.getMessage(),
"Error");
LOG.warn(e1);
}
}
}
super.doOKAction();
}
@Nullable
@Override
protected ValidationInfo doValidate() {
if (!PATTERN.matcher(myTimeSpentField.getText()).matches()) return new ValidationInfo("Time Spent has broken format");
return null;
}
@Nullable
@Override
protected String getDimensionServiceKey() {
return "com.intellij.tasks.timeTracking.TasksToolWindowPanel";
}
}
@@ -8,9 +8,7 @@ import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.IconLoader;
import com.intellij.tasks.LocalTask;
import com.intellij.tasks.TaskListenerAdapter;
import com.intellij.tasks.TaskManager;
import com.intellij.tasks.*;
import com.intellij.tasks.actions.GotoTaskAction;
import com.intellij.tasks.actions.SwitchTaskAction;
import com.intellij.ui.LayeredIcon;
@@ -136,47 +134,25 @@ public class TasksToolWindowPanel extends SimpleToolWindowPanel implements Dispo
updateTable();
}
});
group.add(new ToggleAction("Auto mode", "Automatic starting and stopping of timer", TasksIcons.AutoMode) {
@Override
public boolean isSelected(final AnActionEvent e) {
return myTimeTrackingManager.getState().autoMode;
}
@Override
public void setSelected(final AnActionEvent e, final boolean state) {
myTimeTrackingManager.setAutoMode(state);
updateTable();
}
});
group.add(new AnAction() {
@Override
public void update(final AnActionEvent e) {
if (myTimeTrackingManager.getState().autoMode) {
e.getPresentation().setEnabled(false);
e.getPresentation().setIcon(TasksIcons.StartTimer);
e.getPresentation().setText("Start timer for active task");
}
else {
e.getPresentation().setEnabled(true);
if (myTaskManager.getActiveTask().isRunning()) {
e.getPresentation().setIcon(TasksIcons.StopTimer);
e.getPresentation().setText("Stop timer for active task");
}
else {
e.getPresentation().setIcon(TasksIcons.StartTimer);
e.getPresentation().setText("Start timer for active task");
}
}
}
group.add(new ModeToggleAction());
group.add(new StartStopAction());
group.add(new AnAction("Post work item to bugtracker", "Post work item to bugtracker", AllIcons.Actions.Export) {
@Override
public void actionPerformed(final AnActionEvent e) {
final LocalTask activeTask = myTaskManager.getActiveTask();
if (activeTask.isRunning()) {
activeTask.setRunning(false);
final LocalTask localTask = myTable.getSelectedObject();
if (localTask == null) return;
new SendTimeTrackingInformationDialog(myProject, localTask).show();
}
@Override
public void update(final AnActionEvent e) {
final LocalTask localTask = myTable.getSelectedObject();
if (localTask == null) {
e.getPresentation().setEnabled(false);
}
else {
activeTask.setRunning(true);
final TaskRepository repository = localTask.getRepository();
e.getPresentation().setEnabled(repository != null && repository.isSupported(TaskRepository.TIME_MANAGEMENT));
}
}
});
@@ -303,4 +279,54 @@ public class TasksToolWindowPanel extends SimpleToolWindowPanel implements Dispo
myTimer.stop();
myTimer = null;
}
private class StartStopAction extends AnAction {
@Override
public void update(final AnActionEvent e) {
if (myTimeTrackingManager.getState().autoMode) {
e.getPresentation().setEnabled(false);
e.getPresentation().setIcon(TasksIcons.StartTimer);
e.getPresentation().setText("Start timer for active task");
}
else {
e.getPresentation().setEnabled(true);
if (myTaskManager.getActiveTask().isRunning()) {
e.getPresentation().setIcon(TasksIcons.StopTimer);
e.getPresentation().setText("Stop timer for active task");
}
else {
e.getPresentation().setIcon(TasksIcons.StartTimer);
e.getPresentation().setText("Start timer for active task");
}
}
}
@Override
public void actionPerformed(final AnActionEvent e) {
final LocalTask activeTask = myTaskManager.getActiveTask();
if (activeTask.isRunning()) {
activeTask.setRunning(false);
}
else {
activeTask.setRunning(true);
}
}
}
private class ModeToggleAction extends ToggleAction {
public ModeToggleAction() {
super("Auto mode", "Automatic starting and stopping of timer", TasksIcons.AutoMode);
}
@Override
public boolean isSelected(final AnActionEvent e) {
return myTimeTrackingManager.getState().autoMode;
}
@Override
public void setSelected(final AnActionEvent e, final boolean state) {
myTimeTrackingManager.setAutoMode(state);
updateTable();
}
}
}