TeamWox SDK: File Storage - Part 2

Introduction

In Part 1 you've learned how to work with TeamWox file storage using methods of the IFilesManager interface. As an example, we have implemented saving of plain text.

In this article we will consider what tools does TeamWox API provide to solve problems, most frequently encountered in custom modules development - HTML validation, attaching files, adding comments. To implement these features, we will upgrade the edit page and add the new page to view data from file storage and DBMS.

Here is the brief list of interfaces that we will implement in the Hello World module:

  • IRichTextEdit - downloading files in WYSIWYG editor.
  • IHTMLCleaner - parsing HTML code of pages and deleting potentially unsafe tags.
  • IAttachments - working with attached files.
  • IComments and ICommentsPage - working with comments.

The functionality will be rich, so we will implement it consequently.

1. The View Page
  1.1. HTTP API
  1.2. User Interface

2. WYSIWYG Editor
  2.1. HTTP API
  2.2. User Interface
  2.3. Demonstration

3. Attachments
  3.1. Extending Manager
  3.2. Processing Request
  3.3. Saving Information in Manager
  3.4. Updating Records in File Storage
  3.5. User Interface
  3.6. Demonstration

4. Comments
  4.1. Getting Interface to Work with Comments
  4.2. HTTP API
  4.3. User Interface
  4.1. Demonstration

 

In attachment to this article you can find source codes of the Hello World module with all the described changes.

 

1. The View Page

In the Hello World module we will create the new view page called PageView, where you can view the contents of records from the file storage (a bit later on the same page you'll be able to add comments).

 

1.1. HTTP API

1.1.1. In the HelloWorld project create the new page class CPageView, similar to class of the edit page CPageEdit. Into this class in addition to the standard methods Process (page processing) and Tag (displaying data contained in tokens), we will also copy the CheckDescription method of reading data from file storage and declare the new template method InitParams, which will later be convenient to use in HTTP API for request processing.

//+------------------------------------------------------------------+
//| Page to view contents of the record                              |
//+------------------------------------------------------------------+
class CPageView : public CPage
  {
..........................................
   //--- handler
   TWRESULT          Process(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);
   //--- functions of displaying
   bool              Tag(const Context *context,const TagInfo *tag);

private:
   //--- Initialize parameters
   template <size_t length>
   TWRESULT          InitParams(const Context *context,IServer *server,
                                const wchar_t *path,CHelloWorldManager *manager,const wchar_t (&url)[length]);
   //--- Read data from file storage into memory
   void              CheckDescription();
};
//+------------------------------------------------------------------+

1.1.2. In the InitParams method we initialize page for further viewing of data, and parse URL to get the ID of record to display. This is the first stage of request processing for any of API handlers, so for convenience we will put it into a separate template function.

//+------------------------------------------------------------------+
//| Parse path and get managers                                      |
//+------------------------------------------------------------------+
template <size_t length>
TWRESULT CPageView::InitParams(const Context *context,IServer *server,
                               const wchar_t *path,CHelloWorldManager *manager,const wchar_t (&url)[length])
  {
   INT64    id =0;
   TWRESULT res=RES_S_OK;
//--- checks
   if(context==NULL || server==NULL || path==NULL || manager==NULL || length==0) ReturnError(RES_E_INVALID_ARGS);
//---
   m_server          =server;
   m_files_manager   =manager->FilesManagerGet();
//---
   if(m_files_manager==NULL)                                                     ReturnError(RES_E_FAIL);
//--- Request data
   if(PathCompare(url,path))
     {
      id=_wtoi64(path+length-1);
      //---
      if(id>0 && RES_FAILED(res=manager->Get(context,id,&m_record)))
         ReturnErrorExt(res,context,"failed to get record");
     }
   else
     {
      return(RES_E_NOT_FOUND);
     }
//---
   return(RES_S_OK);
  }

1.1.3. In the CHelloWorldModule::ProcessPage method add new rule of redirecting requests to the view page.

#include "Pages\PageView.h"
...................................
if(PathCompare(L"number_two/view",path))           return(CPageView().Process(context,m_server,path,&m_manager));

1.1.4. In the CPageView::Process method process request for the view page.

//+------------------------------------------------------------------+
//| Process request                                                  |
//+------------------------------------------------------------------+
TWRESULT CPageView::Process(const Context *context,
                            IServer *server,
                            const wchar_t *path,
                            CHelloWorldManager *manager)
  {
   TWRESULT res=RES_S_OK;
   INT64    id =0;
//--- Checks
   if(context==NULL || path==NULL)   ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL)        ReturnError(RES_E_INVALID_CONTEXT);
//--- Get server
   if(server==NULL || manager==NULL) ReturnError(RES_E_FAIL);
//--- Initialize parameters and environment
   if(RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/view/")))
      ReturnError(res);
//---
   return(server->PageProcess(context, L"templates\\view.tpl", this, TW_PAGEPROCESS_NOCACHE));
  }

 

1.2. User Interface

1.2.1. Let's design user interface for the view page in the view.tpl template file. For convenience, we'll use the same tokens as for the edit page.

//---
top.TeamWox.Start(window);

// Page Header
TeamWox.Control("PageHeader","#41633C")
        .Command("<lngj:MENU_HELLOWORLD_LIST>","/helloworld/number_two","<lngj:MENU_HELLOWORLD_LIST>");

var parties={
             0:'<lngj:HELLOWORLD_REPORT_COMPILED />', 
             1:'<lngj:HELLOWORLD_REPORT_INTERPRETED />',
             2:'<lngj:HELLOWORLD_REPORT_NOCATEGORY />'
             };
//--- Data for display
var viewPage = TeamWox.Control('Layout',
    {
        type    : 'lines', // Line type of displaying. Each element of the items array is displayed in a separate line
        margin  : 5,       // Margin between lines
        items   : [
                    
                    //--- Name of programming language
                    [
                        TeamWox.Control('Text','<lngj:HELLOWORLD_NAME>').Style({"font-weight":"bold",width:"120px"}),
                        TeamWox.Control('Text','<tw:name />')
                    ],
                    //--- Category of programming language
                    [
                        TeamWox.Control('Text','<lngj:HELLOWORLD_CATEGORY>').Style({"font-weight":"bold",width:"120px"}), 
                        TeamWox.Control('Text',parties[<tw:category />])
                    ],
                    // Contents of record from file storage
                    TeamWox.Control("HtmlWrapper","<tw:description />")
        ]
    }).Style({margin:"15px"});      

1.2.2. To view records on the PageView page, we will enhance the template of the PageNumberTwo page by making rows in the 'name' column as hyperlinks. When you hover your cursor over the corresponding row, the record ID will be determined dynamically and the appropriate URL will be generated.

//--- Write data into the records array
records.push([
    {id:'number', content:data[i].id},
    {id:'name',   content:['<a href="/helloworld/number_two/view/',data[i].id,'">',data[i].name,'</a>'].join(''),
                     toolbar:[['edit',top.Toolbar.Edit],['delete',top.Toolbar.Delete]]},
    {id:'party',  content:data[i].party}
]);

Here's how it will look in the browser window.

Link to the view page is generated by the record id

 

2. WYSIWYG Editor

In the first part we have implemented saving of plain text. To edit formatted text and add various files, we will replace simple text area with convenient WYSIWYG editor. Here we face two problems, that we have to solve:

Validating HTML code. HTML code, saved in editor, may contain unsafe tags (e.g. <script>, <embed>, <object> etc.). Before you save the code it should be secured by clearing such tags. For this purpose we use the IHtmlCleaner interface.

Downloading files. In visual mode of editing users can upload images, videos and other supported formats. For this we need to implement upload handler, provided by the IRichTextEdit interface. In addition, to display uploaded files on the view page, we also need to implement the appropriate handler.

 

2.1. HTTP API

2.1.1. Let's clean up the HTML code from unsafe tags using the IHtmlCleaner interface by enhancing the CPageEdit::StoreDescription method as follows.

TWRESULT CPageEdit::StoreDescription(const Context *context,const wchar_t *description)
  {
   const wchar_t *cleaned        =NULL;
   size_t         description_len=0;
   TWRESULT       res            =RES_S_OK;
//--- Checks
   if(m_server==NULL || m_files_manager==NULL || context==NULL || description==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->user==NULL)                                                           ReturnError(RES_E_INVALID_CONTEXT);
//---
   CSmartInterface<IHtmlCleaner> cleaner;
//---
   if(RES_FAILED(res=m_server->GetInterface(L"IHtmlCleaner",(void**)&cleaner)) || cleaner==NULL)
     {
      ExtLogger(context,LOG_STATUS_ERROR) << "CPageEdit::StoreDescription: failed to get 'IHtmlCleaner' interface [" << res << "]";
      return(res);
     }
//--- Specify URL on which we will count images
   cleaner->SetImageUrl(L"/helloworld/download/");
   cleaner->SetHandler(IHtmlCleaner::HANDLER_A_BLANK);
   cleaner->SetHandler(IHtmlCleaner::HANDLER_OBJECT_VIDEO);
//--- Launch processing via cleaner and get link to the buffer with result
   if(RES_FAILED(res=cleaner->Process(context,L"PostRule",description)) || (cleaned=cleaner->GetBuffer(NULL))==NULL)
     {
      ReturnErrorExt(res,context,"failed to clean description");
     }
//---
   FileInfo description_info={0};
//--- Fill out the structure with information about the record
   description_info.type=TW_FILE_TYPE_HTML;
   description_info.size=(wcslen(cleaned)+1)*sizeof(wchar_t);
   StringCchCopy(description_info.mime_type,_countof(description_info.mime_type),L"text/html");
  • The SetImageUrl method specifies the prefix of URL, on which the downloaded files will be available in WYSIWYG editor (see below).
  • The SetHandler method sets flags to process links and downloaded videos.
  • The main Process method directly cleans up records using the PostRule rule. Then the GetBuffer method zeroizes the buffer.
Rules of cleaning HTML tags are configured in <TeamWox server>\config\ cleaner.ini.

The IHTMLCleaner interface incorporates the Release method that frees allocated memory. This method should be always called, when you end working with the interface. In order to automate this task, the specially designed template class CSmartInterface is used. In our case, the memory allocated for the interface IHTMLCleaner will be automatically released, when the StoreDescription method end its work.

CSmartInterface - is utility class of "smart" interfaces, that controls releasing of resources, when you exit out of scope.

To use the CSmartInterface class include the smart_interface.h header file into stdafx.h.

//---
#include "..\..\SDK\Common\SmartLogger.h"
#include "..\..\SDK\Common\smart_sql.h"
#include "..\..\SDK\Common\smart_interface.h"
#include "..\..\SDK\Common\tools_errors.h"
#include "..\..\SDK\Common\tools_strings.h"
#include "..\..\SDK\Common\tools_js.h"
#include "..\..\SDK\Common\Page.h"

2.1.2. In the CHelloWorldModule::ProcessPage method of processing module page implement processing of file upload request.

//--- upload  images, video, etc. from editor
   if(PathCompare(L"upload",path))
     {
      IRichTextEdit *rte=NULL;
      if(RES_SUCCEEDED(res=m_server->GetInterface(L"IRichTextEdit",(void **)&rte)) && rte!=NULL)
        {
         res=rte->Upload(context, L"/helloworld/download/");
         rte->Release();
         return(res);
        }
      //---
      ReturnError(res);
     } 

To work with files in WYSIWYG editor you need to get the IRichTextEdit interface. Its method IRichTextEdit::Upload performs all the functions to save file in file storage. As a result it returns the download URL for the uploaded file.

  • The IRichTextEdit::Upload method can be called from any module. Therefore, in order for each downloaded file to have its own unique URL you must specify the second parameter - URL prefix. For convenience, we have included the name of module into prefix for easier handling of file download URL.
  • The IRichTextEdit::Release method frees allocated memory. Method named as Release is declared in many TeamWox API interfaces. Always use this method, when you finish your work with interface.

2.1.3. Let's implement request processing of loading file contents from file storage.

  • CHelloWorldModule::ProcessPage.
//---
   if(PathCompare(L"download",path))                  return(CPageEdit().Process(context,m_server,path,&m_manager));
  • CPageEdit::Process. In the CPageEdit page class, we will take event of loading files into the separate function OnDownload.
//--- Request data
   if(PathCompare(L"download/",path))
     {
      return(OnDownload(context,manager,_wtoi64(path+9)));
     }

2.1.4. In the CPageEdit::OnDownload function we will consistently make three actions: check for record in file storage, check access right and then, if all checks are passed successfully, return the downloaded data for display.

  • Declaration.
private:
   //--- Download images and attachments
   TWRESULT          OnDownload(const Context *context,CHelloWorldManager *manager,const INT64 download_id);
  • Implementation.
#include "..\Managers\HelloWorldManager.h"
..........................................
//+------------------------------------------------------------------+
//| Process event of downloading images and attachments              |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::OnDownload(const Context *context,CHelloWorldManager *manager,const INT64 download_id)
  {
//--- Checks
   if(context==NULL || manager==NULL || m_files_manager==NULL || download_id<=0) ReturnError(RES_E_INVALID_ARGS);
   if(context->user==NULL)                                                       ReturnError(RES_E_INVALID_CONTEXT);
//---
   FileInfo  file_info={0};
   TWRESULT  res      =RES_S_OK;
//--- Check for record with attachment
   if(RES_FAILED(res=m_files_manager->FileInfoGet(context,download_id,&file_info)))
     {
      //--- Write into log only if error occurs, but not if record isn't found
      if(res!=RES_E_NOT_FOUND)
         ExtLogger(context,LOG_STATUS_ERROR) << "CPageEdit::OnDownload: failed to get file info #" << download_id;
      //---
      return(res);
     }
//--- Check access rights
   if(RES_FAILED(res=manager->CheckAccess(context,download_id,&file_info)))
      return(res);
//---
   return(m_files_manager->FileSend(context,download_id,IFilesManager::SEND_MODE_CACHE));
  }
The SEND_MODE_CACHE flag, specified in the FileSend method call, adds HTTP headers into server response, that tell browser to cache file. So, when you reload a page, request will not be sent to the server.

2.1.5. To verify the right to access records in the file repository in the manager module implement the method.

  • Declaration.
public:
   //--- Check rights to access records in file storage
   TWRESULT          CheckAccess(const Context *context,INT64 file_id,const FileInfo *info); 
  • Implementation.
//+------------------------------------------------------------------+
//| Check rights to access records in file storage                   |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::CheckAccess(const Context *context,INT64 file_id,const FileInfo *info)
  {
//--- Checks
   if(context==NULL || file_id<=0 || info==NULL) ReturnError(RES_E_ACCESS_DENIED);
   if(context->user==NULL)                       ReturnError(RES_E_ACCESS_DENIED);
//--- For temporary files
   if(info->flags&FILE_FLAG_TEMP)
     {
      //--- Here we check only by author
      if(info->author_id!=context->user->GetId()) ReturnError(RES_E_ACCESS_DENIED);
     }
   else //--- For constant
      if(!context->user->PermissionCheck(HELLOWORLD_MODULE_ID,0))
        {
         return(RES_E_ACCESS_DENIED);
        }
//---
   return(RES_S_OK);
  }

 

2.2. User Interface

2.2.1. In the edit.tpl template change the type of the Input control from textarea to rte.

[
    TeamWox.Control('Label','<lngj:HELLOWORLD_DESCRIPTION>','description').Style({"vertical-align":"top"}),
    TeamWox.Control("Input","rte","description","<tw:description />",
                    {upload:"/helloworld/upload",validate:TeamWox.Validator.NOT_EMPTY}
                   ).Style("height","500px"),
]

For the Input.Rte control we've specified two parameters:

  • upload - URL used to upload files to the server (we've implemented it in HTTP API). URL text contains the module name (from the ModuleInfo structure).

  • validate - Validation of input field contents. The NOT_EMPTY flag doesn't allow to save record, if the input field doesn't have any contents, and highlights this filed with a red frame.

2.2.2. Adjust the width of the Form control. For convenience, let's define a small margin for the frame, in which the page is displayed.

.Style('margin','5px')

Dimensions of the control are set in page template using CSS styles, passed into the Style method as parameters (see TeamWox controls documentation for more information).

 

2.3. Demonstration

Compile the module, update templates on server, run TeamWox server and open page of editing record. In editor you can add links, images and other supporting files.

Editing record in WYSIWYG editor

 

3. Attachments

To attach a file to message, you must upload it to the file storage and associated it with record ID.

Mechanism of saving attachments works as follows:

1. The POST request, sent to the server, contains files to be saved as attachments and the list of files, that must be removed from attachments list. Lists of new and deleted files are formed in user interface by the Attachments control.

2. Line of deleted files is processed and then you get the list of real IDs. Files, sent in the POST request, are copied to the file storage as temporary (FileCopy). As a result, we get 2 arrays. One contains the list of files to be deleted, the second - the list of new files.

3. Information is saved in manager. The final list of files is modified and then it is stored in DBMS record.

4. Deleting data from the file storage (FileDelete) and writing new files into the file storage (FilesCommit).

Such separation into stages is due to the fact that TeamWox can't simultaneously modify the list of attachment. In addition, any of these stages, for whatever reasons, may fail. It is unsafe to perform everything at the same time.

On the edit page we will add functionality to attach files to the message, and on the view page - displaying the list of attached files.

 

3.1. Extending Manager

3.1.1. To save the list of attachment IDs in DBMS we need to expand the record structure.

//+------------------------------------------------------------------+
//| Record structure                                                 |
//+------------------------------------------------------------------+
struct HelloWorldRecord
  {
   INT64             id;
   wchar_t           name[256];
   int               category;
   INT64             description_id;
   INT64             attachments_id[32];
  };

Here we limit attachments up to 32 files maximum. This is quite enough for everyday work (this constraint applies to modules included in standard delivery pack of TeamWox) and doesn't affect system performance. If you want to attach more files, it will be better to compress them into a single archive.

3.1.2. Create a new field in DBMS table.

//---
   if(RES_FAILED(res=sql->CheckTable("HELLOWORLD",
       "ID              BIGINT        DEFAULT 0   NOT NULL,"
       "NAME            VARCHAR(256)  DEFAULT ''  NOT NULL,"
       "CATEGORY        INTEGER       DEFAULT 0   NOT NULL,"
       "DESCRIPTION_ID  BIGINT DEFAULT 0 NOT NULL,"
       "ATTACHMENTS_ID  CHAR(256) CHARACTER SET OCTETS",
       "PRIMARY KEY (ID)",
       "DESCENDING INDEX IDX_HELLOWORLD_ID_DESC (ID)",
                                             NULL,
                                             NULL,
                                              0)))
      ReturnError(res);

3.1.3. In the CHelloWorldManager::InfoGet, CHelloWorldManager::Get and CHelloWorldManager::Update methods accordingly amend the texts of SQL queries and binding to parameters of request (for details see file attached to the article).

3.1.4. To process the list of attachments, create the new structure in manager. This structure will contain two lists. Files from the first list will be committed (FilesCommit), and files from the second will be deleted from file storage.

//+------------------------------------------------------------------+
//| Record with data to modify list of attachments                   |
//+------------------------------------------------------------------+
struct AttachmentsModify
  {
   INT64             new_files[32];
   INT64             deleted_files[32];
  };

 

3.2. Processing Request

3.2.1. As files are attached on the edit page, we will modify the event handling method CPageEdit::OnUpdate. In particular, we will add the new stage of saving attachments into file storage and update the corresponding method of saving records in DBMS table. Similarly to the method of saving records in file storage, we will take saving of attached files into the separate method StoreAttachments.

//---
AttachmentsModify attachments={0};
............................................
//--- Saving attachments
   if(RES_FAILED(res=StoreAttachments(context,&attachments)))
      ExtLogger(context,LOG_STATUS_ERROR) << "failed to store attachments";
//--- Saving record in DBMS
   if(RES_FAILED(res=manager->Update(context,&m_record,&attachments)))
      ReturnErrorExt(res,context,"failed to update record");

3.2.2. StoreAttachments - attachments processing.

  • Declaration
TWRESULT          StoreAttachments(const Context *context,AttachmentsModify *attachments);
  • Implementation. Here, the attachments - is the name of parameter of new files list, that is used in user interface when creating the Attachments control. The name of deleted files list parameter is supplemented with the _deleted postfix, i.e. in our case - attachments_deleted.

//+------------------------------------------------------------------+
//| Saving attachments                                               |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::StoreAttachments(const Context *context,AttachmentsModify *attachments)
  {
//--- Checks
   if(m_server==NULL || m_manager==NULL || m_files_manager==NULL || context==NULL || attachments==NULL) 
      ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL)                                                                           
      ReturnError(RES_E_INVALID_CONTEXT);
//---
   FileDescription    file_desc={0};
   size_t             count    =0;
   INT64              attach_id=0;
   IRequest::Iterator it       ={0};  // iterator for request of attachments series
   TWRESULT           res      =RES_S_OK;
   const wchar_t     *str      =NULL;
//--- DELETED FILES
   if(context->request->Exist(IRequest::POST,L"attachments_deleted"))
     {
      //--- Parse string and save deleted files
      count=_countof(attachments->deleted_files);
      str  =context->request->GetString(IRequest::POST,L"attachments_deleted");
      CHelloWorldManager::IdStringParse(str,attachments->deleted_files,&count,m_record.attachments_id,_countof(m_record.attachments_id));
     }
//--- NEW FILES
   count=0;
   context->request->PrepareIterator(&it);
   while(context->request->GetFile(L"attachments",NULL,&file_desc,&it) && count<_countof(attachments->new_files))
     {
      attach_id=0;
      if(RES_SUCCEEDED(res=m_manager->StoreAttachnmentFile(context,m_record.id,&file_desc,&attach_id)) && attach_id>0)
        {
         attachments->new_files[count++]=attach_id;
        }
     }
//---
   return(RES_S_OK);
}

If you want to delete existing attachments, first the string of POST request is processed. From this string the IdStringParse utility function (see its implementation in the project, attached to this article) extracts list of IDs of files to be deleted.

Then new files are copied into the file storage and their list is generated.

3.2.3. In manager, implement the new method StoreAttachnmentFile, that copies an attachment into the file storage.

This is the key moment, as data uploaded in POST request are not saved directly into file storage, but are written as temporary files on disk. And then, if this stage is successfully passed, in CHelloWorldManager::Update we will confirm the transaction.

This approach provides greater safety when working with file storage and reduces the server load.

  • Description
public:
//--- Saving attachment
   TWRESULT          StoreAttachnmentFile(const Context *context,INT64 record_id,FileDescription *file_desc,INT64 *file_id);
   
  • Implementation
//+------------------------------------------------------------------+
//| Saving attachment in file storage                                |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::StoreAttachnmentFile(const Context *context,INT64 record_id,FileDescription *file_desc,INT64 *file_id)
  {
//--- Checks
   if(context==NULL || file_desc==NULL || file_id==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(m_files_manager==NULL)                             ReturnError(RES_E_FAIL);
//---
   TWRESULT res      =RES_S_OK;
   FileInfo file_info={0};
//--- Prepare data to save
   CopyString(file_info.mime_type,file_desc->mime_type);
   CopyString(file_info.filename, file_desc->filename);
//---
   file_info.size     =file_desc->size;
   file_info.type_id  =0;
   file_info.record_id=record_id;
//---
   if(RES_FAILED(res=m_files_manager->FileCopy(context,file_desc->path,&file_info,0,file_id,false)))
     {
      ExtLogger(context, LOG_STATUS_ERROR) << "failed store file for record #" << record_id;
      //---
      return(res);
     }
//---
   return(RES_S_OK);
  }

 

3.3. Saving Information in Manager

3.3.1. Generate the final list of attachments. For this include the new stage of request processing into CHelloWorldManager::Update.

//+------------------------------------------------------------------+
//| Adding and saving record in HELLOWORLD table                     |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::Update(const Context *context,HelloWorldRecord *record,AttachmentsModify *attachments)
  {
   TWRESULT res      =RES_S_OK;
   size_t   count    =0;
   size_t   new_count=0;
//--- Checks
   if(context==NULL || m_server==NULL || record==NULL || attachments==NULL) 
      ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)                                                   
      ReturnError(RES_E_INVALID_CONTEXT);
//--- Prepare list of attachments
   if(!PrepareAttachments(record,attachments,&new_count))                
      ReturnErrorExt(RES_E_FAIL,context,"failed to prepare attachments");

3.3.2. Implementation of CHelloWorldManager::PrepareAttachments.

  • Description
private:
//---
   static bool       PrepareAttachments(HelloWorldRecord *record,AttachmentsModify *attachments,size_t *new_count);
  • Implementation
//+------------------------------------------------------------------+
//| Generate list of attachments                                     |
//+------------------------------------------------------------------+
bool CHelloWorldManager::PrepareAttachments(HelloWorldRecord *record,AttachmentsModify *attachments,size_t *new_count)
  {
//--- Checks
   if(record==NULL || attachments==NULL || new_count==NULL) ReturnError(false);
//---
   size_t count         =0;
   INT64  files_list[32]={0};
   size_t files_index   =0;
//--- Generate new list of attachments
//--- 1. Delete file IDs from list
   for(size_t i=0;i<_countof(record->attachments_id) && files_index<_countof(files_list) && record->attachments_id[i]>0;i++)
     {
      bool found=false;
      for(size_t j=0;j<_countof(attachments->deleted_files) && attachments->deleted_files[j]>0;j++)
        {
         if(record->attachments_id[i]==attachments->deleted_files[j])
           {
            found=true;
            break;
           }
        }
      //---
      if(!found) files_list[files_index++]=record->attachments_id[i];
     }
//--- 2. Add new
   for((*new_count)=0;
       (*new_count)<_countof(attachments->new_files) && files_index<_countof(files_list) && attachments->new_files[(*new_count)]>0;
       (*new_count)++)
     {
      files_list[files_index++]=attachments->new_files[(*new_count)];
     }
//--- 3. Copy list into record
   memcpy(record->attachments_id,files_list,sizeof(files_list));
//---
   return(true);
  }

 

3.4. Updating Records in File Storage

Once in the CHelloWorldManager::Update we have finalized the list of attachments, you must update records in the file storage.

3.4.1. Delete attachments from file storage.

//--- Delete files
   for(size_t i=0;i<_countof(attachments->deleted_files) && attachments->deleted_files[i]>0;i++)
     {
      if(RES_FAILED(res=m_files_manager->FileDelete(context,attachments->deleted_files[i])))
         ExtLogger(context,LOG_STATUS_ERROR) << "failed to delete attachments file #" 
                                             << attachments->deleted_files[i] << " [" << res << "]";
     }

3.4.2. Commit new attachments.

//--- Commit added attachments
   count=0;
   for(;count<_countof(attachments->new_files) && count<new_count && attachments->new_files[count]!=0;count++);
   if(count>0)
      m_files_manager->FilesCommit(context,attachments->new_files,(int)count,0,record->id);

3.4.3. Delete record from the HELLOWORLD table (CHelloWorldManager::InfoDelete).

//--- Delete attachments
   for(size_t i=0;i<_countof(record.attachments_id);i++)
     {
      if(record.attachments_id[i]<=0) continue;
      //---
      if(RES_FAILED(res=m_files_manager->FileDelete(context,record.attachments_id[i])))
         ExtLogger(context, LOG_STATUS_ERROR) << "failed to delete attachments #" << record.attachments_id[i];
     }

 

3.5. User Interface

To add/edit attachments in page user interface, the Attachments control is used, and to display list of attachments - the AttachmentsList control.

3.5.1. Page of editing records. After WYSIWYG editor area add the control to attach files.

[
   TeamWox.Control('Label',''), 
   TeamWox.Control("Attachments","attachments",[<tw:attachments />],
                   "/helloworld/download/",top.TeamWox.ATTACHMENTS_ALL,"/helloworld/upload")
]

The first parameter - is the name of POST variable with added files. The second parameter - is the list of attachments, implemented in the <tw:attachments /> token. The third parameter - is the URL prefix, by which attachments will be available for download. The fourth (optional) parameter - is the flag of attachments. In this case, the ATTACHMENTS_ALL flag allows you to attach all types of files created in TeamWox. The fifth parameter - is the URL to upload attachments to the server.

3.5.2. The view page. After WYSIWYG editor area add the control, that displays attachments.

TeamWox.Control("AttachmentsList",[<tw:attachments />],"/helloworld/download/")

Here everything is similar. The first parameter - is the list of attachments, the second - is the URL prefix for their download.

3.5.3. In the edit page (CPageEdit::Tag) and the view page (CPageView::Tag) implement the <tw:attachments /> token.

//---
   if(TagCompare(L"attachments",tag))
     {
      IAttachments *attachments=NULL;
      size_t        count      =0;
      //---
      if(RES_FAILED(m_server->GetInterface(TWX_SERVER,L"IAttachments",(void **)&attachments)) || attachments==NULL) 
        return(false);
      //--- Calculate count
      for(;count<_countof(m_record.attachments_id) && m_record.attachments_id[count]!=0;count++);
      //---
      attachments->WriteList(context,m_server,m_record.attachments_id,count);
      //---
      return(false);
     }

 

3.6. Demonstration

3.6.1. Compile the module, update templates on server, run TeamWox server and open page of editing record. In WYSIWYG editor you can now attach files.

Attaching a File in a WYSIWYG Editor

3.6.2. After saving the record, the view page displays the list of attachments.

Attachment List

 

4. Comments

r4.1. Getting Interface to Work With Comments

4.1.1. To work with comments TeamWox API provides the IComments interface. Let's get it in CHelloWorldManager.

  • Description
private:
   IComments        *m_comments_manager;               // Comments manager
  • Implementation
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHelloWorldManager::CHelloWorldManager() : m_server(NULL), m_files_manager(NULL), m_comments_manager(NULL), m_next_id(0)
  {
//---
//---
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CHelloWorldManager::~CHelloWorldManager()
  {
//---
   m_server          =NULL;
   m_comments_manager=NULL;
   m_files_manager   =NULL;
//---
  }
  • Module initialization - CHelloWorldManager::Initialize(IServer *server, int prev_build)
//---
   if(RES_FAILED(res=m_server->GetInterface(L"IComments",(void**)&m_comments_manager)) || m_comments_manager==NULL)
      ReturnErrorExt(res,NULL,"failed to get IComments interface");

4.1.2. Similarly to the IFilesManager interface, the IComments interface is got once and then it is not modified. And also for convenience, in its declaration we will implement method, that allows you to quickly get this interface while processing requests for pages.

public:
   IComments*        CommentsManagerGet() { return(m_comments_manager); };

 

4.2. HTTP API

Basic functionality of working with comments includes saving, reading and deleting comments. We will implement the corresponding methods in the view page (PageView).

Since each module has its own system of checking rights to access specific record, processing of necessary HTTP requests is implemented in each module. Before calling method of corresponding interface, each module implements and checks access rights to the record, which a comment or a file are corresponding to.

4.2.1. In the CPageView class declare three event-handling methods for comments.

public:
   //--- Get comments list in JSON format
   TWRESULT          OnCommentGet(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);
   //--- Add/Update comment
   TWRESULT          OnCommentUpdate(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);
   //--- Delete comment
   TWRESULT          OnCommentDelete(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);

4.2.2. In these methods, we will save comments into file storage and display comments list on a page. For this let's declare two appropriate methods. The main method WriteComments will save comments into file storage, and the ShowCommentsJSON method will display comments data on a page.

private:
   //--- Display comments list in JSON format
   bool              ShowCommentsJSON(const Context *context);
   //--- Write comment
   bool              WriteComments(const Context *context);

4.2.3. In the CHelloWorldModule::ProcessPage method add appropriate rules of redirecting requests.

if(PathCompare(L"number_two/comment/get",path))    return(CPageView().OnCommentGet(context,m_server,path,&m_manager));
if(PathCompare(L"number_two/comment/update",path)) return(CPageView().OnCommentUpdate(context,m_server,path,&m_manager));
if(PathCompare(L"number_two/comment/delete",path)) return(CPageView().OnCommentDelete(context,m_server,path,&m_manager));

4.2.4. Implementation of the WriteComments method.

ICommentsPage - is the interface of page, that controls the user interface of comments. By passing the object of this class by reference into the IComments::PageCreate method, we create a page to display comments.

//+------------------------------------------------------------------+
//| Displays comments                                                |
//+------------------------------------------------------------------+
bool CPageView::WriteComments(const Context *context)
  {
//---
   if(context==NULL || m_comments_manager==NULL) ReturnError(false);
//---
   CSmartInterface<ICommentsPage> page;
//---
   if(RES_FAILED(m_comments_manager->PageCreate(&page)) || page==NULL)
      ReturnErrorExt(false,context,"failed to create comments page");
//---
   page->SetPerPage(COMMENTS_PER_PAGE);
   page->SetFlags(0,ICommentsPage::SHOW_NEW | 
                    ICommentsPage::SHOW_REPLY | 
                    ICommentsPage::SHOW_LARGE | 
                    ICommentsPage::SHOW_CHECK_DAYS |
                    ICommentsPage::SHOW_DOCS_COPY);
//---
   page->ShowJSON(context,HELLOWORLD_COMMENT_TYPE_COMMENTS,m_record.id);
//---
   return(true);
  }

The ICommentsPage::SetPerPage method specifies the number of comments per page. When specified value is exceeded, the user interface will automatically create the PageNumerator control. Let's define the number of comments in the PageView class.

private:
   enum
     {
      COMMENTS_PER_PAGE=25
     } 

The ICommentsPage::SetFlags method sets flags that define the set of commands in comments user interface. In our case, it will show the following commands: creating new comment (SHOW_NEW), reply to comment (SHOW_REPLY), copy attachments into the Documents module (SHOW_DOCS_COPY). Also, the SHOW_LARGE flag sets the increased size of comment adding window. The SHOW_CHECK_DAYS flag prohibits comments editing, when allowed date (in days), specified by the administrator in the Settings tab of the Administration module, has been expired.

Define the type of comment in the EnHelloWorldCommentsType enumeration, which we will declare in manager. In this case, we will have only one type of comment posted by users. Your module may also have system comments (like in the Service Desk module, when you change the list of assigned, category, status, etc.).

enum EnHelloWorldCommentsType
  {
   HELLOWORLD_COMMENT_TYPE_COMMENTS=0x01
  };

Finally, the ICommentsPage::ShowJSON method displays comments data in JSON format.

4.2.5. Implementation of the ShowCommentsJSON method. The ShowCommentsJSON method stows comments data into JSON object, and before this it sets the value for the Content-Type HTTP header.

//+------------------------------------------------------------------+
//| Displays comments                                                |
//+------------------------------------------------------------------+
bool CPageView::ShowCommentsJSON(const Context *context)
  {
//--- Checks
   if(context==NULL)           ReturnError(false);
   if(context->response==NULL) ReturnError(false);
//---
   context->response->SetHeader(HTTP_HEADER_CONTENTTYPE,"application/json; charset=utf-8");
//---
   CJSON json(context->response);
   json << CJSON::OBJECT;
//--- Display comments
   json << L"comments" << CJSON::OBJECT;
   WriteComments(context);
   json << CJSON::CLOSE;
//---
   json << CJSON::CLOSE;
//---
   return(true);
  }

4.2.6. Implementation on event handling methods for comments.

  • OnCommentGet. Here everything is pretty simple - using the InitParams template function we process request and display comments using the ShowCommentsJSON method.
//+------------------------------------------------------------------+
//| Get list of comments in JSON format                              |
//+------------------------------------------------------------------+
TWRESULT CPageView::OnCommentGet(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager)
  {
   INT64    id =0;
   TWRESULT res=RES_S_OK;
//--- Checks
   if(context==NULL || server==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->response==NULL)                                      ReturnError(RES_E_INVALID_CONTEXT);
//--- Initialize parameters and environment
   if(RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/comment/get/")))
      ReturnError(res);
//---
   ShowCommentsJSON(context);
//---
   return(RES_S_OK);
  }
  • OnCommentUpdate. Saving new or modified comment is performed by the OnDelete method, provided by the ICommentsPage interface.
//+------------------------------------------------------------------+
//| Adding/Updating comment                                          |
//+------------------------------------------------------------------+
TWRESULT CPageView::OnCommentUpdate(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager)
  {
   INT64    id =0;
   TWRESULT res=RES_S_OK;
//--- Checks
   if(context==NULL || server==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->response==NULL || context->request==NULL)            ReturnError(RES_E_INVALID_CONTEXT);
//--- Initialize parameters and environment
   if    (RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/comment/update/")))
      ReturnError(res);
//---
   CSmartInterface<ICommentsPage> page;
//--- Create page and give control to it
   if(RES_FAILED(res=m_comments_manager->PageCreate(&page)) || page==NULL)
      ReturnErrorExt(res,context,"failed to create comments page");
//---
   page->SetSearchId(HELLOWORLD_COMMENT_SEARCH_ID);
   page->SetRecordTitle(m_record.name);
//---
   if(RES_SUCCEEDED(res=page->OnUpdate(context,HELLOWORLD_COMMENT_TYPE_COMMENTS,m_record.id,L"/helloworld/download/",0,NULL,0,NULL)))
     {
      context->response->SetHeader(HTTP_HEADER_CONTENTTYPE,"text/plain; charset=utf-8");
      context->response->Write(L"OK");
     }
//---
   return(res);
  }

The ICommentsPage::SetSearchId method sets the search ID of comment type, used in the search module. Implementation of search and filtering will be considered in one of the following articles.

The ICommentsPage::SetRecordTitle method sets the name for the record in file storage. Then, once the ICommentsPage::OnUpdate method executed successfully, the comment data are saved into file storage with reference to the record ID of original message. In the end, we set the value for the Content-Type HTTP header and return the successful result of execution.

  • OnCommentDelete. Deleting comment for the message by specified record ID. For this purpose we use the OnDelete method, provided by the ICommentsPage interface.
//+------------------------------------------------------------------+
//| Delete comment                                                   |
//+------------------------------------------------------------------+
TWRESULT CPageView::OnCommentDelete(const     Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager)
  {
   TWRESULT res=RES_S_OK;
//--- Checks
   if(context==NULL || server==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->response==NULL || context->request==NULL)            ReturnError(RES_E_INVALID_CONTEXT);
//--- Initialize parameters and environment
   if(RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/comment/delete/")))
      ReturnError(res);
//---
   CSmartInterface<ICommentsPage> page;
//--- Create page and give control to it
   if(RES_FAILED(res=m_comments_manager->PageCreate(&page)) || page==NULL)
      ReturnErrorExt(res,context,"failed to create comments page");
//---
   if(RES_SUCCEEDED(res=page->OnDelete(context,HELLOWORLD_COMMENT_TYPE_COMMENTS,m_record.id)))
     {
      context->response->SetHeader(HTTP_HEADER_CONTENTTYPE,"text/plain; charset=utf-8");
      context->response->Write(L"OK");
     }
//---
   return(res);
  }

4.2.7. To display comments in user interface of page, add handler for the <tw:comments /> token. To this end, in the CPageView::Tag method add the following block of code.

//---
   if(TagCompare(L"comments",tag))
     {
      WriteComments(context);
      //---
      return(false);
     }

 

4.3. User Interface

Now we need to extend the user interface of the view page to be able to work with comments. For this purpose TeamWox leverages the Comments control.

4.3.1. In the view.tpl template, after the code of the view page, add the Comments control.

//--- Comments
var m_comments_data, m_comments;
//---
m_comments_data = {<tw:comments />};

m_comments = TeamWox.Control("Comments",m_comments_data,
            {updateUrl:"/helloworld/number_two/comment/update/<tw:id/>",
             deleteUrl:"/helloworld/number_two/comment/delete/<tw:id/>",
             uploadUrl:"/helloworld/upload",
             attachLink:"/helloworld/download/"})
             .Append("oncontent",LoadComments);

Comments data are taken from the previously implemented <tw:comments /> token. For the Comments control we've specified four URLs as its parameters: requests to change and to delete comment by ID, requests to add and to download attachments.

4.3.2. To update the contents of comments, add the LoadComments custom handler function for the oncontent JavaScript event. In the implemented handler, an AJAX request is sent to server. As as result, the list of comments is updated without reloading the page contents.

//--- Process updated contents of comments
function LoadComments(from,perpage)
  {
   TeamWox.LockFrameContent();
   // Temporary measure, organize request per page
   var page = Math.ceil(from / perpage)+1;
   TeamWox.Ajax.get("/helloworld/number_two/comment/get/<tw:id />",{json:'',p_comment:page},
     {
      onready:function (text)
        {
         try
           {
            var data = TeamWox.Ajax.json(text);
            if(data.comments)
              m_comments.Show(data.comments);
           }
         catch(e)
           {
            alert(e.message);
           }
         TeamWox.LockFrameContent(true);
        },
      onerror:function ()
        {
         TeamWox.LockFrameContent(true);
        }
     });
  }

 

4.4. Demonstration

4.4.1. Compile the module, update templates on server, run TeamWox server and open the record page. Below the page with message now you can see the user interface of adding comments.

User interface of adding comments

4.4.2. When you add comments, all the features of adding attachments are available to you. You don't need to implement additionally - all the required functionality is used by the IComments interface automatically.

Comment to record

 

Conclusion

In future articles we will highlight several important topics of TeamWox modules development: searching and filtering, caching of SQL requests, data import/export, etc.


helloworld-filestorage-part2-en.zip (195.77 KB)

2011.03.02