diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..b59f569 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/vendor +/coverage diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..94630ba --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,29 @@ +build: + environment: + php: + version: 5.5.12 + dependencies: + before: + - sudo composer self-update && composer --version + - composer global require "fxp/composer-asset-plugin:1.0.0-beta4" + tests: + override: + - phpunit +imports: + - php +checks: + php: + code_rating: true + duplication: true +tools: + php_sim: false + php_cpd: false + php_pdepend: true + php_analyzer: true + php_changetracking: true + external_code_coverage: + timeout: 2100 # Timeout in seconds. +filter: + excluded_paths: + - tests/* + - vendor/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a16f5e0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,33 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + - hhvm-nightly + +matrix: + fast_finish: true + allow_failures: + - php: hhvm + - php: hhvm-nightly + +sudo: false + +cache: + directories: + - vendor + +install: + - travis_retry composer self-update && composer --version + - travis_retry composer global require "fxp/composer-asset-plugin:1.0.0-beta4" + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - travis_retry composer install --prefer-dist --no-interaction + +script: + - phpunit --verbose --coverage-clover=coverage/coverage.clover + +after_script: + - travis_retry wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage/coverage.clover \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..460b28e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Revin Roman Borisovich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Module.php b/Module.php new file mode 100755 index 0000000..55dfc1e --- /dev/null +++ b/Module.php @@ -0,0 +1,39 @@ +getAuthManager(); + $ItsMyCommentRule = new \rmrevin\yii\module\Comments\rbac\ItsMyComment(); + + $AuthManager->add($ItsMyCommentRule); + + $AuthManager->add(new \yii\rbac\Role(['name' => Permission::CREATE])); + $AuthManager->add(new \yii\rbac\Role(['name' => Permission::UPDATE])); + $AuthManager->add(new \yii\rbac\Role(['name' => Permission::UPDATE_OWN, 'ruleName' => $ItsMyCommentRule->name])); + $AuthManager->add(new \yii\rbac\Role(['name' => Permission::DELETE])); + $AuthManager->add(new \yii\rbac\Role(['name' => Permission::DELETE_OWN, 'ruleName' => $ItsMyCommentRule->name])); + + if ($this->userIdentityClass === null) { + $this->userIdentityClass = \Yii::$app->getUser()->identityClass; + } + } +} + diff --git a/Permission.php b/Permission.php new file mode 100644 index 0000000..cd8f5ad --- /dev/null +++ b/Permission.php @@ -0,0 +1,21 @@ + [ + // ... + 'comments' => 'rmrevin\yii\module\Comments\Module', + ], + // ... +]; +``` + +In auth manager add rules: +```php +use \yii\rbac\Role; +use \rmrevin\yii\module\Comments\Permission; +use \rmrevin\yii\module\Comments\rbac\ItsMyComment; + +$AuthManager = \Yii::$app->getAuthManager(); +$ItsMyCommentRule = new ItsMyComment(); + +// Rules +$AuthManager->add($ItsMyCommentRule); + +// Permissions +$AuthManager->add(new Role(['name' => Permission::CREATE])); +$AuthManager->add(new Role(['name' => Permission::UPDATE])); +$AuthManager->add(new Role(['name' => Permission::UPDATE_OWN, 'ruleName' => $ItsMyCommentRule->name])); +$AuthManager->add(new Role(['name' => Permission::DELETE])); +$AuthManager->add(new Role(['name' => Permission::DELETE_OWN, 'ruleName' => $ItsMyCommentRule->name])); +``` + +Usage +----- +In view +```php + (string) 'photo-15', // type and id +]); + +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d0f8dac --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "rmrevin/yii2-comments", + "description": "Comments module for Yii2", + "keywords": [ + "yii", + "comment", + "widget", + "module" + ], + "type": "yii2-extension", + "license": "MIT", + "minimum-stability": "stable", + "support": { + "issues": "https://github.com/rmrevin/yii2-comments/issues", + "source": "https://github.com/rmrevin/yii2-comments" + }, + "authors": [ + { + "name": "Roman Revin", + "email": "xgismox@gmail.com", + "homepage": "http://rmrevin.ru/" + } + ], + "require": { + "php": ">=5.4.0", + + "yiisoft/yii2": "2.0.1", + "rmrevin/yii2-fontawesome": "2.6.2" + }, + "autoload": { + "psr-4": { + "rmrevin\\yii\\module\\Comments\\": "" + } + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..015b28c --- /dev/null +++ b/composer.lock @@ -0,0 +1,507 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "9e34b16bc28910661333fb994bc07682", + "packages": [ + { + "name": "bower-asset/jquery", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/jquery/jquery.git", + "reference": "8f2a9d9272d6ed7f32d3a484740ab342c02541e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jquery/jquery/zipball/8f2a9d9272d6ed7f32d3a484740ab342c02541e0", + "reference": "8f2a9d9272d6ed7f32d3a484740ab342c02541e0", + "shasum": "" + }, + "require-dev": { + "bower-asset/qunit": "1.14.0", + "bower-asset/requirejs": "2.1.10", + "bower-asset/sinon": "1.8.1", + "bower-asset/sizzle": "2.1.1-patch2" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "dist/jquery.js", + "bower-asset-ignore": [ + "**/.*", + "build", + "speed", + "test", + "*.md", + "AUTHORS.txt", + "Gruntfile.js", + "package.json" + ] + }, + "license": [ + "MIT" + ], + "keywords": [ + "javascript", + "jquery", + "library" + ] + }, + { + "name": "bower-asset/jquery.inputmask", + "version": "3.1.52", + "source": { + "type": "git", + "url": "https://github.com/RobinHerbots/jquery.inputmask.git", + "reference": "6afe1c66735b96dd45303b67152917cbbfa4d087" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/6afe1c66735b96dd45303b67152917cbbfa4d087", + "reference": "6afe1c66735b96dd45303b67152917cbbfa4d087", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.7" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "./dist/inputmask/jquery.inputmask.js", + "./dist/inputmask/jquery.inputmask.extensions.js", + "./dist/inputmask/jquery.inputmask.date.extensions.js", + "./dist/inputmask/jquery.inputmask.numeric.extensions.js", + "./dist/inputmask/jquery.inputmask.phone.extensions.js", + "./dist/inputmask/jquery.inputmask.regex.extensions.js" + ], + "bower-asset-ignore": [ + "**/.*", + "qunit/", + "nuget/", + "tools/", + "js/", + "*.md", + "build.properties", + "build.xml", + "jquery.inputmask.jquery.json" + ] + }, + "license": [ + "http://opensource.org/licenses/mit-license.php" + ], + "description": "jquery.inputmask is a jquery plugin which create an input mask.", + "keywords": [ + "form", + "input", + "inputmask", + "jQuery", + "mask", + "plugins" + ] + }, + { + "name": "bower-asset/punycode", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/bestiejs/punycode.js.git", + "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", + "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", + "shasum": "" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "punycode.js", + "bower-asset-ignore": [ + "coverage", + "tests", + ".*", + "component.json", + "Gruntfile.js", + "node_modules", + "package.json" + ] + } + }, + { + "name": "bower-asset/yii2-pjax", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/jquery-pjax.git", + "reference": "fb92be865c0fd6583714475cb7d629020749d73f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/fb92be865c0fd6583714475cb7d629020749d73f", + "reference": "fb92be865c0fd6583714475cb7d629020749d73f", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.8" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "./jquery.pjax.js", + "bower-asset-ignore": [ + ".travis.yml", + "Gemfile", + "Gemfile.lock", + "vendor/", + "script/", + "test/" + ] + } + }, + { + "name": "cebe/markdown", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/cebe/markdown.git", + "reference": "9d6c36d6623497523ed421a31d940bc1d7435578" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cebe/markdown/zipball/9d6c36d6623497523ed421a31d940bc1d7435578", + "reference": "9d6c36d6623497523ed421a31d940bc1d7435578", + "shasum": "" + }, + "require": { + "lib-pcre": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "cebe/indent": "*", + "facebook/xhprof": "*@dev", + "phpunit/phpunit": "3.7.*" + }, + "bin": [ + "bin/markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "cebe\\markdown\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Creator" + } + ], + "description": "A super fast, highly extensible markdown parser for PHP", + "homepage": "https://github.com/cebe/markdown#readme", + "keywords": [ + "extensible", + "fast", + "gfm", + "markdown", + "markdown-extra" + ], + "time": "2014-10-25 16:16:49" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd", + "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com", + "role": "Developer" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2013-11-30 08:25:19" + }, + { + "name": "fortawesome/font-awesome", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/FortAwesome/Font-Awesome.git", + "reference": "a65bd93d81e9e6bd5ebfa41757a4474960b973b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FortAwesome/Font-Awesome/zipball/a65bd93d81e9e6bd5ebfa41757a4474960b973b4", + "reference": "a65bd93d81e9e6bd5ebfa41757a4474960b973b4", + "shasum": "" + }, + "require-dev": { + "jekyll": "1.0.2", + "lessc": "1.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OFL-1.1", + "MIT" + ], + "authors": [ + { + "name": "Dave Gandy", + "email": "dave@fontawesome.io", + "homepage": "http://twitter.com/davegandy", + "role": "Developer" + } + ], + "description": "The iconic font and CSS framework", + "homepage": "http://fontawesome.io/", + "keywords": [ + "FontAwesome", + "awesome", + "bootstrap", + "font", + "icon" + ], + "time": "2014-08-26 16:36:44" + }, + { + "name": "rmrevin/yii2-fontawesome", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/rmrevin/yii2-fontawesome.git", + "reference": "8550d89131a87e823c93ebc15cdb69d6035d4548" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rmrevin/yii2-fontawesome/zipball/8550d89131a87e823c93ebc15cdb69d6035d4548", + "reference": "8550d89131a87e823c93ebc15cdb69d6035d4548", + "shasum": "" + }, + "require": { + "fortawesome/font-awesome": "4.2.*", + "php": ">=5.4.0", + "yiisoft/yii2": "2.0.*" + }, + "type": "yii2-extension", + "autoload": { + "psr-4": { + "rmrevin\\yii\\fontawesome\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Revin", + "email": "xgismox@gmail.com", + "homepage": "http://rmrevin.ru/" + } + ], + "description": "Asset Bundle for Yii2 with Font Awesome", + "keywords": [ + "asset", + "awesome", + "bundle", + "font", + "yii" + ], + "time": "2014-12-10 20:49:45" + }, + { + "name": "yiisoft/yii2", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-framework.git", + "reference": "7ed175b4b71ac96eaf86aadc322186ecdc58498d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/7ed175b4b71ac96eaf86aadc322186ecdc58498d", + "reference": "7ed175b4b71ac96eaf86aadc322186ecdc58498d", + "shasum": "" + }, + "require": { + "bower-asset/jquery": "2.1.*@stable | 1.11.*@stable", + "bower-asset/jquery.inputmask": "3.1.*", + "bower-asset/punycode": "1.3.*", + "bower-asset/yii2-pjax": ">=2.0.1", + "cebe/markdown": "~1.0.0", + "ext-mbstring": "*", + "ezyang/htmlpurifier": "4.6.*", + "lib-pcre": "*", + "php": ">=5.4.0", + "yiisoft/yii2-composer": "*" + }, + "bin": [ + "yii" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "http://www.yiiframework.com/", + "role": "Founder and project lead" + }, + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "http://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + } + ], + "description": "Yii PHP Framework Version 2", + "homepage": "http://www.yiiframework.com/", + "keywords": [ + "framework", + "yii2" + ], + "time": "2014-12-07 16:42:41" + }, + { + "name": "yiisoft/yii2-composer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-composer.git", + "reference": "7f300dd23b6c4d1e7effc81c962b3889f83e43c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/7f300dd23b6c4d1e7effc81c962b3889f83e43c0", + "reference": "7f300dd23b6c4d1e7effc81c962b3889f83e43c0", + "shasum": "" + }, + "require": { + "composer-plugin-api": "1.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "yii\\composer\\Plugin", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\composer\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The composer plugin for Yii extension installer", + "keywords": [ + "composer", + "extension installer", + "yii2" + ], + "time": "2014-12-07 16:42:41" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [] +} diff --git a/forms/CommentCreateForm.php b/forms/CommentCreateForm.php new file mode 100644 index 0000000..3d48372 --- /dev/null +++ b/forms/CommentCreateForm.php @@ -0,0 +1,97 @@ +Comment; + + if (false === $this->Comment->isNewRecord) { + $this->id = $Comment->id; + $this->entity = $Comment->entity; + $this->text = $Comment->text; + } + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['entity', 'text'], 'required'], + [['entity', 'text'], 'string'], + [['id'], 'integer'], + [['id'], 'exist', 'targetClass' => Comments\models\Comment::class, 'targetAttribute' => 'id'], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'entity' => \Yii::t('app', 'Entity'), + 'text' => \Yii::t('app', 'Text'), + ]; + } + + /** + * @return bool + * @throws \yii\web\NotFoundHttpException + */ + public function save() + { + $Comment = $this->Comment; + + if (empty($this->id)) { + $Comment = new Comments\models\Comment(); + } elseif ($this->id > 0 && $Comment->id !== $this->id) { + $Comment = Comments\models\Comment::find() + ->byId($this->id) + ->one(); + + if (!($Comment instanceof Comments\models\Comment)) { + throw new \yii\web\NotFoundHttpException; + } + } + + $Comment->entity = $this->entity; + $Comment->text = $this->text; + + $result = $Comment->save(); + + if ($Comment->hasErrors()) { + foreach ($Comment->getErrors() as $attribute => $messages) { + foreach ($messages as $mes) { + $this->addError($attribute, $mes); + } + } + } + + $this->Comment = $Comment; + + return $result; + } +} \ No newline at end of file diff --git a/interfaces/CommentatorInterface.php b/interfaces/CommentatorInterface.php new file mode 100644 index 0000000..0b5d7f9 --- /dev/null +++ b/interfaces/CommentatorInterface.php @@ -0,0 +1,30 @@ +db->driverName === 'mysql') { + // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable('{{%comment}}', [ + 'id' => Schema::TYPE_PK, + 'entity' => Schema::TYPE_STRING, + 'text' => Schema::TYPE_TEXT, + 'created_by' => Schema::TYPE_INTEGER, + 'updated_by' => Schema::TYPE_INTEGER, + 'created_at' => Schema::TYPE_INTEGER, + 'updated_at' => Schema::TYPE_INTEGER, + ], $tableOptions); + + $this->createIndex('index_entity', '{{%comment}}', ['entity']); + $this->createIndex('index_created_by', '{{%comment}}', ['created_by']); + $this->createIndex('index_created_at', '{{%comment}}', ['created_at']); + } + + public function safeDown() + { + $this->dropTable('{{%comment}}'); + } +} \ No newline at end of file diff --git a/models/Comment.php b/models/Comment.php new file mode 100644 index 0000000..46ce3dc --- /dev/null +++ b/models/Comment.php @@ -0,0 +1,154 @@ + self::NOT_DELETED], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => \Yii::t('app', 'ID'), + 'entity' => \Yii::t('app', 'Entity'), + 'text' => \Yii::t('app', 'Text'), + 'created_by' => \Yii::t('app', 'Created by'), + 'updated_by' => \Yii::t('app', 'Updated by'), + 'created_at' => \Yii::t('app', 'Created at'), + 'updated_at' => \Yii::t('app', 'Updated at'), + ]; + } + + /** + * @return bool + */ + public function isEdited() + { + return $this->created_at !== $this->updated_at; + } + + /** + * @return bool + */ + public function isDeleted() + { + return $this->deleted === self::DELETED; + } + + /** + * @return bool + */ + public static function canCreate() + { + return true || \Yii::$app->getUser()->can(Comments\Permission::CREATE); + } + + /** + * @return bool + */ + public function canUpdate() + { + return true || \Yii::$app->getUser()->can(Comments\Permission::UPDATE) || \Yii::$app->getUser()->can(Comments\Permission::UPDATE_OWN, ['Comment' => $this]); + } + + /** + * @return bool + */ + public function canDelete() + { + return true || \Yii::$app->getUser()->can(Comments\Permission::DELETE) || \Yii::$app->getUser()->can(Comments\Permission::DELETE_OWN, ['Comment' => $this]); + } + + /** + * @return queries\CommentQuery + */ + public function getAuthor() + { + /** @var Comments\Module $Module */ + $Module = \Yii::$app->getModule('comments'); + + return $this->hasOne($Module->userIdentityClass, ['id' => 'created_by']); + } + + /** + * @return queries\CommentQuery + */ + public function getLastUpdateAuthor() + { + /** @var Comments\Module $Module */ + $Module = \Yii::$app->getModule('comments'); + + return $this->hasOne($Module->userIdentityClass, ['id' => 'updated_by']); + } + + /** + * @return queries\CommentQuery + */ + public static function find() + { + return new queries\CommentQuery(get_called_class()); + } + + /** + * @inheritdoc + */ + public static function tableName() + { + return '{{%comment}}'; + } + + const NOT_DELETED = 0; + const DELETED = 1; +} \ No newline at end of file diff --git a/models/queries/CommentQuery.php b/models/queries/CommentQuery.php new file mode 100644 index 0000000..078f528 --- /dev/null +++ b/models/queries/CommentQuery.php @@ -0,0 +1,49 @@ +andWhere(['id' => $id]); + + return $this; + } + + /** + * @param string|array $entity + * @return self + */ + public function byEntity($entity) + { + $this->andWhere(['entity' => $entity]); + + return $this; + } + + /** + * @return self + */ + public function withoutDeleted() + { + $this->andWhere(['deleted' => Comments\models\Comment::NOT_DELETED]); + + return $this; + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d6af986 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + ./tests + ./vendor + + + + + ./tests/unit/comments + + + + + + \ No newline at end of file diff --git a/rbac/ItsMyComment.php b/rbac/ItsMyComment.php new file mode 100644 index 0000000..9912c65 --- /dev/null +++ b/rbac/ItsMyComment.php @@ -0,0 +1,25 @@ +created_by; + } +} \ No newline at end of file diff --git a/widgets/CommentFormAsset.php b/widgets/CommentFormAsset.php new file mode 100644 index 0000000..fb9db72 --- /dev/null +++ b/widgets/CommentFormAsset.php @@ -0,0 +1,21 @@ +getView()); + + $CommentCreateForm = new Comments\forms\CommentCreateForm([ + 'Comment' => $this->Comment, + 'entity' => $this->entity, + ]); + + if ($CommentCreateForm->load(\Yii::$app->getRequest()->post())) { + if ($CommentCreateForm->validate()) { + if ($CommentCreateForm->save()) { + \Yii::$app->getResponse() + ->refresh('#comment-' . $CommentCreateForm->Comment->id) + ->send(); + + exit; + } + } + } + + return $this->render('comment-form', [ + 'CommentCreateForm' => $CommentCreateForm, + ]); + } +} \ No newline at end of file diff --git a/widgets/CommentListAsset.php b/widgets/CommentListAsset.php new file mode 100644 index 0000000..ccca0f8 --- /dev/null +++ b/widgets/CommentListAsset.php @@ -0,0 +1,30 @@ + 'comments-widget']; + + /** @var string */ + public $entity; + + /** @var array */ + public $pagination = [ + 'pageParam' => 'page', + 'pageSizeParam' => 'per-page', + 'pageSize' => 20, + 'pageSizeLimit' => [1, 50], + ]; + + /** @var array */ + public $sort = [ + 'defaultOrder' => [ + 'id' => SORT_ASC, + ], + ]; + + /** @var bool */ + public $showDeleted = true; + + /** @var bool */ + public $showCreateForm = true; + + public function init() + { + parent::init(); + + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * @inheritdoc + */ + public function run() + { + CommentListAsset::register($this->getView()); + + $this->processDelete(); + + $CommentsQuery = Comments\models\Comment::find() + ->byEntity($this->entity); + + if (false === $this->showDeleted) { + $CommentsQuery->withoutDeleted(); + } + + $CommentsDataProvider = new \yii\data\ActiveDataProvider([ + 'query' => $CommentsQuery->with(['author', 'lastUpdateAuthor']), + 'pagination' => $this->pagination, + 'sort' => $this->sort, + ]); + + $content = $this->render('comment-list', [ + 'CommentsDataProvider' => $CommentsDataProvider, + ]); + + return \yii\helpers\Html::tag('div', $content, $this->options); + } + + private function processDelete() + { + $delete = (int)\Yii::$app->getRequest()->get('delete-comment'); + if ($delete > 0) { + /** @var Comments\models\Comment $Comment */ + $Comment = Comments\models\Comment::find() + ->byId($delete) + ->one(); + + if ($Comment->isDeleted()) { + return; + } + + if (!($Comment instanceof Comments\models\Comment)) { + throw new \yii\web\NotFoundHttpException(\Yii::t('app', 'Comment not found.')); + } + + if (!$Comment->canDelete()) { + throw new \yii\web\ForbiddenHttpException(\Yii::t('app', 'Access Denied.')); + } + + $Comment->deleted = Comments\models\Comment::DELETED; + $Comment->update(); + } + } +} \ No newline at end of file diff --git a/widgets/_assets/comment-form.css b/widgets/_assets/comment-form.css new file mode 100644 index 0000000..23f4b09 --- /dev/null +++ b/widgets/_assets/comment-form.css @@ -0,0 +1 @@ +.comment-form { margin-top: 20px; } diff --git a/widgets/_assets/comment-list.css b/widgets/_assets/comment-list.css new file mode 100644 index 0000000..3096f1f --- /dev/null +++ b/widgets/_assets/comment-list.css @@ -0,0 +1,15 @@ +.comment-title { margin-top: 20px; } +.comments-list { } +.comments-list .comment { margin-top: 20px; padding-top: 20px; border-top: 1px solid #DDDDDD; } +.comments-list .comment.last { padding-bottom: 20px; border-bottom: 1px solid #DDDDDD; } +.comments-list .comment .date { font-size: 0.8em; margin-left: 20px; } +.comments-list .comment .author { } +.comments-list .comment .author img.avatar { width: 25px; height: 25px; margin-right: 10px; } +.comments-list .comment .author strong.name { } +.comments-list .comment .text { margin-left: 35px; } +.comments-list .comment .edit { margin-left: 35px; display: none; } +.comments-list .comment .edit .actions { margin: 0; } +.comments-list .comment .actions { margin: 10px 0 0 35px; } +.comments-list .comment .actions > a { margin-right: 10px; padding: 0 10px; } +.comments-list .comment .actions > a > i.fa { margin-right: 5px; } +.comments-list .comment.deleted .text { font-style: italic; } diff --git a/widgets/_assets/comment-list.js b/widgets/_assets/comment-list.js new file mode 100644 index 0000000..811abb8 --- /dev/null +++ b/widgets/_assets/comment-list.js @@ -0,0 +1,56 @@ +(function ($) { + $.fn.yiiCommentsList = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.yiiCommentsList'); + return false; + } + }; + + var commentsData = []; + + var methods = { + init: function (comments) { + commentsData = comments; + } + }; + + $('[data-role="reply"]').bind('click', function (e) { + var comment_id = $(this).closest('[data-comment]').data('comment'), + $textarea = $('textarea[data-role="new-comment"]'); + + $textarea + .focus() + .val(blockquote(commentsData[comment_id].text)); + + location.hash = null; + location.hash = 'commentcreateform'; + + e.preventDefault(); + }); + + $('[data-role="edit"]').bind('click', function (e) { + var $link = $(this), + $comment = $link.closest('[data-comment]'), + comment_id = $comment.data('comment'), + comment = commentsData[comment_id], + $edit_block = $comment.find('.edit'); + + $edit_block.show(); + + $edit_block.find('form').bind('reset', function () { + $edit_block.hide(); + }); + + e.preventDefault(); + }); + + function blockquote(text) { + return text.split('\n').map(function (value) { + return '> ' + value; + }).join('\n') + '\n\n'; + } +})(window.jQuery); diff --git a/widgets/views/comment-form.php b/widgets/views/comment-form.php new file mode 100644 index 0000000..963624f --- /dev/null +++ b/widgets/views/comment-form.php @@ -0,0 +1,49 @@ +context; + +?> + + +
+
+ Comment->isNewRecord) { + $options['data-role'] = 'new-comment'; + } + echo $form->field($CommentCreateForm, 'text') + ->textarea($options); + + ?> +
+ 'btn btn-primary', + ]); + echo Html::resetButton(\Yii::t('app', 'Cancel'), [ + 'class' => 'btn btn-link', + ]); + ?> +
+ +
+
\ No newline at end of file diff --git a/widgets/views/comment-list.php b/widgets/views/comment-list.php new file mode 100644 index 0000000..eba4da9 --- /dev/null +++ b/widgets/views/comment-list.php @@ -0,0 +1,178 @@ +context; + +$comments = []; + +echo Html::tag('h3', Yii::t('app', 'Comments'), ['class' => 'comment-title']); + +echo yii\widgets\ListView::widget([ + 'dataProvider' => $CommentsDataProvider, + 'options' => ['class' => 'comments-list'], + 'layout' => "{items}\n{pager}", + 'itemView' => + function (Comments\models\Comment $Comment, $key, $index, yii\widgets\ListView $Widget) + use (&$comments, $CommentListWidget) { + ob_start(); + + $Formatter = Yii::$app->getFormatter(); + + $Author = $Comment->author; + + $comments[$Comment->id] = $Comment->attributes; + + $options = [ + 'data-comment' => $Comment->id, + 'class' => 'row comment', + ]; + + if ($index === 0) { + Html::addCssClass($options, 'first'); + } + + if ($index === ($Widget->dataProvider->getCount() - 1)) { + Html::addCssClass($options, 'last'); + } + + if ($Comment->isDeleted()) { + Html::addCssClass($options, 'deleted'); + } + + ?> +
> +
+
+ getCommentatorAvatar(); + $name = $Author->getCommentatorName(); + $name = empty($name) ? Yii::t('app', 'Unknown author') : $name; + $url = $Author->getCommentatorUrl(); + } + + $name_html = Html::tag('strong', $name); + + if (false === $avatar) { + $avatar_html = Html::tag('div', FA::icon('male'), [ + 'class' => 'avatar fake', + 'title' => Yii::t('app', 'Unknown author'), + ]); + } else { + $avatar_html = Html::img($avatar, [ + 'class' => 'avatar', + 'alt' => Yii::t('app', 'Author avatar'), + 'title' => $name, + ]); + } + + if (false !== $url) { + echo Html::a($avatar_html, $url, ['target' => '_blank']); + echo Html::a($name_html, $url, ['target' => '_blank']); + } else { + echo $avatar_html; + echo $name_html; + } + + if ((time() - $Comment->created_at) > (86400 * 2)) { + echo Html::tag('span', $Formatter->asDatetime($Comment->created_at), ['class' => 'date']); + } else { + echo Html::tag('span', $Formatter->asRelativeTime($Comment->created_at), ['class' => 'date']); + } + ?> +
+
+ isDeleted()) { + echo Yii::t('app', 'Comment was deleted.'); + } else { + echo yii\helpers\Markdown::process($Comment->text, 'gfm-comment'); + + if ($Comment->isEdited()) { + echo Html::tag('small', Yii::t('app', 'Updated at {date-relative}', [ + 'date' => $Formatter->asDate($Comment->updated_at), + 'date-time' => $Formatter->asDatetime($Comment->updated_at), + 'date-relative' => $Formatter->asRelativeTime($Comment->updated_at), + ])); + } + } + ?> +
+ canUpdate() && !$Comment->isDeleted()) { + ?> +
+ $CommentListWidget->entity, + 'Comment' => $Comment, + ]); + ?> +
+ +
+ isDeleted()) { + echo Html::a(FA::icon('reply') . ' ' . Yii::t('app', 'Reply'), '#', [ + 'class' => 'btn btn-info btn-xs', + 'data-role' => 'reply', + ]); + + if ($Comment->canUpdate()) { + echo Html::a( + FA::icon('pencil') . ' ' . Yii::t('app', 'Edit'), + '#', + [ + 'data-role' => 'edit', + 'class' => 'btn btn-primary btn-xs', + ] + ); + } + + if ($Comment->canDelete()) { + echo Html::a( + FA::icon('times') . ' ' . Yii::t('app', 'Delete'), + ['', 'delete-comment' => $Comment->id], + ['class' => 'btn btn-danger btn-xs'] + ); + } + } + ?> +
+
+
+ showCreateForm && Comments\models\Comment::canCreate()) { + echo Html::tag('h3', Yii::t('app', 'Add comment'), ['class' => 'comment-title']); + + echo Comments\widgets\CommentFormWidget::widget([ + 'entity' => $CommentListWidget->entity, + 'Comment' => new Comments\models\Comment(), + ]); +} + +$CommentListWidget->view + ->registerJs('jQuery("#' . $CommentListWidget->options['id'] . '").yiiCommentsList(' . Json::encode($comments) . ');'); \ No newline at end of file