001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.workspaces.tasks; 017 018import java.time.LocalDate; 019import java.time.ZonedDateTime; 020import java.time.format.DateTimeFormatter; 021import java.util.ArrayList; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.cocoon.servlet.multipart.Part; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.cms.repository.comment.Comment; 034import org.ametys.core.observation.Event; 035import org.ametys.core.ui.Callable; 036import org.ametys.core.user.User; 037import org.ametys.core.user.UserIdentity; 038import org.ametys.plugins.repository.AmetysRepositoryException; 039import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 040import org.ametys.plugins.workspaces.ObservationConstants; 041import org.ametys.plugins.workspaces.members.ProjectMemberManager; 042import org.ametys.plugins.workspaces.project.objects.Project; 043import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 044import org.ametys.plugins.workspaces.tasks.Task.CheckItem; 045import org.ametys.plugins.workspaces.tasks.jcr.JCRTask; 046import org.ametys.plugins.workspaces.tasks.jcr.JCRTaskFactory; 047import org.ametys.plugins.workspaces.tasks.json.TaskJSONHelper; 048import org.ametys.runtime.authentication.AccessDeniedException; 049 050/** 051 * DAO for interacting with tasks of a project 052 */ 053public class WorkspaceTaskDAO extends AbstractWorkspaceTaskDAO 054{ 055 /** The Avalon role */ 056 public static final String ROLE = WorkspaceTaskDAO.class.getName(); 057 058 /** The project member manager */ 059 protected ProjectMemberManager _projectMemberManager; 060 061 /** The tag provider extension point */ 062 protected ProjectTagProviderExtensionPoint _tagProviderExtPt; 063 064 /** The task JSON helper */ 065 protected TaskJSONHelper _taskJSONHelper; 066 067 /** The task list DAO */ 068 protected WorkspaceTasksListDAO _workspaceTasksListDAO; 069 070 @Override 071 public void service(ServiceManager manager) throws ServiceException 072 { 073 super.service(manager); 074 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 075 _tagProviderExtPt = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 076 _taskJSONHelper = (TaskJSONHelper) manager.lookup(TaskJSONHelper.ROLE); 077 _workspaceTasksListDAO = (WorkspaceTasksListDAO) manager.lookup(WorkspaceTasksListDAO.ROLE); 078 } 079 080 /** 081 * Get the tasks from project 082 * @return the list of tasks 083 */ 084 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 085 public List<Map<String, Object>> getTasks() 086 { 087 Project project = _workspaceHelper.getProjectFromRequest(); 088 089 if (!_projectRightHelper.hasReadAccessOnModule(project, TasksWorkspaceModule.TASK_MODULE_ID)) 090 { 091 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to get tasks without reader right"); 092 } 093 094 List<Map<String, Object>> tasksInfo = new ArrayList<>(); 095 for (Task task : getProjectTasks(project)) 096 { 097 tasksInfo.add(_taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName())); 098 } 099 100 return tasksInfo; 101 } 102 103 /** 104 * Add a new task to the tasks list 105 * @param tasksListId the tasks list id 106 * @param parameters The task parameters 107 * @param newFiles the files to add 108 * @param newFileNames the file names to add 109 * @return The task data 110 */ 111 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 112 public Map<String, Object> addTask(String tasksListId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames) 113 { 114 Project project = _workspaceHelper.getProjectFromRequest(); 115 116 if (StringUtils.isBlank(tasksListId)) 117 { 118 throw new IllegalArgumentException("Tasks list id is mandatory to create a new task"); 119 } 120 121 ModifiableTraversableAmetysObject tasksRoot = _getTasksRoot(project, true); 122 123 _checkUserRights(tasksRoot, RIGHTS_HANDLE_TASK); 124 125 int index = 1; 126 String name = "task-1"; 127 while (tasksRoot.hasChild(name)) 128 { 129 index++; 130 name = "task-" + index; 131 } 132 133 JCRTask task = (JCRTask) tasksRoot.createChild(name, JCRTaskFactory.TASK_NODETYPE); 134 task.setTasksListId(tasksListId); 135 task.setPosition(Long.valueOf(_workspaceTasksListDAO.getChildTask(tasksListId).size())); 136 137 ZonedDateTime now = ZonedDateTime.now(); 138 task.setCreationDate(now); 139 task.setLastModified(now); 140 task.setAuthor(_currentUserProvider.getUser()); 141 142 Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, new ArrayList<>()); 143 144 tasksRoot.saveChanges(); 145 146 Map<String, Object> eventParams = new HashMap<>(); 147 eventParams.put(ObservationConstants.ARGS_TASK, task); 148 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 149 150 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CREATED, _currentUserProvider.getUser(), eventParams)); 151 152 Map<String, Object> results = new HashMap<>(); 153 results.put("task", _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName())); 154 results.putAll(attributesResults); 155 return results; 156 } 157 158 /** 159 * Edit a task 160 * @param taskId The id of the task to edit 161 * @param parameters The JS parameters 162 * @param newFiles the new file to add 163 * @param newFileNames the file names to add 164 * @param deleteFiles the file to delete 165 * @return The task data 166 */ 167 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 168 public Map<String, Object> editTask(String taskId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 169 { 170 JCRTask task = _resolver.resolveById(taskId); 171 172 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 173 174 _checkUserRights(tasksRoot, RIGHTS_HANDLE_TASK); 175 176 Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, deleteFiles); 177 task.setLastModified(ZonedDateTime.now()); 178 task.saveChanges(); 179 180 Map<String, Object> eventParams = new HashMap<>(); 181 eventParams.put(ObservationConstants.ARGS_TASK, task); 182 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 183 184 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_UPDATED, _currentUserProvider.getUser(), eventParams)); 185 186 // Closed status has changed 187 if (attributesResults.containsKey("isClosed")) 188 { 189 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CLOSED_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams)); 190 } 191 192 // Assigments have changed 193 if (attributesResults.containsKey("changedAssignments")) 194 { 195 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_ASSIGNED, _currentUserProvider.getUser(), eventParams)); 196 } 197 198 Map<String, Object> results = new HashMap<>(); 199 results.put("task", _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName())); 200 results.putAll(attributesResults); 201 return results; 202 } 203 204 /** 205 * Move task to new position 206 * @param tasksListId the tasks list id 207 * @param taskId the task id to move 208 * @param newPosition the new position 209 * @return The task data 210 */ 211 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 212 public Map<String, Object> moveTask(String tasksListId, String taskId, long newPosition) 213 { 214 Task task = _resolver.resolveById(taskId); 215 216 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 217 _checkUserRights(task.getParent(), RIGHTS_HANDLE_TASK); 218 219 if (tasksListId != task.getTaskListId()) 220 { 221 List<Task> childTasks = _workspaceTasksListDAO.getChildTask(task.getTaskListId()); 222 long position = 0; 223 for (Task childTask : childTasks) 224 { 225 if (!childTask.getId().equals(taskId)) 226 { 227 childTask.setPosition(position); 228 position++; 229 } 230 } 231 } 232 233 task.setTasksListId(tasksListId); 234 List<Task> childTasks = _workspaceTasksListDAO.getChildTask(tasksListId); 235 int size = childTasks.size(); 236 if (newPosition > size) 237 { 238 throw new IllegalArgumentException("New position (" + newPosition + ") can't be greater than tasks child size (" + size + ")"); 239 } 240 241 long position = 0; 242 task.setPosition(newPosition); 243 for (Task childTask : childTasks) 244 { 245 if (position == newPosition) 246 { 247 position++; 248 } 249 250 if (childTask.getId().equals(taskId)) 251 { 252 childTask.setPosition(newPosition); 253 } 254 else 255 { 256 childTask.setPosition(position); 257 position++; 258 } 259 } 260 261 tasksRoot.saveChanges(); 262 263 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 264 } 265 266 /** 267 * Remove a task 268 * @param taskId the task id to remove 269 * @return The task data 270 */ 271 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 272 public Map<String, Object> deleteTask(String taskId) 273 { 274 Task task = _resolver.resolveById(taskId); 275 276 // Check user right 277 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 278 _checkUserRights(tasksRoot, RIGHTS_DELETE_TASK); 279 280 Map<String, Object> results = new HashMap<>(); 281 282 Map<String, Object> eventParams = new HashMap<>(); 283 eventParams.put(ObservationConstants.ARGS_TASK, task); 284 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 285 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETING, _currentUserProvider.getUser(), eventParams)); 286 287 String tasksListId = task.getTaskListId(); 288 task.remove(); 289 290 // Reorder tasks position 291 long position = 0; 292 for (Task childTask : _workspaceTasksListDAO.getChildTask(tasksListId)) 293 { 294 childTask.setPosition(position); 295 position++; 296 } 297 298 tasksRoot.saveChanges(); 299 300 eventParams = new HashMap<>(); 301 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 302 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETED, _currentUserProvider.getUser(), eventParams)); 303 304 return results; 305 } 306 307 /** 308 * Comment a task 309 * @param taskId the task id 310 * @param commentText the comment text 311 * @return The task data 312 */ 313 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 314 public Map<String, Object> commentTask(String taskId, String commentText) 315 { 316 Task task = _resolver.resolveById(taskId); 317 318 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 319 320 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 321 322 Comment comment = createComment(task, commentText, tasksRoot); 323 324 // Notify listeners 325 Map<String, Object> eventParams = new HashMap<>(); 326 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 327 eventParams.put(ObservationConstants.ARGS_TASK_COMMENT_ID, comment.getId()); 328 UserIdentity currentUser = _currentUserProvider.getUser(); 329 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_TASK_COMMENTED, currentUser, eventParams)); 330 331 332 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 333 } 334 335 /** 336 * Edit a task comment 337 * @param taskId the task id 338 * @param commentId the comment Id 339 * @param commentText the comment text 340 * @return The task data 341 */ 342 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 343 public Map<String, Object> editCommentTask(String taskId, String commentId, String commentText) 344 { 345 Task task = _resolver.resolveById(taskId); 346 347 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 348 349 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 350 351 editComment(task, commentId, commentText, tasksRoot); 352 353 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 354 } 355 356 /** 357 * Answer to a task's comment 358 * @param taskId the task id 359 * @param commentId the comment id 360 * @param commentText the comment text 361 * @return The task data 362 */ 363 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 364 public Map<String, Object> answerCommentTask(String taskId, String commentId, String commentText) 365 { 366 Task task = _resolver.resolveById(taskId); 367 368 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 369 370 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 371 372 Comment comment = answerComment(task, commentId, commentText, tasksRoot); 373 374 // Notify listeners 375 Map<String, Object> eventParams = new HashMap<>(); 376 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 377 eventParams.put(ObservationConstants.ARGS_TASK_COMMENT_ID, comment.getId()); 378 UserIdentity currentUser = _currentUserProvider.getUser(); 379 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_TASK_COMMENTED, currentUser, eventParams)); 380 381 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 382 } 383 384 /** 385 * Delete a task's comment 386 * @param taskId the task id 387 * @param commentId the comment id 388 * @return The task data 389 */ 390 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 391 public Map<String, Object> deleteCommentTask(String taskId, String commentId) 392 { 393 Task task = _resolver.resolveById(taskId); 394 395 // Check user right 396 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 397 398 UserIdentity userIdentity = _currentUserProvider.getUser(); 399 User user = _userManager.getUser(userIdentity); 400 401 Comment comment = task.getComment(commentId); 402 String authorEmail = comment.getAuthorEmail(); 403 if (!authorEmail.equals(user.getEmail())) 404 { 405 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 406 } 407 408 deleteComment(task, commentId, tasksRoot); 409 410 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 411 } 412 413 /** 414 * Like or unlike a task's comment 415 * @param taskId the task id 416 * @param commentId the comment id 417 * @param liked true if the comment is liked, otherwise the comment is unliked 418 * @return The task data 419 */ 420 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 421 public Map<String, Object> likeOrUnlikeCommentTask(String taskId, String commentId, Boolean liked) 422 { 423 Task task = _resolver.resolveById(taskId); 424 425 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 426 427 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 428 429 likeOrUnlikeComment(task, commentId, liked, tasksRoot); 430 431 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 432 } 433 434 /** 435 * Set task's attributes 436 * @param task The task to edit 437 * @param parameters The JS parameters 438 * @param newFiles the new file to add to the task 439 * @param newFileNames the new file names to add to the task 440 * @param deleteFiles the file to remove from the task 441 * @return the map of results 442 */ 443 protected Map<String, Object> _setTaskAttributes(JCRTask task, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 444 { 445 Map<String, Object> results = new HashMap<>(); 446 447 String label = (String) parameters.get(JCRTask.ATTRIBUTE_LABEL); 448 task.setLabel(label); 449 450 String description = (String) parameters.get(JCRTask.ATTRIBUTE_DESCRIPTION); 451 task.setDescription(description); 452 453 _setTaskDates(task, parameters); 454 _setTaskCloseInfo(task, parameters, results); 455 _setAttachments(task, newFiles, newFileNames, deleteFiles); 456 457 @SuppressWarnings("unchecked") 458 List<Map<String, Object>> assignmentIds = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_ASSIGNMENTS, new ArrayList<>()); 459 List<UserIdentity> users = assignmentIds.stream() 460 .map(m -> (String) m.get("id")) 461 .map(UserIdentity::stringToUserIdentity) 462 .collect(Collectors.toList()); 463 464 if (!task.getAssignments().equals(users)) 465 { 466 task.setAssignments(users); 467 results.put("changedAssignments", true); 468 } 469 470 @SuppressWarnings("unchecked") 471 List<Map<String, Object>> checkListItems = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_CHECKLIST, new ArrayList<>()); 472 List<CheckItem> checkItems = checkListItems.stream() 473 .map(e -> new CheckItem((String) e.get(JCRTask.ATTRIBUTE_CHECKLIST_LABEL), (boolean) e.get(JCRTask.ATTRIBUTE_CHECKLIST_ISCHECKED))) 474 .collect(Collectors.toList()); 475 task.setCheckListItem(checkItems); 476 477 @SuppressWarnings("unchecked") 478 List<Object> tags = (List<Object>) parameters.getOrDefault(JCRTask.ATTRIBUTE_TAGS, new ArrayList<>()); 479 480 List<Map<String, Object>> createdTagsJson = _handleTags(task, tags); 481 482 results.put("newTags", createdTagsJson); 483 return results; 484 } 485 486 private void _setTaskDates(JCRTask task, Map<String, Object> parameters) 487 { 488 String startDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_STARTDATE); 489 LocalDate startDate = Optional.ofNullable(startDateAsStr) 490 .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)) 491 .orElse(null); 492 task.setStartDate(startDate); 493 494 String dueDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_DUEDATE); 495 LocalDate dueDate = Optional.ofNullable(dueDateAsStr) 496 .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)) 497 .orElse(null); 498 task.setDueDate(dueDate); 499 } 500 501 private void _setTaskCloseInfo(JCRTask task, Map<String, Object> parameters, Map<String, Object> results) 502 { 503 @SuppressWarnings("unchecked") 504 Map<String, Object> closeInfo = (Map<String, Object>) parameters.get("closeInfo"); 505 if (closeInfo != null && !task.isClosed()) 506 { 507 task.close(true); 508 task.setCloseAuthor(_currentUserProvider.getUser()); 509 task.setCloseDate(LocalDate.now()); 510 511 results.put("isClosed", true); 512 } 513 else if (closeInfo == null && task.isClosed()) 514 { 515 task.close(false); 516 task.setCloseAuthor(null); 517 task.setCloseDate(null); 518 519 results.put("isClosed", false); 520 } 521 } 522 523 /** 524 * Get all tasks from given projets 525 * @param project the project 526 * @return All tasks as JSON 527 */ 528 public List<Task> getProjectTasks(Project project) 529 { 530 TasksWorkspaceModule taskModule = _workspaceModuleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID); 531 ModifiableTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true); 532 return tasksRoot.getChildren() 533 .stream() 534 .filter(Task.class::isInstance) 535 .map(Task.class::cast) 536 .collect(Collectors.toList()); 537 } 538 539 /** 540 * Get the total number of tasks of the project 541 * @param project The project 542 * @return The number of tasks, or null if the module is not activated 543 */ 544 public Long getTasksCount(Project project) 545 { 546 return Long.valueOf(getProjectTasks(project).size()); 547 } 548 549 /** 550 * Get project members 551 * @return the project members 552 * @throws AmetysRepositoryException if an error occurred 553 */ 554 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 555 public Map<String, Object> getProjectMembers() throws AmetysRepositoryException 556 { 557 String projectName = getProjectName(); 558 String lang = getSitemapLanguage(); 559 560 return _projectMemberManager.getProjectMembers(projectName, lang, true); 561 } 562}