From 037d0cdce941a529615a13fcf205fa21206fd6dc Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 25 Mar 2015 15:42:49 +0300 Subject: [PATCH] init --- classes/Controller/API/Datasource/Data.php | 66 + .../Controller/API/Datasource/Document.php | 124 ++ classes/Controller/Datasources/Data.php | 67 + classes/Controller/Datasources/Section.php | 200 +++ classes/Controller/System/Datasource.php | 71 + .../Controller/System/Datasource/Document.php | 207 +++ classes/Datasource/Data/Manager.php | 279 ++++ classes/Datasource/Document.php | 964 ++++++++++++++ classes/Datasource/Exception/Document.php | 11 + classes/Datasource/Exception/Section.php | 11 + classes/Datasource/Folder.php | 123 ++ classes/Datasource/Section.php | 1152 +++++++++++++++++ classes/Datasource/Section/Headline.php | 326 +++++ classes/Model/Widget/Datasource/Search.php | 174 +++ composer.json | 25 + config/datasources.php | 5 + config/permissions.php | 23 + config/widgets.php | 7 + i18n/ru.php | 53 + init.php | 129 ++ install/schema.sql | 24 + media/css/datasource.css | 119 ++ media/js/datasource.js | 212 +++ messages/validation.php | 7 + views/datasource/content.php | 34 + views/datasource/data/index.php | 40 + views/datasource/data/menu.php | 110 ++ views/datasource/section/actions.php | 44 + views/datasource/section/create.php | 46 + views/datasource/section/edit.php | 14 + views/datasource/section/headline.php | 66 + views/datasource/section/information_form.php | 50 + views/widgets/backend/datasource_search.php | 41 + views/widgets/frontend/datasource_search.php | 17 + 34 files changed, 4841 insertions(+) create mode 100644 classes/Controller/API/Datasource/Data.php create mode 100644 classes/Controller/API/Datasource/Document.php create mode 100644 classes/Controller/Datasources/Data.php create mode 100644 classes/Controller/Datasources/Section.php create mode 100644 classes/Controller/System/Datasource.php create mode 100644 classes/Controller/System/Datasource/Document.php create mode 100644 classes/Datasource/Data/Manager.php create mode 100644 classes/Datasource/Document.php create mode 100644 classes/Datasource/Exception/Document.php create mode 100644 classes/Datasource/Exception/Section.php create mode 100644 classes/Datasource/Folder.php create mode 100644 classes/Datasource/Section.php create mode 100644 classes/Datasource/Section/Headline.php create mode 100644 classes/Model/Widget/Datasource/Search.php create mode 100644 composer.json create mode 100644 config/datasources.php create mode 100644 config/permissions.php create mode 100644 config/widgets.php create mode 100644 i18n/ru.php create mode 100644 init.php create mode 100644 install/schema.sql create mode 100644 media/css/datasource.css create mode 100644 media/js/datasource.js create mode 100644 messages/validation.php create mode 100644 views/datasource/content.php create mode 100644 views/datasource/data/index.php create mode 100644 views/datasource/data/menu.php create mode 100644 views/datasource/section/actions.php create mode 100644 views/datasource/section/create.php create mode 100644 views/datasource/section/edit.php create mode 100644 views/datasource/section/headline.php create mode 100644 views/datasource/section/information_form.php create mode 100644 views/widgets/backend/datasource_search.php create mode 100644 views/widgets/frontend/datasource_search.php diff --git a/classes/Controller/API/Datasource/Data.php b/classes/Controller/API/Datasource/Data.php new file mode 100644 index 0000000..9d73b9a --- /dev/null +++ b/classes/Controller/API/Datasource/Data.php @@ -0,0 +1,66 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Controller_Api_Datasource_Data extends Controller_System_API +{ + public function get_menu() + { + $ds_id = (int) $this->param('ds_id', NULL); + + $tree = Datasource_Data_Manager::get_tree(); + $menu = View::factory('datasource/data/menu', array( + 'tree' => $tree, + 'folders' => Datasource_Folder::get_all(), + 'datasource' => DataSource_Section::load($ds_id) + )); + + $this->response((string) $menu); + } + + public function post_menu() + { + $folder_id = (int) $this->param('folder_id', 0); + $ds_id = (int) $this->param('ds_id', NULL, TRUE); + + $ds = DataSource_Section::load($ds_id); + + if ($ds->loaded()) + { + $ds->move_to_folder($folder_id); + $this->status = TRUE; + } + } + + public function get_folder() + { + $folder_id = (int) $this->param('id', 0); + $this->response(DataSource_Folder::get($folder_id)); + } + + public function put_folder() + { + $name = $this->param('name', NULL, TRUE); + + if (DataSource_Folder::add($name)) + { + $this->status = TRUE; + } + } + + public function delete_folder() + { + $folder_id = (int) $this->param('id', 0); + + if (DataSource_Folder::delete($folder_id)) + { + $this->status = TRUE; + } + } +} \ No newline at end of file diff --git a/classes/Controller/API/Datasource/Document.php b/classes/Controller/API/Datasource/Document.php new file mode 100644 index 0000000..9b1e34c --- /dev/null +++ b/classes/Controller/API/Datasource/Document.php @@ -0,0 +1,124 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Controller_Api_Datasource_Document extends Controller_System_API +{ + public function get_headline() + { + $ds_id = $this->param('ds_id', NULL, TRUE); + $page = (int) $this->param('page', 1); + + $ds = $this->_get_datasource($ds_id); + + $this->response($this->_get_headline($ds, $page)); + } + + public function post_create() + { + $ds_id = $this->param('ds_id', NULL, TRUE); + + $ds = $this->_get_datasource($ds_id); + + $doc = $ds->get_empty_document(); + + $doc->read_values($this->params()) + ->validate(); + + $doc = $ds->create_document($doc); + + $this->message('Document created'); + $this->response($doc->values()); + } + + public function post_update() + { + $ds_id = $this->param('ds_id', NULL, TRUE); + $id = $this->param('id', NULL, TRUE); + + $ds = $this->_get_datasource($ds_id); + + $doc = $ds->get_document((int) $id); + + $doc->read_values($this->params()) + ->validate(); + + $doc = $ds->update_document($doc); + + $this->message('Document updated'); + $this->response($doc->values()); + } + + public function post_publish() + { + $doc_ids = $this->param('doc', array(), TRUE); + $ds_id = $this->param('ds_id', NULL, TRUE); + + if (empty($doc_ids)) + { + throw HTTP_API_Exception::factory(API::ERROR_UNKNOWN, + 'Error'); + } + + $ds = $this->_get_datasource($ds_id); + + $ds->publish($doc_ids); + $this->response($doc_ids); + } + + public function post_unpublish() + { + $doc_ids = $this->param('doc', array(), TRUE); + $ds_id = $this->param('ds_id', NULL, TRUE); + + if (empty($doc_ids)) + { + throw HTTP_API_Exception::factory(API::ERROR_UNKNOWN, + 'Error'); + } + + $ds = $this->_get_datasource($ds_id); + + $ds->unpublish($doc_ids); + $this->response($doc_ids); + } + + public function post_remove() + { + $doc_ids = $this->param('doc', array(), TRUE); + $ds_id = $this->param('ds_id', NULL, TRUE); + + if (empty($doc_ids)) + { + throw HTTP_API_Exception::factory(API::ERROR_UNKNOWN, + 'Error'); + } + + $ds = $this->_get_datasource($ds_id); + + $ds->remove_documents( $doc_ids ); + $this->response($doc_ids); + } + + protected function _get_datasource($ds_id) + { + $ds = Datasource_Section::load($ds_id); + if ($ds === NULL) + { + throw HTTP_API_Exception::factory(API::ERROR_UNKNOWN, 'Datasource section not found'); + } + + return $ds; + } + + protected function _get_headline($ds, $page = 1) + { + return (string) $ds->headline()->set_page($page)->render(); + } +} \ No newline at end of file diff --git a/classes/Controller/Datasources/Data.php b/classes/Controller/Datasources/Data.php new file mode 100644 index 0000000..917409b --- /dev/null +++ b/classes/Controller/Datasources/Data.php @@ -0,0 +1,67 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Controller_Datasources_Data extends Controller_System_Datasource +{ + public function action_index() + { + Assets::package('jquery-ui'); + + $cur_ds_id = (int) Arr::get($this->request->query(), 'ds_id', Cookie::get('ds_id')); + $tree = Datasource_Data_Manager::get_tree(); + + $cur_ds_id = Datasource_Data_Manager::exists($cur_ds_id) + ? $cur_ds_id + : Datasource_Data_Manager::$first_section; + + $ds = $this->section($cur_ds_id); + + $this->template->content = View::factory('datasource/content', array( + 'content' => View::factory('datasource/data/index'), + 'menu' => View::factory('datasource/data/menu', array( + 'tree' => $tree, + 'folders' => Datasource_Folder::get_all() + )) + )); + + $this->template->footer = NULL; + $this->template->breadcrumbs = NULL; + + if ($ds instanceof Datasource_Section) + { + $this->set_title($ds->name); + + $limit = (int) Arr::get($this->request->query(), 'limit', Cookie::get('limit')); + + Cookie::set('ds_id', $cur_ds_id); + + $keyword = $this->request->query('keyword'); + + if (!empty($limit)) + { + Cookie::set('limit', $limit); + $this->section()->headline()->limit($limit); + } + + $this->template->content->content->headline = $this->section()->headline()->render(); + $this->template->content->content->toolbar = View::factory('datasource/' . $ds->type() . '/toolbar', array( + 'keyword' => $keyword + )); + + $this->template->set_global(array('datasource' => $ds)); + $this->template_js_params['DS_ID'] = $this->_section->id(); + $this->template_js_params['DS_TYPE'] = $this->_section->type(); + } + else + { + $this->template->content->content = NULL; + } + } +} \ No newline at end of file diff --git a/classes/Controller/Datasources/Section.php b/classes/Controller/Datasources/Section.php new file mode 100644 index 0000000..002827b --- /dev/null +++ b/classes/Controller/Datasources/Section.php @@ -0,0 +1,200 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Controller_Datasources_Section extends Controller_System_Datasource +{ + public function before() + { + if ($this->request->action() != 'create') + { + $ds_id = (int) $this->request->param('id'); + $this->section($ds_id); + + if ($this->section()->has_access_edit()) + { + $this->allowed_actions[] = 'edit'; + } + + if ($this->section()->has_access_remove()) + { + $this->allowed_actions[] = 'remove'; + } + } + else + { + $type = strtolower($this->request->param('id')); + if (ACL::check($type . '.section.create')) + { + $this->allowed_actions[] = 'create'; + } + } + + parent::before(); + } + + public function action_create() + { + $type = $this->request->param('id'); + $type = strtolower($type); + + $types = Datasource_Data_Manager::types(); + if (Arr::get($types, $type) === NULL) + { + throw new Kohana_Exception('Datasource type :type not found', array(':type' => $type)); + } + + if ($this->request->method() === Request::POST) + { + return $this->_create($type); + } + + $this->set_title(__('Add section :type', array(':type' => Arr::get($types, $type)))); + + try + { + $this->template->content = View::factory('datasource/'.$type.'/section/create', array( + 'type' => $type, + 'data' => Flash::get('post_data'), + 'users' => ORM::factory('user')->find_all()->as_array('id', 'username') + )); + } + catch (Exception $exc) + { + $this->template->content = View::factory('datasource/section/create', array( + 'type' => $type, + 'data' => Flash::get('post_data'), + 'users' => ORM::factory('user')->find_all()->as_array('id', 'username') + )); + } + } + + /** + * + * @param string $type + */ + private function _create($type) + { + $section = Datasource_Section::factory($type); + + $data = $this->request->post(); + + $data['created_by_id'] = Auth::get_id(); + + try + { + $ds_id = $section + ->validate($data) + ->create($data); + } + catch (Validation_Exception $e) + { + Messages::errors($e->errors('validation')); + $this->go_back(); + } + catch (DataSource_Exception_Section $e) + { + Messages::errors($e->getMessage()); + $this->go_back(); + } + + Messages::success( __( 'Datasource has been saved!' ) ); + + $this->go(Route::get('datasources')->uri(array( + 'directory' => 'datasources', + 'controller' => 'section', + 'action' => 'edit', + 'id' => $ds_id + ))); + } + + public function action_edit() + { + if ($this->request->method() === Request::POST) + { + return $this->_edit($this->section()); + } + + $this->breadcrumbs + ->add($this->section()->name, Route::get('datasources')->uri(array( + 'controller' => 'data', + 'directory' => 'datasources', + )) . URL::query(array('ds_id' => $this->section()->id()), FALSE)); + + $this->set_title(__('Edit section :name', array( + ':name' => $this->section()->name + ))); + + $this->template_js_params['DS_ID'] = $this->section()->id(); + $this->template_js_params['DS_TYPE'] = $this->section()->type(); + + try + { + $this->template->content = View::factory('datasource/'.$this->section()->type().'/section/edit', array( + 'ds' => $this->section(), + 'users' => ORM::factory('user')->find_all()->as_array('id', 'username') + )); + } + catch (Exception $exc) + { + $this->template->content = View::factory('datasource/section/edit', array( + 'ds' => $this->section(), + 'users' => ORM::factory('user')->find_all()->as_array('id', 'username') + )); + } + } + + /** + * + * @param Datasource_Section $ds + */ + private function _edit($ds) + { + $data = $this->request->post(); + + try + { + $ds->values($data); + $ds->update(); + } + catch (Validation_Exception $e) + { + Messages::errors($e->errors('validation')); + $this->go_back(); + } + catch (DataSource_Exception_Section $e) + { + Messages::errors($e->getMessage()); + $this->go_back(); + } + + Messages::success( __( 'Datasource has been saved!' ) ); + + // save and quit or save and continue editing? + if ( $this->request->post('commit') !== NULL ) + { + $this->go( Route::get('datasources')->uri(array( + 'directory' => 'datasources', + 'controller' => 'data' + )) . URL::query(array('ds_id' => $ds->id()), FALSE)); + } + else + { + $this->go_back(); + } + } + + public function action_remove() + { + $this->section()->remove(); + + Messages::success(__('Datasource has been deleted!')); + $this->go_back(); + } +} \ No newline at end of file diff --git a/classes/Controller/System/Datasource.php b/classes/Controller/System/Datasource.php new file mode 100644 index 0000000..50520ef --- /dev/null +++ b/classes/Controller/System/Datasource.php @@ -0,0 +1,71 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Controller_System_Datasource extends Controller_System_Backend +{ + public $allowed_actions = array( + 'index' + ); + + /** + * + * @var DataSource_Section + */ + protected $_section = NULL; + + public function before() + { + parent::before(); + + Assets::js('datasource', ADMIN_RESOURCES . 'js/datasource.js', 'global'); + Assets::css('datasource', ADMIN_RESOURCES . 'css/datasource.css', 'global'); + } + + /** + * + * @param integer $id + * @return DataSource_Section + * @throws HTTP_Exception_404 + */ + public function section($id = NULL) + { + if ($this->_section instanceof DataSource_Section) + { + return $this->_section; + } + + if ($id === NULL) + { + Messages::errors(__('Datasource section not loaded')); + $this->go_home(); + } + + $this->_section = Datasource_Data_Manager::load((int) $id); + + if ( + $this->request->action() == 'index' + AND + ! $this->_section->has_access_view() + ) + { + $this->_deny_access(); + } + + if (empty($this->_section)) + { + Messages::errors(__('Datasource section :id not found', array( + ':id' => $id))); + + $this->go_home(); + } + + return $this->_section; + } +} \ No newline at end of file diff --git a/classes/Controller/System/Datasource/Document.php b/classes/Controller/System/Datasource/Document.php new file mode 100644 index 0000000..eac8ce6 --- /dev/null +++ b/classes/Controller/System/Datasource/Document.php @@ -0,0 +1,207 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Controller_System_Datasource_Document extends Controller_System_Datasource +{ + public function before() + { + $ds_id = (int) Arr::get($this->request->query(), 'ds_id', $this->request->post('ds_id')); + + $this->section($ds_id); + + $this->_check_acl(); + + parent::before(); + } + + public function action_create() + { + return $this->action_view(); + } + + public function action_view() + { + Assets::package('backbone'); + $id = (int) $this->request->query('id'); + + $doc = $this->_get_document($id); + + WYSIWYG::load_all(); + + $this->_load_session_data($doc); + + $doc->onControllerLoad(); + + $this->breadcrumbs + ->add($this->section()->name, Route::get('datasources')->uri(array( + 'directory' => 'datasources', + 'controller' => 'data' + )) . URL::query(array('ds_id' => $this->section()->id()), FALSE)); + + $this->template_js_params['API_FORM_ACTION'] = '/datasource-document.' . ($doc->loaded() ? 'update' : 'create'); + $this->template_js_params['SECTION_ID'] = (int) $this->section()->id(); + $this->template_js_params['DOCUMENT_ID'] = $id; + + if (!$doc->loaded()) + { + $this->set_title(__('New document')); + } + else + { + $this->set_title($doc->header); + } + + $this->_load_template($doc); + } + + /** + * + * @param Datasource_Section $ds + * @param Datasource_Document $doc + */ + public function action_post() + { + $id = (int) $this->request->post('id'); + $doc = $this->_get_document($id); + + Session::instance()->set('post_data', $this->request->post()); + + try + { + $doc + ->read_values($this->request->post()) + ->read_files($_FILES) + ->validate(); + } + catch (Validation_Exception $e) + { + Messages::errors($e->errors('validation')); + $this->go_back(); + } + catch (DataSource_Exception_Document $e) + { + Messages::errors($e->getMessage()); + $this->go_back(); + } + + if ($doc->loaded()) + { + $this->section()->update_document($doc); + } + else + { + $doc = $this->section()->create_document($doc); + } + + Messages::success(__('Document saved')); + + Session::instance()->delete('post_data'); + + // save and quit or save and continue editing? + if ($this->request->post('commit') !== NULL) + { + $this->go(Route::get('datasources')->uri(array( + 'directory' => 'datasources', + 'controller' => 'data' + )) . URL::query(array('ds_id' => $this->section()->id()), FALSE)); + } + else + { + $this->go(Route::get('datasources')->uri(array( + 'directory' => $this->section()->type(), + 'controller' => 'document', + 'action' => 'view' + )) . URL::query(array('ds_id' => $this->section()->id(), 'id' => $doc->id), FALSE)); + } + } + + /** + * + * @param Datasource_Document $doc + */ + protected function _load_template($doc) + { + $this->template->content = View::factory('datasource/'.$this->section()->type().'/document/edit') + ->set(array( + 'action' => $this->request->action() + )); + + View::set_global(array( + 'form' => array( + 'label_class' => 'control-label col-md-2 col-sm-3', + 'input_container_class' => 'col-md-10 col-lg-10 col-sm-9', + 'input_container_offset_class' => 'col-md-offset-2 col-sm-offset-3 col-md-10 col-sm-9' + ), + 'document' => $doc, + 'datasource' => $this->section(), + )); + } + + /** + * + * @param Datasource_Document $doc + * return Datasource_Document + */ + protected function _load_session_data($doc) + { + $post_data = Session::instance()->get_once('post_data'); + + if (!empty($post_data)) + { + unset($post_data['id']); + $doc->read_values($post_data); + } + + return $doc; + } + + /** + * + * @param string $type + * @param integer $ds_id + */ + protected function _check_acl() + { + if ( + $this->section()->has_access('document.view') + OR + $this->section()->has_access('document.edit') + ) + { + $this->allowed_actions[] = 'view'; + } + + if ($this->section()->has_access('document.create')) + { + $this->allowed_actions[] = 'create'; + $this->allowed_actions[] = 'post'; + } + } + + protected function _get_document($id) + { + if (empty($id)) + { + $doc = $this->section()->get_empty_document(); + } + else + { + $doc = $this->section()->get_document($id); + + if (!$doc) + { + throw new HTTP_Exception_404( + 'Document ID :id not found', array(':id' => $id)); + } + } + + return $doc; + } +} \ No newline at end of file diff --git a/classes/Datasource/Data/Manager.php b/classes/Datasource/Data/Manager.php new file mode 100644 index 0000000..f8e0f4c --- /dev/null +++ b/classes/Datasource/Data/Manager.php @@ -0,0 +1,279 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Datasource_Data_Manager { + + /** + * + * @var integer + */ + public static $first_section = NULL; + + /** + * + * @var array + */ + protected static $_cache = array(); + + /** + * Добавление раздела в меню Backend + * + * @param Datasource_Section $section + * @param Model_Navigation_Section $parent_section + * return Model_Navigation_Section; + */ + public static function add_section_to_menu(Datasource_Section $section, Model_Navigation_Section $parent_section = NULL) + { + if ($parent_section === NULL) + { + $parent_section = Model_Navigation::get_root_section(); + } + + if (!$section->has_access_view()) + { + return $parent_section; + } + + return $parent_section + ->add_page(new Model_Navigation_Page(array( + 'name' => $section->name, + 'url' => Route::get('datasources')->uri(array( + 'controller' => 'data', + 'directory' => 'datasources', + )) . URL::query(array('ds_id' => $section->id())), + 'icon' => $section->icon(), + 'permissions' => 'ds_id.' . $section->id() . '.section.view' + )), 999); + } + + /** + * Список всех типов разделв + * + * @return array + */ + public static function types() + { + return Config::get('datasources.types'); + } + + /** + * Загрузить дерево всех разделов + * Если есть разделы, модули для которых отключены, они будут игнорироваться + * + * @return array array([Type][ID] => Datasource_Section) + */ + public static function get_tree( $type = NULL ) + { + $result = array(); + + $sections = self::get_all($type); + + foreach ($sections as $section) + { + if (!Datasource_Section::exists($section->type())) + { + continue; + } + + if (self::$first_section === NULL) + { + self::$first_section = $section->id(); + } + + $result[$section->type()][$section->id()] = $section; + } + + return $result; + } + + /** + * Получить список всех разделов + * + * @param string $type Фильтрация по типу разделов + * @return array array([ID] => Datasource_Section, ...) + */ + public static function get_all($type = NULL) + { + if (is_array($type)) + { + if (empty($type)) + { + return array(); + } + } + else if ($type !== NULL) + { + $type = array($type); + } + else + { + $type = array_keys(self::types()); + } + + $sections = array(); + + foreach ($type as $i => $key) + { + if (isset(self::$_cache[$key])) + { + foreach (self::$_cache[$key] as $id => $section) + { + $sections[$id] = $section; + } + unset($type[$i]); + } + } + + if (empty($type)) + { + return $sections; + } + + $query = DB::select() + ->from('datasources') + ->order_by('type') + ->order_by('name'); + + if ($type !== NULL) + { + $query->where('type', 'in', $type); + } + + $db_sections = $query->execute()->as_array('id'); + + foreach ($db_sections as $id => $section) + { + if (!Datasource_Section::exists($section['type'])) + { + continue; + } + + $section = Datasource_Section::load_from_array($section); + $sections[$id] = $section; + self::$_cache[$section->type()][$id] = $section; + } + + return $sections; + } + + /** + * Получение списка разделов для выпадающего списка + * @return array + */ + public static function get_all_as_options( $type = NULL ) + { + $datasources = self::get_all($type); + + $options = array(__('--- Not set ---')); + foreach ($datasources as $id => $section) + { + $options[$id] = $section->name; + } + + return $options; + } + + + /** + * Загрузка разедла по ID + * + * @param integer $id + * @return null|Datasource_Section + */ + public static function load($id) + { + return Datasource_Section::load($id); + } + + /** + * Проверка раздела на существование по ID + * + * @param integer $ds_id Datasource ID + * @return boolean + */ + public static function exists($ds_id) + { + return (bool) DB::select('id') + ->from('datasources') + ->where('id', '=', (int) $ds_id) + ->limit(1) + ->execute() + ->get('id'); + } + + /** + * + * @param integer $ds_id Datasource ID + * @return array + */ + public static function get_info($ds_id) + { + return DB::select('id', 'type', 'name', 'description') + ->from(array('datasources', 'ds')) + ->where('ds.id', '=', (int) $ds_id) + ->limit(1) + ->execute() + ->current(); + } + + /** + * + * @param string $type + * @return string + */ + public static function get_icon($type) + { + $class_name = 'DataSource_Section_' . ucfirst($type); + + if (class_exists($class_name)) + { + return call_user_func($class_name . '::default_icon'); + } + + return Datasource_Section::default_icon(); + } + + /** + * + * @param integer $ds_id + * @return array + */ + public static function clear_cache( $ds_id, array $widget_types = array() ) + { + $objects = Widget_Manager::get_widgets($widget_types); + + $cleared_ids = array(); + + foreach ($objects as $id => $data) + { + $widget = Widget_Manager::load($id); + if ($widget->ds_id == $ds_id) + { + $cleared_ids[] = $widget->id; + $widget->clear_cache(); + } + } + + return $cleared_ids; + } + + /** + * + * @return array + */ + public static function get_all_indexed() + { + return DB::select('id', 'type', 'name', 'description') + ->from('datasources') + ->where('indexed', '!=', 0) + ->order_by('name') + ->execute() + ->as_array('id'); + } +} \ No newline at end of file diff --git a/classes/Datasource/Document.php b/classes/Datasource/Document.php new file mode 100644 index 0000000..3dd1041 --- /dev/null +++ b/classes/Datasource/Document.php @@ -0,0 +1,964 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Datasource_Document implements ArrayAccess { + + /** + * Список системных полей + * @var array + */ + protected $_system_fields = array( + 'id' => NULL, + 'ds_id' => NULL, + 'published' => NULL, + 'header' => NULL + ); + + /** + * Значения, которые могут понадобится в документе, + * но которые не попадут в БД. + * + * @var array + */ + protected $_temp_fields = array(); + + /** + * + * @var array + */ + protected $_changed_fields = array(); + + /** + * Объект раздела + * @var DataSource_Hybrid_Section + */ + protected $_section = NULL; + + /** + * Статус загрузки документа + * @var boolean + */ + protected $_loaded = FALSE; + + /** + * Статус обновления документа + * @var boolean + */ + protected $_updated = FALSE; + + /** + * Статус создания документа + * @var boolean + */ + protected $_created = FALSE; + + /** + * Документ имеет автора + * @var boolean + */ + protected $_is_authored = FALSE; + + /** + * Документ в режиме для чтения + * @var boolean + */ + protected $_read_only = FALSE; + + /** + * + * @param DataSource_Hybrid_Section $section + */ + public function __construct( DataSource_Section $section ) + { + $this->_section = $section; + + $this->ds_id = $section->id(); + + $this->reset(); + } + + /** + * Список значений полей по умолчанию + * @return array + */ + public function defaults() + { + return array( + 'published' => 1 + ); + } + + /** + * Правила фильтрации полей документа + * @return array + */ + public function filters() + { + return array( + 'id' => array( + array('intval') + ), + 'ds_id' => array( + array('intval') + ), + 'published' => array( + array(array($this, 'set_published')) + ) + ); + } + + /** + * Правила валидации полей документа + * @return type + */ + public function rules() + { + return array( + 'header' => array( + array('not_empty') + ) + ); + } + + /** + * Заголовки полей + * @return type + */ + public function labels() + { + return array( + 'id' => __('ID'), + 'header' => __('Header'), + 'published' => __('Published') + ); + } + + /** + * Сеттер. Присваивает значение полю документа + * + * @param string $field + * @param string $value + */ + public function __set($field, $value) + { + $this->set($field, $value); + } + + /** + * Геттер значений полей документов + * + * @param string $field + * @return mixed + */ + public function __get($field) + { + return $this->get($field); + } + + /** + * Проаверка существаования поля в документе + * + * @param string $field + * @return boolean + */ + public function __isset($field) + { + return isset($this->_system_fields[$field]) OR isset($this->_temp_fields[$field]); + } + + /** + * + * @param string $field + */ + public function __unset($field) + { + $this->_system_fields[$field] = NULL; + $this->_temp_fields[$field] = NULL; + } + + /** + * Проверка существования документа + * + * @return boolean + */ + public function loaded() + { + return (bool) $this->_loaded; + } + + /** + * Проверка создания документа + * + * @return boolean + */ + public function created() + { + return (bool) $this->_created; + } + + /** + * Проверка обновления документа + * + * @return boolean + */ + public function updated() + { + return (bool) $this->_updated; + } + + /** + * Получение объекта раздела + * + * @return DataSource_Section + */ + public function section() + { + return $this->_section; + } + + /** + * Геттер значений полей документов + * + * @param string $field + * @param mixed $default + * @return mixed + */ + public function get($field, $default = NULL) + { + if (array_key_exists($field, $this->system_fields())) + { + if (!$this->loaded() AND (Arr::get($this->_system_fields, $field) === NULL)) + { + return Arr::get($this->defaults(), $field, $default); + } + + return $this->_system_fields[$field]; + } + + return Arr::get($this->_temp_fields, $field, $default); + } + + /** + * Сеттер. Присваивает значение полю документа + * Если поле не существует, значение попадает в массив _temp_fields + * + * @param string $field + * @param string $value + * @return \Datasource_Document + */ + public function set($field, $value) + { + if ($this->is_read_only()) + { + return $this; + } + + if (array_key_exists($field, $this->system_fields())) + { + $this->_changed_fields[$field] = $this->_system_fields[$field]; + $this->_system_fields[$field] = $this->_run_filter($field, $value); + } + else + { + $this->_temp_fields[$field] = $value; + } + + return $this; + } + + public function set_published($value) + { + return (bool) $value ? 1 : 0; + } + + /** + * Установка документа в режим для чтения + * @return \Datasource_Document + */ + public function set_read_only() + { + $this->_read_only = TRUE; + + return $this; + } + + /** + * + * @return boolean + */ + public function is_published() + { + return (bool) $this->published; + } + + /** + * Установка документа в режим для чтения + * @return boolean + */ + public function is_read_only() + { + return $this->_read_only; + } + + /** + * Получение старого значения поля, до присвоения нового + * + * @param string $field + * @param mixed $default + * @return mixed + */ + public function get_old_value($field, $default = NULL) + { + return Arr::get($this->_changed_fields, $field); + } + + /** + * Проверка поля на изменение значения + * + * @param string $field + * @return boolean + */ + public function is_changed( $field ) + { + return ( + $this->get_old_value($field) !== NULL + AND + $this->{$field} != $this->get_old_value($field) + ); + } + + /** + * Получение всех значений полей + * + * @return array array([Field name] => [value]) + */ + public function values() + { + return $this->system_fields(); + } + + /** + * Получение списка системных полей + * + * @return array array([Field name]) + */ + public function system_fields() + { + if ($this->_is_authored === TRUE AND ! isset($this->_system_fields['created_by_id'])) + { + $this->_system_fields['created_by_id'] = NULL; + } + + return $this->_system_fields; + } + + /** + * Загрузка данных из массива + * + * @param array $array Массив значений полей документа + * @param array $expected + * @return \DataSource_Document + */ + public function read_values(array $array = NULL, array $expected = NULL) + { + // Default to expecting everything except the primary key + if ($expected === NULL) + { + $expected = $this->system_fields(); + } + else + { + $fields = $this->system_fields(); + foreach ($fields as $key => $value) + { + if (!in_array($key, $expected)) + { + unset($fields[$key]); + } + } + + $expected = $fields; + } + + foreach ($expected as $key => $value) + { + if ($key == 'id') + { + continue; + } + + $this->{$key} = Arr::get($array, $key); + unset($array[$key]); + } + + $this->_temp_fields = $array; + + return $this; + } + + /** + * Загрузка файлов из массива + * + * @param array $array + * @return \DataSource_Document + */ + public function read_files($array) + { + foreach ($array as $key => $value) + { + $this->{$key} = $value; + } + + return $this; + } + + /** + * Сброс значений полей документа + * + * @return \DataSource_Document + */ + public function reset() + { + foreach ($this->system_fields() as $key => $value) + { + $this->_system_fields[$key] = NULL; + } + + return $this; + } + + /** + * Фильтрация полей документа согласно правилам + * + * @see DataSource_Hybrid_Document::filters() + * + * @param string $field + * @param mixed $value + * @return mixed + */ + protected function _run_filter($field, $value) + { + $filters = $this->filters(); + + // Get the filters for this column + $wildcards = empty($filters[TRUE]) ? array() : $filters[TRUE]; + + // Merge in the wildcards + $filters = empty($filters[$field]) ? $wildcards : array_merge($wildcards, $filters[$field]); + + // Bind the field name and model so they can be used in the filter method + $_bound = array + ( + ':field' => $field, + ':document' => $this, + ); + + foreach ($filters as $array) + { + // Value needs to be bound inside the loop so we are always using the + // version that was modified by the filters that already ran + $_bound[':value'] = $value; + + // Filters are defined as array($filter, $params) + $filter = $array[0]; + $params = Arr::get($array, 1, array(':value')); + + foreach ($params as $key => $param) + { + if (is_string($param) AND array_key_exists($param, $_bound)) + { + // Replace with bound value + $params[$key] = $_bound[$param]; + } + } + + if (is_array($filter) OR ! is_string($filter)) + { + // This is either a callback as an array or a lambda + $value = call_user_func_array($filter, $params); + } + elseif (strpos($filter, '::') === FALSE) + { + // Use a function call + $function = new ReflectionFunction($filter); + + // Call $function($this[$field], $param, ...) with Reflection + $value = $function->invokeArgs($params); + } + else + { + // Split the class and method of the rule + list($class, $method) = explode('::', $filter, 2); + + // Use a static method call + $method = new ReflectionMethod($class, $method); + + // Call $Class::$method($this[$field], $param, ...) with Reflection + $value = $method->invokeArgs(NULL, $params); + } + } + + return $value; + } + + /** + * Загрузка документа по его ID + * + * $ds = Datasource_Data_Manager::load($ds_id); + * $doc = $ds->get_document($id); + * + * Проверка загрузки документа + * + * $doc->loaded(); + * + * @param integer $id + * @return \DataSource_Document + */ + public function load($id) + { + return $this->load_by('id', (int) $id); + } + + /** + * Загрузка документа по названию поля значению + * + * @param string $field + * @param string $value + * @return \DataSource_Document + */ + public function load_by($field, $value) + { + $result = DB::select() + ->select_array(array_keys($this->system_fields())) + ->from($this->section()->table()) + ->where('ds_id', '=', (int) $this->section()->id()) + ->where($field, '=', $value) + ->limit(1) + ->execute() + ->current(); + + if (empty($result)) + { + return $this; + } + + $this->_loaded = TRUE; + + foreach ($result as $field => $value) + { + $this->{$field} = $value; + } + + return $this; + } + + /** + * Создание документа + * + * $ds = Datasource_Data_Manager::load($ds_id); + * $doc = $ds->get_empty_document(); + * $doc + * ->read_values($this->request->post()) + * ->read_files($_FILES) + * ->validate(); + * $doc = $ds->create_document($doc); + * + * Проверка создания документа + * + * $doc->created() + * + * @return DataSource_Document + */ + public function create() + { + if ($this->is_read_only()) + { + throw new DataSource_Exception_Document('Document is read only'); + } + + if (!$this->has_access_create()) + { + throw new DataSource_Exception_Document('You do not have permission to create document'); + } + + $values = $this->system_fields(); + + $values['ds_id'] = $this->section()->id(); + $values['created_on'] = date('Y-m-d H:i:s'); + $values['updated_on'] = $values['created_on']; + + if ($this->_is_authored === TRUE) + { + $values['created_by_id'] = (int) Auth::get_id(); + } + + unset($values['id']); + + $query = DB::insert($this->section()->table()) + ->columns(array_keys($values)) + ->values(array_values($values)) + ->execute(); + + $id = $query[0]; + + if (empty($id)) + { + return $this; + } + + $this->id = $id; + + $this->_created = TRUE; + + return $this; + } + + /** + * Обновление документа + * + * $ds = Datasource_Data_Manager::load($ds_id); + * $doc = $ds->get_document($id); + * $doc + * ->read_values($this->request->post()) + * ->read_files($_FILES) + * ->validate(); + * + * $doc = $ds->update_document($doc); + * + * Проверка обновленияя документа + * + * $doc->updated() + * + * @return DataSource_Document + */ + public function update() + { + if ($this->is_read_only()) + { + throw new DataSource_Exception_Document('Document is read only'); + } + + if (!$this->has_access_edit()) + { + throw new DataSource_Exception_Document('You do not have permission to update document'); + } + + if (!$this->loaded()) + { + return $this; + } + + $values = $this->system_fields(); + unset($values['id'], $values['ds_id'], $values['created_on']); + + $values['updated_on'] = date('Y-m-d H:i:s'); + + DB::update($this->section()->table()) + ->set($values) + ->where('ds_id', '=', (int) $this->section()->id()) + ->where('id', '=', $this->id) + ->execute(); + + $this->_updated = TRUE; + + return $this; + } + + /** + * Метод удаления документа + * + * $ds = Datasource_Data_Manager::load($ds_id); + * $doc = $ds->get_document($id); + * + * @return null|boolean + */ + public function remove() + { + if ($this->is_read_only()) + { + throw new DataSource_Exception_Document('Document is read only'); + } + + if (!$this->has_access_remove()) + { + throw new DataSource_Exception_Document('You do not have permission to remove document'); + } + + if (!$this->loaded()) + { + return FALSE; + } + + DB::delete($this->section()->table()) + ->where('ds_id', '=', (int) $this->section()->id()) + ->where('id', '=', $this->id) + ->execute(); + + $this->reset(); + + return TRUE; + } + + /** + * Валидация полей документа согласно правилам валидации + * + * @see DataSource_Document::rules() + * @see DataSource_Document::labels() + * + * $doc = $ds->get_document($id); + * $doc + * ->read_values($this->request->post()) + * ->read_files($_FILES) + * ->validate($this->request->post() + $_FILES); + * + * @param Validation $extra_validation + * @param array $expected + * @return boolean|Validation + */ + public function validate(Validation $extra_validation = NULL, array $expected = NULL) + { + // Determine if any external validation failed + $extra_errors = ($extra_validation AND ! $extra_validation->check()); + + $values = Arr::merge($this->values(), $this->_temp_fields); + + $validation = Validation::factory( $values ); + + $validation->rules('csrf', array( + array('not_empty'), array('Security::check') + )); + + // Default to expecting everything except the primary key + if ($expected === NULL) + { + $expected = $this->rules(); + } + else + { + $rules = $this->rules(); + foreach ($rules as $field => $_rules) + { + if (!in_array($field, $expected)) + { + unset($rules[$field]); + } + } + + $expected = $rules; + } + + foreach ($expected as $field => $rules) + { + $validation->rules($field, $rules); + } + + foreach ($this->labels() as $field => $label) + { + $validation->label($field, $label); + } + + if (!$validation->check() OR $extra_errors) + { + $exception = new Validation_Exception($validation); + + if ($extra_errors) + { + // Merge any possible errors from the external object + $exception->add_object($extra_validation); + } + + throw $exception; + } + + return TRUE; + } + + /** + * Событие вызываемое в момент загрузки контроллера + */ + public function onControllerLoad() {} + + /** + * Событие вызываемое в момент ошибки создания документа + */ + public function onCreateException(Kohana_Exception $exception) + { + Messages::errors($exception->getMessage()); + } + + /** + * Событие вызываемое в момент ошибки обновления документа + */ + public function onUpdateException(Kohana_Exception $exception) + { + Messages::errors($exception->getMessage()); + } + + /** + * Событие вызываемое в момент ошибки удаления документа + */ + public function onRemoveException(Kohana_Exception $exception) {} + + /************************************************************************** + * ACL + **************************************************************************/ + /** + * Пользователь - создатель документа + * + * @param integer $user_id + * @return boolean + */ + public function is_creator($user_id = NULL) + { + if ($this->_is_authored === TRUE) + { + if ($user_id === NULL) + { + $user_id = Auth::get_id(); + } + + $created_by_id = (int) Arr::get($this->system_fields(), 'created_by_id'); + return ACL::is_admin($user_id) OR ( $created_by_id == (int) $user_id); + } + + return TRUE; + } + + /** + * Пользователь имеет права на создание документа + * @param integer $user_id + * @return boolean + */ + public function has_access_view($user_id = NULL) + { + return ( + $this->section()->has_access('document.view') + OR + $this->has_access_create($user_id) + OR + $this->has_access_edit($user_id) + ); + } + + /** + * Пользователь имеет права на создание документа + * @param integer $user_id + * @return boolean + */ + public function has_access_create() + { + return ( + $this->section()->has_access('document.create') + OR + $this->section()->is_creator() + ); + } + + /** + * Пользователь имеет права на редактирование документа + * @param integer $user_id + * @return boolean + */ + public function has_access_edit($user_id = NULL, $check_own = TRUE) + { + return ( + ($check_own === TRUE AND $this->is_creator($user_id)) + OR + $this->section()->has_access('document.edit') + ); + } + + /** + * @param integer $user_id + * @return boolean + */ + public function has_access_change($user_id = NULL, $check_own = TRUE) + { + return ( + ($this->loaded() AND $this->has_access_edit($user_id, $check_own)) + OR + (!$this->loaded() AND $this->has_access_create()) + ); + } + + /** + * Пользователь имеет права на редактирование документа + * @param integer $user_id + * @return boolean + */ + public function has_access_remove($user_id = NULL, $check_own = TRUE) + { + return ( + ($check_own === TRUE AND $this->is_creator($user_id)) + OR + $this->section()->has_access('document.remove') + ); + } + + /************************************************************************** + * Links + **************************************************************************/ + /** + * + * @return string + */ + public function edit_link() + { + return $this->view_link(); + } + + /** + * + * @return string + */ + public function view_link() + { + return Route::get('datasources')->uri(array( + 'controller' => 'document', + 'directory' => $this->section()->type(), + 'action' => 'view' + )) . URL::query(array( + 'ds_id' => $this->section()->id(), + 'id' => $this->id + )); + } + + /************************************************************************** + * ArrayAccess + **************************************************************************/ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + public function offsetExists($offset) + { + return $this->__isset($offset); + } + + public function offsetUnset($offset) + { + return $this->__unset($offset); + } + + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * + * @return string ID + */ + public function __toString() + { + return (string) $this->id; + } +} \ No newline at end of file diff --git a/classes/Datasource/Exception/Document.php b/classes/Datasource/Exception/Document.php new file mode 100644 index 0000000..cb8c867 --- /dev/null +++ b/classes/Datasource/Exception/Document.php @@ -0,0 +1,11 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class DataSource_Exception_Document extends Kohana_Exception {} diff --git a/classes/Datasource/Exception/Section.php b/classes/Datasource/Exception/Section.php new file mode 100644 index 0000000..653653f --- /dev/null +++ b/classes/Datasource/Exception/Section.php @@ -0,0 +1,11 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class DataSource_Exception_Section extends Kohana_Exception {} diff --git a/classes/Datasource/Folder.php b/classes/Datasource/Folder.php new file mode 100644 index 0000000..e0fa471 --- /dev/null +++ b/classes/Datasource/Folder.php @@ -0,0 +1,123 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Datasource_Folder { + + /** + * Список всех папок + * + * @return array + */ + public static function get_all() + { + $query = DB::select() + ->from('datasource_folders') + ->order_by('position') + ->as_object() + ->execute(); + + $folders = array(); + + foreach($query as $row) + { + $folders[$row->id] = array( + 'name' => $row->name, + 'sections' => array() + ); + } + + return $folders; + } + + /** + * + * @param integer $id + * @return boolean + */ + public static function exists($id) + { + return (bool) DB::select('id') + ->from('datasource_folders') + ->where('id', '=', (int) $id) + ->limit(1) + ->execute() + ->get('id'); + } + + /** + * + * @param integer $id + * @return array + */ + public static function get($id) + { + return DB::select() + ->from('datasource_folders') + ->where('id', '=', (int) $id) + ->limit(1) + ->execute(); + } + + /** + * + * @param string $name + * @param string $description + * @return integer + */ + public static function add($name) + { + list($id, $total) = DB::insert('datasource_folders') + ->columns(array('name')) + ->values(array($name)) + ->execute(); + + return $id; + } + + /** + * + * @param string $name + * @param string $description + * @return integer + */ + public static function update($name) + { + return (bool) DB::update('datasource_folders') + ->set(array( + 'name' => $name + )) + ->execute(); + } + + /** + * + * @param integer $id + * @return boolean + */ + public static function delete($id) + { + if ((bool) DB::delete('datasource_folders') + ->where('id', '=', (int) $id) + ->execute()) + { + DB::update('datasources') + ->set(array( + 'folder_id' => 0 + )) + ->where('folder_id', '=', (int) $id) + ->execute(); + + return TRUE; + } + + return FALSE; + } +} + \ No newline at end of file diff --git a/classes/Datasource/Section.php b/classes/Datasource/Section.php new file mode 100644 index 0000000..d1a6df6 --- /dev/null +++ b/classes/Datasource/Section.php @@ -0,0 +1,1152 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Datasource_Section { + + /** + * Загруженные разделы из БД + * @var array + */ + protected static $_cached_sections = array(); + + /** + * Фабрика создания раздела данных + * + * @param string $type Тип раздела + * + * @return \Datasource_Section + */ + public static function factory($type) + { + if (!self::exists($type)) + { + throw new DataSource_Exception_Section('Class :class_name not exists', + array(':class_name' => $class)); + } + + $class = 'Datasource_Section_' . ucfirst($type); + return new $class($type); + } + + /** + * Проверка класса на существование по типу раздела + * + * @param string $type + * @return boolean + */ + public static function exists($type) + { + $class = 'Datasource_Section_' . ucfirst($type); + + return class_exists($class); + } + + /** + * + * @param string $action + * @param integer|string $ds_id + * @return string + */ + public static function uri($action = 'view', $ds_id = NULL) + { + if ($action == 'view') + { + $uri = Route::get('datasources')->uri(array( + 'controller' => 'data', + 'directory' => 'datasources', + )); + + return $ds_id !== NULL + ? $uri . URL::query(array('ds_id' => (int) $ds_id)) + : $uri; + } + + return Route::get('datasources')->uri(array( + 'controller' => 'section', + 'directory' => 'datasources', + 'action' => $action, + 'id' => $ds_id + )); + } + + /** + * + * @return string + */ + public static function default_icon() + { + return 'folder-open-o'; + } + + /** + * Загрузка разедла по ID + * + * @param integer $id + * @return null|Datasource_Section + */ + public static function load($id) + { + if ($id === NULL) + { + return NULL; + } + + if (isset(self::$_cached_sections[$id])) + { + return self::$_cached_sections[$id]; + } + + $query = DB::select() + ->from('datasources') + ->where('id', '=', (int) $id) + ->execute() + ->current(); + + if ($query == NULL OR ( $section = self::load_from_array($query)) === NULL) + { + return NULL; + } + + self::$_cached_sections[$id] = $section; + return $section; + } + + /** + * Загрузка разедла из массива данных + * + * @param array $data + * @return Datasource_Section + */ + public static function load_from_array(array $data) + { + $section = Kohana::unserialize($data['code']); + + $section->_id = $data['id']; + $section->name = $data['name']; + $section->description = Arr::get($data, 'description'); + $section->_docs = (int) Arr::get($data, 'docs'); + $section->_is_indexable = (bool) Arr::get($data, 'indexed'); + $section->_created_by_id = (int) Arr::get($data, 'created_by_id'); + $section->_folder_id = (int) Arr::get($data, 'folder_id'); + + return $section; + } + + /** + * Идентификатор раздела + * + * @var integer + */ + protected $_id; + + /** + * Тип раздела + * + * @var string + */ + protected $_type; + + /** + * Название раздела + * + * @var string + */ + public $name; + + /** + * Иконка раздела + * + * @var string + */ + public $icon; + + /** + * Описание раздела + * + * @var string + */ + public $description; + + /** + * Кол-во документов в разделе + * + * @var integer + */ + protected $_docs = 0; + + /** + * Идентификатор папки раздела + * + * @var integer + */ + protected $_folder_id = 0; + + /** + * Создатель раздела + * + * @var integer + */ + protected $_created_by_id = NULL; + + /** + * Таблица раздела в БД + * + * @var string + */ + protected $_ds_table; + + /** + * Индексировать раздел + * + * @var boolean + */ + protected $_is_indexable = FALSE; + + /** + * Показывать в корне меню + * + * @var boolean + */ + protected $_show_in_root_menu = FALSE; + + /** + * Объект загрузки списка документов + * + * @var Datasource_Section_Headline + */ + protected $_headline = NULL; + + /** + * Название класса документа + * + * @var string + */ + protected $_document_class_name = NULL; + + /** + * Типы виджетов для которых очищать кеш при обновлении данных в документах + * + * @var array + */ + protected $_widget_types = array(); + + /** + * + * @param string $type + */ + public function __construct($type) + { + $this->_type = $type; + + $this->_initialize(); + $this->_init_headline(); + + if (!class_exists($this->_document_class_name)) + { + throw new DataSource_Exception_Section('Document class :class_name not exists', + array(':class_name' => $this->_document_class_name)); + } + } + + /** + * + * @param Model_Navigation_Section $parent_section + * @return Model_Navigation_Section + */ + public function add_to_menu(Model_Navigation_Section $parent_section = NULL) + { + return Datasource_Data_Manager::add_section_to_menu($this, $parent_section); + } + + /** + * Возвращает тип раздела + * + * @return string + */ + public function type() + { + return $this->_type; + } + + /** + * Возвращает идентификатор раздела + * + * @return integer + */ + public function id() + { + return $this->_id; + } + + /** + * @return integer + */ + public function created_by_id() + { + return (int) $this->_created_by_id; + } + + /** + * @return integer + */ + public function folder_id() + { + return (int) $this->_folder_id; + } + + /** + * Проверка раздела на существование + * + * @return boolean + */ + public function loaded() + { + return $this->_id !== NULL; + } + + /** + * Возвращает отбъект списка документов + * + * @return Datasource_Section_Headline + */ + public function headline() + { + return $this->_headline; + } + + /** + * Возвращает название таблицы раздела + * + * @return string + */ + public function table() + { + return $this->_ds_table; + } + + /** + * + * @return boolean + */ + public function show_in_root_menu() + { + return (bool) $this->_show_in_root_menu; + } + + /** + * + * @return string + */ + public function icon() + { + if (!empty($this->icon)) + { + return $this->icon; + } + + $class = get_called_class(); + return $class::default_icon(); + } + + /** + * Создание раздела + * + * @param array $values Массив полей раздела + * + * @return integer Идентификатор раздела + * @throws DataSource_Exception_Section + */ + public function create(array $values) + { + if (!$this->has_access_create()) + { + throw new DataSource_Exception_Section('You do not have permission to create section'); + } + + $this->name = Arr::get($values, 'name'); + $this->description = Arr::get($values, 'description'); + $this->icon = Arr::get($values, 'icon'); + $this->_is_indexable = (bool) Arr::get($values, 'is_indexable'); + $this->_show_in_root_menu = (bool) Arr::get($values, 'show_in_root_menu'); + $this->_created_by_id = (int) Arr::get($values, 'created_by_id', Auth::get_id()); + $this->_folder_id = (int) Arr::get($values, 'folder_id'); + + $data = array( + 'type' => $this->_type, + 'indexed' => (bool) $this->_is_indexable, + 'description' => $this->description, + 'name' => $this->name, + 'created_on' => date('Y-m-d H:i:s'), + 'created_by_id' => $this->_created_by_id, + 'folder_id' => $this->_folder_id, + 'code' => Kohana::serialize($this), + ); + + $query = DB::insert('datasources') + ->columns(array_keys($data)) + ->values(array_values($data)) + ->execute(); + + $this->_id = $query[0]; + + if (empty($this->_id)) + { + throw new DataSource_Exception_Section('Datasource section :name not created', + array(':name' => $this->name)); + } + + unset($query, $values); + + Observer::notify('datasource_after_create', $this->_id); + + return $this->_id; + } + + /** + * + * @param array $values + * @throws Validation_Exception + */ + public function values(array $values = array()) + { + $this->validate($values); + + $this->name = Arr::get($values, 'name'); + $this->description = Arr::get($values, 'description'); + $this->icon = Arr::get($values, 'icon'); + $this->_show_in_root_menu = (bool) Arr::get($values, 'show_in_root_menu'); + + if (!empty($values['folder_id'])) + { + $this->_folder_id = (int) Arr::get($values, 'folder_id'); + } + + if (!empty($values['created_by_id'])) + { + $this->_created_by_id = (int) $values['created_by_id']; + } + + $this->set_indexable(Arr::get($values, 'is_indexable', FALSE)); + $this->_headline->set_sorting(Arr::get($values, 'doc_order', array())); + + return $this; + } + + /** + * Обновление раздела. + * + * При сохранении раздела в БД происходит его сериализация и сохарение данных + * в поле "code". Список полей, которые не должын попадать в БД указывается в + * методе {@see _serialize()} + * + * @param array $values + * @throws DataSource_Exception_Section + * @return boolean + */ + public function update() + { + if (!$this->has_access_edit()) + { + throw new DataSource_Exception_Section('You do not have permission to update section'); + } + + if (!$this->loaded()) + { + return FALSE; + } + + DB::update('datasources') + ->set(array( + 'indexed' => $this->_is_indexable, + 'name' => $this->name, + 'description' => $this->description, + 'updated_on' => date('Y-m-d H:i:s'), + 'created_by_id' => $this->_created_by_id, + 'folder_id' => $this->_folder_id, + 'code' => Kohana::serialize($this) + )) + ->where( 'id', '=', $this->_id ) + ->execute(); + + $this->update_size(); + + Observer::notify('datasource_after_save', $this->_id); + + return TRUE; + } + + /** + * Удаление раздела + * + * При удалении раздела происходит удаление документов. + * + * @return \Datasource_Section + */ + public function remove() + { + if (!$this->has_access_remove()) + { + throw new DataSource_Exception_Section('You do not have permission to remove section'); + } + + $ids = DB::select('id') + ->from($this->table()) + ->where('ds_id', '=', $this->id()) + ->execute() + ->as_array(NULL, 'id'); + + $this->remove_documents($ids); + + DB::delete('datasources') + ->where('id', '=', $this->id()) + ->execute(); + + $id = $this->_id; + $this->_id = NULL; + + Observer::notify('datasource_after_remove', $id); + + return $this; + } + + /** + * + * @param integer $folder_id + * @return \Datasource_Section + */ + public function move_to_folder($folder_id) + { + DB::update('datasources') + ->set(array('folder_id' => (int) $folder_id)) + ->where( 'id', '=', $this->_id ) + ->execute(); + + $this->_folder_id = (int) $folder_id; + + return $this; + } + + /** + * Создание нового документа + * + * @param DataSource_Document $doc + * @return NULL|DataSource_Document + */ + public function create_document(DataSource_Document $doc) + { + try + { + $doc->create(); + } + catch (DataSource_Exception_Document $ex) + { + $doc->onCreateException($ex); + } + catch (Kohana_Exception $ex) + { + $doc->onCreateException($ex); + } + + if ($doc->loaded()) + { + $this->update_size(); + $this->add_to_index(array($doc->id)); + $this->clear_cache(); + } + + return $doc; + } + + /** + * Обновление документа + * + * @param DataSource_Document $doc + * @return DataSource_Document + */ + public function update_document(DataSource_Document $doc) + { + $old = $this->get_document($doc->id); + + if (empty($old) OR ! $doc->loaded()) + { + return FALSE; + } + + try + { + $doc->update(); + } + catch (DataSource_Exception_Document $ex) + { + $doc->onUpdateException($ex); + } + catch (Kohana_Exception $ex) + { + $doc->onUpdateException($ex); + } + + if ($old->published != $doc->published) + { + if( $doc->published === TRUE ) + { + $this->add_to_index(array($old->id)); + } + else + { + $this->remove_from_index(array($old->id)); + } + } + else if ($old->published === TRUE) + { + $this->update_index(array($old->id)); + } + + $this->clear_cache(); + + return $doc; + } + + /** + * Удаление документов по ID + * + * @see DataSource_Document::remove() + * + * @param array $ids + * @return \DataSource_Section + */ + public function remove_documents(array $ids = NULL) + { + if (empty($ids)) + { + return $this; + } + + $deleted_documents = array(); + + foreach ($ids as $id) + { + $document = $this->get_document($id); + + try + { + if ($document->loaded()) + { + $document->remove(); + $deleted_documents[] = $id; + } + } + catch (DataSource_Exception_Document $ex) + { + $document->onRemoveException($ex); + continue; + } + } + + $this->update_size(); + $this->remove_from_index($deleted_documents); + $this->clear_cache(); + + return $this; + } + + /** + * Загрузка документа по ID + * + * @param integer $id + * @return \DataSource_Document + */ + public function get_document($id = NULL) + { + $document = $this->get_empty_document(); + if (empty($id)) + { + return $document; + } + + return $document->load($id); + } + + /** + * Получение пустого объекта документа + * + * @return \DataSource_Document + */ + public function get_empty_document() + { + return new $this->_document_class_name($this); + } + + /** + * Публикация документов по ID + * + * @param array $ids + * @return \Datasource_Section + */ + public function publish(array $ids) + { + return $this->_publish($ids, TRUE); + } + + /** + * Снятие документов с публикации по ID + * + * @param array $ids + * @return \Datasource_Section + */ + public function unpublish(array $ids) + { + return $this->_publish($ids, FALSE); + } + + /** + * Смена статуса документов по ID. + * + * @param array $ids + * @param boolean $status + * @return \Datasource_Section + */ + protected function _publish(array $ids, $status) + { + DB::update($this->_ds_table) + ->set(array( + 'published' => (bool) $status, + 'updated_on' => date('Y-m-d H:i:s'), + )) + ->where('id', 'in', $ids) + ->where('ds_id', '=', $this->_id) + ->execute(); + + if($status === TRUE) + { + $this->add_to_index($ids); + } + else + { + $this->remove_from_index($ids); + } + + return $this; + } + + /** + * Обновление поля кол-ва документов в разделе + * + * @return \Datasource_Section + */ + public function update_size() + { + if($this->_ds_table) + { + DB::update('datasources') + ->set(array( + 'docs' => DB::select(DB::expr('COUNT("*")')) + ->from($this->_ds_table) + ->where('ds_id', '=', $this->_id) + )) + ->where('id', '=', $this->_id) + ->execute(); + } + + return $this; + } + + /** + * Валидация данных полей раздела + * + * @param array $array + * @throws Validation_Exception + */ + public function validate(array $array = NULL) + { + $validation = Validation::factory($array) + ->rules('name', array( + array('not_empty') + )) + ->rules('created_by_id', array( + array('not_empty'), + array('numeric') + )) + ->label('name', __('Header')) + ->label('created_by_id', __('Author')); + + if (!$validation->check()) + { + throw new Validation_Exception($validation); + } + + return $this; + } + + /** + * Очистка кеша виджетов раздела + * + * @return \Datasource_Section + */ + public function clear_cache( ) + { + Datasource_Data_Manager::clear_cache( $this->id(), $this->_widget_types); + + return $this; + } + + /** + * Вызывается при сохранении раздела в БД + * + * @return array + */ + public function __sleep() + { + return array_keys($this->_serialize()); + } + + /** + * Список параметров объекта, которые должны сохраняться в БД. + * + * @return array + */ + protected function _serialize() + { + $vars = get_object_vars($this); + unset( + $vars['_id'], + $vars['_docs'], + $vars['_is_indexable'], + $vars['_created_by_id'], + $vars['_type'], + $vars['name'], + $vars['description'], + $vars['_document_class_name'], + $vars['_ds_table'], + $vars['_widget_types'] + ); + + return $vars; + } + + /** + * При заггрузке данных раздела из БД происходит десериализация объекта из поля + * "Code", что по сути является загрузкой раздела и в этот момент вызывается этот метод. + * + * Если после загрузки раздела необходимо восстановить связи с другими объектами, их + * необходимо описывать в методе {@see _initialize()} + */ + public function __wakeup() + { + $this->_initialize(); + + if($this->_headline === NULL) + { + $this->_init_headline(); + } + $this->_headline->set_section($this); + } + + /** + * Инициализация данных раздела при создании или загрузке + * @throws Kohana_Exception + */ + protected function _initialize() + { + $this->_docs = 0; + $this->_is_indexable = FALSE; + $this->_document_class_name = 'Datasource_' . ucfirst($this->type()) . '_Document'; + } + + protected function _init_headline() + { + $headline_class = 'Datasource_Section_' . ucfirst($this->type()) . '_Headline'; + if(!class_exists($headline_class)) + { + throw new Kohana_Exception('Headline class :class not found', array( + ':class' => $headline_class + )); + } + + $this->_headline = new $headline_class(); + $this->_headline->set_section($this); + } + + /************************************************************************** + * ACL + **************************************************************************/ + /** + * + * @return array + */ + public function acl_actions() + { + return array( + array( + 'action' => 'section.view', + 'description' => 'View section' + ), + array( + 'action' => 'section.edit', + 'description' => 'Edit section' + ), + array( + 'action' => 'section.remove', + 'description' => 'Remove section' + ), + array( + 'action' => 'document.view', + 'description' => 'View documents' + ), + array( + 'action' => 'document.create', + 'description' => 'Create documents' + ), + array( + 'action' => 'document.edit', + 'description' => 'Edit documents' + ), + array( + 'action' => 'document.remove', + 'description' => 'Remove documents' + ) + ); + } + + /** + * Пользователь - создатель раздела + * + * @param integer $user_id + * @return boolean + */ + public function is_creator($user_id = NULL) + { + if($user_id === NULL) + { + $user_id = Auth::get_id(); + } + + return ACL::is_admin($user_id) OR ($this->_created_by_id == (int) $user_id); + } + /** + * Проверка прав доступа + * @param string $acl_type + * @return boolean + */ + public function has_access($acl_type = 'section.edit', $check_own = TRUE, $user_id = NULL) + { + return ( + ACL::check('ds_id.' . $this->id() . '.' . $acl_type) + OR + ( + $check_own === TRUE + AND + $this->is_creator($user_id) + ) + ); + } + + /** + * Проверка прав на редактирование + * @return boolean + */ + public function has_access_edit($user_id = NULL) + { + return $this->has_access('section.edit', TRUE, $user_id); + } + + /** + * Проверка прав на редактирование + * @return boolean + */ + public function has_access_create() + { + return ACL::check($this->type() . '.' . 'section.create'); + } + + /** + * Проверка прав на просмотр + * @return boolean + */ + public function has_access_view($user_id = NULL) + { + return $this->has_access('section.view', TRUE, $user_id); + } + + /** + * Проверка прав на удаление + * @return boolean + */ + public function has_access_remove($user_id = NULL) + { + return $this->has_access('section.remove', TRUE, $user_id); + } + + /************************************************************************** + * Search indexation + **************************************************************************/ + + /** + * Состояние поисковой индексации раздела + * + * @return boolean + */ + public function is_indexable() + { + return (bool) $this->_is_indexable; + } + + /** + * Смена статуса поисковой индексации раздела + * + * @param boolean $state + * @return \Datasource_Section + */ + public function set_indexable($state) + { + $state = (bool) $state; + + if (!$this->loaded()) + { + $this->_is_indexable = $state; + + return $this; + } + + if ($state == $this->is_indexable()) + { + return $this; + } + + if ($state) + { + $this->_is_indexable = $state; + $this->add_to_index(); + } + else + { + $this->remove_from_index(); + $this->_is_indexable = $state; + } + + return $this; + } + + /** + * Загрузка списка документов по ID в формате для индексации + * + * @param integer|array $id + * @return array array([ID] => array('id', 'header', 'content', 'intro'), ...) + */ + public function get_indexable_documents(array $id = NULL) + { + $result = DB::select_array($this->get_indexable_fields()) + ->from($this->_ds_table) + ->where('published', '=', 1) + ->where('ds_id', '=', $this->_id); + + if( ! empty($id) ) + { + $result->where('id', 'in', $id); + } + + return $result + ->execute() + ->as_array('id'); + } + + /** + * + * @return array + */ + public function get_indexable_fields() + { + return array('id', 'header', 'content', 'intro'); + } + + /** + * Добавление документов раздела в поисковый индекс + * + * При передаче массива ID другие параметры указывать не нужно, т.к. они + * загрузятся автоматически + * + * @see Datasource_Section::get_indexable_documents() + * + * @param array $ids Индентификаторы документов + * @param string $header Заголовок документа + * @param string $content Индексируемый текст + * @param string $intro Описание документа + * @return \Datasource_Section + */ + public function add_to_index(array $ids = array(), $header = NULL, $content = NULL, $intro = NULL, array $params = NULL) + { + if (!$this->is_indexable()) + { + return $this; + } + + if (count($ids) == 1 AND $header !== NULL) + { + Search::instance()->add_to_index('ds_' . $this->id(), $ids[0], $header, $content, $intro, $params); + } + else + { + $docs = $this->get_indexable_documents($ids); + + foreach ($docs as $doc) + { + Search::instance()->add_to_index('ds_' . $this->id(), $doc['id'], $doc['header'], $doc['content'], $doc['intro'], Arr::get($doc, 'params')); + } + } + } + + /** + * Обновление поискового индекса документов раздела + * + * При передаче массива ID другие параметры указывать не нужно, т.к. они + * загрузятся автоматически + * + * @see Datasource_Section::get_indexable_documents() + * + * @param array $ids + * @param string $header + * @param string $content + * @param string $intro + * @return \Datasource_Section + */ + public function update_index(array $ids = array(), $header = NULL, $content = NULL, $intro = NULL, array $params = NULL) + { + if (!$this->is_indexable()) + { + return $this; + } + + return $this->add_to_index($ids, $header, $content, $intro, $params); + } + + /** + * Удаление документов из поискового индекса + * + * @param array $ids + * @return \Datasource_Section + */ + public function remove_from_index(array $ids = NULL) + { + if (!$this->is_indexable()) + { + return $this; + } + + Search::instance()->remove_from_index('ds_' . $this->id(), $ids); + } + + /** + * + * @return string ID + */ + public function __toString() + { + return (string) $this->id; + } +} \ No newline at end of file diff --git a/classes/Datasource/Section/Headline.php b/classes/Datasource/Section/Headline.php new file mode 100644 index 0000000..a5f0bbe --- /dev/null +++ b/classes/Datasource/Section/Headline.php @@ -0,0 +1,326 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +abstract class Datasource_Section_Headline { + + /** + * Объект раздела + * + * @var Datasource_Section + */ + protected $_section = NULL; + + /** + * Правила сортировки списка документов + * @var array + */ + protected $_sorting = array( + array('created_on' => 'desc') + ); + + /** + * Объект постраничной навигации + * @var Pagination + */ + protected $_pagination = NULL; + + + /** + * Кол-во документов выводимых на 1 странице + * По умолчанию 20 + * + * Используется объектом постраничной навигации + * + * @var integer + */ + protected $_limit = 20; + + /** + * Кол-во пропускаемых документов + * + * Используется объектом постраничной навигации + * + * @var integer + */ + protected $_offset = 0; + + /** + * Текущая страница + * @var integer + */ + protected $_page = NULL; + + /** + * + * @param Datasource_Section $section + */ + public function __construct() + { + $this->_pagination = Pagination::factory(); + } + + /** + * Поля раздела, которые отображаются в списке + * + * @return array Fields array([Field name] => array('name' => [Field header])) + */ + public function fields() + { + return array( + 'id' => array( + 'name' => 'ID', + 'width' => 50, + 'class' => 'text-right text-muted', + 'visible' => TRUE, + 'db_name' => 'd.id' + ), + 'header' => array( + 'name' => 'Header', + 'width' => NULL, + 'type' => 'link', + 'visible' => TRUE, + 'db_name' => 'd.header' + ) + ); + } + + /** + * Рендер View спсика документов раздела + * + * @param type $template Путь для своего шаблона + * @return View + */ + public function render($template = NULL) + { + if ($template === NULL) + { + $template = 'datasource/' . $this->_section->type() . '/headline'; + } + + if (Kohana::find_file('views', $template) === FALSE) + { + $template = 'datasource/section/headline'; + } + + return View::factory($template, array( + 'fields' => $this->fields(), + 'data' => $this->get(), + 'pagination' => $this->pagination(), + 'datasource' => $this->_section + )); + } + + /** + * Рендер View спсика документов раздела + * @return string + */ + public function __toString() + { + return (string) $this->render(); + } + + /** + * Геттер и Сеттер лимита + * + * @param integer $limit + * @return integer + */ + public function limit($limit = NULL) + { + if ($limit !== NULL) + { + $this->_limit = (int) $limit; + + if ($this->_limit < 1) + { + $this->_limit = 1; + } + } + + return (int) $this->_limit; + } + + /** + * Возвращает кол-во пропускаемых документов + * + * @param integer $limit + * @return integer + */ + public function offset() + { + return (int) $this->_offset; + } + + /** + * + * @param integer $num + */ + public function set_page($num) + { + $this->_page = (int) $num; + + return $this; + } + + public function set_query_params() + { + $_GET['ds_id'] = $this->_section->id(); + } + + /** + * Формирование данных для постраничной навигации + * + * @param array $ids + * @param string $search_word + * @return Pagination + */ + public function pagination(array $ids = NULL) + { + $this->set_query_params(); + + $options = array( + 'items_per_page' => $this->limit(), + 'total_items' => $this->count_total($ids), + 'current_page' => array( + 'source' => 'query_string', + 'key' => 'page', + 'uri' => Route::get('datasources')->uri() + ) + ); + + if (!empty($this->_page)) + { + $options['current_page']['page'] = $this->_page; + } + + $this->_pagination->setup($options); + + $this->_offset = (int) $this->_pagination->offset; + + return $this->_pagination; + } + + /** + * Получение списка документов в виде массива + * + * + * @param array $ids + * @return array array( 'total' => ..., 'documents' => array([id] => array([Field name] => $value, ....))) + */ + abstract public function get(array $ids = NULL); + + /** + * Подсчет кол-ва документов в разделе + * + * @param array $ids + * @param string $search_word + * @return integer + */ + abstract public function count_total(array $ids = NULL); + + /** + * Метод используется для поиска по документам по ключевому слову. + * + * Ключевое слово передается в качестве $_GET запроса с ключем "keyword" + * + * @param Database_Query $query + * @return Database_Query + */ + public function search_by_keyword(Database_Query $query) + { + $keyword = Request::initial()->query('keyword'); + + if (empty($keyword)) + { + return $query; + } + + if ($this->_section->is_indexable()) + { + $ids = Search::instance()->find_by_keyword($keyword, FALSE, 'ds_' . $this->_section->id(), NULL); + $ids = Arr::get($ids, 'ds_' . $this->_section->id()); + + if(!empty($ids)) + { + $query->where('d.id', 'in', array_keys($ids)); + } + else + { + $query->where('d.id', '=', 0); + } + } + else + { + $query + ->where_open() + ->where('d.id', 'like', '%' . $keyword . '%') + ->or_where('d.header', 'like', '%' . $keyword . '%') + ->where_close(); + } + + return $query; + } + + /** + * + * @param Datasource_Section $section + * @return \Datasource_Section_Headline + */ + public function set_section(Datasource_Section $section) + { + $this->_section = $section; + + return $this; + } + + /** + * Указание порядка сортировки + * + * @param array $orders array(array([FIELD NAME] => [ASC], ...)) + * @return \Datasource_Section_Headline + */ + public function set_sorting(array $orders = NULL) + { + $this->_sorting = $orders; + return $this; + } + + /** + * + * @return array + */ + public function sorting() + { + return (array) $this->_sorting; + } + + public function __sleep() + { + return array_keys($this->_serialize()); + } + + protected function _serialize() + { + $vars = get_object_vars($this); + unset( + $vars['_section'], + $vars['_pagination'], + $vars['_page'], + $vars['_offset'] + ); + + return $vars; + } + + public function __wakeup() + { + $this->_pagination = Pagination::factory(); + } +} \ No newline at end of file diff --git a/classes/Model/Widget/Datasource/Search.php b/classes/Model/Widget/Datasource/Search.php new file mode 100644 index 0000000..9ac17f0 --- /dev/null +++ b/classes/Model/Widget/Datasource/Search.php @@ -0,0 +1,174 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class Model_Widget_Datasource_Search extends Model_Widget_Decorator_Pagination { + + /** + * + * @var integer + */ + protected $_total = 0; + + /** + * + * @var array + */ + public $sources = array(); + + /** + * + * @var array + */ + public $source_hrefs = array(); + + /** + * + * @var array + */ + public $search_key = 'keyword'; + + /** + * @param array $data + */ + public function set_values(array $data) + { + if (empty($data['sources'])) + { + $data['sources'] = array(); + } + + return parent::set_values($data); + } + + /** + * + * @return string + */ + public function keyword() + { + return HTML::chars($this->_ctx->get($this->search_key)); + } + + public function on_page_load() + { + parent::on_page_load(); + + $this->count_total(); + } + + /** + * + * @return array + */ + public function sources() + { + $sources_list = Datasource_Data_Manager::get_all(); + + $sources = array(); + + foreach ($sources_list as $id => $source) + { + $sources[$id] = $source->name; + } + + return $sources; + } + + /** + * + * @return array + */ + public function modules() + { + $modules = array(); + + foreach ($this->sources as $source_id) + { + $modules[] = 'ds_' . $source_id; + } + + return $modules; + } + + /** + * + * @return array [$total_found, $results, $keyword] + */ + public function fetch_data() + { + $keyword = $this->keyword(); + + $return = array( + 'total_found' => 0, + 'results' => array(), + 'keyword' => $keyword + ); + + $modules = $this->modules(); + + $ids = Search::instance()->find_by_keyword($keyword, FALSE, $modules, $this->list_size, $this->list_offset); + + if (empty($ids)) + { + return $return; + } + + $results = array(); + + foreach ($this->source_hrefs as $id => $href) + { + if (!isset($ids['ds_' . $id])) + { + continue; + } + + foreach ($ids['ds_' . $id] as $item) + { + $item['href'] = str_replace(':id', $item['id'], $href); + + if (!empty($item['params'])) + { + foreach ($item['params'] as $field => $value) + { + $item['href'] = str_replace(':' . $field, $value, $href); + } + } + + $results[] = $item; + } + } + + $return['results'] = $results; + $return['total_found'] = count($results); + + return $return; + } + + /** + * + * @return integer + */ + public function count_total() + { + $keyword = $this->keyword(); + $this->_total = Search::instance()->count_by_keyword($keyword, FALSE, $this->modules()); + + return $this->_total; + } + + /** + * + * @return string + */ + public function get_cache_id() + { + return 'Widget::' . $this->type() . '::' . $this->id . '::' . $this->keyword(); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..db41b85 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "kodicms/datasource", + "type": "kodicms-module", + "description": "The official KodiCMS dashboard module", + "homepage": "http://www.kodicms.ru/", + "license": "BSD-3-Clause", + "keywords": ["kohana", "framework", "kodicms", "cms", "module", "datasource"], + "authors": [ + { + "name": "Pavel Buchnev", + "email": "butschster@gmail.com", + "homepage": "http://www.kodicms.ru/", + "role": "developer" + } + ], + "support": { + "issues": "https://github.com/KodiCMS/module-datasource/issues", + "forum": "http://www.kodicms.ru/forum", + "source": "https://github.com/KodiCMS/module-datasource" + }, + "require": { + "kodicms/core": "dev-master", + "kodicms/search": "dev-master" + } +} diff --git a/config/datasources.php b/config/datasources.php new file mode 100644 index 0000000..a6a6c1e --- /dev/null +++ b/config/datasources.php @@ -0,0 +1,5 @@ + array() +); \ No newline at end of file diff --git a/config/permissions.php b/config/permissions.php new file mode 100644 index 0000000..bcf9bd9 --- /dev/null +++ b/config/permissions.php @@ -0,0 +1,23 @@ + $section) +{ + $actions = $section->acl_actions(); + $actions['title'] = __('Datasource :name', array(':name' => $section->name)); + $perms['ds_id.' . $id] = $actions; +} + +foreach (Datasource_Data_Manager::types() as $type => $title) +{ + $perms[$type.'.section'] = array( + 'title' => 'Datasource', + array( + 'action' => 'create', + 'description' => 'Create '.$type.' section' + ) + ); +} + +return $perms; \ No newline at end of file diff --git a/config/widgets.php b/config/widgets.php new file mode 100644 index 0000000..9974ab2 --- /dev/null +++ b/config/widgets.php @@ -0,0 +1,7 @@ + array( + 'datasource_search' => __('Datasource search'), + ), +); \ No newline at end of file diff --git a/i18n/ru.php b/i18n/ru.php new file mode 100644 index 0000000..328192c --- /dev/null +++ b/i18n/ru.php @@ -0,0 +1,53 @@ + 'Раздел', + 'Datasources' => 'Datasources', + 'Create Document' => 'Создать документ', + 'Apply' => 'Применить', + 'Actions' => 'Действия', + 'Remove' => 'Удалить', + 'Publish' => 'Опубликовать', + 'Unpublish' => 'Снять с публикации', + 'Unpublished' => 'Не опубликован', + 'Section' => 'Раздел', + 'Total documents: :num' => 'Всего документов: :num', + 'Create section' => 'Создать раздел', + 'Edit' => 'Редактировать', + 'Datasource Information' => 'Информация о разделе', + 'Datasource Key' => 'Ключ', + 'Datasource Header' => 'Название', + 'Datasource Description' => 'Описание', + 'Datasource Fields' => 'Поля раздела', + 'Search indexation' => 'Поисковая индексация', + 'Datasource has been saved!' => 'Раздел сохранен!', + 'Datasource has been deleted!' => 'Раздел удален!', + 'Search in sources' => 'Искать в разделах', + 'Datasources' => 'Разделы', + 'Datasource search' => 'Поиск по разделам', + 'Add section :type' => 'Создание раздела :type', + 'Edit section :name' => 'Редактирование раздела :name', + 'Datasource section :id not found' => 'Раздел :id не найден', + 'Datasource section not loaded' => 'Раздел не загружен', + 'Document saved' => 'Документ сохранен', + 'Document ID :id not found' => 'Документ c ID :id не найден', + 'Check all' => 'Выбрать все', + 'Uncheck all' => 'Отменить выбор', + 'Create document' => 'Создать документ', + 'Section is empty' => 'Раздел пустой', + 'Create folder' => 'Создать папку', + 'New folder' => 'Новая папка', + 'Folder name' => 'Название', + 'Datasource Author' => 'Автор', + 'Datasource properties' => 'Настройки', + 'Datasource :name' => 'Раздел :name', + 'View section' => 'Просмотр раздела', + 'Edit section' => 'Редактировать раздел', + 'Remove section' => 'Удалить раздел', + 'View documents' => 'Просмотре документов', + 'Create documents' => 'Создание документов', + 'Edit documents' => 'Редактирование документов', + 'Remove documents' => 'Удаление документов', + 'Datasource icon' => 'Иконка раздела', + 'Show datasource in root menu' => 'Отображать раздел в корне меню' +); \ No newline at end of file diff --git a/init.php b/init.php new file mode 100644 index 0000000..13426d8 --- /dev/null +++ b/init.php @@ -0,0 +1,129 @@ +(/(/(/)))', array( + 'directory' => '(datasources|' . implode('|', array_keys(Datasource_Data_Manager::types())) . ')' + )) + ->defaults(array( + 'directory' => 'datasources', + 'controller' => 'data', + 'action' => 'index', + )); +} + +Observer::observe('modules::after_load', function() { + + if (!IS_BACKEND OR ! Auth::is_logged_in()) + { + return; + } + + $types = Datasource_Data_Manager::types(); + + if (empty($types)) + { + return; + } + + try + { + $ds_section = Model_Navigation::get_section('Datasources'); + $ds_section->icon = 'tasks'; + $sections_list = Datasource_Data_Manager::get_tree(array_keys($types)); + + $datasource_is_empty = empty($sections_list); + $folders = Datasource_Folder::get_all(); + $root_sections = array(); + + foreach ($sections_list as $type => $sections) + { + foreach ($sections as $id => $section) + { + if ($section->show_in_root_menu()) + { + $root_sections[] = $section; + unset($sections_list[$type][$id]); + continue; + } + + if (array_key_exists($section->folder_id(), $folders)) + { + $folders[$section->folder_id()]['sections'][] = $section; + unset($sections_list[$type][$id]); + } + } + } + + if (!empty($root_sections)) + { + foreach ($root_sections as $id => $section) + { + $section->add_to_menu(); + } + } + + foreach ($folders as $folder_id => $folder) + { + if (empty($folder['sections'])) + { + continue; + } + + $folder_section = Model_Navigation::get_section($folder['name'], $ds_section); + + foreach ($folder['sections'] as $id => $section) + { + $section->add_to_menu($folder_section); + } + } + + foreach ($sections_list as $type => $sections) + { + foreach ($sections as $id => $section) + { + $section->add_to_menu($ds_section); + } + } + + $_create_section = Model_Navigation::get_section(__('Create section'), $ds_section, 999); + + foreach ($types as $id => $type) + { + $_create_section + ->add_page(new Model_Navigation_Page(array( + 'name' => $type, + 'url' => Route::get('datasources')->uri(array( + 'controller' => 'section', + 'directory' => 'datasources', + 'action' => 'create', + 'id' => $id + )), + 'permissions' => $id.'.section.create' + ))); + } + + unset($sections_list, $folders, $root_section); + } + catch (Exception $ex) + { + + } +}); + +Observer::observe('update_search_index', function() { + + $ds_ids = Datasource_Data_Manager::get_all(); + + foreach ($ds_ids as $ds_id => $data) + { + $ds = Datasource_Data_Manager::load($ds_id); + + if (!$ds->loaded()) + { + continue; + } + + $ds->update_index(); + } +}); diff --git a/install/schema.sql b/install/schema.sql new file mode 100644 index 0000000..ec8a483 --- /dev/null +++ b/install/schema.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS `__TABLE_PREFIX__datasources` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `folder_id` int(11) NOT NULL DEFAULT '0', + `type` varchar(64) NOT NULL, + `docs` int(6) unsigned NOT NULL DEFAULT '0', + `indexed` int(1) NOT NULL DEFAULT '0', + `name` varchar(64) NOT NULL DEFAULT '', + `description` varchar(255) DEFAULT NULL, + `created_on` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_on` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_by_id` int(11) unsigned NOT NULL DEFAULT '0', + `locks` int(3) unsigned NOT NULL DEFAULT '0', + `code` text, + PRIMARY KEY (`id`), + KEY `type` (`type`), + KEY `docs` (`docs`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `__TABLE_PREFIX__datasource_folders` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL DEFAULT '', + `position` int(11) DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/media/css/datasource.css b/media/css/datasource.css new file mode 100644 index 0000000..73dc615 --- /dev/null +++ b/media/css/datasource.css @@ -0,0 +1,119 @@ +#body_datasources_data_index { + background: #fff; +} + +#body_datasources_data_index #content-wrapper { + padding: 45px 0 18px 0; +} + +.theme-default .page-mail .mail-nav-header { + color: #333333; +} +.page-mail .mail-nav { + border-color: #e2e2e2; +} + +.page-mail .mail-nav .compose-btn { + border-bottom-width: 0; +} + +.sections-list { + min-height: 50px; +} + +.folder-container { + background: #fff; +} + +.sections { + margin: 0 !important; +} + +.dropable-hover { + border: 2px dashed #ccc; + background: #fff; +} + +.page-mail .mail-nav .mail-nav-header { + padding-right: 16px; + margin-top: 0; + padding-top: 12px; + padding-bottom: 12px; + border-top: 1px solid #e2e2e2; + cursor: pointer; +} + +.dropable-hover .mail-nav-header:first-child { + border-top-width: 0px !important; +} + +.page-mail .mail-nav .mail-nav-header .remove-folder { + cursor: pointer; +} + +.page-mail .mail-nav .sections li a .section-draggable { + margin: 12px 18px 0 0; + cursor: move; + color: #c2c2c2; +} + +.headline tbody tr.unpublished th, +.headline tbody tr.unpublished td { + color: #ccc; +} + + .headline tbody tr.unpublished th a, + .headline tbody tr.unpublished td a { + color: #ccc; + } + + +#html_fields textarea { + width: 100%; +} + + #html_fields .nav { + padding: 10px 10px 0; + } + + #html_fields .tabs-content { + position: relative; + height: 240px; + background-color: #fff; + margin-top: -20px; + } + #html_fields .tabs-content .tab-pane { + height: 240px; + width: 100%; + + z-index: 1; + + position: absolute; + } + + #html_fields .tabs-content .tab-pane.active { + z-index: 2; + } + + #html_fields .tabs-content .tab-pane:after { + clear: both; + } + +#field-options fieldset { + display: none; +} + +#array_fields .select2-search-choice, +#document_fields .select2-search-choice { + float: none; + padding: 10px 0 10px 25px; +} + + #array_fields .select2-container-multi .select2-search-choice-close, + #document_fields .select2-container-multi .select2-search-choice-close { + top: 10px; left: 10px; + } + + #document_fields .select2-search-choice + .select2-search-field { + display: none; + } \ No newline at end of file diff --git a/media/js/datasource.js b/media/js/datasource.js new file mode 100644 index 0000000..46aeab9 --- /dev/null +++ b/media/js/datasource.js @@ -0,0 +1,212 @@ +cms.init.add(['datasources_data_index'], function() { + init_section_folders(); + + // Open nav on mobile + $('.mail-nav .navigation li.active a').click(function () { + $('.mail-nav .navigation').toggleClass('open'); + return false; + }); + + // Fix navigation if main menu is fixed + if ($('body').hasClass('main-menu-fixed')) { + $('.mail-nav').addClass('fixed'); + } + + $(document).on('click', '.headline tr[data-id]', function(event) { + if (event.target.type !== 'checkbox') { + $(':checkbox', this).trigger('click'); + } + }); + + $(document).on('change', '.headline [data-id] .doc-checkbox', function() { + checkbox_check(); + }); + + checkbox_check(); + + $('.checkbox-control .action[data-action]').on('click', function(e) { + var action = $(this).data('action'); + var sibling = ':checked'; + if(action == 'check_all') + sibling = ':not(:checked)'; + + $('.headline [data-id] .doc-checkbox' + sibling).trigger('click'); + + e.preventDefault(); + }); + + $('.headline-actions .doc-actions .action').on('click', function() { + var action = $(this).data('action'); + + var data = $('.headline [data-id] .doc-checkbox:checked') + .serialize(); + + var page = $.query.get('page'); + data = $.query.parseNew(data) + .set('page', page) + .set('ds_id', DS_ID) + .toString().substring(1); + + if (data.length == 0 || !action) { + return; + } + + if (!confirm(__('Are you sure?'))) + return; + + Api.post('/datasource-document.' + action, data, function(response) { + update_headline(); + }); + }); + + $('.form-search').on('click', '.btn', function(e) { + headline_search(e, $(this).closest('.form-search').find('.form-control')); + }); + + $('.form-search').on('keypress', '.form-control', function(e) { + if(e.which == 13) { + headline_search(e, $(this)); + } + }); +}); + +function headline_search(e, $input) { + e.preventDefault(); + + var $fields = $('.form-search .form-control').serializeObject(); + + update_headline($fields); +} + +function update_headline(keyword) { + var data = { + page: $.query.get('page'), + ds_id: DS_ID + }; + + Api.get('/datasource-document.headline', _.extend(data, keyword), function(response) { + if(response.response) { + $('.headline').html(response.response); + cms.ui.init('icon'); + } + }); +} + +function checkbox_check() { + var $checkboxes = $('.headline [data-id] .doc-checkbox'); + var $total_checked = $checkboxes.filter(':checked').length; + + if($total_checked > 0) + $('.headline-actions .doc-actions .action').removeClass('disabled'); + else + $('.headline-actions .doc-actions .action').addClass('disabled'); + + $checkboxes.each(function() { + if (!$(this).prop('checked')) { + $(this).closest('[data-id]').removeClass("info"); + } else { + $(this).closest('[data-id]').addClass("info"); + } + }); +} + +function init_section_folders() { + $('.page-mail').on('click', '.create-folder-button', function() { + $('#folder-modal').modal(); + }); + + $('#folder-modal').on('submit', 'form', function(e) { + cms.clear_error(); + var field = $(this).find('input[name="folder-name"]'); + + if(field.val()) { + Api.put('/datasource-data.folder', { + name: field.val() + }, function(resp) { + if(resp.status) reload_menu(); + field.val(''); + }); + + $('#folder-modal').modal('hide') + } else { + cms.error_field(field, __('Pleas set folder name')); + } + e.preventDefault(); + }); + + $('.page-mail').on('click', '.mail-nav-header', function() { + var $sections = $(this).next('.sections'); + if($('li', $sections).length == 0) return; + + $sections.toggle(); + + var data = {}; + $('.folder-container .mail-nav-header').each(function() { + data[$(this).data('id')] = !$(this).next('.sections').is(':hidden'); + }); + + if(!_.isEmpty(data)) + Api.post('user-meta', {key: 'datasource_folders', value: data}); + }); + + $('.page-mail').on('click', '.remove-folder', function() { + if (!confirm(__('Are you sure?'))) + return; + + Api.delete('/datasource-data.folder', { + id: $(this).closest('.mail-nav-header').data('id') + }, function(response) { + reload_menu(); + }); + }); + + init_sections_sortable(); +} + +function init_sections_sortable() { + if($('.folders-list').size() == 0) { + $('.section-draggable').remove(); + return; + } + + $(".page-mail .mail-nav .sections li").draggable({ + handle: ".section-draggable", + axis: "y", + revert: "invalid" + }); + + $(".folder-container .sections li[data-id="+DS_ID+"]") + .parent() + .show(); + + $(".folder-container") + .add('.sections-list') + .droppable({ + hoverClass: "dropable-hover", + drop: function (event, ui) { + var self = $(this); + if(self.hasClass('sections-list')) { + var folder_id = 0; + } else { + var folder_id = self.find('.mail-nav-header').data('id'); + } + + Api.post('/datasource-data.menu', { + ds_id: ui.draggable.data('id'), + folder_id: parseInt(!folder_id ? 0 : folder_id) + }, function(response) { + if(response.status) reload_menu(); + }); + + ui.draggable.remove(); + } + }); +} + +function reload_menu() { + Api.get('/datasource-data.menu', {ds_id: DS_ID}, function(response) { + $('.page-mail .mail-nav').html(response.response); + cms.ui.init('icon'); + init_sections_sortable(); + }); +} \ No newline at end of file diff --git a/messages/validation.php b/messages/validation.php new file mode 100644 index 0000000..63128a5 --- /dev/null +++ b/messages/validation.php @@ -0,0 +1,7 @@ + 'Field :field must be unique', + 'Valid::url' => ':field must be a url', + 'Upload::type' => ':field must be one of the available options' +); diff --git a/views/datasource/content.php b/views/datasource/content.php new file mode 100644 index 0000000..fa7c720 --- /dev/null +++ b/views/datasource/content.php @@ -0,0 +1,34 @@ +
+
+ +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/views/datasource/data/index.php b/views/datasource/data/index.php new file mode 100644 index 0000000..f46c1f5 --- /dev/null +++ b/views/datasource/data/index.php @@ -0,0 +1,40 @@ +
+ icon()); ?> name; ?> + +
+ has_access_edit()) +{ + echo UI::button(NULL, array( + 'href' => Datasource_Section::uri('edit', $datasource->id()), + 'icon' => UI::icon( 'wrench' ), + 'class' => 'btn btn-default', + 'title' => __('Edit'), + 'hotkeys' => 'ctrl+e' + )); + } + + if ($datasource->has_access_remove()) +{ + echo UI::button(NULL, array( + 'href' => Datasource_Section::uri('remove', $datasource->id()), + 'icon' => UI::icon( 'trash-o fa-inverse' ), + 'class' => 'btn btn-danger btn-confirm', + 'title' => __('Remove') + )); + } + ?> +
+
+
+ +
+ +
+ +
+ + +
+ +
\ No newline at end of file diff --git a/views/datasource/data/menu.php b/views/datasource/data/menu.php new file mode 100644 index 0000000..1cc50af --- /dev/null +++ b/views/datasource/data/menu.php @@ -0,0 +1,110 @@ + $title) +{ + if (!ACL::check($type . '.section.create')) + { + unset($sections[$type]); + } +} + +foreach ($tree as $type => $data) +{ + foreach ($data as $id => $section) + { + if (array_key_exists($section->folder_id(), $folders)) + { + $folders[$section->folder_id()]['sections'][$id] = $section; + unset($tree[$type][$id]); + } + } +} + +$folders_status = Model_User_Meta::get('datasource_folders', array()); +?> + + + +has_access_view()) + { + return; + } + + $result = ''; + $selected = ($id == $ds_id) ? 'active' : ''; + $title = $section->name; + $result .= '
  • '; + $result .= HTML::anchor(Datasource_Section::uri('view', $id), $section->name . UI::icon('ellipsis-v fa-lg', array( + 'class' => 'pull-right section-draggable' + )), array('data-icon' => $section->icon())); + $result .= '
  • '; + + return $result; + } +?> \ No newline at end of file diff --git a/views/datasource/section/actions.php b/views/datasource/section/actions.php new file mode 100644 index 0000000..824a296 --- /dev/null +++ b/views/datasource/section/actions.php @@ -0,0 +1,44 @@ + \ No newline at end of file diff --git a/views/datasource/section/create.php b/views/datasource/section/create.php new file mode 100644 index 0000000..bae6131 --- /dev/null +++ b/views/datasource/section/create.php @@ -0,0 +1,46 @@ +uri(), array( + 'class' => 'form-horizontal panel' +)); ?> +
    + +
    +
    +
    + +
    + 'form-control', 'id' => 'name' + )); ?> +
    +
    + +
    + +
    + 'form-control', 'id' => 'description', 'rows' => 3 + )); ?> +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/views/datasource/section/edit.php b/views/datasource/section/edit.php new file mode 100644 index 0000000..577e6e9 --- /dev/null +++ b/views/datasource/section/edit.php @@ -0,0 +1,14 @@ +uri(), array( + 'class' => 'form-horizontal panel' +)); ?> + id()); ?> + + $users, + 'ds' => $ds + )); ?> + + + \ No newline at end of file diff --git a/views/datasource/section/headline.php b/views/datasource/section/headline.php new file mode 100644 index 0000000..02222c1 --- /dev/null +++ b/views/datasource/section/headline.php @@ -0,0 +1,66 @@ + 0): ?> + + + has_access('document.edit')): ?> + + + + $field): ?> + + /> + + + + + has_access('document.edit')): ?> + + + + $field): ?> + + + + + + + + $document): ?> + + has_access('document.edit')): ?> + + + + $field): ?> + + $key)): ?> + + has_access_view()): ?> + + + + + + + + + + + + + + +
    + has_access_edit()): ?> + 'doc-checkbox')); ?> + + + + edit_link(), $document->$key); ?> + + $key; ?>$key; ?>
    + $data['total'])); ?> +
    + + +

    + diff --git a/views/datasource/section/information_form.php b/views/datasource/section/information_form.php new file mode 100644 index 0000000..fbcd691 --- /dev/null +++ b/views/datasource/section/information_form.php @@ -0,0 +1,50 @@ +
    + +
    +
    +
    + +
    + name, array( + 'class' => 'form-control', 'id' => 'name' + )); ?> +
    +
    + +
    + +
    + description, array( + 'class' => 'form-control', 'id' => 'ds_description', 'rows' => 4 + )); ?> +
    +
    + +
    + +
    + created_by_id(), array( + 'class' => 'form-control', 'id' => 'created_by_id' + )); ?> +
    +
    + +
    + +
    + $ds->icon())); ?> +
    +
    + +
    +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/views/widgets/backend/datasource_search.php b/views/widgets/backend/datasource_search.php new file mode 100644 index 0000000..aa0780c --- /dev/null +++ b/views/widgets/backend/datasource_search.php @@ -0,0 +1,41 @@ +
    +
    + +
    + search_key, array( + 'id' => 'search_key', 'class' => 'form-control' + )); ?> +
    +
    +
    + +
    + +
    +
    +
    + +
    + sources(), (array) $widget->sources, array( + 'id' => 'sources', 'class' => 'form-control' + )); ?> +
    +
    +
    + +
    + +
    +
    + sources() as $id => $header): if (!in_array($id, $widget->sources)) continue; ?> +
    + +
    + source_hrefs, $id), array( + 'id' => 'source' . $id, 'class' => 'form-control' + )); ?> +
    +
    + +
    + diff --git a/views/widgets/frontend/datasource_search.php b/views/widgets/frontend/datasource_search.php new file mode 100644 index 0000000..0ca4c85 --- /dev/null +++ b/views/widgets/frontend/datasource_search.php @@ -0,0 +1,17 @@ + +

    + + +

    $total_found)); ?>

    + + +
    +
    +

    + +

    + +
    +
    +
    + \ No newline at end of file