TeamWox SDK: Файловое хранилище - Часть 2

Введение

В первой части статьи вы узнали, как работать с файловым хранилищем TeamWox, используя методы интерфейса IFilesManager. В качестве примера, мы реализовали сохранение простого текста.

В этой статье мы рассмотрим, какие инструменты предоставляет TeamWox API для решения задач, наиболее часто встречающихся при разработке пользовательских модулей - валидация HTML кода, прикрепление файлов, добавление комментариев. Для реализации этих возможностей мы расширим функционал страницы редактирования, а также добавим новую страницу для просмотра данных из файлового хранилища и СУБД.

Перечислим кратко интерфейсы, которые мы будем реализовывать в модуле Hello World:

  • IRichTextEdit - загрузка файлов в WYSIWYG редакторе;
  • IHTMLCleaner - обработка HTML кода страниц и удаление потенциально опасных тэгов;
  • IAttachments - работа с прикрепленными файлами;
  • IComments и ICommentsPage - работа с комментариями.

Функционал будет достаточно широкий, реализовывать его мы будем поэтапно.

1. Страница просмотра
  1.1. HTTP API
  1.2. Пользовательский интерфейс

2. WYSIWYG-редактор
  2.1. HTTP API
  2.2. Пользовательский интерфейс
  2.3. Демонстрация работы

3. Прикрепленные файлы
  3.1. Расширение менеджера
  3.2. Выполнение и обработка запроса
  3.3. Сохранение информации в менеджере
  3.4. Обновление записей в файловом хранилище
  3.5. Пользовательский интерфейс
  3.6. Демонстрация работы

4. Комментарии
  4.1. Получение интерфейса для работы с комментариями
  4.2. HTTP API
  4.3. Пользовательский интерфейс
  4.1. Демонстрация работы

 

Прикрепленный к статье файл содержит исходные коды модуля Hello World со всеми описываемыми изменениями.

 

1. Страница просмотра

В модуле Hello World создадим новую страницу PageView, на которой можно будет просматривать содержимое записи из файлового хранилища (чуть позже на этой же странице можно будет добавлять комментарии).

 

1.1. HTTP API

1.1.1. В проекте HelloWorld создайте новый класс страницы CPageView, аналогичный классу страницы редактирования CPageEdit. В этот класс помимо стандартных методов Process (обработка страницы) и Tag (отображение данных, содержащихся в токенах) мы также скопируем метод чтения данных из файлового хранилища CheckDescription, а также объявим в нем новый шаблонный метод InitParams, который в дальнейшем будет удобно использовать в HTTP API для обработки запросов.

//+------------------------------------------------------------------+
//| Страница просмотра содержимого записей                           |
//+------------------------------------------------------------------+
class CPageView : public CPage
  {
..........................................
   //--- обработчик
   TWRESULT          Process(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);
   //--- функции показа
   bool              Tag(const Context *context,const TagInfo *tag);

private:
   //--- инициализация параметров
   template <size_t length>
   TWRESULT          InitParams(const Context *context,IServer *server,
                                const wchar_t *path,CHelloWorldManager *manager,const wchar_t (&url)[length]);
   //--- чтение данных из файлового хранилища в память
   void              CheckDescription();
};
//+------------------------------------------------------------------+

1.1.2. В методе InitParams инициализируется страница для дальнейшего просмотра данных и происходит парсинг URL для получения ID записи, которую нужно показать. Это первый этап обработки запросов для любого из обработчиков API, поэтому для удобства мы вынесем его в отдельную шаблонную функцию.

//+------------------------------------------------------------------+
//| Парсим путь и получаем менеджеры                                 |
//+------------------------------------------------------------------+
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;
//--- проверки
   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);
//--- запросим данные
   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. В методе CHelloWorldModule::ProcessPage добавьте новое правило перенаправления запросов на страницу просмотра.

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

1.1.4. Обработаем запрос на страницу просмотра CPageView::Process.

//+------------------------------------------------------------------+
//| Обработка запроса                                                |
//+------------------------------------------------------------------+
TWRESULT CPageView::Process(const Context *context,
                            IServer *server,
                            const wchar_t *path,
                            CHelloWorldManager *manager)
  {
   TWRESULT res=RES_S_OK;
   INT64    id =0;
//--- проверки
   if(context==NULL || path==NULL)   ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL)        ReturnError(RES_E_INVALID_CONTEXT);
//--- получаем сервер
   if(server==NULL || manager==NULL) ReturnError(RES_E_FAIL);
//--- инициализируем параметры и окружение
   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. Пользовательский интерфейс

1.2.1. Спроектируем пользовательский интерфейс страницы просмотра в шаблоне view.tpl. Для удобства будем использовать такие же токены, что и для страницы редактирования.

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

// Шапка страницы
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 />'
             };
//--- Данные для отображения
var viewPage = TeamWox.Control('Layout',
    {
        type    : 'lines', // Строчный тип отображения. Каждый элемент массива items отображается в отдельной строке
        margin  : 5,       // Отступ между строками
        items   : [
                    
                    //--- Название языка программирования
                    [
                        TeamWox.Control('Text','<lngj:HELLOWORLD_NAME>').Style({"font-weight":"bold",width:"120px"}),
                        TeamWox.Control('Text','<tw:name />')
                    ],
                    //--- Категория языка программирования
                    [
                        TeamWox.Control('Text','<lngj:HELLOWORLD_CATEGORY>').Style({"font-weight":"bold",width:"120px"}), 
                        TeamWox.Control('Text',parties[<tw:category />])
                    ],
                    // Содержимое записи из файлового хранилища
                    TeamWox.Control("HtmlWrapper","<tw:description />")
        ]
    }).Style({margin:"15px"});      

1.2.2. Чтобы просмотреть записи на странице PageView, мы дополним шаблон страницы PageNumberTwo, сделав строки в столбце name гиперссылками. При наведении курсора на соответствующую строку будет динамически определяться ID записи и формироваться соответствующий URL.

//--- Записываем данные в массив records
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}
]);

Вот как это будет выглядеть в окне браузера.

Ссылка на страницу просмотра формируется по id записи

 

2. WYSIWYG редактор

В первой части статьи мы реализовали сохранение простого текста. Для редактирования текста с возможностью форматирования и добавления различных файлов, обычное поле ввода мы заменим удобным WYSIWYG редактором. Здесь сразу же возникают 2 задачи, которые нам предстоит решить:

Валидация HTML кода. Сохраняемый в редакторе HTML код может содержать небезопасные тэги (например, <script>, <embed>, <object> и т.д.). Перед сохранением код следует обезопасить, очистив такие тэги. Для этой цели мы применим интерфейс IHtmlCleaner.

Загрузка файлов. В визуальном режиме редактирования пользователи могут загружать изображения, видео и другие поддерживаемые форматы. Для этого нам потребуется реализовать обработчик загрузки, который предоставляет интерфейс IRichTextEdit. Кроме того, для отображения загруженных файлов на странице просмотра нам также потребуется реализовать соответствующий обработчик.

 

2.1. HTTP API

2.1.1. Очистим HTML код от небезопасных тэгов с помощью интерфейса IHtmlCleaner, дополнив метод CPageEdit::StoreDescription следующим образом.

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;
//--- проверки
   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);
     }
//--- укажем URL на котором подсчитываем картинки
   cleaner->SetImageUrl(L"/helloworld/download/");
   cleaner->SetHandler(IHtmlCleaner::HANDLER_A_BLANK);
   cleaner->SetHandler(IHtmlCleaner::HANDLER_OBJECT_VIDEO);
//--- запускаем обработку через чистильщик и получим ссылку на буфер с результатом
   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};
//--- заполним структуру с информацией о записи
   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");
  • Методом SetImageUrl указывается префикс URL, по которому будут доступны загруженные в редакторе файлы (об этом ниже).
  • Методом SetHandler устанавливаются флаги обработки ссылок и загруженных видеофайлов.
  • Основной метод Process непосредственно выполняет очистку записи по заданному правилу PostRule. Затем методом GetBuffer обнуляем буфер.
Правила очистки HTML-тэгов настраиваются в файле <сервер TeamWox>\config\cleaner.ini.

Интерфейс IHTMLCleaner имеет в своем составе метод Release, освобождающий выделенную память. Этот метод следует обязательно вызывать после завершения работы с интерфейсом. Для автоматизации этой задачи используется специально созданный шаблонный класс CSmartInterface. В нашем случае память, выделенная для интерфейса IHTMLCleaner, будет автоматически освобождена по окончании работы метода StoreDescription.

CSmartInterface - это утилитный класс "умных" интерфейсов, который управляет освобождением ресурсов при выходе за пределы области видимости.

Для использования класса CSmartInterface включите заголовочный файл smart_interface.h в 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. В методе обработки страниц модуля CHelloWorldModule::ProcessPage реализуем обработку запроса выгрузки файлов.

//--- загрузка рисунков, фильмов из редактора
   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);
     } 

Для работы с файлами в редакторе необходимо получить интерфейс IRichTextEdit. Его метод IRichTextEdit::Upload выполняет все функции по сохранению файла в хранилище, а вкачестве результата сообщает редактору URL, по которому загруженный файл будет доступен для скачивания.

  • Метод IRichTextEdit::Upload может вызываться из любого модуля. Поэтому, чтобы каждый загруженный файл имел свой уникальный URL, вторым параметром указывается префикс URL. Для удобства, в префикс мы включили имя модуля, чтобы было проще обрабатывать URL загрузки сохраненного файла.
  • Методом IRichTextEdit::Release освобождается выделенная память. Метод с именем Release объявлен во многих интерфейсах TeamWox API. Используйте такой метод всегда по завершении работы с интерфейсом.

2.1.3. Реализуем обработку запроса загрузки содержимого из файлового хранилища.

  • CHelloWorldModule::ProcessPage.
//---
   if(PathCompare(L"download",path))                  return(CPageEdit().Process(context,m_server,path,&m_manager));
  • CPageEdit::Process. В классе страницы CPageEdit событие загрузки файлов вынесем в отдельную функцию OnDownload.
//--- запросим данные
   if(PathCompare(L"download/",path))
     {
      return(OnDownload(context,manager,_wtoi64(path+9)));
     }

2.1.4. В функции CPageEdit::OnDownload мы последовательно выполним три действия: проверим наличие записи в файловом хранилище, проверим права доступа, а затем, если проверки пройдены успешно, отдадим загруженные данные для отображения.

  • Описание.
private:
   //--- Загрузка изображений и прикрепленных файлов
   TWRESULT          OnDownload(const Context *context,CHelloWorldManager *manager,const INT64 download_id);
  • Реализация.
#include "..\Managers\HelloWorldManager.h"
..........................................
//+------------------------------------------------------------------+
//| Обработка события скачивания изображений и прикрепленных файлов  |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::OnDownload(const Context *context,CHelloWorldManager *manager,const INT64 download_id)
  {
//--- проверки
   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;
//--- проверим на наличие комментария, у которого запрашивают приложенный файл
   if(RES_FAILED(res=m_files_manager->FileInfoGet(context,download_id,&file_info)))
     {
      //--- запишем в лог при ошибке, а не то что не смогли найти
      if(res!=RES_E_NOT_FOUND)
         ExtLogger(context,LOG_STATUS_ERROR) << "CPageEdit::OnDownload: failed to get file info #" << download_id;
      //---
      return(res);
     }
//--- проверим права доступа
   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, заданный при вызове метода FileSend, добавляет в ответ сервера HTTP заголовки, которые указывают браузеру кешировать файл, чтобы при повторной загрузке страницы не посылать запрос на сервер.

2.1.5. Для проверки права доступа к записям в файловом хранилище, в менеджере модуля реализуем соответствующий метод.

  • Описание.
public:
   //--- Проверка прав доступа к записям в фаловом хранилище
   TWRESULT          CheckAccess(const Context *context,INT64 file_id,const FileInfo *info); 
  • Реализация.
//+------------------------------------------------------------------+
//| Проверка прав доступа к записям в файловом хранилище              |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::CheckAccess(const Context *context,INT64 file_id,const FileInfo *info)
  {
//--- проверки
   if(context==NULL || file_id<=0 || info==NULL) ReturnError(RES_E_ACCESS_DENIED);
   if(context->user==NULL)                       ReturnError(RES_E_ACCESS_DENIED);
//--- для временных файлов
   if(info->flags&FILE_FLAG_TEMP)
     {
      //--- здесь проверка только по автору
      if(info->author_id!=context->user->GetId()) ReturnError(RES_E_ACCESS_DENIED);
     }
   else //--- для постоянных
      if(!context->user->PermissionCheck(HELLOWORLD_MODULE_ID,0))
        {
         return(RES_E_ACCESS_DENIED);
        }
//---
   return(RES_S_OK);
  }

 

2.2. Пользовательский интерфейс

2.2.1. В шаблоне edit.tpl измените тип элемента управления Input с textarea на 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"),
]

Для элемента управления Input.Rte мы указали два параметра:

  • upload - URL, по которому будет осуществляться выгрузка файлов на сервер (его мы реализовали в HTTP API). В текст URL входит имя модуля (из структуры ModuleInfo).

  • validate - Валидация содержимого поля ввода. Флаг NOT_EMPTY не дает сохранить запись, если в поле ввода отсутствует содержимое, и подсвечивает поле красной рамкой.

2.2.2. Настройте ширину элемента управления Form. Для удобства зададим небольшой отступ от краев соседних элементов фрейма, в котором отображается страница.

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

Размеры элементов управления задаются в шаблоне страницы с помощью стилей CSS, которые метод Style принимает в качестве параметров (подробнее см. документацию по элементам управления TeamWox).

 

2.3. Демонстрация работы

Скомпилируйте модуль, обновите шаблоны на сервере, запустите сервер TeamWox и откройте страницу редактирования записи. В редакторе можно добавлять ссылки, изображения и другие поддерживаемые файлы.

Редактирование записи в WYSIWYG-редакторе

 

3. Прикрепленные файлы

Для прикрепления файла к сообщению необходимо загрузить его в файловое хранилище и связать с записью по идентификатору.

Механизм сохранения прикрепленных файлов действует следующим образом:

1. В POST запросе на сервер присылаются файлы, которые должны быть сохранены как прикрепленные, и список файлов, которые должны быть удалены из списка прикрепленных. Списки новых и удаляемых файлов формируются в пользовательском интерфейсе элементом управления Attachments.

2. Происходит обработка строки удаляемых файлов и получение списка реальных идентификаторов. Присланные в POST запросе файлы копируются в файловое хранилище как временные (FileCopy). В результате получаем 2 массива. Один содержит список файлов на удаление, второй - список новых файлов.

3. Сохранение информации в менеджере. Модифицируется окончательный список файлов, который затем сохраняется в записи в СУБД.

4. Удаление данных из файлового хранилища (FileDelete) и запись новых файлов в хранилище (FilesCommit).

Такое поэтапное разделение обусловлено тем, что в TeamWox нельзя одновременно модифицировать список прикрепленных файлов. Кроме того, на любом из этих этапов по каким-либо причинам может произойти ошибка. Выполнять все одновременно небезопасно.

На странице редактирования мы добавим возможность прикрепления файлов к сообщению, а на странице просмотра - вывод списка прикрепленных файлов.

 

3.1. Расширение менеджера

3.1.1. Для сохранения списка идентификаторов прикрепленных файлов в СУБД нам потребуется расширить структуру записи.

//+------------------------------------------------------------------+
//| Структура записи                                                 |
//+------------------------------------------------------------------+
struct HelloWorldRecord
  {
   INT64             id;
   wchar_t           name[256];
   int               category;
   INT64             description_id;
   INT64             attachments_id[32];
  };

Здесь мы задаем возможность прикрепления максимум 32 файлов. Этого вполне достаточно для повседневной работы (именно такое ограничение существует для модулей, входящих в стандартную поставку TeamWox) и не сказывается на быстродействии системы. Если вам требуется прикрепить больше файлов, разумнее будет предварительно запаковать их в архив.

3.1.2. Создадим новое поле в таблице СУБД.

//---
   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. В методах CHelloWorldManager::InfoGet, CHelloWorldManager::Get и CHelloWorldManager::Update соответственным образом дополните тексты SQL-запросов и привязки данных к параметрам запроса (подробнее см. прикрепленный к статье файл).

3.1.4. Для обработки списка прикрепленных файлов, в менеджере создадим новую структуру, которая будет содержать два списка. Для файлов из первого списка будет фиксироваться транзакция (FilesCommit), а файлы из второго будут удалены из хранилища.

//+------------------------------------------------------------------+
//| Запись с данными для изменения списка прикрепленных файлов       |
//+------------------------------------------------------------------+
struct AttachmentsModify
  {
   INT64             new_files[32];
   INT64             deleted_files[32];
  };

 

3.2. Выполнение и обработка запроса

3.2.1. Поскольку файлы прикрепляются на странице редактирования, в методе обработки события сохранения записи CPageEdit::OnUpdate мы добавим новый этап сохранения прикрепленных файлов в хранилище, а также обновим соответствующий метод сохранения записи в таблице СУБД. Аналогично методу сохранения записи в файловом хранилище, вынесем сохранение прикрепленных файлов в отдельный метод StoreAttachments.

//---
AttachmentsModify attachments={0};
............................................
//--- Сохранение прикрепленных файлов
   if(RES_FAILED(res=StoreAttachments(context,&attachments)))
      ExtLogger(context,LOG_STATUS_ERROR) << "failed to store attachments";
//--- Сохранение записи в СУБД
   if(RES_FAILED(res=manager->Update(context,&m_record,&attachments)))
      ReturnErrorExt(res,context,"failed to update record");

3.2.2. StoreAttachments - обработка прикрепленных файлов.

  • Объявление
TWRESULT          StoreAttachments(const Context *context,AttachmentsModify *attachments);
  • Реализация. Здесь attachments - название параметра списка новых файлов, которое используется в пользовательском интерфейсе при создании элемента управления Attachments. К названию параметра списка удаляемых файлов добавляется постфикс _deleted, т.е. в нашем случае - attachments_deleted.

//+------------------------------------------------------------------+
//| Сохранение прикрепленных файлов                                  |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::StoreAttachments(const Context *context,AttachmentsModify *attachments)
  {
//--- проверки
   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};  // итератор для запроса последовательности присоединенных файлов
   TWRESULT           res      =RES_S_OK;
   const wchar_t     *str      =NULL;
//--- УДАЛЕННЫЕ ФАЙЛЫ
   if(context->request->Exist(IRequest::POST,L"attachments_deleted"))
     {
      //--- распарсим строку и сохраним удаляемые файлы
      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));
     }
//--- НОВЫЕ ФАЙЛЫ
   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);
}

Если необходимо удалить существующие прикрепленные файлы, сначала происходит обработка строки POST-запроса, из которой с помощью утилитной функции IdStringParse (ее реализацию см. в прикрепленном к статье файле) извлекается список идентификаторов файлов на удаление.

Новые файлы затем копируются в файловое хранилище и составляется их список.

3.2.3. В менеджере реализуем новый метод StoreAttachnmentFile для копирования прикрепленного файла в хранилище.

Это ключевой момент, поскольку загружаемые в POST-запросе данные не сохраняются сразу в файловое хранилище, а записываются в виде временных файлов на диск. И только затем, если этот этап успешно пройден, в CHelloWorldManager::Update мы подтвердим транзакцию.

Такой подход обеспечивает большую безопасность при работе с файловым хранилищем и снижает нагрузку на сервер.

  • Описание
public:
//--- Сохранение прикрепленного файла
   TWRESULT          StoreAttachnmentFile(const Context *context,INT64 record_id,FileDescription *file_desc,INT64 *file_id);
   
  • Реализация
//+------------------------------------------------------------------+
//| Сохранение прикрепленного файла в хранилище                      |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::StoreAttachnmentFile(const Context *context,INT64 record_id,FileDescription *file_desc,INT64 *file_id)
  {
//--- проверки
   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};
//--- подготовим данные для сохранения
   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. Сохранение информации в менеджере

3.3.1. Сформируем окончательный список прикрепленных файлов. Для этого включите новый этап обработки запроса в CHelloWorldManager::Update.

//+------------------------------------------------------------------+
//| Добавление, сохранение записи в таблицу HELLOWORLD               |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::Update(const Context *context,HelloWorldRecord *record,AttachmentsModify *attachments)
  {
   TWRESULT res      =RES_S_OK;
   size_t   count    =0;
   size_t   new_count=0;
//--- проверки
   if(context==NULL || m_server==NULL || record==NULL || attachments==NULL) 
      ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)                                                   
      ReturnError(RES_E_INVALID_CONTEXT);
//--- подготовим список прикрепленных файлов
   if(!PrepareAttachments(record,attachments,&new_count))                
      ReturnErrorExt(RES_E_FAIL,context,"failed to prepare attachments");

3.3.2. Реализация CHelloWorldManager::PrepareAttachments.

  • Описание
private:
//---
   static bool       PrepareAttachments(HelloWorldRecord *record,AttachmentsModify *attachments,size_t *new_count);
  • Реализация
//+------------------------------------------------------------------+
//| Сформируем список прикрепленных файлов                           |
//+------------------------------------------------------------------+
bool CHelloWorldManager::PrepareAttachments(HelloWorldRecord *record,AttachmentsModify *attachments,size_t *new_count)
  {
//--- проверки
   if(record==NULL || attachments==NULL || new_count==NULL) ReturnError(false);
//---
   size_t count         =0;
   INT64  files_list[32]={0};
   size_t files_index   =0;
//--- сформируем новый список прикрепленных файлов
//--- 1. удалим идентификаторы файлов из списка
   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. добавим новые
   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. скопируем список в запись
   memcpy(record->attachments_id,files_list,sizeof(files_list));
//---
   return(true);
  }

 

3.4. Обновление записей в файловом хранилище

После того как в CHelloWorldManager::Update мы подготовили окончательный список прикрепленных файлов, необходимо обновить записи в файловом хранилище.

3.4.1. Удаление прикрепленных файлов из файлового хранилища.

//--- удалим файлы
   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. Подтверждение транзакции новых прикрепленных файлов.

//--- коммитим добавленные прикрепленные файлы
   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. Удаление записи из таблицы HELLOWORLD (CHelloWorldManager::InfoDelete).

//--- удаляем прикрепленные файлы
   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. Пользовательский интерфейс

Для добавления/редактирования прикрепленных файлов в пользовательском интерфейсе страниц применяется элемент управления Attachments, а для просмотра списка прикрепленных файлов - элемент управления AttachmentsList.

3.5.1. Страница редактирования. После области с WYSIWYG-редактором добавим элемент управления для прикрепления файлов.

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

Первый параметр - имя POST переменной с добавленными файлами. Второй параметр - список прикрепляемых файлов, реализованный в токене <tw:attachments />. Третий параметр - префикс URL, по которому прикрепленные файлы будут доступны для загрузки. Четвертый (необязательный) параметр - флаг прикрепленных файлов. В данном случае флаг ATTACHMENTS_ALL позволяет прикреплять все типы файлов, созданных в TeamWox. Пятый параметр - URL для выгрузки прикрепляемых файлов на сервер.

3.5.2. Страница просмотра. После блока с WYSIWYG-редактором добавим элемент управления, отображающий список прикрепленных файлов.

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

Здесь все аналогично. Первый параметр - список прикрепленных файлов, второй - префикс URL для их загрузки.

3.5.3. В странице редактирования (CPageEdit::Tag) и странице просмотра (CPageView::Tag) реализуйте токен <tw:attachments />.

//---
   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);
      //--- подсчитаем количество
      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. Демонстрация работы

3.6.1. Скомпилируйте модуль, обновите шаблоны на сервере, запустите сервер TeamWox и откройте страницу редактирования записи. В редакторе теперь можно прикреплять файлы.

Прикрепление файла в WYSIWYG-редакторе

3.6.2. После сохранения записи, на странице просмотра отображается список прикрепленных файлов.

Список прикрепленных файлов

 

4. Комментарии

4.1. Получение интерфейса для работы с комментариями

4.1.1. Для работы с комментариями TeamWox API предоставляет интерфейс IComments. Получим его в менеджере модуля CHelloWorldManager.

  • Описание
private:
   IComments        *m_comments_manager;               // менеджер комментариев
  • Реализация
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CHelloWorldManager::CHelloWorldManager() : m_server(NULL), m_files_manager(NULL), m_comments_manager(NULL), m_next_id(0)
  {
//---
//---
  }
//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CHelloWorldManager::~CHelloWorldManager()
  {
//---
   m_server          =NULL;
   m_comments_manager=NULL;
   m_files_manager   =NULL;
//---
  }
  • Инициализация модуля -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. Аналогично интерфейсу IFilesManager, интерфейс IComments получается один раз и не изменяется. И также для удобства в объявлении мы реализуем метод, который позволит быстро получать интерфейс при обработке запросов на страницах.

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

 

4.2. HTTP API

Базовый функционал работы с комментариями включает сохранение, чтение и удаление комментария. Соответствующие методы мы реализуем в странице просмотра PageView.

Поскольку каждый из модулей имеет свою собственную систему проверки прав и разрешений на конкретную запись, обработка необходимых HTTP запросов реализуется в каждом модуле. Перед вызовом метода соответствующего интерфейса каждый модуль реализует и проводит проверку прав на запись, к которой относится комментарий или файл.

4.2.1. В классе страницы CPageView объявите три метода обработки событий для комментариев.

public:
   //--- Получение списка комментариев в формате JSON
   TWRESULT          OnCommentGet(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);
   //--- Добавление/сохранение комментария
   TWRESULT          OnCommentUpdate(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);
   //--- Удаление комментария
   TWRESULT          OnCommentDelete(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager);

4.2.2. В этих методах мы будем записывать комментарии в файловое хранилище и выводить список комментариев для отображения на странице. Для этих целей объявим еще два соответствующих метода. Основной метод WriteComments будет записывать данные комментариев в файловое хранилище, а метод ShowCommentsJSON будет отображать данные комментариев на странице.

private:
   //--- Вывод списка комментариев в формате JSON
   bool              ShowCommentsJSON(const Context *context);
   //--- Запись комментария
   bool              WriteComments(const Context *context);

4.2.3. В методе CHelloWorldModule::ProcessPage добавьте соответствующие новые правила перенаправления запросов.

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. Реализация метода WriteComments.

Получим интерфейс ICommentsPage - это интерфейс страницы, которая управляет пользовательским интерфейсом комментариев. Передавая объект этого класса по ссылке в метод IComments::PageCreate, мы создаем страницу для отображения комментариев.

//+------------------------------------------------------------------+
//| Выводит комментарии                                              |
//+------------------------------------------------------------------+
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);
  }

Методом ICommentsPage::SetPerPage задаем количество комментариев на странице. При превышении заданного значения в пользовательском интерфейсе будет автоматически создан элемент управления PageNumerator. Количество комментариев зададим в классе страницы PageView.

private:
   enum
     {
      COMMENTS_PER_PAGE=25
     } 

Методом ICommentsPage::SetFlags мы задаем флаги, которые определяют набор команд в пользовательском интерфейсе комментариев. В нашем случае будут отображаться команды: создание нового комментария (SHOW_NEW), ответ на комментарий (SHOW_REPLY), копирование прикрепленных файлов в модуль Документы (SHOW_DOCS_COPY). Кроме того флаг SHOW_LARGE задает увеличенный размер окна добавления комментария, а флаг SHOW_CHECK_DAYS запрещает изменение комментариев по истечении срока (в днях), заданного администратором на вкладке Настройки в модуле Управление.

Тип комментария зададим в перечислении EnHelloWorldCommentsType, которое объявим в менеджере. В данном случае у нас будет только один тип комментария, который оставляется пользователем. В вашем модуле могут быть также системные комментарии (например, как в модуле Сервисдеск при изменении списка назначенных, категории, статуса и пр.).

enum EnHelloWorldCommentsType
  {
   HELLOWORLD_COMMENT_TYPE_COMMENTS=0x01
  };

Наконец, метод ICommentsPage::ShowJSON выводит данные комментариев в формате JSON.

4.2.5. Реализация метода ShowCommentsJSON. Метод ShowCommentsJSON упаковывает данные комментариев в объект JSON, а перед этим устанавливает значение для HTTP заголовка Content-Type.

//+------------------------------------------------------------------+
//| Выводит комментарии                                              |
//+------------------------------------------------------------------+
bool CPageView::ShowCommentsJSON(const Context *context)
  {
//--- проверки
   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;
//--- вывод комментариев
   json << L"comments" << CJSON::OBJECT;
   WriteComments(context);
   json << CJSON::CLOSE;
//---
   json << CJSON::CLOSE;
//---
   return(true);
  }

4.2.6. Реализация методов обработки событий для комментариев.

  • OnCommentGet. Здесь все просто - с помощью шаблонной функции InitParams обрабатываем запрос и выводим комментарии с помощью ShowCommentsJSON.
//+------------------------------------------------------------------+
//| Получение списка комментариев в формате JSON                             |
//+------------------------------------------------------------------+
TWRESULT CPageView::OnCommentGet(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager)
  {
   INT64    id =0;
   TWRESULT res=RES_S_OK;
//--- проверки
   if(context==NULL || server==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->response==NULL)                                      ReturnError(RES_E_INVALID_CONTEXT);
//--- инициализируем параметры и окружение
   if(RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/comment/get/")))
      ReturnError(res);
//---
   ShowCommentsJSON(context);
//---
   return(RES_S_OK);
  }
  • OnCommentUpdate. Сохранение нового или измененного комментария осуществляется методом OnDelete, который предоставляет интерфейс ICommentsPage.
//+------------------------------------------------------------------+
//| Добавление/сохранение комментария                                |
//+------------------------------------------------------------------+
TWRESULT CPageView::OnCommentUpdate(const Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager)
  {
   INT64    id =0;
   TWRESULT res=RES_S_OK;
//--- проверки
   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);
//--- инициализируем параметры и окружение
   if    (RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/comment/update/")))
      ReturnError(res);
//---
   CSmartInterface<ICommentsPage> page;
//--- создадим страницу и передадим управление ей
   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);
  }

В методе ICommentsPage::SetSearchId задается поисковый идентификатор типа комментариев, используемый в модуле поиска. Реализацию поиска и фильтрации мы рассмотрим в одной из следующих статей.

Метод ICommentsPage::SetRecordTitle задает имя для записи в файловом хранилище. Затем при успешном выполнении метода ICommentsPage::OnUpdate данные комментария сохраняется в файловое хранилище с привязкой к идентификатору записи исходного сообщения. В конце мы устанавливаем значение для HTTP заголовка Content-Type и возвращаем успешный результат выполнения.

  • OnCommentDelete. Удаление комментария для сообщения по указанному идентификатору записи. Для этой цели мы воспользуемся методом OnDelete из арсенала интерфейса ICommentsPage.
//+------------------------------------------------------------------+
//| Удаление комментария                                             |
//+------------------------------------------------------------------+
TWRESULT CPageView::OnCommentDelete(const     Context *context,IServer *server,const wchar_t *path,CHelloWorldManager *manager)
  {
   TWRESULT res=RES_S_OK;
//--- проверки
   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);
//--- инициализируем параметры и окружение
   if(RES_FAILED(res=InitParams(context,server,path,manager,L"number_two/comment/delete/")))
      ReturnError(res);
//---
   CSmartInterface<ICommentsPage> page;
//--- создадим страницу и передадим управление ей
   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. Для отображения комментариев в пользовательском интерфейсе страницы, добавьте обработчик токена <tw:comments />. Для этого в методе CPageView::Tag добавьте следующий блок.

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

 

4.3. Пользовательский интерфейс

Теперь нам нужно расширить пользовательский интерфейс страницы просмотра для работы с комментариями. Для этой цели в TeamWox применяется элемент управления Comments.

4.3.1. В шаблоне view.tpl после кода страницы просмотра добавьте элемент управления 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);

Данные комментариев берутся из реализованного ранее токена <tw:comments />. В качестве параметров для элемента управления Comments мы указали четыре URL: запросы на изменение и удаление комментария по идентификатору, добавление и загрузку прикрепленных файлов.

4.3.2. Для обновлении содержимого комментариев, добавьте пользовательскую функцию-обработчик LoadComments для JavaScript события oncontent. В реализованном обработчике на сервер шлется AJAX-запрос, в результате которого список комментариев обновляется без перезагрузки содержимого страницы.

//--- Обработка обновленного содержимого комментариев
function LoadComments(from,perpage)
  {
   TeamWox.LockFrameContent();
   // временная мера, организуем запрос постранично
   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. Демонстрация работы

4.4.1. Скомпилируйте модуль, обновите шаблоны на сервере, запустите сервер TeamWox и откройте страницу записи. Под страницей сообщения теперь отображается пользовательский интерфейс добавления комментариев.

Пользовательский интерфейс добавления комментария

4.4.2. При добавлении комментариев доступны все возможности добавления прикрепленных файлов. Дополнительно ничего реализовывать не нужно (весь необходимый функционал интерфейс IComments задействует автоматически).

Комментарий к записи

 

Заключение

В следующих статьях мы планируем осветить еще несколько важных тем по разработке модулей TeamWox: поиск и фильтрация, кэширование SQL-запросов, импорт/экспорт данных и др.


helloworld-filestorage-part2-ru.zip (197.34 KB)

2011.02.22