* @author Shohei Nakajima * @link http://www.netcommons.org NetCommons Project * @license http://www.netcommons.org/license.txt NetCommons License * @copyright Copyright 2014, NetCommons Project */ App::uses('TopicsAppModel', 'Topics.Model'); App::uses('WorkflowComponent', 'Workflow.Controller/Component'); /** * Topic Model * * @author Shohei Nakajima * @package NetCommons\Topics\Model */ class Topic extends TopicsAppModel { /** * 公開のタイプ(非公開) * * @var int */ const TYPE_PRIVATE = '0'; /** * 公開のタイプ(公開) * * @var int */ const TYPE_PUBLIC = '1'; /** * 公開のタイプ(期限付き公開) * * @var int */ const TYPE_LIMITED = '2'; /** * タイトル表示時の文字数 * * @var int */ const DISPLAY_TITLE_LENGTH = 64; /** * 概要の文字数 * * @var int */ const DISPLAY_SUMMARY_LENGTH = 1000; /** * ルーム名表示時の文字数 * * @var int */ const DISPLAY_ROOM_NAME_LENGTH = 24; /** * カテゴリ名表示時の文字数 * * @var int */ const DISPLAY_CATEGORY_NAME_LENGTH = 24; /** * 公開前ステータス * * @var string */ const STATUS_BEFORE_PUBLISH = '5'; /** * 終了ステータス * * @var string */ const STATUS_ANSWER_END = '6'; /** * 回答済みステータス * * @var string */ const STATUS_ANSWERED = '7'; /** * 未回答ステータス * * @var string */ const STATUS_UNANSWERED = '8'; /** * 未確認ステータス * * @var string */ const STATUS_UNCONFIRMED = '13'; /** * ブロック非公開ステータス * * @var string */ const STATUS_BLOCK_PRIVATE = '9'; /** * ブロック期限付き公開(公開前)ステータス * * @var string */ const STATUS_BLOCK_BEFORE_PUBLISH = '10'; /** * ブロック期限付き公開(公開中)ステータス * * @var string */ const STATUS_BLOCK_PUBLISH = '11'; /** * ブロック期限付き公開(公開終了)ステータス * * @var string */ const STATUS_BLOCK_END_PUBLISH = '12'; /** * ステータス配列 * * __constractorでセットする * * @var array */ public $statuses = array(); /** * Validation rules * * @var array */ public $validate = array(); /** * 現在日時 * * @var array */ public $now = null; /** * use behaviors * * @var array */ public $actsAs = array( 'Wysiwyg.Purifiable' => array( 'fields' => array('Topic' => array('summary')), 'forcePurify' => true ), ); /** * belongsTo associations * * @var array */ public $belongsTo = array( 'Block' => array( 'className' => 'Blocks.Block', 'foreignKey' => 'block_id', 'conditions' => '', 'fields' => array('id', 'key'), 'order' => '' ), 'Category' => array( 'className' => 'Categories.Category', 'foreignKey' => 'category_id', 'conditions' => '', 'fields' => array('id', 'key'), 'order' => '' ), 'Frame' => array( 'className' => 'Frames.Frame', 'foreignKey' => 'frame_id', 'conditions' => '', 'fields' => array('id', 'key'), 'order' => '' ), 'Language' => array( 'className' => 'M17n.Language', 'foreignKey' => 'language_id', 'conditions' => '', 'fields' => array('id', 'code'), 'order' => '' ), 'Room' => array( 'className' => 'Rooms.Room', 'foreignKey' => 'room_id', 'conditions' => '', 'fields' => array('id', 'space_id'), 'order' => '' ), ); /** * hasMany associations * * @var array */ public $hasMany = array( 'TopicReadable' => array( 'className' => 'Topics.TopicReadable', 'foreignKey' => 'topic_id', 'dependent' => false, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'exclusive' => '', 'finderQuery' => '', 'counterQuery' => '' ), 'TopicUserStatus' => array( 'className' => 'Topics.TopicUserStatus', 'foreignKey' => 'topic_id', 'dependent' => false, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'exclusive' => '', 'finderQuery' => '', 'counterQuery' => '' ) ); /** * Constructor. Binds the model's database table to the object. * * @param bool|int|string|array $id Set this ID for this model on startup, * can also be an array of options, see above. * @param string $table Name of database table to use. * @param string $ds DataSource connection name. * @see Model::__construct() * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function __construct($id = false, $table = null, $ds = null) { parent::__construct($id, $table, $ds); $this->statuses = array( //一時保存 WorkflowComponent::STATUS_IN_DRAFT => array( 'key' => WorkflowComponent::STATUS_IN_DRAFT, 'class' => 'label-info', 'message' => __d('net_commons', 'Temporary'), ), //承認待ち WorkflowComponent::STATUS_APPROVAL_WAITING => array( 'key' => WorkflowComponent::STATUS_APPROVAL_WAITING, 'class' => 'label-warning', 'message' => __d('net_commons', 'Approving'), ), //差し戻し WorkflowComponent::STATUS_DISAPPROVED => array( 'key' => WorkflowComponent::STATUS_DISAPPROVED, 'class' => 'label-warning', 'message' => __d('net_commons', 'Disapproving'), ), //公開前 self::STATUS_BEFORE_PUBLISH => array( 'key' => self::STATUS_BEFORE_PUBLISH, 'class' => 'label-default', 'message' => __d('topics', 'Before publishing'), ), //受付終了 self::STATUS_ANSWER_END => array( 'key' => self::STATUS_ANSWER_END, 'class' => 'label-default', 'message' => __d('topics', 'Answer end'), ), //回答済み self::STATUS_ANSWERED => array( 'key' => self::STATUS_ANSWERED, 'class' => 'label-default', 'message' => __d('topics', 'Answered'), ), //未回答 self::STATUS_UNANSWERED => array( 'key' => self::STATUS_UNANSWERED, 'class' => 'label-success', 'message' => __d('topics', 'Unanswered'), ), //未確認 self::STATUS_UNCONFIRMED => array( 'key' => self::STATUS_UNCONFIRMED, 'class' => 'label-primary', 'message' => __d('topics', 'Unconfirmed'), ), //ブロック非公開 self::STATUS_BLOCK_PRIVATE => array( 'key' => self::STATUS_BLOCK_PRIVATE, 'class' => 'label-default', 'message' => __d('blocks', 'Private'), ), //ブロック期限付き公開(公開前) self::STATUS_BLOCK_BEFORE_PUBLISH => array( 'key' => self::STATUS_BLOCK_BEFORE_PUBLISH, 'class' => 'label-default', 'message' => __d('blocks', 'Public before'), ), //ブロック期限付き公開(公開中) self::STATUS_BLOCK_PUBLISH => array( 'key' => self::STATUS_BLOCK_PUBLISH, 'class' => 'label-default', 'message' => __d('blocks', 'Limited'), ), //ブロック期限付き公開(公開終了) self::STATUS_BLOCK_END_PUBLISH => array( 'key' => self::STATUS_BLOCK_END_PUBLISH, 'class' => 'label-default', 'message' => __d('blocks', 'Public end'), ), ); } /** * Called during validation operations, before validation. Please note that custom * validation rules can be defined in $validate. * * @param array $options Options passed from Model::save(). * @return bool True if validate operation should continue, false to abort * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforevalidate * @see Model::save() */ public function beforeValidate($options = array()) { $this->validate = array_merge($this->validate, array( 'language_id' => array( 'numeric' => array( 'rule' => array('numeric'), 'message' => __d('net_commons', 'Invalid request.'), ), ), 'room_id' => array( 'numeric' => array( 'rule' => array('numeric'), 'message' => __d('net_commons', 'Invalid request.'), ), ), 'block_id' => array( 'numeric' => array( 'rule' => array('numeric'), 'message' => __d('net_commons', 'Invalid request.'), 'on' => 'update', // Limit validation to 'create' or 'update' operations ), ), 'content_id' => array( 'numeric' => array( 'rule' => array('numeric'), 'message' => __d('net_commons', 'Invalid request.'), 'on' => 'update', // Limit validation to 'create' or 'update' operations ), ), 'content_key' => array( 'notBlank' => array( 'rule' => array('notBlank'), 'message' => __d('net_commons', 'Invalid request.'), 'on' => 'update', // Limit validation to 'create' or 'update' operations ), ), 'category_id' => array( 'numeric' => array( 'rule' => array('numeric'), 'message' => __d('net_commons', 'Invalid request.'), 'allowEmpty' => true, 'required' => false, ), ), 'plugin_key' => array( 'notBlank' => array( 'rule' => array('notBlank'), 'message' => __d('net_commons', 'Invalid request.'), ), ), 'path' => array( 'notBlank' => array( 'rule' => array('notBlank'), 'message' => __d('net_commons', 'Invalid request.'), ), ), 'public_type' => array( 'numeric' => array( 'rule' => array('numeric'), 'message' => __d('net_commons', 'Invalid request.'), 'required' => false, ), ), 'is_active' => array( 'boolean' => array( 'rule' => array('boolean'), 'message' => __d('net_commons', 'Invalid request.'), 'required' => false, ), ), 'is_latest' => array( 'boolean' => array( 'rule' => array('boolean'), 'message' => __d('net_commons', 'Invalid request.'), 'required' => false, ), ), )); } /** * Called after each find operation. Can be used to modify any results returned by find(). * Return value should be the (modified) results. * * @param mixed $results The results of the find operation * @param bool $primary Whether this model is being queried directly (vs. being queried as an association) * @return mixed Result of the find operation * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#afterfind */ public function afterFind($results, $primary = false) { foreach ($results as $key => $value) { if (isset($results[$key][$this->alias]['path'])) { if ($value[$this->alias]['frame_id']) { $url = Router::url( $value[$this->alias]['path'] . '?frame_id=' . $value[$this->alias]['frame_id'], true ); } else { $url = Router::url( $value[$this->alias]['path'], true ); } $results[$key][$this->alias]['url'] = $url; } if (!empty($results[$key][$this->alias]['thumbnail_path'])) { $results[$key][$this->alias]['thumbnail_url'] = Router::url( $value[$this->alias]['thumbnail_path'] . '/thumb', true ); } } return $results; } /** * 新着データ取得のオプション生成 * * @param int $status ステータス * @param array $options マージするオプション * @return array */ public function getQueryOptions($status, $options = array()) { $this->loadModels([ 'Room' => 'Rooms.Room', 'Role' => 'Roles.Role', ]); //現在時刻をセット(テストのチェックで必要なためメンバ変数にセットする) $this->now = gmdate('Y-m-d H:i:s'); $now = $this->now; //ステータスの条件生成 $statusConditions = $this->__getStatusConditions($now, $status); //閲覧可のルームの条件生成 list($roomConditions, $blockPubConditions) = $this->__getRoomsConditions($now); //クエリ $this->__bindModel(); if (! isset($options['conditions'])) { $options['conditions'] = array(); } $options['conditions'][] = array( $this->TopicReadable->alias . '.topic_id NOT' => null, $this->alias . '.language_id' => Current::read('Language.id'), ); if ($blockPubConditions) { $options['conditions'][] = $blockPubConditions; } if ($roomConditions) { $options['conditions'][] = $roomConditions; } if ($statusConditions) { $options['conditions'][] = $statusConditions; } $result = Hash::merge(array( 'recursive' => 0, 'order' => array( $this->alias . '.publish_start' => 'desc', $this->alias . '.id' => 'desc' ), ), $options); return $result; } /** * 新着データ取得のためのRooms条件を取得する * * @param string $now 現在時刻 * @param int $status ステータス * @return array */ private function __getStatusConditions($now, $status) { $statusConditions = array(); if ($status === self::STATUS_UNANSWERED) { //未回答 $statusConditions = array( 'Topic.is_answer' => true, 'OR' => array( 'TopicUserStatus.id' => null, 'TopicUserStatus.answered' => false, ) ); } elseif ($status === self::STATUS_ANSWERED) { //回答済み $statusConditions = array( 'TopicUserStatus.answered' => true, ); } elseif ($status === self::STATUS_ANSWER_END) { //終了 $statusConditions = array( 'Topic.answer_period_end <' => $now, ); } elseif ($status === self::STATUS_BEFORE_PUBLISH) { //公開前 $statusConditions = array( 'Topic.publish_start >' => $now, ); } elseif (in_array((int)$status, array_keys($this->statuses), true)) { $statusConditions = array( 'Topic.status' => $status, ); } return $statusConditions; } /** * 新着データ取得のためのRooms条件を取得する * * @param string $now 現在時刻 * @return array */ private function __getRoomsConditions($now) { //閲覧できるルームリスト取得 $rooms = $this->Room->find('all', Hash::merge( $this->Room->getReadableRoomsConditions(array('Room.page_id_top NOT' => false)), array( 'recursive' => -1, 'fields' => ['Room.id', 'Room.space_id', 'RolesRoom.room_id', 'RolesRoom.role_key'] ) ) ); //room_idの取得 $editableRoomIds = []; // 編集権限の条件生成 $readableRoomIds = []; // 閲覧権限の条件生成 $blockEditableRoomIds = []; //ブロック公開設定の条件生成 foreach ($rooms as $room) { switch ($room['RolesRoom']['role_key']) { case Role::ROOM_ROLE_KEY_ROOM_ADMINISTRATOR: case Role::ROOM_ROLE_KEY_CHIEF_EDITOR: $editableRoomIds[] = $room['RolesRoom']['room_id']; $blockEditableRoomIds[] = $room['RolesRoom']['room_id']; break; case Role::ROOM_ROLE_KEY_EDITOR: $editableRoomIds[] = $room['RolesRoom']['room_id']; break; } $readableRoomIds[] = $room['Room']['id']; } $readableRoomIds = array_diff( $readableRoomIds, $editableRoomIds ); $roomConditions = array(); //非会員を受け付けるどうか(パブリックスペースのみ有効) if (! Current::read('User.id')) { $roomConditions[$this->alias . '.is_no_member_allow'] = true; } //is_latestのデータが見れる条件生成 $roomConditions = $this->__getIsLatestConditions($now, $roomConditions, $editableRoomIds); //is_activeの条件生成 $roomConditions = $this->__getIsActiveConditions($now, $roomConditions, $readableRoomIds); $blockPubConditions['OR'] = array( array($this->Block->alias . '.public_type' => self::TYPE_PUBLIC), array( $this->Block->alias . '.public_type' => self::TYPE_LIMITED, array('OR' => array( $this->Block->alias . '.publish_start <=' => $now, $this->Block->alias . '.publish_start' => null, )), array('OR' => array( $this->Block->alias . '.publish_end >=' => $now, $this->Block->alias . '.publish_end' => null, )), ), ); if ($blockEditableRoomIds) { $blockPubConditions['OR'][2][$this->Block->alias . '.room_id'] = $blockEditableRoomIds; } return array($roomConditions, $blockPubConditions); } /** * $roomConditionsのis_latestについてセットする * * @param string $now 現在時刻 * @param array $roomConditions roomConditions配列 * @param array $editableRoomIds 編集権限のあるルームIDリスト * @return array $roomConditions */ private function __getIsLatestConditions($now, $roomConditions, $editableRoomIds) { //is_latestのデータが見れる条件生成 if ($editableRoomIds) { $roomConditions['OR'][] = array( $this->alias . '.room_id' => array_unique($editableRoomIds), $this->alias . '.is_latest' => true ); } if (Current::read('User.id')) { $roomConditions['OR'][] = array( 'TopicReadable.user_id' => Current::read('User.id'), $this->alias . '.is_active' => true, $this->alias . '.is_in_room' => false, ); $roomConditions['OR'][] = array( $this->alias . '.created_user' => Current::read('User.id'), $this->alias . '.is_latest' => true ); } return $roomConditions; } /** * is_activeについてセットする * * @param string $now 現在時刻 * @param array $roomConditions roomConditions配列 * @param array $readableRoomIds 閲覧権限のあるルームIDリスト * @return array $roomConditions */ private function __getIsActiveConditions($now, $roomConditions, $readableRoomIds) { //is_activeの条件生成 if ($readableRoomIds) { if (Current::read('User.id')) { $roomConditions['OR'][] = array( 'OR' => array( $this->alias . '.room_id' => array_unique($readableRoomIds), 'TopicReadable.user_id' => Current::read('User.id') ), $this->alias . '.created_user !=' => Current::read('User.id'), $this->alias . '.is_active' => true, //公開設定の条件生成 array( $this->alias . '.public_type' => [self::TYPE_PUBLIC, self::TYPE_LIMITED], $this->alias . '.publish_start <=' => $now, 'OR' => array( $this->alias . '.publish_end >=' => $now, $this->alias . '.publish_end' => null, ), ) ); } else { $roomConditions['OR'][] = array( $this->alias . '.room_id' => array_unique($readableRoomIds), $this->alias . '.is_active' => true, //公開設定の条件生成 array( $this->alias . '.public_type' => [self::TYPE_PUBLIC, self::TYPE_LIMITED], $this->alias . '.publish_start <=' => $now, 'OR' => array( $this->alias . '.publish_end >=' => $now, $this->alias . '.publish_end' => null, ), ) ); } } return $roomConditions; } /** * 新着データ取得のためのbindModel * * @return void */ private function __bindModel() { //ページネーションで使うため、第二引数をfalseとする $this->bindModel(array( 'belongsTo' => array( 'RoomsLanguage' => array( 'className' => 'Rooms.RoomsLanguage', 'fields' => array('id', 'name'), 'foreignKey' => false, 'type' => 'INNER', 'conditions' => array( 'RoomsLanguage.room_id' . ' = ' . $this->alias . '.room_id', 'RoomsLanguage.language_id' => Current::read('Language.id', '0'), ), ), 'BlocksLanguage' => array( 'className' => 'Blocks.BlocksLanguage', 'fields' => array('name'), 'foreignKey' => false, 'type' => 'LEFT', 'conditions' => array( 'BlocksLanguage.block_id' . ' = ' . $this->alias . '.block_id', 'BlocksLanguage.language_id' => Current::read('Language.id', '0'), ), ), 'CategoriesLanguage' => array( 'className' => 'Categories.CategoriesLanguage', 'fields' => array('name'), 'foreignKey' => false, 'type' => 'LEFT', 'conditions' => array( 'CategoriesLanguage.category_id' . ' = ' . $this->alias . '.category_id', 'CategoriesLanguage.language_id' => Current::read('Language.id', '0'), ), ), 'Plugin' => array( 'className' => 'PluginManager.Plugin', 'fields' => array('id', 'key', 'name'), 'foreignKey' => false, 'type' => 'INNER', 'conditions' => array( 'Plugin.key' . ' = ' . $this->alias . '.plugin_key', 'Plugin.language_id' => Current::read('Language.id', '0'), ), ), 'TopicReadable' => array( 'className' => 'Topics.TopicReadable', 'fields' => array('id', 'topic_id', 'user_id'), 'foreignKey' => false, 'type' => 'INNER', 'conditions' => array( $this->TopicReadable->alias . '.topic_id' . ' = ' . $this->alias . '.id', $this->TopicReadable->alias . '.user_id' => array(Current::read('User.id', '0'), '0'), ), ), 'TopicUserStatus' => array( 'className' => 'Topics.TopicUserStatus', 'fields' => array('id', 'topic_id', 'user_id', 'read', 'answered'), 'foreignKey' => false, 'type' => 'LEFT', 'conditions' => array( $this->TopicUserStatus->alias . '.topic_id' . ' = ' . $this->alias . '.id', $this->TopicUserStatus->alias . '.user_id' => Current::read('User.id', '0'), ), ), ) ), false); } }