•  
  •  
  •  
  •  
1 1 1 1 1 1 1 1 1 1 Рейтинг 5.00 (1 Голос)
Процесс разработки расширений для Joomla! 3.0 – начало (Статья 2) - 5.0 out of 5 based on 1 vote

Этап 1: Описание основной схемы элемента

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

Элементы компонента

Название: Lendr

Имя компонента: com_lendr

Описание: Lendr является компонентом Joomla! 3.0 (использующим Bootstrap), которым обеспечивается для пользователей создание профилей, добавление книг в коллекцию в собственную библиотеку, просмотр библиотек иных пользователей, одалживание книг, добавление книг в список желаемого (wishlist), а также подписка на предзаказ определенного типа книг.

Описание основных функций

Новому компоненту Lendr будет присущ такой набор с возможностями:

  • Аккаунт пользователя / Профиль
  • Книги / Пользовательская библиотека
  • Список желаемого
  • Процесс одалживания / Заимствования книг
  • Запрос на одалживание книги
  • Списки ожидания для уже заимствованных книг

Теперь нами должна быть написана базовая структура с необходимыми для нас файлами. Она не будет представлена исчерпывающим списком, так как он будет содержать изменения, которые делаются во время написания. Тем не менее, наличие изначальной схемы поможет нам в ориентировании на общее направлении. Представляем первоначальную схему с ключевыми файлами, необходимыми для Lendr:

Каким образом разрабатываются расширения для Joomla! 3.0 – начинаем разработку (Статья 2)

Перечень основных файлов

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

Этап 2: Создание файлов баз данных

Мы приступаем к созданию файлов базы данных. Нами будут сохраняться данные файлы в папке  таблиц, которая находится во фронт-энде нашего элемента. Необходимо создание всех файлов, которые были описаны в нашей схеме. Вотодинподобныхфайлов /components/com_lendr/site/tables/book.php:

<?php defined( '_JEXEC' ) or die( 'Restricted access' );
class TableBook extends JTable
{
    /**
    * Constructor
    *
    * @param object Database connector object
    */
    function __construct( &$db ) 
    {
        parent::__construct('#__lendr_books', 'book_id', $db);
    }
} 

Файлы таблицы оснащены одной функцией конструктора. Этой функцией описывается название таблицы, которая связана с этим JTable файлом. Подобная функция также занимается определением поля с первичным ключом book_id.

Во время создания подобных таблиц следует приступить к созданию скрипта install.mysql.sql, которым можно будет воспользоваться во время установки компонента с помощью панели управления Joomla. Такойвидприсущначалуфайла /administrator/components/com_lendr/admin/install.mysql.sql:

CREATE TABLE IF NOT EXISTS `#__lendr_books` (
 `book_id` int(11) NOT NULL AUTO_INCREMENT,
 `user_id` int(11) DEFAULT NULL,
 `isbn` varchar(255) DEFAULT NULL,
 `title` varchar(255) DEFAULT NULL,
 `summary` text DEFAULT NULL,
 `pages` varchar(55) DEFAULT NULL,
 `image` varchar(255) DEFAULT NULL,
 `publish_date` varchar(255) DEFAULT NULL,
 `created` datetime NOT NULL,
 `modified` datetime NOT NULL,
 `lent` tinyint(2) DEFAULT NULL,
 `due_date` datetime NOT NULL,
 `lent_uid` varchar(255) DEFAULT NULL,
 `published` tinyint(2) DEFAULT 0,
 PRIMARY KEY (`book_id`)
);

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

Этап 3: Начало создания папок и файлов компонента

Создав таблицу баз данных, переходим к созданию структуры файлов под наш компонент. Ниже приводим базовую структуру папок:

com_lendr/
    admin/
        controllers/
        models/
        views/
        index.html
        install.mysql.sql
        lendr.php
    site/
        assets/
        controllers/
        helpers/
        language/
        models/
        tables/
        views/
        index.html
        lendr.php
        router.php
install.php
lendr.xml

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

Этап 4: Создание установочных файлов, точки входа, контроллеров и контроллеров представления

Этот этап позволит нам добавить контент для некоторых файлов. Вначале нами будут рассмотрены установочные файлы, далее мы перейдем к контроллерам и, в конце, займемся добавлением наших контроллеров представлений.

Установочные файлы

Файлы, которые относятся к корневому уровню, представлены элементами, которыми пользуется Joomla! при установке компонента. Они находятся в папке вашего компонента com_lendr, за папками site и admin. Это XML-файл (еще он называется «манифест-файлом») установки, в котором содержится пример описания компонента, связанных файлов, меню с языковыми файлами. Сюда же относится файл install.php, в котором содержатся определенные функции, которые выполняются при установке. Название файлу можно дать любое, но ссылка на него должна быть строго определенной в XML-файле. Использование этих функций не является обязательным, но они способствуют выполнению определенных действий при установке компонента.

lendr.xml

<extension type="component" version="2.5.0" method="upgrade">
    <name>COM_LENDR</name>
    <creationDate>2013-01-31</creationDate>
    <author>Spark</author>
    <authorEmail>info [at] sparkbuilt.com<;/authorEmail>
    <authorUrl>http://lendr.sparkbuilt.com</authorUrl>
    <copyright>Copyright Info</copyright>
    <license>License Info</license>
    <version>1.0.0</version>
    <description>COM_LENDR_DESCRIPTION</description>

В первом блоке деталей определяется информация относительно компонента. Данная информация показывается «Менеджером расширений» Joomla!. Она, также, может быть найдена в таблице расширений (#__extensions).

<install>
    <sql>
    <file charset="utf8" driver="mysql">mysql.install.sql</file>
    </sql>
</install>

В этом блоке находится информация о том Joomla!, где можно найти SQL файлы компонента. Их исполнение происходит при установке, чтобы создать необходимые таблицы баз данных. Вами может быть установлена собственная кодировка символов, а также выбран тип драйвера.

Каким образом разрабатываются расширения для Joomla! 3.0 – начинаем разработку (Статья 2)

<files folder="site">
    <folder>assets</folder>
    <folder>controllers</folder>
    <folder>helpers</folder>
    <folder>languages</folder>
    <folder>models</folder>
    <folder>tables</folder>
    <folder>views</folder>
    <filename>index.html</filename>
    <filename>lendr.php</filename>
    <filename>router.php</filename>
</files>

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

<scriptfile>install.php</scriptfile>

Файлом скрипта определяется набор функций, которые проходят исполнение при установке. Для нашего примера он назван install.php.

<languages folder="site">
    <language tag="en-GB">languages/en-GB/en-GB.com_lendr.ini</language>
</languages>

Разделом «languages» определяется набор необходимых языковых файлов. Они устанавливаются в папку languages с соответствующим языковым тегом.

<administration>
    <menu link="option=com_lendr" img="components/com_lendr/assets/images/lendr_icon.png">COM_LENDR</menu>
    <submenu>
       <menu view="settings" img="components/com_lendr/assets/images/settings_icon.png"
       alt="LENDR/Settings">COM_LENDR_SETTINGS</menu>
    </submenu>

Далее следует блок, которым определяются элементы админки. К ней относятся файлы деталей админ части, а также пункты меню. Путь к картинкам, если необходимо их применение, являются относительными папками administrator компонента.

<files folder="admin">
        <folder>controllers</folder>
        <folder>languages</folder>
        <folder>models</folder>
        <folder>views</folder>
        <filename>lendr.php</filename>
        <filename>index.html</filename>
        <filename>install.sql</filename>
    </files>
     
    <languages folder="admin">
        <language tag="en-GB">languages/en-GB/en-GB.com_lendr.ini</language>
        <language tag="en-GB">languages/en-GB/en-GB.com_lendr.sys.ini</language>
    </languages>
    </administration>
</extension>

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

install.php

defined( '_JEXEC' ) or die( 'Restricted access' );
jimport('joomla.installer.installer');
jimport('joomla.installer.helper');

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

/**
* Method to install the component
*
* @param mixed $parent The class calling this method
* @return void
*/
function install($parent)
{
    echo JText::_('COM_LENDR_INSTALL_SUCCESSFULL');
}

Функция install выполняется при окончании процесса установки элемента. Чаще всего, в ней заключается сообщение о том, что установка успешно завершена. Текстом должны использоваться языковые строки, которые определяет администраторская языковая папка в файле XX-XX.com_lendr.sys.ini.

/**
* Method to update the component
*
* @param mixed $parent The class calling this method
* @return void
*/
function update($parent)
{
    echo JText::_('COM_LENDR_UPDATE_SUCCESSFULL');
}

Обновление происходит в том случае, если метод для установки определяется в качестве обновления.

/**
* Method to run before an install/update/uninstall method
*
* @param mixed $parent The class calling this method
* @return void
*/
function preflight($type, $parent)
{
    ...
}
 
function postflight($type, $parent)
{
    ...
}

Здесь вами может быть определено использование специфических функций, которые вам необходимо выполнить либо перед установкой, либо после её завершения. Подробная информация относительно функций вами может быть получена из статьи "Процесс разработки компонентов для Joomla 2.5 - скрипт установки / обновление / удаление и сервер обновлений".

Корневой файл (lendr.php)

Файлом lendr.php используется корневая папка site. Он является первым файлом, который распознается и читается Joomla! При завершении установки компонента. Он выступает в роли точки входа для компонента, которая обеспечивает перенаправление задач на контроллеры, подключение хелперов, CSS-файлов и JS-файлов, библиотек плагинов и других базовых вещей, в которых нуждается компонент. Файлом будут загружаться таблицы, которые относятся к компоненту; импортироваться все плагины, которые существуют в группе плагинов "lendr"; определяться запрошенные пользователями контроллеры. После этого следует выполнение соответствующего контроллера, основываясь на данном запросе. Во время написания компонента данный файл будет систематически дополняться.

<?php // No direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
 
//sessions
jimport( 'joomla.session.session' );
 
//load tables
JTable::addIncludePath(JPATH_COMPONENT.'/tables');
 
//load classes
JLoader::registerPrefix('Lendr', JPATH_COMPONENT);
 
//Load plugins
JPluginHelper::importPlugin('lendr');
 
//application
$app = JFactory::getApplication();
 
// Require specific controller if requested
if($controller = $app->input->get('controller','default')) {
    require_once (JPATH_COMPONENT.'/controllers/'.$controller.'.php');
}
 
// Create the controller
$classname = 'LendrController'.$controller;
$controller = new $classname();
 
// Perform the Request task
$controller->execute();

О контроллерах

Контроллер в компоненте создается в виде класса, который имеет одну функцию. Чаще всего, названием контроллера определяется задача данного контроллера. Это отличается от предыдущих версий Joomla!, где контроллером выполняется одновременно набор задач, которые относятся к определенным частям компонента. Процесс создания элементов, которые включают в себя единственную функцию, призван обеспечить связь контроллеров в объединенную цепь, что приводит к формированию легкодоступного пути для отслеживания действий и устранения ошибок. Для них используется папка site/controllers/. Далее нами приводится пример подобного контроллера, который нами определен в Lendr, а также контроллер по умолчанию, который имеет базовые функции.

/components/com_lender/controllers/edit.php

<?php // no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
 
class LendrControllersEdit extends LendrControllersDefault
{
    function execute()
    {
        $app = JFactory::getApplication();
        $viewName = $app->input->get('view');
        $app->input->set('layout','edit');
        $app->input->set('view', $viewName);
        //display view
        return parent::execute();
    }
}

Это является весьма простой реализацией контроллера. Она будет далее усложняться в последующих статьях.

Стоит обратить внимание к тому, что нашим контроллером расширяется LendrControllersDefault.LendrControllersDefaul. Нами расширен контроллер для отображения нужного шаблона.

Далее приводится пример контроллера по умолчанию /components/com_lendr/controllers/default.php

<?php defined( '_JEXEC' ) or die( 'Restricted access' );
class LendrControllersDefault extends JControllerBase
{
    public function execute()
    {
        // Get the application
        $app = $this->getApplication();
        
        // Get the document object.
        $document = $app->getDocument();
        
        $viewName = $app->input->getWord('view', 'dashboard');
        $viewFormat = $document->getType();
        $layoutName = $app->input->getWord('layout', 'default');
        
        $app->input->set('view', $viewName);
        
        // Register the layout paths for the view
        $paths = new SplPriorityQueue;
        $paths->insert(JPATH_COMPONENT . '/views/' . $viewName . '/tmpl', 'normal');
        
        $viewClass = 'LendrViews' . ucfirst($viewName) . ucfirst($viewFormat);
        $modelClass = 'LendrModels' . ucfirst($viewName);
        
        if (false === class_exists($modelClass))
        {
            $modelClass = 'LendrModelsDefault';
        }
        
        $view = new $viewClass(new $modelClass, $paths);
        $view->setLayout($layoutName);
        
        // Render our view.
        echo $view->render();
        
        return true;
    }
}

Этим контроллером будет получено из запроса представление. Далее следует нахождение соответствующего файла представления, загрузка соответствующей модели и отображение представления. В Joomla! 3 каждый файл представления загружается с моделью, поэтому, следуя соглашению по наименованию представлений и моделей, они могут быть связаны без нужды в дополнительных строках кода.

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

Контроллеры представлений

Joomla! Предоставляет уникальные возможности по обработке представлений. Joomla! Позволяет пользоваться вторичными контроллерами, которые помогают во время рендеринга данных и назначения переменных, которые используются в шаблонах. Вторичный контроллер расположен в папке представления (site/views/view_name/) и чаще всего называется в зависимости от необходимого вида рендеринга. Прошлые версии Joomla! данные файлы называли view.html.php, view.raw.php и т.д. Ниже мы приводим примеры некоторых контроллеров представлений, которыми пользуется Lendr.

/components/com_lendr/views/book/html.php

<?php defined( '_JEXEC' ) or die( 'Restricted access' );
 
class LendrViewsBookHtml extends JViewHtml
{
    function render()
    {
        $app = JFactory::getApplication();
        $type = $app->input->get('type');
        $id = $app->input->get('id');
        $view = $app->input->get('view');
         
        //retrieve task list from model
        $model = new LendrModelBook();
         
        $this->book = $model->getBook($id,$view,FALSE);
        //display
        return parent::render();
    }
}

Этим контроллером представлений отображаются детали конкретной книги, которая основывается на id. Определением функции модели getBook() мы займемся в следующей статье. Следует обратить внимание на то, что переменные, которые будут применяться для шаблона, назначаются в текущем объекте. В зависимости от ситуации контроллерами представлений может содержаться разное количество логики. В представлении, приведенном выше, содержится наименьший объем логики.

/components/com_lendr/views/book/raw.php

<?php defined( '_JEXEC' ) or die( 'Restricted access' );
 
class LendrViewsBookRaw extends JViewHtml
{
    function render()
    {
        $app = JFactory::getApplication();
        $type = $app->input->get('type');
        $id = $app->input->get('id');
        $view = $app->input->get('view');
         
        //retrieve task list from model
        $model = new LendrModelBook();
         
        $this->book = $model->getBook($id,$view,FALSE);
        //display
        echo $this->book;
    }
}

Этим контроллером представления производится отображение чистых (не форматированных) деталей книги. В основании этого метода используется id.

Этап 5: создание моделей

Модель для Joomla! Отличается тем же рабочим процессом, который присущ большинству MVC системам. Они ведут обработку, и извлечение большей части данных. Моделям Lendr нами будет уделено пристальное внимание в одной из следующих статей, а в данный момент мы лишь ознакомимся с базовой структурой.

/components/com_lendr/models/book.php

<?php // no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
 
class LendrModelsBook extends LendrModelsDefault
{
    function __construct()
    {
        parent::__construct();
    }
 
    function store()
    {
        …
    }
 
    function getBook()
    {
        …
    }
 
    function getBooks()
    {
        …
    }
     
    function populateState()
    {
        …
    }
}

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

/components/com_lendr/models/default.php

<?php // no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
 
class LendrModelsDefault extends JModelBase
{
    var $__state_set = null;
    var $_total = null;
    var $_pagination = null;
    var $_db = null;
    var $id = null;
     
    function __construct()
    {
        parent::__construct();
        $this->_db = JFactory::getDBO();
         
        $app = JFactory::getApplication();
        $ids = $app->input->get("cids",null,'array');
         
        $id = $app->input->get("id");
        if ( $id && $id > 0 ){
            $this->id = $id;
        }else if ( count($ids) == 1 ){
            $this->id = $ids[0];
        }else{
            $this->id = $ids;
        }
    }
     
    /**
    * Modifies a property of the object, creating it if it does not already exist.
    *
    * @param string $property The name of the property.
    * @param mixed $value The value of the property to set.
    *
    * @return mixed Previous value of the property.
    *
    * @since 11.1
    */
    public function set($property, $value = null)
    {
        $previous = isset($this->$property) ? $this->$property : null;
        $this->$property = $value;
     
        return $previous;
    }
     
    /**
    * Gets an array of objects from the results of database query.
    *
    * @param string $query The query.
    * @param integer $limitstart Offset.
    * @param integer $limit The number of records.
    *
    * @return array An array of results.
    *
    * @since 11.1
    */
    protected function _getList($query, $limitstart = 0, $limit = 0)
    {
        $db = JFactory::getDBO();
        $db->setQuery($query, $limitstart, $limit);
        $result = $db->loadObjectList();
     
        return $result;
    }
     
    /**
    * Returns a record count for the query
    *
    * @param string $query The query.
    *
    * @return integer Number of rows for query
    *
    * @since 11.1
    */
    protected function _getListCount($query)
    {
        $db = JFactory::getDBO();
        $db->setQuery($query);
        $db->query();
     
        return $db->getNumRows();
    }
     
    /* Method to get model state variables
    *
    * @param string $property Optional parameter name
    * @param mixed $default Optional default value
    *
    * @return object The property where specified, the state object where omitted
    *
    * @since 11.1
    */
    public function getState($property = null, $default = null)
    {
        if (!$this->__state_set)
        {
            // Protected method to auto-populate the model state.
            $this->populateState();
             
            // Set the model state set flag to true.
            $this->__state_set = true;
        }
     
        return $property === null ? $this->state : $this->state->get($property, $default);
    }
    /**
    * Get total number of rows for pagination
    */
    function getTotal()
    {
        if ( empty ( $this->_total ) )
        {
            $query = $this->_buildQuery();
            $this->_total = $this->_getListCount($query);
        }
        return $this->_total;
    }
     
    /**
    * Generate pagination
    */
    function getPagination()
    {
        // Lets load the content if it doesn't already exist
        if (empty($this->_pagination))
        {
            $this->_pagination = new JPagination( $this->getTotal(), $this->getState($this->_view.'_limitstart'), $this->getState($this->_view.'_limit'),null,JRoute::_('index.php?view='.$this->_view.'&layout='.$this->_layout));
        }
        return $this->_pagination;
    }
}

Моделью по умолчанию содержится ряд важных функций, которые будут применяться снова в компоненте. Мы обсудим их детальнее в ближайшей статье.

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

Итоги начального этапа разработки

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

Каким образом разрабатываются расширения для Joomla! 3.0 – начинаем разработку (Статья 2)

Следующая часть позволит заняться написание непосредственного функционала модели.

Все статьи цикла:

  1. Каким образом разрабатываются расширения для Joomla! 3.0 – подготовка (Статья 1)
  2. Текущая статья
  3. Каким образом разрабатываются расширения для Joomla! 3.0 – создаем ядро (Статья 3)
  4. Каким образом разрабатываются расширения для Joomla! 3.0 – больше функционала (Статья 4)
  5. Каким образом разрабатываются расширения для Joomla! 3.0 – интерфейс администратора и доработка кода (Статья 5)
Портфолио
Память о Вас и Ваших близких на многие поколения
Подробнее
Прокат металла
Подробнее
Интернет-магазин кожи и меха
Подробнее
100% оригинальная парфюмерия в Москве
Подробнее
Вьетнамский ресторан премиум класса
Подробнее
Внедрение информационных систем
Подробнее
Организация международных конференций
Подробнее
Производство молочной продукции
Подробнее
Спортивный сайт
Подробнее
Интернет-магазин мебели и аксессуаров
Подробнее
Интернет-магазин электротранспорта
Подробнее
Сайт института актуальной экономики
Подробнее
Наши клиенты
Парк развлечений Boom Zoom
Алгор
Норбит
Molga Consulting
Metrotile
Нетология
Monqi
Премиум Пак
Aasha Herbals
Аджва
Салон красоты Сударушка
Пава
ТЦ &quot;Панфиловский&quot;
Фитнес Лаборатория
Система Главбух
Vanguard
GoAsia
ТЦ «Солнечный ветер»
Teledoc
Tchernov Cable
Отзывы
Благодарю компанию web-now.pro за помощь в разработке и запуске проекта POLITSECRETS.RU. Перед нами стояла задача – внедрить проект в сжатые сроки и по оптимальной цене. Порадовало то, что мне подроб...
Вера БлашенковаСекреты успешных выборов, Москва... апр.2016
Мне очень понравился подход с которым нас встретили "Ваша задача заниматься бизнесом, наша - сделать Вам представительство в сети". После этого ребята разработали полное тз на проект, мы внесли пожела...
МаксимIT-TASK, Москва... янв.2016
Работа проделана хорошо! Дизайнер и менеджер на отлично. Надеюсь на сотрудничество в дальнейшем. Есть шероховатости в деталях по задачам, но приятно сказывается оперативность и желание исправить, внес...
БруноСоциальная сеть След Жизни, Москва... янв.2016
Работой остались очень довольны. К работе подходят ответственно, аккуратно, открыто. Проект был сдан чуть раньше срока, по ходу работы возникали изменения, все они принимались безоговорочно, работа вы...
ЕвгенийМагазин текстиля, Москва... дек.2015
Сотрудничаем с 2007 года и сделали не один проект. Самое главное - команда умеет отстаивать своё мнение и постоянно развивается.
МарияМеждународные конференции, Москва... дек.2015
Спасибо всему коллективу компании! Разработали красивый и что самое главной рабочий интернет магазин! Реклама настроена и запущена, продажи идут, бизнес развивается! Нам постоянно подсказывают о новых...
ВадимИнтернет магазин Aromatic.pro, Москва... сен.2015
Большое спасибо команде за оперативность, качественные работы, отличный креатив и привлекательные цены!
Виктория, ОАО "Фармстандарт... июль.2015
Здравствуйте уважаемые партнеры! С наступающим Новым Годом! Желаю Вам дальнейшего процветания и успехов в Вашей благородной работе! С вами приятно сотрудничать! Очень благодарен Вам за своевременное о...
Сергей ЮрченкоКинотруд, Москва... дек.2014
Благодарим команду Brand Now и лично Дениса Логинова за оригинальное видение,разнообразие идей, четкость взаимодействия и безукоризненное соблюдение сроков выполнения проекта! Планируем продолжить сот...
ТатьянаBizness Linkerz... июль.2014
Компания КУН выражает благодарность за сотрудничество: непростая задача была выполнена в требуемые сроки и полностью удовлетворила заявленному ТЗ. Приятно удивила готовность Генерального директора нач...
Мария, Компания КУНhttp://www.kuhn.com/... апр.2014
Все отзывы
Добавить отзыв