diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9501566..eb2068b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,10 +15,11 @@ name: tests jobs: setup: name: setup - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Slack Notification on Start uses: rtCamp/action-slack-notify@v2.2.0 + if: env.SLACK_WEBHOOK != '' env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_TESTS }} SLACK_CHANNEL: notify-nc3-tests @@ -28,7 +29,7 @@ jobs: tests: name: tests needs: setup - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest strategy: matrix: php: [ '7.1', '7.2', '7.3', '7.4' ] @@ -44,6 +45,7 @@ jobs: MYSQL_VERSION: ${{ matrix.mysql }} MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: cakephp_test + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v2 @@ -91,46 +93,53 @@ jobs: docker-compose exec -T nc3app bash /opt/scripts/app-build.sh - name: phpcs (PHP CodeSniffer) + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose exec -T nc3app bash /opt/scripts/phpcs.sh - name: phpmd (PHP Mess Detector) + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose exec -T nc3app bash /opt/scripts/phpmd.sh - name: phpcpd (PHP Copy/Paste Detector) + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose exec -T nc3app bash /opt/scripts/phpcpd.sh - name: gjslint (JavaScript Style Check) + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose exec -T nc3app bash /opt/scripts/gjslint.sh - name: phpdoc (PHP Documentor) + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose exec -T nc3app bash /opt/scripts/phpdoc.sh - name: phpunit (PHP UnitTest) + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose exec -T nc3app bash /opt/scripts/phpunit.sh sudo -s chmod a+w -R ${NC3_BUILD_DIR}/build - - name: push coveralls - env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: ${{ matrix.php }} - run: | - cd ${NC3_BUILD_DIR} - ls -la ${NC3_BUILD_DIR} - vendors/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v +# - name: push coveralls +# env: +# COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# COVERALLS_FLAG_NAME: ${{ matrix.php }} +# run: | +# cd ${NC3_BUILD_DIR} +# ls -la ${NC3_BUILD_DIR} +# vendors/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v - name: docker-compose remove + if: always() run: | cd ${NC3_DOCKER_DIR} docker-compose rm -f @@ -138,7 +147,7 @@ jobs: # テスト失敗時はこちらのステップが実行される - name: Slack Notification on Failure uses: rtCamp/action-slack-notify@v2.2.0 - if: failure() + if: env.SLACK_WEBHOOK != '' && failure() env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_TESTS }} SLACK_CHANNEL: notify-nc3-tests @@ -147,13 +156,13 @@ jobs: teardown: name: teardown - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest needs: tests steps: # テスト成功時はこちらのステップが実行される - name: Slack Notification on Success - if: success() uses: rtCamp/action-slack-notify@v2.2.0 + if: env.SLACK_WEBHOOK != '' && success() env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_TESTS }} SLACK_CHANNEL: notify-nc3-tests diff --git a/Controller/BbsArticlesController.php b/Controller/BbsArticlesController.php index 4ddb506..31ab71e 100644 --- a/Controller/BbsArticlesController.php +++ b/Controller/BbsArticlesController.php @@ -50,7 +50,8 @@ class BbsArticlesController extends BbsesAppController { 'NetCommons.Permission' => array( //アクセスの権限 'allow' => array( - 'add,edit,delete' => 'content_creatable', + 'add' => 'content_creatable', + 'edit,delete' => 'content_comment_creatable', 'reply' => 'content_comment_creatable', 'approve' => 'content_comment_publishable', ), @@ -105,7 +106,7 @@ public function index() { $query['conditions'] = $this->BbsArticle->getWorkflowConditions(array( 'BbsArticleTree.parent_id' => null, 'BbsArticle.bbs_key' => $this->viewVars['bbs']['key'], - )); + ), true); //ソート $options = array( @@ -215,7 +216,7 @@ public function view() { $this->BbsArticle->alias . '.bbs_key' => $this->viewVars['bbs']['key'], $this->BbsArticle->alias . '.key' => $bbsArticleKey ) - )); + ), true); if (! $bbsArticle) { return $this->throwBadRequest(); } @@ -242,7 +243,7 @@ public function view() { //子記事の取得 $this->BbsArticleTree->Behaviors->load('Tree', array( - 'scope' => $this->BbsArticle->getWorkflowConditions() + 'scope' => $this->BbsArticle->getWorkflowConditions([], true) )); if ($this->viewVars['bbsFrameSetting']['display_type'] === BbsFrameSetting::DISPLAY_TYPE_FLAT) { @@ -269,7 +270,7 @@ public function view() { $bbsArticleChildren[$child['BbsArticleTree']['id']] = $child; } - $this->set('bbsArticleChildren', $children); + $this->set('bbsArticleChildren', $bbsArticleChildren); } /** @@ -324,7 +325,7 @@ public function reply() { $this->BbsArticle->alias . '.bbs_key' => $this->viewVars['bbs']['key'], $this->BbsArticle->alias . '.key' => $bbsArticleKey ) - )); + ), true); if (!isset($bbsArticle['BbsArticle']['status']) || $bbsArticle['BbsArticle']['status'] !== WorkflowComponent::STATUS_PUBLISHED) { @@ -399,7 +400,7 @@ public function edit() { $this->BbsArticle->alias . '.bbs_key' => $this->viewVars['bbs']['key'], $this->BbsArticle->alias . '.key' => $bbsArticleKey ) - )); + ), true); if (empty($bbsArticle)) { return $this->throwBadRequest(); } @@ -451,7 +452,7 @@ public function delete() { $this->BbsArticle->alias . '.bbs_key' => $this->viewVars['bbs']['key'], $this->BbsArticle->alias . '.key' => $this->data['BbsArticle']['key'] ) - )); + ), true); //削除権限チェック if (! $this->BbsArticle->canDeleteWorkflowContent($bbsArticle)) { @@ -465,7 +466,7 @@ public function delete() { 'conditions' => array( $this->BbsArticleTree->alias . '.id' => $bbsArticle['BbsArticleTree']['parent_id'], ) - )); + ), true); if (! $parentBbsArticle) { return $this->throwBadRequest(); } @@ -502,7 +503,7 @@ public function approve() { $this->BbsArticle->alias . '.bbs_key' => $this->data['BbsArticle']['bbs_key'], $this->BbsArticle->alias . '.key' => $this->data['BbsArticle']['key'] ) - )); + ), true); if (! $bbsArticle) { return $this->throwBadRequest(); } @@ -597,7 +598,7 @@ private function __setBbsArticleByTreeId($viewVarsKey, $bbsArticleTreeId) { 'conditions' => array( $this->BbsArticleTree->alias . '.id' => $bbsArticleTreeId, ) - )); + ), true); if (! $bbsArticle) { return false; } diff --git a/Model/BbsArticle.php b/Model/BbsArticle.php index 79dc5ee..e336c16 100644 --- a/Model/BbsArticle.php +++ b/Model/BbsArticle.php @@ -14,6 +14,7 @@ */ App::uses('BbsesAppModel', 'Bbses.Model'); +App::uses('MailQueueBehavior', 'Mails.Model/Behavior'); /** * BbsArticle Model @@ -54,8 +55,9 @@ class BbsArticle extends BbsesAppModel { 'Likes.Like', 'NetCommons.OriginalKey', 'Workflow.WorkflowComment', - 'Workflow.Workflow', + 'Bbses.BbsesWorkflow', 'Mails.MailQueue' => array( + 'workflowType' => MailQueueBehavior::MAIL_QUEUE_WORKFLOW_TYPE_WORKFLOW, 'embedTags' => array( 'X-SUBJECT' => 'BbsArticle.title', 'X-BODY' => 'BbsArticle.content', @@ -197,6 +199,34 @@ public function beforeValidate($options = array()) { return parent::beforeValidate($options); } +/** + * Called before each save operation, after validation. Return a non-true result + * to halt the save. + * + * @param array $options Options passed from Model::save(). + * @return bool True if the operation should continue, false if it should abort + * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave + * @see Model::save() + */ + public function beforeSave($options = array()) { + if ($this->Behaviors->loaded('Mails.MailQueue') && + ! empty($this->data['BbsArticleTree']['root_id'])) { + /** @see MailQueueBehavior::setSetting() */ + $this->setSetting('editablePermissionKey', 'content_comment_editable'); + $this->setSetting('publishablePermissionKey', 'content_comment_publishable'); + $this->setSetting('workflowType', MailQueueBehavior::MAIL_QUEUE_WORKFLOW_TYPE_COMMENT); + $this->data[$this->alias]['plugin_key'] = Current::read('Plugin.key'); + } + + if (! isset($this->data['Bbs'])) { + //メールの件名や本文に掲示板名が含まれているため、もし設定されていなかった場合、セットする。 + $bbs = $this->Bbs->getBbs(); + $this->data['Bbs'] = $bbs['Bbs']; + } + + parent::beforeSave($options); + } + /** * Called after each successful save operation. * @@ -282,8 +312,6 @@ public function saveBbsArticle($data) { * @throws InternalErrorException */ public function deleteBbsArticle($data) { - //トランザクションBegin - $this->begin(); $this->set($data); $bbsArticleTree = $this->BbsArticleTree->find('first', array( @@ -308,6 +336,8 @@ public function deleteBbsArticle($data) { ), )); + //トランザクションBegin + $this->begin(); try { //BbsArticleの削除 $this->contentKey = $bbsArticleTrees; @@ -331,6 +361,14 @@ public function deleteBbsArticle($data) { $data['Bbs']['key'], $data['BbsArticle']['language_id'] ); + if (isset($this->data['BbsArticleTree']['root_id']) && + $this->data['BbsArticleTree']['root_id']) { + $this->updateBbsArticleChildCount( + $this->data['BbsArticleTree']['root_id'], + $this->data[$this->alias]['language_id'] + ); + } + //トランザクションCommit $this->commit(); @@ -370,7 +408,14 @@ public function canEditWorkflowContent($data) { if ($childArticle) { return false; } - return (Current::permission('content_creatable') && + + if (empty($data['BbsArticleTree']['root_id'])) { + $permission = 'content_creatable'; + } else { + $permission = 'content_comment_creatable'; + } + + return (Current::permission($permission) && ((int)$data['BbsArticle']['created_user'] === (int)Current::read('User.id'))); } diff --git a/Model/Behavior/BbsArticleBehavior.php b/Model/Behavior/BbsArticleBehavior.php index 4776a40..2844c39 100644 --- a/Model/Behavior/BbsArticleBehavior.php +++ b/Model/Behavior/BbsArticleBehavior.php @@ -152,11 +152,11 @@ public function getChildrenArticleCounts(Model $model, $bbsKey, $bbsArticles, $a $query = array( 'recursive' => -1, - 'fields' => ['BbsArticleTree.root_id', 'COUNT(*) AS bbs_article_child_count'], + 'fields' => ['BbsArticleTree.root_id', 'BbsArticle.is_active'], 'conditions' => $model->getWorkflowConditions(array( 'BbsArticleTree.root_id' => $articleTreeIds, 'BbsArticleTree.bbs_key' => $bbsKey, - )), + ), true), 'joins' => [ [ 'type' => 'INNER', @@ -165,22 +165,30 @@ public function getChildrenArticleCounts(Model $model, $bbsKey, $bbsArticles, $a 'conditions' => $model->belongsTo['BbsArticleTree']['conditions'], ] ], - 'group' => array('BbsArticleTree.root_id'), + //'group' => array('BbsArticleTree.root_id', 'BbsArticle.is_active'), ); $results = $model->find('all', $query); $counts = []; foreach ($results as $result) { - $counts[$result['BbsArticleTree']['root_id']] = $result[0]['bbs_article_child_count']; + $rootId = $result['BbsArticleTree']['root_id']; + $isActive = (int)$result['BbsArticle']['is_active']; + if (! isset($counts[$rootId])) { + $counts[$rootId] = [0 => 0, 1 => 0]; + } + $counts[$rootId][$isActive]++; } foreach ($bbsArticles as $i => $article) { if (isset($counts[$article['BbsArticleTree']['id']])) { - $count = $counts[$article['BbsArticleTree']['id']]; + $id = $article['BbsArticleTree']['id']; + $activeCount = $counts[$id][1]; + $latestCount = $counts[$id][0]; + $count = $activeCount + $latestCount; - $beforeCount = $count - $bbsArticles[$i]['BbsArticleTree']['bbs_article_child_count']; - $bbsArticles[$i]['BbsArticleTree']['approval_bbs_article_child_count'] = (string)$beforeCount; + $approveCount = $count - $activeCount; + $bbsArticles[$i]['BbsArticleTree']['approval_bbs_article_child_count'] = (string)$approveCount; - $bbsArticles[$i]['BbsArticleTree']['bbs_article_child_count'] = $count; + $bbsArticles[$i]['BbsArticleTree']['bbs_article_child_count'] = (string)$count; } } @@ -213,7 +221,7 @@ public function getChildrenArticleTitles(Model $model, $articleTreeIds) { ], $trackable), 'conditions' => $model->getWorkflowConditions(array( 'BbsArticleTree.root_id' => $articleTreeIds, - )), + ), true), ); $results = $model->find('all', $query); diff --git a/Model/Behavior/BbsesWorkflowBehavior.php b/Model/Behavior/BbsesWorkflowBehavior.php new file mode 100644 index 0000000..fc45d2d --- /dev/null +++ b/Model/Behavior/BbsesWorkflowBehavior.php @@ -0,0 +1,62 @@ + + * @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('WorkflowBehavior', 'Workflow.Model/Behavior'); + +/** + * Workflow Behavior + * + * @author Shohei Nakajima + * @package Bbses\Workflow\Model\Befavior + */ +class BbsesWorkflowBehavior extends WorkflowBehavior { + +/** + * beforeValidate is called before a model is validated, you can use this callback to + * add behavior validation rules into a models validate array. Returning false + * will allow you to make the validation fail. + * + * @param Model $model Model using this behavior + * @param array $options Options passed from Model::save(). + * @return mixed False or null will abort the operation. Any other result will continue. + * @see Model::save() + */ + public function beforeValidate(Model $model, $options = array()) { + if (empty($model->data['BbsArticleTree']['root_id'])) { + return parent::beforeValidate($model, $options); + } + + if (Current::permission('content_comment_publishable')) { + $statuses = WorkflowBehavior::$statuses; + } else { + $statuses = WorkflowBehavior::$statusesForEditor; + } + + $model->validate = ValidateMerge::merge($model->validate, array( + 'status' => array( + 'numeric' => array( + 'rule' => array('numeric'), + 'message' => __d('net_commons', 'Invalid request.'), + 'allowEmpty' => false, + 'required' => true, + 'on' => 'create', // Limit validation to 'create' or 'update' operations + ), + 'inList' => array( + 'rule' => array('inList', $statuses), + 'message' => __d('net_commons', 'Invalid request.'), + ) + ), + )); + + return true; + } + +} diff --git a/Test/Case/Controller/BbsArticlesController/ViewTest.php b/Test/Case/Controller/BbsArticlesController/ViewTest.php index f9a3843..bcaada4 100644 --- a/Test/Case/Controller/BbsArticlesController/ViewTest.php +++ b/Test/Case/Controller/BbsArticlesController/ViewTest.php @@ -288,11 +288,11 @@ public function dataProviderViewByEditable() { */ public function testViewError($urlOptions, $assert, $exception = null, $return = 'view') { //Exception - ClassRegistry::removeObject('WorkflowBehavior'); - $workflowBehaviorMock = $this->getMock('WorkflowBehavior', ['canReadWorkflowContent']); - ClassRegistry::addObject('WorkflowBehavior', $workflowBehaviorMock); - $this->BbsArticle->Behaviors->unload('Workflow'); - $this->BbsArticle->Behaviors->load('Workflow', $this->BbsArticle->actsAs['Workflow.Workflow']); + ClassRegistry::removeObject('BbsesWorkflowBehavior'); + $workflowBehaviorMock = $this->getMock('BbsesWorkflowBehavior', ['canReadWorkflowContent']); + ClassRegistry::addObject('BbsesWorkflowBehavior', $workflowBehaviorMock); + $this->BbsArticle->Behaviors->unload('BbsesWorkflow'); + $this->BbsArticle->Behaviors->load('BbsesWorkflow', $this->BbsArticle->actsAs['Bbses.BbsesWorkflow']); $workflowBehaviorMock ->expects($this->once()) diff --git a/Test/Case/Model/BbsArticle/SaveBbsArticleTest.php b/Test/Case/Model/BbsArticle/SaveBbsArticleTest.php index e2b1116..b789078 100644 --- a/Test/Case/Model/BbsArticle/SaveBbsArticleTest.php +++ b/Test/Case/Model/BbsArticle/SaveBbsArticleTest.php @@ -129,6 +129,8 @@ public function setUp() { Current::write('Language.id', '2'); parent::setUp(); + Current::writePermission('2', 'content_comment_publishable', true); + $model = $this->_modelName; $this->$model->Behaviors->unload('Like'); $this->$model->Behaviors->unload('Topics'); @@ -136,6 +138,42 @@ public function setUp() { $this->$model->BbsArticleTree->Behaviors->unload('Like'); } +/** + * Test to call BbsesWorkflowBehavior::beforeSave + * + * BbsesWorkflowBehaviorをモックに置き換えて登録処理を呼び出します。
+ * BbsesWorkflowBehavior::beforeSaveが1回呼び出されることをテストします。
+ * ##### 参考URL + * http://stackoverflow.com/questions/19833495/how-to-mock-a-cakephp-behavior-for-unit-testing] + * + * @param array $data 登録データ + * @dataProvider dataProviderSave + * @return void + * @throws CakeException Workflow.Workflowがロードされていないとエラー + */ + public function testCallWorkflowBehavior($data) { + $model = $this->_modelName; + $method = $this->_methodName; + + if (! $this->$model->Behaviors->loaded('Bbses.BbsesWorkflow')) { + $error = '"Workflow.Workflow" not loaded in ' . $this->$model->alias . '.'; + throw new CakeException($error); + } + + ClassRegistry::removeObject('BbsesWorkflowBehavior'); + $workflowBehaviorMock = $this->getMock('BbsesWorkflowBehavior', ['beforeSave']); + ClassRegistry::addObject('BbsesWorkflowBehavior', $workflowBehaviorMock); + $this->$model->Behaviors->unload('BbsesWorkflow'); + $this->$model->Behaviors->load('BbsesWorkflow', $this->$model->actsAs['Bbses.BbsesWorkflow']); + + $workflowBehaviorMock + ->expects($this->once()) + ->method('beforeSave') + ->will($this->returnValue(true)); + + $this->$model->$method($data); + } + /** * SaveのDataProvider * diff --git a/VERSION.txt b/VERSION.txt index fa7adc7..86fb650 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -3.3.5 +3.3.7 diff --git a/View/Elements/BbsArticles/all/index_bbs_article.ctp b/View/Elements/BbsArticles/all/index_bbs_article.ctp index cb37348..f5583cf 100644 --- a/View/Elements/BbsArticles/all/index_bbs_article.ctp +++ b/View/Elements/BbsArticles/all/index_bbs_article.ctp @@ -26,7 +26,7 @@
- + - + request->data = $bbsArticle; ?> NetCommonsForm->create('BbsArticle', array( 'div' => false, diff --git a/View/Elements/BbsArticles/edit_form.ctp b/View/Elements/BbsArticles/edit_form.ctp index a16b613..44905a4 100644 --- a/View/Elements/BbsArticles/edit_form.ctp +++ b/View/Elements/BbsArticles/edit_form.ctp @@ -51,17 +51,19 @@
params['action'] === 'edit' && $this->data['BbsArticleTree']['root_id']) { - echo $this->BbsesForm->replyEditButtons('BbsArticle.status'); - } else { + if ($this->params['action'] === 'edit' && $this->data['BbsArticleTree']['root_id'] || + $this->params['action'] === 'reply') { if ($this->params['action'] === 'reply') { - $key = isset($currentBbsArticle['BbsArticle']['key']) - ? $currentBbsArticle['BbsArticle']['key'] - : null; - $cancelUrl = NetCommonsUrl::blockUrl( - array('action' => 'view', 'key' => $key) - ); - } elseif ($this->params['action'] === 'edit') { + $key = $currentBbsArticle['BbsArticle']['key'] ?? null; + } else { + $key = $this->request->data['BbsArticle']['key'] ?? null; + } + $cancelUrl = NetCommonsUrl::blockUrl( + array('action' => 'view', 'key' => $key) + ); + echo $this->BbsesForm->replyButtons('BbsArticle.status', $cancelUrl); + } else { + if ($this->params['action'] === 'edit') { $key = isset($this->request->data['BbsArticle']['key']) ? $this->request->data['BbsArticle']['key'] : null; diff --git a/View/Elements/BbsArticles/flat/index_bbs_article.ctp b/View/Elements/BbsArticles/flat/index_bbs_article.ctp index 23244cc..0f918ba 100644 --- a/View/Elements/BbsArticles/flat/index_bbs_article.ctp +++ b/View/Elements/BbsArticles/flat/index_bbs_article.ctp @@ -33,7 +33,7 @@
- +
- + '; @@ -55,13 +56,6 @@ public function replyEditButtons($statusFieldName) { //変更前のstatusを保持する $output .= $this->NetCommonsForm->hidden('status_', array('value' => $status)); - $key = null; - if (isset($this->_View->request->data['BbsArticle']['key'])) { - $key = $this->_View->request->data['BbsArticle']['key']; - } - $cancelUrl = NetCommonsUrl::blockUrl( - array('action' => 'view', 'key' => $key) - ); $cancelOptions = array( 'ng-class' => '{disabled: sending}', 'ng-click' => 'sending=true', @@ -74,7 +68,7 @@ public function replyEditButtons($statusFieldName) { 'ng-class' => '{disabled: sending}' ); - if (Current::permission('content_publishable')) { + if (Current::permission('content_comment_publishable')) { $saveOptions = array( 'label' => __d('net_commons', 'OK'), 'class' => 'btn btn-primary' . $this->Button->getButtonSize() . ' btn-workflow',