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
andICommentsPage
- 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
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.
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 thePostRule
rule. Then theGetBuffer
method zeroizes the buffer.
<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 asRelease
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 theCPageEdit
page class, we will take event of loading files into the separate functionOnDownload
.
//--- 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)); }
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 theModuleInfo
structure).validate
- Validation of input field contents. TheNOT_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.
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 theAttachments
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.
3.6.2. After saving the record, the view page displays the list of attachments.
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 theInitParams
template function we process request and display comments using theShowCommentsJSON
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 theOnDelete
method, provided by theICommentsPage
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 theOnDelete
method, provided by theICommentsPage
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.
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.
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.
- How to Add a Ready-made Module to TeamWox
- How to Add Page into TeamWox Module
- Building User Interface
- Interaction with DBMS
- Creating Custom Reports
- TeamWox File Storage - Part 1
- TeamWox File Storage - Part 2
- Setting Up Custom Modules Environment - Part 1
- Setting Up Custom Modules Environment - Part 2
- Search and Filtering - Part 1
- Search and Filtering - Part 2
- Setting Up Online Assistant On Your Site
- How To Create Additional Language Package For TeamWox
2011.03.02