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.lang.IllegalClassException;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.repository.comment.Comment;
035import org.ametys.core.observation.Event;
036import org.ametys.core.right.RightManager.RightResult;
037import org.ametys.core.ui.Callable;
038import org.ametys.core.user.User;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
041import org.ametys.plugins.repository.AmetysObject;
042import org.ametys.plugins.repository.AmetysRepositoryException;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.workspaces.ObservationConstants;
045import org.ametys.plugins.workspaces.html.HTMLTransformer;
046import org.ametys.plugins.workspaces.members.ProjectMemberManager;
047import org.ametys.plugins.workspaces.project.objects.Project;
048import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint;
049import org.ametys.plugins.workspaces.tasks.Task.CheckItem;
050import org.ametys.plugins.workspaces.tasks.jcr.JCRTask;
051import org.ametys.plugins.workspaces.tasks.jcr.JCRTaskFactory;
052import org.ametys.plugins.workspaces.tasks.json.TaskJSONHelper;
053
054/**
055 * DAO for interacting with tasks of a project
056 */
057public class WorkspaceTaskDAO extends AbstractWorkspaceTaskDAO
058{
059    /** The Avalon role */
060    public static final String ROLE = WorkspaceTaskDAO.class.getName();
061    
062    /** The HTML transformer */
063    protected HTMLTransformer _htmlTransformer;
064    
065    /** The project member manager */
066    protected ProjectMemberManager _projectMemberManager;
067    
068    /** The tag provider extension point */
069    protected ProjectTagProviderExtensionPoint _tagProviderExtPt;
070        
071    /** The task JSON helper */
072    protected TaskJSONHelper _taskJSONHelper;
073    
074    /** The task list DAO */
075    protected WorkspaceTasksListDAO _workspaceTasksListDAO;
076    
077    @Override
078    public void service(ServiceManager manager) throws ServiceException
079    {
080        super.service(manager);
081        _htmlTransformer = (HTMLTransformer) manager.lookup(HTMLTransformer.ROLE);
082        _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
083        _tagProviderExtPt = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE);
084        _taskJSONHelper = (TaskJSONHelper) manager.lookup(TaskJSONHelper.ROLE);
085        _workspaceTasksListDAO = (WorkspaceTasksListDAO) manager.lookup(WorkspaceTasksListDAO.ROLE);
086    }
087
088    /**
089     * Get the tasks from project
090     * @return the list of tasks
091     * @throws IllegalAccessException If an error occurs when checking the rights
092     */
093    @Callable
094    public List<Map<String, Object>> getTasks() throws IllegalAccessException
095    {
096        String projectName = _getProjectName();
097        
098        ModifiableResourceCollection moduleRoot = _getModuleRoot(projectName);
099        if (!_rightManager.currentUserHasReadAccess(moduleRoot))
100        {
101            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to get tasks without reader right");
102        }
103        
104        List<Map<String, Object>> tasksInfo = new ArrayList<>();
105        Project project = _projectManager.getProject(projectName);
106        for (Task task : getProjectTasks(project))
107        {
108            tasksInfo.add(_taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()));
109        }
110        
111        return tasksInfo;
112    }
113    
114    /**
115     * Add a new task to the tasks list
116     * @param tasksListId the tasks list id
117     * @param parameters The task parameters
118     * @param newFiles the files to add
119     * @param newFileNames the file names to add 
120     * @return The task data
121     * @throws IllegalAccessException If an error occurs when checking the rights
122     */
123    @Callable
124    public Map<String, Object> addTask(String tasksListId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames) throws IllegalAccessException
125    {
126        String projectName = _getProjectName();
127        
128        if (StringUtils.isBlank(tasksListId))
129        {
130            throw new IllegalArgumentException("Tasks list id is mandatory to create a new task");
131        }
132        
133        ModifiableTraversableAmetysObject tasksRoot = _getTasksRoot(projectName);
134        
135        // Check user right
136        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
137        {
138            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to add task without convenient right [" + RIGHTS_HANDLE_TASK + "]");
139        }
140
141        int index = 1;
142        String name = "task-1";
143        while (tasksRoot.hasChild(name))
144        {
145            index++;
146            name = "task-" + index;
147        }
148        
149        JCRTask task = (JCRTask) tasksRoot.createChild(name, JCRTaskFactory.TASK_NODETYPE);
150        task.setTasksListId(tasksListId);
151        task.setPosition(Long.valueOf(_workspaceTasksListDAO.getChildTask(tasksListId).size()));
152        
153        ZonedDateTime now = ZonedDateTime.now();
154        task.setCreationDate(now);
155        task.setLastModified(now);
156        task.setAuthor(_currentUserProvider.getUser());
157
158        Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, new ArrayList<>());
159        
160        tasksRoot.saveChanges();
161        
162        Map<String, Object> eventParams = new HashMap<>();
163        eventParams.put(ObservationConstants.ARGS_TASK, task);
164        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId());
165        
166        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CREATED, _currentUserProvider.getUser(), eventParams));
167
168        Map<String, Object> results = new HashMap<>();
169        results.put("task", _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()));
170        results.putAll(attributesResults);
171        return results;
172    }
173    
174    /**
175     * Edit a task
176     * @param taskId The id of the task to edit
177     * @param parameters The JS parameters
178     * @param newFiles the new file to add
179     * @param newFileNames the file names to add
180     * @param deleteFiles the file to delete
181     * @return The task data
182     * @throws IllegalAccessException If an error occurs when checking the rights
183     */
184    @Callable
185    public Map<String, Object> editTask(String taskId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) throws IllegalAccessException
186    {
187        AmetysObject object = _resolver.resolveById(taskId);
188        if (!(object instanceof JCRTask))
189        {
190            throw new IllegalClassException(JCRTask.class, object.getClass());
191        }
192        
193        // Check user right
194        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
195        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
196        {
197            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit task without convenient right [" + RIGHTS_HANDLE_TASK + "]");
198        }
199        
200        JCRTask task = (JCRTask) object;
201        Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, deleteFiles);
202        
203        ZonedDateTime now = ZonedDateTime.now();
204        task.setLastModified(now);
205            
206        task.saveChanges();
207            
208        Map<String, Object> eventParams = new HashMap<>();
209        eventParams.put(ObservationConstants.ARGS_TASK, task);
210        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId);
211        
212        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_UPDATED, _currentUserProvider.getUser(), eventParams));
213        
214        // Closed status has changed
215        if (attributesResults.containsKey("isClosed"))
216        {
217            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CLOSED_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams));
218        }
219
220        // Assigments have changed
221        if (attributesResults.containsKey("changedAssignments"))
222        {
223            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_ASSIGNED, _currentUserProvider.getUser(), eventParams));
224        }
225        
226         
227        Map<String, Object> results = new HashMap<>();
228        results.put("task", _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()));
229        results.putAll(attributesResults);
230        return results;
231    }
232    
233    /**
234     * Move task to new position
235     * @param tasksListId the tasks list id
236     * @param taskId the task id to move
237     * @param newPosition the new position
238     * @return The task data
239     * @throws IllegalAccessException If an error occurs when checking the rights
240     */
241    @Callable
242    public Map<String, Object> moveTask(String tasksListId, String taskId, long newPosition) throws IllegalAccessException
243    {
244        AmetysObject object = _resolver.resolveById(taskId);
245        if (!(object instanceof Task))
246        {
247            throw new IllegalClassException(Task.class, object.getClass());
248        }
249        
250        // Check user right
251        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
252        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
253        {
254            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to move task without convenient right [" + RIGHTS_HANDLE_TASK + "]");
255        }
256        
257        Task task = (Task) object;
258        if (tasksListId != task.getTaskListId())
259        {
260            List<Task> childTasks = _workspaceTasksListDAO.getChildTask(task.getTaskListId());
261            long position = 0;
262            for (Task childTask : childTasks)
263            {
264                if (!childTask.getId().equals(taskId))
265                {
266                    childTask.setPosition(position);
267                    position++;
268                }
269            }
270        }
271        
272        task.setTasksListId(tasksListId);
273        List<Task> childTasks = _workspaceTasksListDAO.getChildTask(tasksListId);
274        int size = childTasks.size();
275        if (newPosition > size)
276        {
277            throw new IllegalArgumentException("New position (" + newPosition + ") can't be greater than tasks child size (" + size + ")");
278        }
279        
280        long position = 0;
281        task.setPosition(newPosition);
282        for (Task childTask : childTasks)
283        {
284            if (position == newPosition)
285            {
286                position++;
287            }
288            
289            if (childTask.getId().equals(taskId))
290            {
291                childTask.setPosition(newPosition);
292            }
293            else
294            {
295                childTask.setPosition(position);
296                position++;
297            }
298        }
299        
300        tasksRoot.saveChanges();
301        
302        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
303    }
304    
305    /**
306     * Remove a task
307     * @param taskId the task id to remove
308     * @return The task data
309     * @throws IllegalAccessException If an error occurs when checking the rights
310     */
311    @Callable
312    public Map<String, Object> deleteTask(String taskId) throws IllegalAccessException
313    {
314        AmetysObject object = _resolver.resolveById(taskId);
315        if (!(object instanceof Task))
316        {
317            throw new IllegalClassException(Task.class, object.getClass());
318        }
319        
320        // Check user right
321        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
322        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_DELETE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
323        {
324            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete task without convenient right [" + RIGHTS_DELETE_TASK + "]");
325        }
326        
327        Map<String, Object> results = new HashMap<>();
328        Task jcrTask = (Task) object;
329        
330        Map<String, Object> eventParams = new HashMap<>();
331        eventParams.put(ObservationConstants.ARGS_TASK, jcrTask);
332        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId);
333        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETING, _currentUserProvider.getUser(), eventParams));
334        
335        String tasksListId = jcrTask.getTaskListId();
336        jcrTask.remove();
337        
338        // Reorder tasks position
339        long position = 0;
340        for (Task childTask : _workspaceTasksListDAO.getChildTask(tasksListId))
341        {
342            childTask.setPosition(position);
343            position++;
344        }
345        
346        tasksRoot.saveChanges();
347
348        eventParams = new HashMap<>();
349        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId);
350        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETED, _currentUserProvider.getUser(), eventParams));
351
352        return results;
353    }
354    
355    /**
356     * Comment a task
357     * @param taskId the task id
358     * @param commentText the comment text
359     * @return The task data
360     * @throws IllegalAccessException If an error occurs when checking the rights
361     */
362    @Callable
363    public Map<String, Object> commentTask(String taskId, String commentText) throws IllegalAccessException
364    {
365        AmetysObject object = _resolver.resolveById(taskId);
366        if (!(object instanceof Task))
367        {
368            throw new IllegalClassException(Task.class, object.getClass());
369        }
370        
371        // Check user right
372        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
373        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
374        {
375            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
376        }
377        
378        Task task = (Task) object;
379        
380        createComment(task, commentText, tasksRoot);
381        
382        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
383    }
384    
385    /**
386     * Edit a task comment
387     * @param taskId the task id
388     * @param commentId the comment Id
389     * @param commentText the comment text
390     * @return The task data
391     * @throws IllegalAccessException If an error occurs when checking the rights
392     */
393    @Callable
394    public Map<String, Object> editCommentTask(String taskId, String commentId, String commentText) throws IllegalAccessException
395    {
396        AmetysObject object = _resolver.resolveById(taskId);
397        if (!(object instanceof Task))
398        {
399            throw new IllegalClassException(Task.class, object.getClass());
400        }
401        
402        // Check user right
403        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
404        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
405        {
406            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
407        }
408        
409        Task task = (Task) object;
410
411        editComment(task, commentId, commentText, tasksRoot);
412        
413        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
414    }
415    
416    /**
417     * Answer to a task's comment
418     * @param taskId the task id
419     * @param commentId the comment id
420     * @param commentText the comment text
421     * @return The task data
422     * @throws IllegalAccessException If an error occurs when checking the rights
423     */
424    @Callable
425    public Map<String, Object> answerCommentTask(String taskId, String commentId, String commentText) throws IllegalAccessException
426    {
427        AmetysObject object = _resolver.resolveById(taskId);
428        if (!(object instanceof Task))
429        {
430            throw new IllegalClassException(Task.class, object.getClass());
431        }
432        
433        // Check user right
434        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
435        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
436        {
437            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
438        }
439        
440        Task task = (Task) object;
441
442        answerComment(task, commentId, commentText, tasksRoot);
443        
444        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
445    }
446    
447    /**
448     * Delete a task's comment
449     * @param taskId the task id
450     * @param commentId the comment id
451     * @return The task data
452     * @throws IllegalAccessException If an error occurs when checking the rights
453     */
454    @Callable
455    public Map<String, Object> deleteCommentTask(String taskId, String commentId) throws IllegalAccessException
456    {
457        AmetysObject object = _resolver.resolveById(taskId);
458        if (!(object instanceof Task))
459        {
460            throw new IllegalClassException(Task.class, object.getClass());
461        }
462        
463        // Check user right
464        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
465        
466        UserIdentity userIdentity = _currentUserProvider.getUser();
467        User user = _userManager.getUser(userIdentity);
468        
469        Task task = (Task) object;
470        Comment comment = task.getComment(commentId);
471        String authorEmail = comment.getAuthorEmail();
472        if (!authorEmail.equals(user.getEmail()))
473        {
474            if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
475            {
476                throw new IllegalAccessException("User '" + userIdentity + "' tried to delete an other user's comment task");
477            }
478        }
479        
480        deleteComment(task, commentId, tasksRoot);
481              
482        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
483    }
484    
485    /**
486     * Like or unlike a task's comment
487     * @param taskId the task id
488     * @param commentId the comment id
489     * @param liked true if the comment is liked, otherwise the comment is unliked
490     * @return The task data
491     * @throws IllegalAccessException If an error occurs when checking the rights
492     */
493    @Callable
494    public Map<String, Object> likeOrUnlikeCommentTask(String taskId, String commentId, Boolean liked) throws IllegalAccessException
495    {
496        AmetysObject object = _resolver.resolveById(taskId);
497        if (!(object instanceof Task))
498        {
499            throw new IllegalClassException(Task.class, object.getClass());
500        }
501        
502        // Check user right
503        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
504        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
505        {
506            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to react a comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
507        }
508        
509        Task task = (Task) object;
510        
511        likeOrUnlikeComment(task, commentId, liked, tasksRoot);
512        
513        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
514    }
515    
516    /**
517     * Set task's attributes
518     * @param task The task to edit
519     * @param parameters The JS parameters
520     * @param newFiles the new file to add to the task
521     * @param newFileNames the new file names to add to the task
522     * @param deleteFiles the file to remove from the task
523     * @return the map of results
524     */
525    protected Map<String, Object> _setTaskAttributes(JCRTask task, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles)
526    {
527        Map<String, Object> results = new HashMap<>();
528        
529        String label = (String) parameters.get(JCRTask.ATTRIBUTE_LABEL);
530        task.setLabel(label);
531
532        String description = (String) parameters.get(JCRTask.ATTRIBUTE_DESCRIPTION);
533        task.setDescription(description);
534        
535        _setTaskDates(task, parameters);
536        _setTaskCloseInfo(task, parameters, results);
537        _setAttachments(task, newFiles, newFileNames, deleteFiles);
538        
539        @SuppressWarnings("unchecked")
540        List<Map<String, Object>> assignmentIds = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_ASSIGNMENTS, new ArrayList<>());
541        List<UserIdentity> users = assignmentIds.stream()
542            .map(m -> (String) m.get("id"))
543            .map(UserIdentity::stringToUserIdentity)
544            .collect(Collectors.toList());
545        
546        if (!task.getAssignments().equals(users))
547        {
548            task.setAssignments(users);
549            results.put("changedAssignments", true);
550        }
551        
552        @SuppressWarnings("unchecked")
553        List<Map<String, Object>> checkListItems = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_CHECKLIST, new ArrayList<>());
554        List<CheckItem> checkItems = checkListItems.stream()
555            .map(e -> new CheckItem((String) e.get(JCRTask.ATTRIBUTE_CHECKLIST_LABEL), (boolean) e.get(JCRTask.ATTRIBUTE_CHECKLIST_ISCHECKED)))
556            .collect(Collectors.toList());
557        task.setCheckListItem(checkItems);
558
559        @SuppressWarnings("unchecked")
560        List<Object> tags = (List<Object>) parameters.getOrDefault(JCRTask.ATTRIBUTE_TAGS, new ArrayList<>()); 
561
562        List<Map<String, Object>> createdTagsJson = _handleTags(task, tags);
563        
564        results.put("newTags", createdTagsJson);
565        return results;
566    }
567    
568    private void _setTaskDates(JCRTask task, Map<String, Object> parameters)
569    {
570        String startDateAsStr = (String) parameters.get("startDate");
571        LocalDate startDate = Optional.ofNullable(startDateAsStr)
572                .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE))
573                .orElse(null);
574        task.setStartDate(startDate);
575        
576        String dueDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_DUEDATE);
577        LocalDate dueDate = Optional.ofNullable(dueDateAsStr)
578                .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE))
579                .orElse(null);
580        task.setDueDate(dueDate);
581    }
582    
583    private void _setTaskCloseInfo(JCRTask task, Map<String, Object> parameters, Map<String, Object> results)
584    {
585        @SuppressWarnings("unchecked")
586        Map<String, Object> closeInfo = (Map<String, Object>) parameters.get("closeInfo");
587        if (closeInfo != null && !task.isClosed())
588        {
589            task.close(true);
590            task.setCloseAuthor(_currentUserProvider.getUser());
591            task.setCloseDate(LocalDate.now());
592
593            results.put("isClosed", true);
594        }
595        else if (closeInfo == null && task.isClosed())
596        {
597            task.close(false);
598            task.setCloseAuthor(null);
599            task.setCloseDate(null);
600
601            results.put("isClosed", false);
602        }
603    }
604    
605    
606    /**
607     * Get all tasks from given projets
608     * @param project the project
609     * @return All tasks as JSON
610     */
611    public List<Task> getProjectTasks(Project project)
612    {
613        TasksWorkspaceModule taskModule = _workspaceModuleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
614        ModifiableTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true);
615        return tasksRoot.getChildren()
616            .stream()
617            .filter(Task.class::isInstance)
618            .map(Task.class::cast)
619            .collect(Collectors.toList());
620    }
621    
622    /**
623     * Get the total number of tasks of the project
624     * @param project The project
625     * @return The number of tasks, or null if the module is not activated
626     */
627    public Long getTasksCount(Project project)
628    {
629        return Long.valueOf(getProjectTasks(project).size());
630    }
631    
632    /**
633     * Get project members 
634     * @return the project members
635     * @throws IllegalAccessException if an error occurred
636     * @throws AmetysRepositoryException if an error occurred
637     */
638    @Callable
639    public Map<String, Object> getProjectMembers() throws IllegalAccessException, AmetysRepositoryException 
640    {
641        String projectName = _getProjectName();
642        String lang = _getSitemapLanguage();
643        
644        return _projectMemberManager.getProjectMembers(projectName, lang, true);
645    }
646}