cancel
Showing results for 
Search instead for 
Did you mean: 

Developing Task List - a multi-user todo list

turgayz
Champ in-the-making
Champ in-the-making
Hi,
On the wiki page http://www.alfresco.org/mediawiki/index.php/Contribution_Area , there are some suggestions of contributions to Alfresco. Among them, I think that implementing the "Task List" will be one of the easiest. So let's start it as a community effort. If you are interested, please write your ideas, suggestions, code… Any help will be appreciated. In the end, I hope we can have a "Task List" contribution to Alfresco.

To start with, I'll try to gather the use cases I can think of - how will the task list be used?

- As a space: A user can create a "Task List" space within his/her home space. And then he can enter lots of "Task"s in it. (So, "Task" looks like a good candidate for a custom type). Later, he will update tasks, mark some of them  as completed, etc. We will be able to sort and filter tasks. There may also be department and company wide task lists.

- Tasks can be attached to other nodes: For example, we have a PDF document. A user will be able to attach a Task to the document. Like a manager attaching a task to a document, asking her assistant to modify some part of the document.

This is the functionality I can think of right now. For a start, please write any other use cases you think can be included in the functionality list.
13 REPLIES 13

turgayz
Champ in-the-making
Champ in-the-making
Gavin had posted some pointers on another thread, which I am copying here:

Thanks for the positive feedback.

Some pointers to get you started…

First of all you will need to create the appropriate types in the model, the following wiki page may help with this: http://www.alfresco.org/mediawiki/index.php/Data_Dictionary_Guide

Add an option into the advanced space wizard for creating a space of your type:

<config evaluator="string-compare" condition="Custom Folder Types">
   <folder-types>
      <type name="fm:forums" icon="/images/icons/forums_large.gif" descriptionMsgId="forums_desc" />
      <type name="your:type" icon="/images/icons/your_icon.gif" description="Description of your space type" />
   </folder-types>
</config>

You can now create spaces of your new type. To get a different "view" of your space when you click on it in the browse view you need to obviously create the JSP and then create some dispatching configuration for it.

In web-client-config.xml add the following (you can use the forums stuff as an example):

<config evaluator="node-type" condition="your:type">
   <navigation>
      <override from-view-id="/jsp/browse/browse.jsp" to-view-id="/jsp/your/page.jsp" />
      <override from-outcome="browse" to-view-id="/jsp/your/page.jsp" />
   </navigation>
</config>

With this config in place when you click on a space in the UI you should get re-directed to your custom page.

I'm working on a wiki page that will explain the dispatching mechanism and also the simple dialog framework we have now too (which is also used by the forums). I'll let you know when it goes live.

turgayz
Champ in-the-making
Champ in-the-making
Hi,
Trying to implement task list functionality, I have done the following:

1) Created a model (taskListModel.xml):

<model name="tl:taskListmodel" xmlns="http://www.alfresco.org/model/dictionary/1.0">

   <description>Task List Model</description>
   <author></author>
   <version>1.0</version>

   <!– Imports are required to allow references to definitions in other models –>  
   <imports>
        <!– Import Alfresco Dictionary Definitions –>
      <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
      <!– Import Alfresco Content Domain Model Definitions –>
      <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
   </imports>

   <namespaces>
      <namespace uri="http://www.alfresco.org/model/taskList/1.0" prefix="tl"/>
   </namespaces>
  
   <types>     
      <type name="tl:taskList">
         <parent>cm:folder</parent>
      </type>
     
      <type name="tl:task">
         <parent>cm:folder</parent>
      </type>
     
      <type name="tl:comment">
         <parent>cm:content</parent>
      </type>
    </types>       
   
    <aspects>
      <aspect name="tl:taskListAppendable">
         <associations>
            <child-association name="tl:taskListOfContent">
               <source>
                  <mandatory>false</mandatory>
                  <many>false</many>
               </source>
               <target>
                  <class>tl:taskList</class>
                  <mandatory>true</mandatory>
                  <many>false</many>
               </target>
               <duplicate>false</duplicate>
            </child-association>
         </associations>
      </aspect>
   </aspects>
      
</model>
2) Defined the model in repository/config/alfresco/extension/extension-context.xml:

<value>alfresco/extension/taskListModel.xml</value>
3) created a class to hold the model definitions (TaskListModel.java):

public class TaskListModel {
   //
    // Task List Model Definitions
    //
  
    static final String TASKLIST_MODEL_URI = "http://www.alfresco.org/model/taskList/1.0";
    static final String TASKLIST_MODEL_PREFIX = "tl";
   
    static final QName TYPE_TASKLIST = QName.createQName(TASKLIST_MODEL_URI, "taskList");
    static final QName TYPE_TASK = QName.createQName(TASKLIST_MODEL_URI, "task");
    static final QName TYPE_COMMENT = QName.createQName(TASKLIST_MODEL_URI, "comment");

    static final QName ASPECT_TASKLISTAPPENDABLE = QName.createQName(TASKLIST_MODEL_URI, "taskListAppendable");
   
    static final QName ASSOC_TASKLIST = QName.createQName(TASKLIST_MODEL_URI, "taskListOfContent");
}
And then, written the following unit tests. First, creating a task list just under the company home:

public class TaskListTest extends TestCase {
   TransactionService transactionService = null;

   NodeService nodeService = null;

   ContentService contentService = null;

   AuthenticationService authenticationService = null;

   @Override
   protected void setUp() throws Exception {
      // initialise app content
      ApplicationContext ctx = ApplicationContextHelper
            .getApplicationContext();
      // get registry of services
      ServiceRegistry serviceRegistry = (ServiceRegistry) ctx
            .getBean(ServiceRegistry.SERVICE_REGISTRY);

      // get individual, required services
      transactionService = serviceRegistry.getTransactionService();
      nodeService = serviceRegistry.getNodeService();
      contentService = serviceRegistry.getContentService();
      authenticationService = serviceRegistry.getAuthenticationService();

      authenticationService.authenticate("admin", "admin".toCharArray());
   }

   public void testCanGetCompanyHome() {
      NodeRef companyHome = getCompanyHome();
      assertTrue(nodeService.exists(companyHome));

      System.out.println(NodeStoreInspector.outputNode(0, nodeService,
            companyHome));
      assertEquals("The company root space", nodeService.getProperty(
            companyHome, ContentModel.PROP_DESCRIPTION));
   }

   public void testCreateTaskListUnderCompanyHome() {
      TransactionWork<Object> exampleWork = new TransactionWork<Object>() {
         public Object doWork() throws Exception {
            NodeRef companyHome = getCompanyHome();
            nodeService.addAspect(companyHome,
                  TaskListModel.ASPECT_TASKLISTAPPENDABLE, null);
            Map<QName, Serializable> nodeProperties = new HashMap<QName, Serializable>(
                  7);

            // add a sample task list to the company home
            nodeProperties.clear();
            nodeProperties.put(ContentModel.PROP_NAME, "Sample Task List");
            ChildAssociationRef assocRef = nodeService.createNode(
                  companyHome, TaskListModel.ASSOC_TASKLIST, QName
                        .createQName(TaskListModel.TASKLIST_MODEL_URI,
                              "sampleTaskList"),
                  TaskListModel.TYPE_TASKLIST, nodeProperties);

            NodeRef taskListRef = assocRef.getChildRef();
            assertEquals("Sample Task List", nodeService.getProperty(
                  taskListRef, ContentModel.PROP_NAME));
            System.out.println(NodeStoreInspector.outputNode(0,
                  nodeService, companyHome));
            return null;
         }
      };
      TransactionUtil.executeInUserTransaction(transactionService,
            exampleWork);

   }

   public void testDeleteTaskListCreatedUnderCompanyHome() {
      TransactionWork<Object> exampleWork = new TransactionWork<Object>() {
         public Object doWork() throws Exception {
            NodeRef companyHome = getCompanyHome();
            List<ChildAssociationRef> childRefs = nodeService
                  .getChildAssocs(companyHome,
                        RegexQNamePattern.MATCH_ALL, QName.createQName(
                              TaskListModel.TASKLIST_MODEL_URI,
                              "sampleTaskList"));

            for (ChildAssociationRef ref : childRefs) {
               NodeRef sampleTaskList = ref.getChildRef();
               nodeService.deleteNode(sampleTaskList);
               System.out.println("deleted 1 task list");
            }
            System.out.println(NodeStoreInspector.outputNode(0,
                  nodeService, companyHome));
            return null;
         }
      };
      TransactionUtil.executeInUserTransaction(transactionService,
            exampleWork);
   }

   private NodeRef getCompanyHome() {
      StoreRef spacesStoreRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE,

            "SpacesStore");
      assertTrue(nodeService.exists(spacesStoreRef));

      NodeRef rootNodeRef = nodeService.getRootNode(spacesStoreRef);
      List<ChildAssociationRef> childRefs = nodeService.getChildAssocs(
            rootNodeRef, RegexQNamePattern.MATCH_ALL, QName.createQName(
                  NamespaceService.APP_MODEL_1_0_URI, "company_home"));
      ChildAssociationRef companyHomeRef = childRefs.get(0);
      NodeRef companyHome = companyHomeRef.getChildRef();
      return companyHome;
   }

}

The tests run without error. First I can create a task list under company home, and then I can successfully find and delete it. These tests don't assert anything yet, I better test the functionality with asserts, not by writing the whole company home node to the console. Smiley Happy

I hope I am doing everything right.

gavinc
Champ in-the-making
Champ in-the-making
The only suggestion I have is to take a look at some of the unit tests in the repository projects. Most of them extend the "org.alfresco.util.BaseAlfrescoSpringTest" class which provides the transaction handling you are doing manually in your tests.

Apart from that its looking good.

turgayz
Champ in-the-making
Champ in-the-making
Thanks, Gavin, the tests look simpler now, after extending BaseAlfrescoSpringTest.

But what will happen when I move this functionality out of the tests to the actual classes? Will I have to start a transaction and end it manually? Or will I use the TransactionUtil.executeInUserTransaction() then?

gavinc
Champ in-the-making
Champ in-the-making
No, you wouldn't use TransactionUtil.executeInUserTransaction() you would use a UserTransaction.

We use the following pattern in the client…


UserTransaction tx = null;
  
try
{
   tx = Repository.getUserTransaction(FacesContext.getCurrentInstance());
   tx.begin();
   ….
   your code
   ….
   tx.commit();
}
catch (Throwable e)
{
   // rollback the transaction
   try { if (tx != null) {tx.rollback();} } catch (Exception ex){}
}        

Take a look at any of the wizard beans in org.alfresco.web.bean.wizard package. They are all examples of this usage.

turgayz
Champ in-the-making
Champ in-the-making
Hi,
I have posted the files for task lists functionality to JIRA. You can find it here:
http://www.alfresco.org/jira/browse/AWC-500

In the zip file, open the README.txt which describes the modified/added files, and where to put each of them.

The zip file also contains some screenshots.

There are some problems I need help for, I'll post another reply here for them.

Any questions/suggestions are welcome.

turgayz
Champ in-the-making
Champ in-the-making
Hi,
I have added 2 properties to my model:

<type name="tl:task">
         <parent>cm:folder</parent>
         <properties>
            <property name="tl:taskStatus">
               <title>Status</title>
               <type>d:text</type>
               <mandatory>true</mandatory>
            </property>
            <property name="tl:taskPercentCompleted">
               <title>Percent Completed</title>
               <type>d:int</type>
               <mandatory>true</mandatory>
            </property>
         </properties>
         <associations>
            <child-association name="tl:commentOfTask">
               <target>
                  <class>tl:comment</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </child-association>
         </associations>
      </type>

I want to show the properties taskStatus and taskPercentCompleted on the web client, and let the user update them. For this, I have added the following to task.jsp:

<div class="mainSubText"><h:outputText value="#{NavigationBean.nodeProperties.taskStatus}" id="msg5" /></div>
<div class="mainSubText"><h:outputText value="#{NavigationBean.nodeProperties.taskPercentCompleted}" id="msg7" /></div>

There are resolvers for the properties in TaskListBean.java:

   public NodePropertyResolver resolverTaskStatus = new NodePropertyResolver() {
      public Object get(Node node) {
         System.out.println("inside NodePropertyResolver.resolverTaskStatus…");
         System.out.println(NodeStoreInspector.dumpNode(nodeService, node.getNodeRef()));
         QNameNodeMap props = (QNameNodeMap) node.getProperties();
         String taskStatus = (String) props.getRaw("tl:taskStatus");

         if (taskStatus == null || taskStatus.equals(""))
            taskStatus = "NOT STARTED";         

         return taskStatus;
      }
   };
   
   public NodePropertyResolver resolverPercentCompleted = new NodePropertyResolver() {
      public Object get(Node node) {
         System.out.println("inside NodePropertyResolver.resolverPercentCompleted…");
         System.out.println(NodeStoreInspector.dumpNode(nodeService, node.getNodeRef()));
         QNameNodeMap props = (QNameNodeMap) node.getProperties();
         Integer percentCompleted = (Integer) props.getRaw("tl:taskPercentCompleted");

         if (percentCompleted == null)
            percentCompleted = 0;         

         return percentCompleted;
      }
   };

The properties don't show up in the jsp page. I must be doing something wrong, what is it?  :?

gavinc
Champ in-the-making
Champ in-the-making
The problem is that the task properties are defined in a different namespace.

Using the syntax #{NavigationBean.nodeProperties.taskStatus} makes the presumption that the taskStatus property is in the "cm:" namespace.

You can try the following syntax instead:

#{NavigationBean.nodeProperties["tl:taskStatus"]}

Let me know if that helps.

turgayz
Champ in-the-making
Champ in-the-making
Yes, it works perfectly, thank you  Smiley Very Happy
Now I need to let the user update those properties.

Updating a property as a text field should not be hard, I can find an example for that. Is there support for selecting a property value from a list box also? For example, I want to let selecting the Status property from a list box, having entries like NOT STARTED, IN PROGRESS, FINISHED, etc. Is there a pointer showing how to do this?