--- /dev/null
+<?php
+
+use \PFF\HtmlBuilder\Text as Text;
+use \PFF\HtmlBuilder\Tag as Tag;
+
+class IPF_Admin_Model extends IPF_Admin_Component
+{
+ public $modelName;
+
+ public function __construct($modelName)
+ {
+ $this->modelName = $modelName;
+ }
+
+ public function verbose_name()
+ {
+ return IPF_Utils::humanTitle($this->modelName);
+ }
+
+ public function slug()
+ {
+ return strtolower($this->modelName);
+ }
+
+ private function table()
+ {
+ return IPF_ORM::getTable($this->modelName);
+ }
+
+ protected function query($searchValue, $filters)
+ {
+ if (method_exists($this->modelName, 'ordering')) {
+ $m = new $this->modelName;
+ $ord = $m->ordering();
+ } elseif ($this->table()->getOrdering()) {
+ $ord = implode(', ', $this->table()->getOrdering());
+ } elseif ($this->table()->hasTemplate('IPF_ORM_Template_Orderable')) {
+ $ord = $this->table()->getTemplate('IPF_ORM_Template_Orderable')->getColumnName();
+ } else {
+ $ord = '1 desc';
+ }
+
+ $q = IPF_ORM_Query::create()->from($this->modelName)->orderby($ord);
+
+ if ($searchValue) {
+ $wh = array();
+ $whv = array();
+ foreach ($this->_searchFields() as $f) {
+ $wh[] = $f.' like ?';
+ $whv[] = '%'.$searchValue.'%';
+ }
+ $q->addWhere($wh, $whv);
+ }
+
+ foreach ($filters as $f)
+ $f->applyToQuery($q);
+
+ return $q;
+ }
+
+ protected function getItems($searchValue, $filters, $page, $pageSize)
+ {
+ $idColumn = $this->table()->getIdentifier();
+
+ $result = array();
+ foreach ($this->query($searchValue, $filters)->limit($pageSize)->offset(($page - 1) * $pageSize)->execute() as $o) {
+ $id = $o->__get($idColumn);
+ $result[$id] = $o;
+ }
+ return $result;
+ }
+
+ protected function itemsCount($searchValue, $filters)
+ {
+ return $this->query($searchValue, $filters)->count();
+ }
+
+ public function getObjectByID($id)
+ {
+ return $this->table()->find($id);
+ }
+
+ public function searcheable()
+ {
+ return count($this->_searchFields()) > 0;
+ }
+
+ protected function _searchFields()
+ {
+ return array();
+ }
+
+ public function list_display()
+ {
+ return $this->table()->getColumnNames();
+ }
+
+ public function renderCell($object, $column)
+ {
+ $value = $object->$column;
+ switch ($object->getTable()->getTypeOf($column)) {
+ case 'boolean':
+ return $value
+ ? '<span class="positive">✔</span>'
+ : '<span class="negative">✘</span>';
+ case 'timestamp':
+ return Text::escape(IPF_Utils::formatDate($value));
+ default:
+ return Text::escape($value);
+ }
+ }
+
+ protected function columnTitle($column)
+ {
+ $columns = $this->table()->getColumns();
+ if (array_key_exists($column, $columns) && array_key_exists('verbose', $columns[$column]))
+ return $columns[$column]['verbose'];
+ else
+ return parent::columnTitle($column);
+ }
+
+ public function _orderable()
+ {
+ return $this->_orderableColumn() !== null;
+ }
+
+ public function _orderableColumn()
+ {
+ if (method_exists($this, 'list_order'))
+ return $this->list_order();
+ elseif ($this->table()->hasTemplate('IPF_ORM_Template_Orderable'))
+ return $this->table()->getTemplate('IPF_ORM_Template_Orderable')->getColumnName();
+ else
+ return null;
+ }
+
+ /* filters */
+
+ protected function _listFilters()
+ {
+ return array();
+ }
+
+ public function listFilters()
+ {
+ $filters = array();
+ foreach ($this->_listFilters() as $f) {
+ if (is_string($f))
+ $f = new IPF_Admin_Model_RelationFilter($this->modelName, $f);
+
+ $filters[] = $f;
+ }
+ return $filters;
+ }
+
+ public function reorder($ids)
+ {
+ if (!$this->_orderable())
+ return false;
+
+ $ord_field = $this->_orderableColumn();
+
+ $table = IPF_ORM::getTable($this->modelName);
+ $conn = $table->getConnection();
+
+ $idColumn = $table->getIdentifier();
+ if (is_array($idColumn))
+ $idColumn = $idColumn[0];
+
+ $questions = str_repeat('?,', count($ids)-1) . '?';
+ $query = 'SELECT ' . $conn->quoteIdentifier($ord_field) .
+ ' FROM ' . $conn->quoteIdentifier($table->getTableName()) .
+ ' WHERE ' . $conn->quoteIdentifier($idColumn) . ' IN (' . $questions . ')' .
+ ' ORDER BY ' . $conn->quoteIdentifier($ord_field);
+ $ords = $conn->fetchColumn($query, $ids);
+
+ $i = 0;
+ foreach ($ids as $id) {
+ $item = $table->find($id);
+ $item[$ord_field] = $ords[$i];
+ $item->save();
+ $i++;
+ }
+
+ return true;
+ }
+
+ /* edit */
+
+ protected function _getForm($model, $data)
+ {
+ $extra = array(
+ 'user_fields' => $this->fields(),
+ 'inlines' => $this->inlines(),
+ );
+ return new IPF_Admin_ModelForm($data, $model, $this->modelName, $extra);
+ }
+
+ public function inlines()
+ {
+ return array();
+ }
+
+ public function saveObject($form, $model)
+ {
+ $model = $form->save();
+ $idColumn = $this->table()->getIdentifier();
+ return array($model->$idColumn, $model);
+ }
+
+ public function deleteObject($model)
+ {
+ $model->delete();
+ }
+
+ public function fields()
+ {
+ return null;
+ }
+}
+
+class IPF_Admin_ModelForm extends IPF_Form_Model
+{
+ private $inlines = array();
+
+ function __construct($data=null, $model=null, $modelName='', $extra=array())
+ {
+ if ($model) {
+ // edit
+ $extra['model'] = $model;
+ $extra['initial'] = $this->getFormData($model);
+ } else {
+ // add
+ $extra['model'] = new $modelName;
+ }
+
+ parent::__construct($data, $extra);
+ }
+
+ function initFields($extra=array())
+ {
+ parent::initFields($extra);
+
+ if ($extra['inlines']) {
+ $this->field_groups[] = array('fields' => array_keys($this->fields));
+ foreach ($extra['inlines'] as $inlineClassName) {
+ $this->inlines[] = new $inlineClassName($extra['model']);
+ }
+ }
+
+ foreach ($this->inlines as $inline) {
+ $name = $inline->getModelName();
+ $this->fields[$name] = $inline->createField();
+
+ $this->field_groups[] = array(
+ 'label' => $inline->getLegend(),
+ 'fields' => array($name),
+ );
+ }
+ }
+
+ private function getFormData($o)
+ {
+ $data = $o->getData();
+ foreach ($o->getTable()->getRelations() as $rname => $rel) {
+ $fields = $this->fields();
+ if (!$fields || in_array($rname, $fields)) {
+ if ($rel->getType() == IPF_ORM_Relation::MANY_AGGREGATE) {
+ $data[$rname] = array();
+ foreach ($rel->fetchRelatedFor($o) as $ri) {
+ $data[$rname][] = $ri->pk();
+ }
+ }
+ }
+ }
+
+ foreach ($this->inlines() as $inlineName => $inlineClassName) {
+ $inlineInstance = new $inlineClassName($o, null);
+
+ $objs = array();
+ foreach ($inlineInstance->getObjects() as $io) {
+ $d = $io->getData();
+ $d['id'] = $io->pk();
+ $objs[] = $d;
+ }
+ $data[$inlineName] = $objs;
+ }
+
+ return $data;
+ }
+
+ function save($commit=true)
+ {
+ $model = parent::save($commit);
+
+ foreach ($this->inlines as $inline) {
+ $this->saveInlineObject($inline, $model);
+ }
+
+ return $model;
+ }
+
+ private function saveInlineObject($inline, $model)
+ {
+ $objIndex = array();
+ foreach ($inline->getObjects() as $obj)
+ $objIndex[$obj->pk()] = $obj;
+
+ $modelName = $inline->getModelName();
+ $fk_local = $inline->getFkLocal();
+
+ foreach ($this->cleaned_data[$modelName] as $objData) {
+ if (array_key_exists('id', $objData) && array_key_exists($objData['id'], $objIndex)) {
+ $obj = $objIndex[$objData['id']];
+ if (isset($objData['is_remove'])) {
+ $obj->delete();
+ } else {
+ $obj->synchronizeWithArray($objData);
+ $obj->save();
+ }
+ } else {
+ $objData[$fk_local] = $model->id;
+ $obj = new $modelName;
+ $obj->synchronizeWithArray($objData);
+ $obj->save();
+ }
+ }
+ }
+}
+
+abstract class IPF_Admin_Model_Filter implements IPF_Admin_ListFilter
+{
+ abstract function applyToQuery($q);
+
+ protected $title, $paramName;
+
+ function title()
+ {
+ return $this->title;
+ }
+
+ protected function link($params, $value, $title)
+ {
+ if ($value !== null)
+ $params[$this->paramName] = $value;
+ else
+ unset($params[$this->paramName]);
+
+ return Tag::a()
+ ->attr('href', '?'.IPF_HTTP_URL::generateParams($params, false))
+ ->append($title);
+ }
+}
+
+class IPF_Admin_Model_RelationFilter extends IPF_Admin_Model_Filter
+{
+ private $relation, $selected = null;
+
+ function __construct($modelName, $relName)
+ {
+ $this->relation = IPF_ORM::getTable($modelName)->getRelation($relName);
+ $this->title = 'By '.IPF_Utils::humanTitle($relName);
+
+ $this->paramName = 'filter_'.$this->relation['local'];
+ }
+
+ function setParams($params)
+ {
+ $this->selected = \PFF\Arr::get($params, $this->paramName);
+ }
+
+ function applyToQuery($q)
+ {
+ // check ???
+ if ($this->selected)
+ $q->addWhere($this->relation['local'].' = ?', array($this->selected));
+ }
+
+ function render($extraParams)
+ {
+ $ul = Tag::ul();
+
+ // reset filter
+ $ul->append(Tag::li()
+ ->toggleClass('selected', !$this->selected)
+ ->append($this->link($extraParams, null, __('All'))));
+
+ // query related
+ $table = IPF_ORM::getTable($this->relation['class']);
+
+ $query = $table->createQuery();
+ if ($table->getOrdering()) {
+ $query->orderBy(implode(', ', $table->getOrdering()));
+ } elseif ($table->hasTemplate('IPF_ORM_Template_Orderable')) {
+ $query->orderBy($table->getTemplate('IPF_ORM_Template_Orderable')->getColumnName());
+ }
+
+ $foreign = $this->relation['foreign'];
+ foreach ($query->execute() as $val) {
+ $id = $val[$foreign];
+ $name = (string)$val;
+
+ $ul->append(Tag::li()
+ ->toggleClass('selected', $this->selected == $id)
+ ->append($this->link($extraParams, $id, $name)));
+ }
+
+ return $ul->html();
+ }
+}
+
+class IPF_Admin_Model_OwnedFilter extends IPF_Admin_Model_Filter
+{
+ private $column, $selected = null;
+
+ function __construct($modelName, $column, $title)
+ {
+ $this->column = $column;
+ $this->title = $title;
+ $this->paramName = 'filter_'.$this->column;
+ }
+
+ function setParams($params)
+ {
+ $this->selected = \PFF\Arr::get($params, $this->paramName);
+ }
+
+ function applyToQuery($q)
+ {
+ // check ???
+ if ($this->selected)
+ $q->addWhere($this->column.' = ?', array($this->selected));
+ }
+
+ function render($extraParams)
+ {
+ $ul = Tag::ul();
+
+ // reset filter
+ $ul->append(Tag::li()
+ ->toggleClass('selected', !$this->selected)
+ ->append($this->link($extraParams, null, __('All'))));
+
+ foreach (User::query()->fetchAll() as $val) {
+ $id = $val->id;
+ $name = (string)$val;
+
+ $ul->append(Tag::li()
+ ->toggleClass('selected', $this->selected == $id)
+ ->append($this->link($extraParams, $id, $name)));
+ }
+
+ return $ul->html();
+ }
+}
+
+class BooleanFilter extends IPF_Admin_Model_Filter
+{
+ private $column, $trueTitle, $falseTitle, $selected;
+
+ public function __construct($column, $title, $trueTitle='Yes', $falseTitle='No')
+ {
+ $this->column = $column;
+ $this->title = $title;
+ $this->trueTitle = $trueTitle;
+ $this->falseTitle = $falseTitle;
+
+ $this->paramName = 'filter_'.$column;
+ }
+
+ function setParams($params)
+ {
+ $this->selected = \PFF\Arr::get($params, $this->paramName);
+ }
+
+ function render($extraParams)
+ {
+ return Tag::ul(null,
+ Tag::li()
+ ->toggleClass('selected', $this->selected !== 'y' && $this->selected !== 'n')
+ ->append($this->link($extraParams, null, __('All'))),
+ Tag::li()
+ ->toggleClass('selected', $this->selected === 'y')
+ ->append($this->link($extraParams, 'y', $this->trueTitle)),
+ Tag::li()
+ ->toggleClass('selected', $this->selected === 'n')
+ ->append($this->link($extraParams, 'n', $this->falseTitle)));
+ }
+
+ function applyToQuery($q)
+ {
+ switch ($this->selected) {
+ case 'y':
+ $query->addWhere($this->column);
+ break;
+ case 'n':
+ $query->addWhere('NOT '.$this->column);
+ break;
+ }
+ }
+}
+
+class DateHierarchyListFilter extends IPF_Admin_Model_Filter
+{
+ private $model, $name;
+ private $day, $month, $year;
+
+ function __construct($title, $modelName, $fieldName)
+ {
+ $this->title = $title;
+ $this->modelName = $modelName;
+ $this->name = $fieldName;
+
+ $this->paramName = $fieldName;
+ }
+
+ function setParams($params)
+ {
+ $date = \PFF\Arr::get($params, $this->paramName, '');
+ $matches = array();
+ if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $date, $matches)) {
+ $this->year = intval($matches[1]);
+ $this->month = intval($matches[2]);
+ $this->day = intval($matches[3]);
+ }
+ }
+
+ function render($extraParams)
+ {
+ $ul = Tag::ul();
+
+ $ul->append(Tag::li()
+ ->toggleClass('selected', !$this->year)
+ ->append($this->link($extraParams, null, __('All'))));
+
+ if ($this->year)
+ $ul->append($this->renderChoice($extraParams, $this->year, 0, 0, $this->year)->addClass('selected'));
+
+ if ($this->month)
+ $ul->append($this->renderChoice($extraParams, $this->year, $this->month, 0, $this->monthName($this->month))->addClass('selected'));
+
+ if ($this->day)
+ $ul->append($this->renderChoice($extraParams, $this->year, $this->month, $this->day, $this->day)->addClass('selected'));
+
+ if ($this->day) {
+ } elseif ($this->month) {
+ $days = $this->choices('DAY(' . $this->name . ')',
+ 'YEAR(' . $this->name . ')', $this->year,
+ 'MONTH(' . $this->name . ')', $this->month);
+ foreach ($days as $day) {
+ $ul->append($this->renderChoice($extraParams, $this->year, $this->month, $day, $day));
+ }
+ } elseif ($this->year) {
+ $months = $this->choices('MONTH(' . $this->name . ')',
+ 'YEAR(' . $this->name . ')', $this->year);
+ foreach ($months as $month) {
+ $ul->append($this->renderChoice($extraParams, $this->year, $month, 0, $this->monthName($month)));
+ }
+ } else {
+ $years = $this->choices('YEAR(' . $this->name . ')');
+ foreach ($years as $year) {
+ $ul->append($this->renderChoice($extraParams, $year, 0, 0, $year));
+ }
+ }
+
+ return $ul->html();
+ }
+
+ function applyToQuery($q)
+ {
+ if ($this->day)
+ $q->addWhere('DAY(' . $this->name . ') = ?', array($this->day));
+ if ($this->month)
+ $q->addWhere('MONTH(' . $this->name . ') = ?', array($this->month));
+ if ($this->year)
+ $q->addWhere('YEAR(' . $this->name . ') = ?', array($this->year));
+ }
+
+ private function choices(/* $what, [$expr, $value, ...]*/)
+ {
+ $args = func_get_args();
+ $what = array_shift($args);
+
+ $q = IPF_ORM_Query::create()
+ ->from($this->modelName)
+ ->select($what . ' AS value')
+ ->groupBy('1')
+ ->orderBy('1');
+
+ while ($args) {
+ $expr = array_shift($args);
+ $value = array_shift($args);
+
+ $q->addWhere($expr . ' = ?', array($value));
+ }
+ return \PFF\Arr::create($q->fetchArray())->pluck('value')->arr();
+ }
+
+ private function renderChoice($extraParams, $year, $month, $day, $label)
+ {
+ return Tag::li(null,
+ $this->link($extraParams, sprintf('%04d-%02d-%02d', $year, $month, $day), $label));
+ }
+
+ protected function monthName($month)
+ {
+ return date('F', mktime(0, 0, 0, $month, 1));
+ }
+}
+