TSK-1275: Support select and claim in one API call
This commit is contained in:
parent
135d78ff54
commit
1dfe54c0b6
|
@ -325,6 +325,21 @@ public interface TaskService {
|
|||
void forceDeleteTask(String taskId)
|
||||
throws TaskNotFoundException, InvalidStateException, NotAuthorizedException;
|
||||
|
||||
/**
|
||||
* Selects and claims the first task which is returned by the task query.
|
||||
*
|
||||
* @param taskQuery the task query.
|
||||
* @return the task that got selected and claimed
|
||||
* @throws TaskNotFoundException if the task with taskId was not found
|
||||
* @throws InvalidStateException if the state of the task with taskId is not READY
|
||||
* @throws InvalidOwnerException if the task with taskId is claimed by someone else
|
||||
* @throws NotAuthorizedException if the current user has no read permission for the
|
||||
* workbasket the task is in
|
||||
*/
|
||||
Task selectAndClaim(TaskQuery taskQuery)
|
||||
throws TaskNotFoundException, NotAuthorizedException, InvalidStateException,
|
||||
InvalidOwnerException;
|
||||
|
||||
/**
|
||||
* Deletes a list of tasks.
|
||||
*
|
||||
|
|
|
@ -149,6 +149,7 @@ public class TaskQueryImpl implements TaskQuery {
|
|||
private List<String> orderColumns;
|
||||
private WildcardSearchField[] wildcardSearchFieldIn;
|
||||
private String wildcardSearchValueLike;
|
||||
private boolean selectAndClaim;
|
||||
|
||||
private boolean useDistinctKeyword = false;
|
||||
private boolean joinWithAttachments = false;
|
||||
|
@ -449,6 +450,11 @@ public class TaskQueryImpl implements TaskQuery {
|
|||
return this;
|
||||
}
|
||||
|
||||
public TaskQuery selectAndClaimEquals(boolean selectAndClaim) {
|
||||
this.selectAndClaim = selectAndClaim;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskQuery parentBusinessProcessIdIn(String... parentBusinessProcessIds) {
|
||||
this.parentBusinessProcessIdIn = parentBusinessProcessIds;
|
||||
|
@ -1105,7 +1111,11 @@ public class TaskQueryImpl implements TaskQuery {
|
|||
}
|
||||
|
||||
public String getLinkToMapperScript() {
|
||||
return DB.DB2.dbProductId.equals(getDatabaseId()) ? LINK_TO_MAPPER_DB2 : LINK_TO_MAPPER;
|
||||
if (DB.DB2.dbProductId.equals(getDatabaseId()) && !selectAndClaim) {
|
||||
return LINK_TO_MAPPER_DB2;
|
||||
} else {
|
||||
return LINK_TO_MAPPER;
|
||||
}
|
||||
}
|
||||
|
||||
public String getLinkToCounterTaskScript() {
|
||||
|
@ -1206,6 +1216,10 @@ public class TaskQueryImpl implements TaskQuery {
|
|||
return isTransferred;
|
||||
}
|
||||
|
||||
public boolean getIsSelectAndClaim() {
|
||||
return selectAndClaim;
|
||||
}
|
||||
|
||||
public String[] getPorCompanyIn() {
|
||||
return porCompanyIn;
|
||||
}
|
||||
|
@ -1931,6 +1945,8 @@ public class TaskQueryImpl implements TaskQuery {
|
|||
+ wildcardSearchFieldIn
|
||||
+ ", wildcardSearchValueLike="
|
||||
+ wildcardSearchValueLike
|
||||
+ ", selectAndClaim="
|
||||
+ selectAndClaim
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,8 +134,11 @@ public interface TaskQueryMapper {
|
|||
+ "<if test='attachmentReferenceLike != null'>AND (<foreach item='item' collection='attachmentReferenceLike' separator=' OR '>UPPER(a.REF_VALUE) LIKE #{item}</foreach>)</if> "
|
||||
+ "<if test='attachmentReceivedIn !=null'> AND ( <foreach item='item' collection='attachmentReceivedIn' separator=' OR ' > ( <if test='item.begin!=null'> a.RECEIVED >= #{item.begin} </if> <if test='item.begin!=null and item.end!=null'> AND </if><if test='item.end!=null'> a.RECEIVED <=#{item.end} </if>)</foreach>)</if> "
|
||||
+ "<if test='wildcardSearchValueLike != null and wildcardSearchFieldIn != null'>AND (<foreach item='item' collection='wildcardSearchFieldIn' separator=' OR '>t.${item} LIKE #{wildcardSearchValueLike}</foreach>)</if> "
|
||||
+ "<if test='selectAndClaim == true'> AND t.STATE = 'READY' </if>"
|
||||
+ "</where>"
|
||||
+ "<if test='!orderBy.isEmpty()'>ORDER BY <foreach item='item' collection='orderBy' separator=',' >${item}</foreach></if> "
|
||||
+ "<if test='selectAndClaim == true'> FETCH FIRST ROW ONLY FOR UPDATE </if>"
|
||||
+ "<if test=\"_databaseId == 'db2'\">WITH RS USE AND KEEP UPDATE LOCKS </if> "
|
||||
+ "</script>")
|
||||
@Results(
|
||||
value = {
|
||||
|
@ -334,6 +337,7 @@ public interface TaskQueryMapper {
|
|||
+ "</if>"
|
||||
+ "<if test=\"addAttachmentClassificationNameToSelectClauseForOrdering\">"
|
||||
+ ", ACNAME "
|
||||
|
||||
+ "</if>"
|
||||
+ ", FLAG ) "
|
||||
+ "AS "
|
||||
|
@ -376,7 +380,8 @@ public interface TaskQueryMapper {
|
|||
+ "${item}"
|
||||
+ "</foreach>"
|
||||
+ "</if> "
|
||||
+ "with UR "
|
||||
+ "<if test='selectAndClaim == true'>FETCH FIRST ROW ONLY FOR UPDATE WITH RS USE AND KEEP UPDATE LOCKS</if>"
|
||||
+ "<if test='selectAndClaim == false'> with UR</if>"
|
||||
+ "</script>")
|
||||
@Results(
|
||||
value = {
|
||||
|
|
|
@ -477,6 +477,37 @@ public class TaskServiceImpl implements TaskService {
|
|||
deleteTask(taskId, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task selectAndClaim(TaskQuery taskQuery)
|
||||
throws TaskNotFoundException, NotAuthorizedException, InvalidStateException,
|
||||
InvalidOwnerException {
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("entry to selectAndClaim(taskQuery = {})", taskQuery);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
taskanaEngine.openConnection();
|
||||
|
||||
((TaskQueryImpl) taskQuery).selectAndClaimEquals(true);
|
||||
|
||||
TaskSummary taskSummary = taskQuery.single();
|
||||
|
||||
if (taskSummary == null) {
|
||||
throw new SystemException(
|
||||
"No tasks matched the specified filter and sorting options,"
|
||||
+ " task query returned nothing!");
|
||||
}
|
||||
|
||||
return claim(taskSummary.getId());
|
||||
|
||||
} finally {
|
||||
LOGGER.debug("exit from selectAndClaim()");
|
||||
taskanaEngine.returnConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BulkOperationResults<String, TaskanaException> deleteTasks(List<String> taskIds)
|
||||
throws InvalidArgumentException, NotAuthorizedException {
|
||||
|
|
|
@ -44,6 +44,7 @@ public abstract class AbstractAccTest {
|
|||
}
|
||||
|
||||
public static void resetDb(boolean dropTables) throws SQLException {
|
||||
|
||||
DataSource dataSource = TaskanaEngineTestConfiguration.getDataSource();
|
||||
String schemaName = TaskanaEngineTestConfiguration.getSchemaName();
|
||||
SampleDataGenerator sampleDataGenerator = new SampleDataGenerator(dataSource, schemaName);
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package acceptance.task;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import acceptance.AbstractAccTest;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.security.auth.Subject;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import pro.taskana.common.api.BaseQuery.SortDirection;
|
||||
import pro.taskana.common.api.exceptions.SystemException;
|
||||
import pro.taskana.common.internal.security.JaasExtension;
|
||||
import pro.taskana.common.internal.security.UserPrincipal;
|
||||
import pro.taskana.common.internal.security.WithAccessId;
|
||||
import pro.taskana.common.internal.util.CheckedConsumer;
|
||||
import pro.taskana.task.api.TaskQuery;
|
||||
import pro.taskana.task.api.TaskService;
|
||||
import pro.taskana.task.api.models.Task;
|
||||
|
||||
@ExtendWith(JaasExtension.class)
|
||||
class SelectAndClaimTaskAccTest extends AbstractAccTest {
|
||||
|
||||
@Test
|
||||
void should_claimDifferentTasks_For_ConcurrentSelectAndClaimCalls() throws Exception {
|
||||
|
||||
List<Task> selectedAndClaimedTasks = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
List<String> accessIds =
|
||||
Collections.synchronizedList(
|
||||
Stream.of("admin", "teamlead-1", "teamlead-2", "taskadmin")
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
Runnable test = getRunnableTest(selectedAndClaimedTasks, accessIds);
|
||||
|
||||
Thread[] threads = new Thread[4];
|
||||
for (int i = 0; i < threads.length; i++) {
|
||||
threads[i] = new Thread(test);
|
||||
threads[i].start();
|
||||
}
|
||||
for (int i = 0; i < threads.length; i++) {
|
||||
threads[i].join();
|
||||
}
|
||||
|
||||
assertThat(selectedAndClaimedTasks.stream().map(Task::getId))
|
||||
.containsExactlyInAnyOrder(
|
||||
"TKI:000000000000000000000000000000000003",
|
||||
"TKI:000000000000000000000000000000000004",
|
||||
"TKI:000000000000000000000000000000000005",
|
||||
"TKI:000000000000000000000000000000000006");
|
||||
|
||||
assertThat(selectedAndClaimedTasks.stream().map(Task::getOwner))
|
||||
.containsExactlyInAnyOrder("admin", "taskadmin", "teamlead-1", "teamlead-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithAccessId(user = "admin")
|
||||
void should_ThrowException_When_TryingToSelectAndClaimNonExistingTask() throws Exception {
|
||||
|
||||
TaskQuery query = taskanaEngine.getTaskService().createTaskQuery();
|
||||
query.idIn("notexisting");
|
||||
ThrowingCallable call =
|
||||
() -> {
|
||||
taskanaEngine.getTaskService().selectAndClaim(query);
|
||||
};
|
||||
assertThatThrownBy(call)
|
||||
.isInstanceOf(SystemException.class)
|
||||
.hasMessageContaining(
|
||||
"No tasks matched the specified filter and sorting options, "
|
||||
+ "task query returned nothing!");
|
||||
}
|
||||
|
||||
private Runnable getRunnableTest(List<Task> selectedAndClaimedTasks, List<String> accessIds) {
|
||||
|
||||
Runnable test =
|
||||
() -> {
|
||||
Subject subject = new Subject();
|
||||
subject.getPrincipals().add(new UserPrincipal(accessIds.remove(0)));
|
||||
|
||||
Consumer<TaskService> consumer =
|
||||
CheckedConsumer.wrap(
|
||||
taskService -> {
|
||||
Task task = taskService.selectAndClaim(getTaskQuery());
|
||||
selectedAndClaimedTasks.add(task);
|
||||
});
|
||||
PrivilegedAction<Void> action =
|
||||
() -> {
|
||||
consumer.accept(taskanaEngine.getTaskService());
|
||||
return null;
|
||||
};
|
||||
Subject.doAs(subject, action);
|
||||
};
|
||||
|
||||
return test;
|
||||
}
|
||||
|
||||
private TaskQuery getTaskQuery() {
|
||||
return taskanaEngine.getTaskService().createTaskQuery().orderByTaskId(SortDirection.ASCENDING);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
logging.level.pro.taskana=INFO
|
||||
logging.level.org.springframework.security=INFO
|
||||
|
||||
server.servlet.context-path=/taskana
|
||||
|
||||
######## Taskana DB #######
|
||||
######## h2 configuration ########
|
||||
########spring.datasource.url=jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0
|
||||
########spring.datasource.driverClassName=org.h2.Driver
|
||||
########spring.datasource.username=sa
|
||||
########spring.datasource.password=sa
|
||||
taskana.schemaName=TASKANA
|
||||
|
||||
######## h2 console configuration ########
|
||||
########spring.h2.console.enabled=true
|
||||
########spring.h2.console.path=/h2-console
|
||||
|
||||
######## db2 configuration ########
|
||||
spring.datasource.driverClassName=com.ibm.db2.jcc.DB2Driver
|
||||
spring.datasource.url=jdbc:db2://localhost:50000/tskdb
|
||||
spring.datasource.username=db2inst1
|
||||
spring.datasource.password=db2inst1-pwd
|
||||
|
||||
######## Postgres configuration ########
|
||||
########spring.datasource.url=jdbc:postgresql://localhost/taskana
|
||||
########spring.datasource.driverClassName=org.postgresql.Driver
|
||||
########spring.datasource.username=postgres
|
||||
########spring.datasource.password=1234
|
||||
########spring.jpa.generate-ddl=true
|
||||
########spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
|
||||
####### property that control rest api security deploy use true for no security.
|
||||
devMode=false
|
||||
|
||||
####### property that control if the database is cleaned and sample data is generated
|
||||
generateSampleData=true
|
||||
|
||||
####### JobScheduler cron expression that specifies when the JobSchedler runs
|
||||
taskana.jobscheduler.async.cron=0 * * * * *
|
||||
####### cache static resources properties
|
||||
spring.resources.cache.cachecontrol.cache-private=true
|
||||
####### for upload of big workbasket- or classification-files
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=10MB
|
||||
|
||||
spring.main.allow-bean-definition-overriding=true
|
||||
|
||||
server.tomcat.max-http-post-size=-1
|
||||
server.tomcat.max-save-post-size=-1
|
||||
server.tomcat.max-swallow-size=-1
|
||||
####### tomcat is not detecting the x-forward headers from bluemix as a trustworthy proxy
|
||||
server.tomcat.internal-proxies=.*
|
||||
server.use-forward-headers=true
|
||||
|
||||
####### Properties for AccessIdController to connect to LDAP
|
||||
taskana.ldap.serverUrl=ldap://localhost:10389
|
||||
taskana.ldap.bindDn=uid=admin
|
||||
taskana.ldap.bindPassword=secret
|
||||
taskana.ldap.baseDn=ou=Test,O=TASKANA
|
||||
taskana.ldap.userSearchBase=cn=users
|
||||
taskana.ldap.userSearchFilterName=objectclass
|
||||
taskana.ldap.userSearchFilterValue=person
|
||||
taskana.ldap.userFirstnameAttribute=givenName
|
||||
taskana.ldap.userLastnameAttribute=sn
|
||||
taskana.ldap.userIdAttribute=uid
|
||||
taskana.ldap.groupSearchBase=cn=groups
|
||||
taskana.ldap.groupSearchFilterName=objectclass
|
||||
taskana.ldap.groupSearchFilterValue=groupOfUniqueNames
|
||||
taskana.ldap.groupNameAttribute=cn
|
||||
taskana.ldap.minSearchForLength=3
|
||||
taskana.ldap.maxNumberOfReturnedAccessIds=50
|
||||
taskana.ldap.groupsOfUser=memberUid
|
||||
|
||||
# Embedded Spring LDAP server
|
||||
spring.ldap.embedded.base-dn= OU=Test,O=TASKANA
|
||||
spring.ldap.embedded.credential.username= uid=admin
|
||||
spring.ldap.embedded.credential.password= secret
|
||||
spring.ldap.embedded.ldif=classpath:taskana-example.ldif
|
||||
spring.ldap.embedded.port= 10389
|
||||
spring.ldap.embedded.validation.enabled=false
|
|
@ -7,11 +7,19 @@ server.servlet.context-path=/taskana
|
|||
########spring.datasource.driverClassName=org.h2.Driver
|
||||
########spring.datasource.username=sa
|
||||
########spring.datasource.password=sa
|
||||
taskana.schemaName=taskana
|
||||
taskana.schemaName=TASKANA
|
||||
|
||||
######## h2 console configuration ########
|
||||
########spring.h2.console.enabled=true
|
||||
########spring.h2.console.path=/h2-console
|
||||
|
||||
######## db2 configuration ########
|
||||
########spring.datasource.driverClassName=com.ibm.db2.jcc.DB2Driver
|
||||
########spring.datasource.url=jdbc:db2://localhost:50000/tskdb
|
||||
########spring.datasource.username=db2inst1
|
||||
########spring.datasource.password=db2inst1-pwd
|
||||
########taskana.schemaName=TASKANA
|
||||
|
||||
######## Postgres configuration ########
|
||||
spring.datasource.url=jdbc:postgresql://localhost/postgres
|
||||
spring.datasource.driverClassName=org.postgresql.Driver
|
||||
|
|
|
@ -15,6 +15,12 @@ taskana.schemaName=TASKANA
|
|||
########spring.h2.console.enabled=true
|
||||
########spring.h2.console.path=/h2-console
|
||||
|
||||
######## db2 configuration ########
|
||||
########spring.datasource.driverClassName=com.ibm.db2.jcc.DB2Driver
|
||||
########spring.datasource.url=jdbc:db2://localhost:50000/tskdb
|
||||
########spring.datasource.username=db2inst1
|
||||
########spring.datasource.password=db2inst1-pwd
|
||||
|
||||
######## Postgres configuration ########
|
||||
########spring.datasource.url=jdbc:postgresql://localhost/taskana
|
||||
########spring.datasource.driverClassName=org.postgresql.Driver
|
||||
|
|
|
@ -34,6 +34,7 @@ public final class Mapping {
|
|||
public static final String URL_TASK_COMMENTS = URL_TASKS + "/comments";
|
||||
public static final String URL_TASK_COMMENT = URL_TASK_COMMENTS + "/{taskCommentId}";
|
||||
public static final String URL_TASKS_ID_CLAIM = URL_TASKS_ID + "/claim";
|
||||
public static final String URL_TASKS_ID_SELECT_AND_CLAIM = URL_TASKS + "/select-and-claim";
|
||||
public static final String URL_TASKS_ID_COMPLETE = URL_TASKS_ID + "/complete";
|
||||
public static final String URL_TASKS_ID_TRANSFER_WORKBASKETID =
|
||||
URL_TASKS_ID + "/transfer/{workbasketId}";
|
||||
|
|
|
@ -85,6 +85,7 @@ public class TaskController extends AbstractPagingController {
|
|||
private static final String EXTERNAL_ID = "external-id";
|
||||
private static final String WILDCARD_SEARCH_VALUE = "wildcard-search-value";
|
||||
private static final String WILDCARD_SEARCH_FIELDS = "wildcard-search-fields";
|
||||
private static final String CUSTOM = "custom";
|
||||
|
||||
private static final String SORT_BY = "sort-by";
|
||||
private static final String SORT_DIRECTION = "order";
|
||||
|
@ -166,6 +167,30 @@ public class TaskController extends AbstractPagingController {
|
|||
return result;
|
||||
}
|
||||
|
||||
@PostMapping(path = Mapping.URL_TASKS_ID_SELECT_AND_CLAIM)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResponseEntity<TaskRepresentationModel> selectAndClaimTask(
|
||||
@RequestParam MultiValueMap<String, String> params)
|
||||
throws TaskNotFoundException, InvalidStateException, InvalidOwnerException,
|
||||
NotAuthorizedException, InvalidArgumentException {
|
||||
|
||||
LOGGER.debug("Entry to selectAndClaimTask");
|
||||
|
||||
TaskQuery query = taskService.createTaskQuery();
|
||||
query = applyFilterParams(query, params);
|
||||
query = applySortingParams(query, params);
|
||||
|
||||
Task selectedAndClaimedTask = taskService.selectAndClaim(query);
|
||||
|
||||
ResponseEntity<TaskRepresentationModel> result =
|
||||
ResponseEntity.ok(taskRepresentationModelAssembler.toModel(selectedAndClaimedTask));
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Exit from selectAndClaimTask(), returning {}", result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@DeleteMapping(path = Mapping.URL_TASKS_ID_CLAIM)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResponseEntity<TaskRepresentationModel> cancelClaimTask(@PathVariable String taskId)
|
||||
|
@ -414,8 +439,15 @@ public class TaskController extends AbstractPagingController {
|
|||
params.remove(EXTERNAL_ID);
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Exit from applyFilterParams(), returning {}", taskQuery);
|
||||
for (int i = 1; i < 17; i++) {
|
||||
if (params.containsKey(CUSTOM + i)) {
|
||||
String[] customValues = extractCommaSeparatedFields(params.get(CUSTOM + i));
|
||||
taskQuery.customAttributeIn(String.valueOf(i), customValues);
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
params.remove(CUSTOM + i);
|
||||
LOGGER.debug("Exit from applyFilterParams(), returning {}", taskQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return taskQuery;
|
||||
|
|
|
@ -539,6 +539,20 @@ class TaskControllerRestDocumentation extends BaseRestDocumentation {
|
|||
responseFields(taskFieldDescriptors)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAndClaimTaskDocTest() throws Exception {
|
||||
this.mockMvc
|
||||
.perform(
|
||||
RestDocumentationRequestBuilders.post(
|
||||
restHelper.toUrl(Mapping.URL_TASKS_ID_SELECT_AND_CLAIM) + "?custom14=abc")
|
||||
.accept("application/hal+json")
|
||||
.header("Authorization", ADMIN_CREDENTIALS))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andDo(
|
||||
MockMvcRestDocumentation.document(
|
||||
"SelectAndClaimTaskDocTest", responseFields(taskFieldDescriptors)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAndDeleteTaskDocTest() throws Exception {
|
||||
|
||||
|
|
|
@ -230,6 +230,24 @@ include::{snippets}/CancelClaimTaskDocTest/http-response.adoc[]
|
|||
The response-body is essentially the same as for getting a single task. +
|
||||
Therefore for the response fields you can refer to the <<task, single task>>.
|
||||
|
||||
=== Select and Claim a task
|
||||
|
||||
A `POST` request is used to select and claim a task
|
||||
|
||||
==== Example Request
|
||||
|
||||
Note the empty request-body in the example.
|
||||
|
||||
include::{snippets}/SelectAndClaimTaskDocTest/http-request.adoc[]
|
||||
|
||||
==== Example Response
|
||||
|
||||
include::{snippets}/SelectAndClaimTaskDocTest/http-response.adoc[]
|
||||
|
||||
==== Response Structure
|
||||
|
||||
The response-body is essentially the same as for getting a single task. +
|
||||
Therefore for the response fields you can refer to the <<task, single task>>.
|
||||
|
||||
=== Complete a task
|
||||
|
||||
|
|
Loading…
Reference in New Issue