Mera, Tektronix Описание проекта «Company manager» Автор: Дмитрий Устимов Нижний Новгород 2012 Contents 1. Техническое задание .......................................................................................................................................... 3 2. Архитектура проекта........................................................................................................................................... 4 3. Серверная сторона ............................................................................................................................................. 5 3.1. 3.1.1. Domain classes ..................................................................................................................................... 5 3.1.2. Controllers ............................................................................................................................................ 5 3.1.3. Services ................................................................................................................................................. 6 3.2. 4. Grails application .......................................................................................................................................... 5 СУБД ............................................................................................................................................................. 6 Клиентская сторона ............................................................................................................................................ 7 4.1. HTML............................................................................................................................................................. 7 4.2. CSS ................................................................................................................................................................ 8 4.3. Javascript ...................................................................................................................................................... 8 1. Техническое задание Существует древовидная структура компании Организация Подразделения Проекты Люди 1) Необходимо обеспечить отображения структуры организации. 2) При щелчке по узлу дерева отображается информация о нем. Для человека вывод информации необходимо разделить на несколько табов. 2) При щелчке правой кнопкой мыши по узлу в дереве появляется контексное меню, которые позволяет довавить/удалить узел. Есть возможность добавить или удалить подразделение, проект или человека. При добавлении необходимо сделать какие-то поля обязательными, какие-то необязательными, заполнение полей контролировать. Для каких то полей сделать проверку на валидность, например, валидность e-mail. Всю функциональность необходимо покрыть юнит-тестами. Проект необходимо реализовать с использованием следующих технологий: 1) Frontend - HTML, CSS, ExtJS, AJAX. 2) Backend - Groovy, Grails. 3) СУБД - MySql. 4) Юнит-тесты - JUnit. Разработку необходимо вести, создав SVN репозиторий, например, используя Google Code. 2. Архитектура проекта “Company manager” –это клиент-серверное приложение. Сервеная сторона содержит СУБД (MySQL), а также приложение, написанное с использованием фреймвёрка для написания web – приложений – Grails. Платформа Grails обеспечивает запуск Web – сервера, который взаимодействует с клиентом посредством протокола HTTP. Серверный код написан на языке Groovy. Клиентская сторона представлена Web – браузером, который отображает web – страницы, используя для этого HTML, CSS и Javascript (использован фреймвёрк ExtJS), загружаемые с web-сервера. Для обеспечения интерактивности web-интерфейса используется технология Ajax. Серверная сторона Клиентская сторона Web Browser Grails application СУБД HTTP Web Server HTML CSS Javascript (ExtJS) 3. Серверная сторона 3.1. Grails application 3.1.1. Domain classes Предметная область представлена 4-мя моделями, классы которых созданы как Domain classes: - Company - Division - Project - Person Для моделирования отношений «один ко многим» между классами предметной области используется свойство hasmany. Для валидации полей классов предметной области используется специальное замыкание constraints. class Company { String title String email Date startDate static hasMany = [ divisions : Division ] static constraints = { title(size: 2..50, blank: false, unique: true) email(email: true, nullable: true) startDate(nullable: true) } String toString() { return "Company: ${title}" } } 3.1.2. Controllers Для каждого класса предметной области создан свой контроллер: - CompanyController - DivisionController - ProjectCcontroller - PersonController Кроме того, создан дополнительный контролер DistributorController, который в зависимости от параметров запроса клиента перенаправляет запросы к котроллерам классов предметной области. CompanyController запрос Клиент запрос Distributor Controller DivisionController ProjectController ProjectController Для перенаправлоения запроса, используется ключевое слово «redirect». DistributorController использует параметр «node» в качестве критерия для выбора контроллера класса предметной области. class DistributorController { def getlist = { if (params.get("node") == "CompanyTreeRoot") { redirect(controller: "division", action: "getlist") } ... } ... } Работа контроллера класса предметной области заключается в том, чтобы: - принять запрос, - передать (при необходимости обработать) параметры запроса соответствующему сервису - отрендерить данные, возвращенные сервисом в формате, ожидаемом клиентом class DivisionController { def DivisionService def getlist = { render DivisionService.getlist() as JSON } ... } 3.1.3. Services Вся бизнес-логика по обработке запросов содержится в слое сервисов. Для каждого котроллера создан класс сервиса: - DistributorService - CompanyService - DivisionSservice - ProjectService - PersonService 3.2. СУБД В проекте Grails СУБД конфигурируется в файле DataSource.groovy. Основные настройки конфигурации - это JDBC драйвер, sql диалект и url базы данных. environments { development { dataSource { dbCreate = "update" // MySql driverClassName = "com.mysql.jdbc.Driver" dialect = "org.hibernate.dialect.MySQL5InnoDBDialect" url = "jdbc:mysql://127.0.0.1/db_company" ... } } При запуске приложенися платформа Grails автоматически создаёт необходимые таблицы в базе данных для Domain classes. Ниже изображена ER – диаграмма для полученной базы данных, позволяющая убедится в правильности отображения сущностей модели данных в таблицах базы данных. Инициализация базы данных производится при первом запуске приложения и описывается замыканием init класса Bootstrap: class BootStrap { def init = {servletContext -> if (!Company.count()) { new Company(title:"Mera", email:"[email protected]", startDate: new Date("1989/1/9")).save() } } ... } 4. Клиентская сторона 4.1. HTML Заглавный файл index.html (располагающийся в папке views проекта) содержит в себе лишь ссылки на необходимые файлы CSS, Javascript. <html> <head> <title>Welcome to Company</title> <!-- ** CSS ** --> ... <link rel="stylesheet" href="css/my.css"></link> <!-- ** Javascript ** --> <script type="text/javascript" src="js/ext/adapter/ext/ext-base.js"></script> <script type="text/javascript" src="js/ext/ext-all-debug.js"></script> <!-- project specific --> <script type="text/javascript" src="js/application.js"></script> ... </head> <body></body> </html> 4.2. CSS Для придания приложению необходимого внешенего вида был добавлен файл my.css, содержащий описания css стилей: Ниже показаны стили для заголовка web-страницы, а также для отображдения иконки «add» контекстного меню. ... #header h1 { font-size: 16px; font-weight: bold; color: #fff; padding: 5px 10px; } .add { background-image: url(../images/icons/add.gif) !important; } ... 4.3. Javascript В проекте используется библиотека ExtJS. Для её корректной работы в файле index.html были добавлены необходимые библиотечные файлы: <!-- ** Javascript ** --> <!-- ExtJS library: base/adapter --> <script type="text/javascript" src="js/ext/adapter/ext/ext-base.js"></script> <!-- ExtJS library: all widgets --> <script type="text/javascript" src="js/ext/ext-all-debug.js"></script> Далее в index.html описаны специфичные для проекта js-файлы: <!-- project specific --> <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" <script type="text/javascript" src="js/application.js"></script> src="js/infoPanelContent.js"></script> src="js/infoPanel.js"></script> src="js/treePanelMenuAdd.js"></script> src="js/treePanelMenuEdit.js"></script> src="js/treePanelMenuDelete.js"></script> src="js/treePanelMenu.js"></script> src="js/treePanel.js"></script> src="js/mainPanel.js"></script> В качестве точки для запуска Javascript – кода используется функция Ext.onReady Ext.onReady(function(){ new mainPanel() }); Внутри функции onReady вызывается конструктор для главной панели приложения. Ниже показана подробная структура ExtJS – проекта: header add treePanel contextMenu edit delete mainPanel commonInfoPanel Fields ... Fields infoPanel personInfoForm1 ... personInfoPanel Fields personInfoForm2 ... Главная панель приложения рендерится в body страницы и занимает всё пространство страцицы. mainPanel служит контейнером для элементов: header, treePanel, infoPanel. В mainPanel используется border layout для компоновки элементов. В регионе «north» располагается header с названием приложения, В регионе «west» - treePanel В регионе «center» - infoPanel Ниже представлена конфигурация mainPanel, позволяющая произвести рассматриваемый метод компоновки элементов: layout: 'border', items : [ { xtype : id : region: html : height: 'box', 'header', 'north', '<h1>Company Manager</h1>', 30 }, new treePanel(), new infoPanel() ] ------------------------------------------------------------------------------------------------------------------------------------------------------Рассмотрим treePanel, конструктор которой запускается при инициализации mainPanel. treePanel загружает данные для отображения своих нод с сервера. url для запроса данных указывается в конфигурации загрузчика дерева, как параметр «dataUrl». При этом в params передается id ноды, для которой отображается список дочерних элементов. var treePanel = Ext.extend(Ext.tree.TreePanel, { . . . loader: new Ext.tree.TreeLoader({ dataUrl: 'distributor/getlist' }), . . . }); Например, для корневой ноды полученные JSON данные могу т выглядеть так: [{"text":"DivisionA","id":"Division_1"},{"text":"DivisionB","id":"Division_2"}] Поле «text» используется для отображения элемента в дереве, поле «id» - для передачи параметра «node» в запросе к серверу. При раскрытии ноды Подразделения (Division) JSON данные могут выглядеть так: [{"text":"Project A1","id":"Project_1"},{"text":"Project A2","id":"Project_2"}] Дерево выглядит так: treePanel содержит обработчик правого клика мыши(для отображения контекстного меню): listeners:{ contextmenu: onCtxMenu, Обработчик создает объект меню: new Ext.menu.Menu({ items: [ { itemId : 'add', iconCls : 'add', handler : addHandler }, Для задания иконки элемента меню полю iconCls присваивается стиль css, обьявленный в одном из css файлов. В зависимости от выбранной ноды элементы меню активируются, либо дезактивируются, а также меняется отображаемый текст: if (node.id =='CompanyTreeRoot') { addItem.setText('Add Division'); editItem.setText('Can\'t edit a company'); deleteItem.setText('Can\'t delete a company'); addItem.enable(); deleteItem.disable(); editItem.disable(); } else { . . . } В отдельных js – файлах описаны обработчики для каждого элемента меню: add, edit, delete. Формы для каждого обработчика создаются динамически в зависимости от выбранного элемента меню(для add и edit набор компонентом будет разным). ------------------------------------------------------------------------------------------------------------------------------------------------------Рассмотрим обработчик «add»: 1)создание и инициализация необходимых компонентов: var createAddForm = function(selNode) { var textAreaTitle = new Ext.form.TextField({. . .}); var textAreaEmail = new Ext.form.TextField({. . .}); var textAreaName = new Ext.form.TextField({. . .}); var birthDateField = new Ext.form.DateField({. . .}); var hiredDateField = new Ext.form.DateField({. . .}); var selNodePath = selNode.getPath(); var treePanel = Ext.getCmp('companyTreePanel'); . . . 2)формирование списка компонентов в зависимости от выбранной ноды в дереве if (selNode.id =='CompanyTreeRoot') { var aItems = [textAreaTitle, textAreaEmail]; var wTitle = "Add new Division"; } else if (selNode.id.match('Division')) { var aItems = [textAreaTitle, textAreaEmail]; var wTitle = "Add new Project"; } else if (selNode.id.match('Project')) { var aItems = [textAreaName,textAreaEmail,birthDateField,hiredDateField]; var wTitle = "Add new Person";} 3)инициализация формы: var form = new Ext.form.FormPanel({ . . . items: aItems }); 4) инициализация окна, которое будет служить контейнером для опеределенной выше формы. Обработчик нажатия кнопки «add» выполнит Ajax запрос на сервер, в котором передаст параметры созданного элемента. По пришествии положительного ответа от сервера, treePanel перегружается. var window = new Ext.Window({ . . . items: form, buttons: [{ text: 'Add', handler: function() { form.getForm().submit({ url : 'distributor/addElement', params : { parentNode : selNode.id }, waitTitle: 'please wait...', waitMsg: 'wait...', success: function(form, action) { window.close(); root = Ext.getCmp('companyTreePanel').getRootNode(); root.reload(); treePanel.expandPath(selNodePath); }, failure: function(form, action) { Ext.Msg.alert('An error occured with the server.'); } }); } }] }); window.show(); }; Форма для добавления Проекта: Форма для добавления Работника: ------------------------------------------------------------------------------------------------------------------------------------------------------Рассмотрим infoPanel, конструктор которой запускается при инициализации mainPanel. infoPanel использует card layout, что позволяет в разные моменты времени менять содержимое панели. var infoPanel = Ext.extend(Ext.Panel, { id : 'infoPanelId', region : 'center', layout : 'card', . . . }); Рассмотрим работу infoPanel: 1) Пользователь кликает по элементу treePanel. 2) Событие «click» обрабатывается в treePanel (Вызывается функция showInfoPanelContent) var treePanel = Ext.extend(Ext.tree.TreePanel, { . . . listeners:{ click: function(node, eventObj){ showInfoPanelContent(node); }, }, . . . }); 3) Посылается Ajax request на сервер с целью получения данных по элементу. В params посылается id ноды, по которой кликнул пользователь. var showInfoPanelContent = function(node){ Ext.Ajax.request({ url : 'distributor/getElement', params : { node: node.id }, 4) ожидаем пожительного ответа от сервера Ext.Ajax.request({ . . . success: function(response, opts) { var responseJson = Ext.decode(response.responseText); if (responseJson.success === "true") { 5) определяем, в зависимости от выбранной ноды, какую панель отобразить – для работника или для Компании/Проекта/Подразделаения. var IsCommonNotPerson; if ((node.id =='CompanyTreeRoot') || (node.id.match('Division')) || (node.id.match('Project'))) { IsCommonNotPerson = true; } else { IsCommonNotPerson = false; } 6) В зависимости от выбранной ноды инициализируем определенный набор компонентов: if (IsCommonNotPerson) { var infoCommonTextAreaTitle = new Ext.form.TextField({. . .}); var infoCommonTextAreaEmail = new Ext.form.TextField({. . .}); var infoCommonStartDateField = new Ext.form.DateField({. . .}); var aItems = [infoCommonTextAreaTitle, infoCommonTextAreaEmail, infoCommonStartDateField]; var commonInfoPanel = new Ext.FormPanel({ . . . items: aItems }); } else { var infoPersonTextAreaName = new Ext.form.TextField({. . .}); var infoPersonTextAreaEmail = new Ext.form.TextField({. . .}); var infoPersonBirthDateField = new Ext.form.DateField({. . .}); var infoPersonHiredDateField = new Ext.form.DateField({. . .}); var paItems = [infoPersonTextAreaName, infoPersonTextAreaEmail]; var pbItems = [infoPersonBirthDateField, infoPersonHiredDateField]; var personInfoForm1 = new Ext.FormPanel({ . . . items: paItems }); var personInfoForm2 = new Ext.FormPanel({ . . . items: pbItems }); var personInfoPanel = new Ext.TabPanel({ . . . items: [personInfoForm1, personInfoForm2] }); } 7) Добавляем проинициализированные выше компоненты в infoPanel, вызываем метод setActiveItem, в котором передаем id необходимой панели: var infopan = Ext.getCmp('infoPanelId'); if (IsCommonNotPerson) { infopan.removeAll(); infopan.add(commonInfoPanel); infopan.layout.setActiveItem('commonInfoPanelId'); } else { infopan.removeAll(); infopan.add(personInfoPanel); infopan.layout.setActiveItem('personInfoPanelId'); } 8) Компоненты отображаются на web – странице. Ниже представлены примеры отображения компонентов: - при клике на Подразделении: - при клике на Работнике: