Введение
В первой части статьи вы узнали, как работать с файловым хранилищем 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. Демонстрация работы
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} ]);
Вот как это будет выглядеть в окне браузера.
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
обнуляем буфер.
<сервер 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 и откройте страницу редактирования записи. В редакторе можно добавлять ссылки, изображения и другие поддерживаемые файлы.
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 и откройте страницу редактирования записи. В редакторе теперь можно прикреплять файлы.
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-запросов, импорт/экспорт данных и др.
- Как добавить готовый модуль в TeamWox
- Как добавить страницу в модуль TeamWox
- Построение пользовательского интерфейса
- Взаимодействие с СУБД
- Создание пользовательских отчетов
- Файловое хранилище - Часть 1
- Файловое хранилище - Часть 2
- Настройка окружения пользовательских модулей - Часть 1
- Настройка окружения пользовательских модулей - Часть 2
- Поиск и фильтрация - Часть 1
- Поиск и фильтрация - Часть 2
- Настройка "Онлайн-консультанта" на вашем сайте
- Как создать дополнительный языковой пакет для TeamWox
2011.02.22