diff --git a/composer.json b/composer.json index 1690531..79bbe45 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,9 @@ "yiisoft/yii2-redis": "2.0.2", "creocoder/yii2-nested-sets": "dev-master", "rych/bencode": "1.0.*", - "himiklab/yii2-recaptcha-widget" : "*" + "himiklab/yii2-recaptcha-widget" : "*", + "bower-asset/tagsinput" : "*", + "bower-asset/typeahead.js": "0.10.*" }, "require-dev": { "yiisoft/yii2-codeception": "*", diff --git a/environments/dev/frontend/config/main-local.php b/environments/dev/frontend/config/main-local.php index 8703bff..66d7b2f 100644 --- a/environments/dev/frontend/config/main-local.php +++ b/environments/dev/frontend/config/main-local.php @@ -45,6 +45,11 @@ 'recordModel' => \common\models\torrent\Torrent::className(), 'salt' => '' ], + 'languageReport' => [ + 'class' => 'frontend\modules\languageReport\LanguageReportModule', + 'recordModel' => \common\models\torrent\Torrent::className(), + 'salt' => '' + ], ], ]; diff --git a/environments/prod/frontend/config/main-local.php b/environments/prod/frontend/config/main-local.php index 56a81f4..dbba8ab 100644 --- a/environments/prod/frontend/config/main-local.php +++ b/environments/prod/frontend/config/main-local.php @@ -79,5 +79,10 @@ 'recordModel' => \common\models\torrent\Torrent::className(), 'salt' => '' ], + 'languageReport' => [ + 'class' => 'frontend\modules\languageReport\LanguageReportModule', + 'recordModel' => \common\models\torrent\Torrent::className(), + 'salt' => '' + ], ], ]; \ No newline at end of file diff --git a/frontend/config/main.php b/frontend/config/main.php index 286f61c..502cf1f 100644 --- a/frontend/config/main.php +++ b/frontend/config/main.php @@ -50,6 +50,13 @@ 'complain' => 'complain.php', ), ], + 'languageReport*' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en', + 'fileMap' => array( + 'complain' => 'languageReport.php', + ), + ], ], ], 'request' => [ diff --git a/frontend/modules/languageReport/Asset.php b/frontend/modules/languageReport/Asset.php new file mode 100644 index 0000000..d4ac5c9 --- /dev/null +++ b/frontend/modules/languageReport/Asset.php @@ -0,0 +1,34 @@ +recordModel)) { + throw new Exception("Unknown class recordModel"); + } + if (empty($this->salt)) { + throw new Exception("Salt is incorrect"); + } + } + +} diff --git a/frontend/modules/languageReport/README.md b/frontend/modules/languageReport/README.md new file mode 100644 index 0000000..c822bd8 --- /dev/null +++ b/frontend/modules/languageReport/README.md @@ -0,0 +1,35 @@ +# Language report module + +Language report is a module designed for [The Open Pirate Bay](https://oldpiratebay.org/100k) +to give user an opportunity to vote for content language. + +## Dependencies +The module depends on the following components: + - yiisoft/yii2 + - yiisoft/yii2-bootstrap + - bower-asset/tagsinput + - bower-asset/typeahead.js + +## Installation + 1. Checkout branch with the module. + 2. Run **composer update**. + 3. Apply migrations with *yii migrate --migrationPath=@frontend/modules/languageReport/migrations*. + 4. You may copy module view file to *@themes/theme_name/modules/languageReport/* and rewrite it to design your own theme. + +## Openbay core injections +1. Adds module record to **main-local.php** +2. Adds i18n record to **frontend/main.php** +3. Adds widget to **@frontend/themes/newopb/modules/torrent/views/default/view.php** + +## Usage +Now the widget is displayed on torrent view page. Any registered user can select +language for torrent. When user starts typing language *typeahead.js* helps user to type. +After sending report to server the content languages are replaced with calculated ones. + +## Improvements +At the moment languages are stored in separate table to decrease number of injection to core code. +It can be either replaced by field in *torrents* table or joined with *tags* field in *torrents* table, +handling the **EVENT_LANGUAGEREPORT_ADD** event. + +When the list of added languages grows, it can be filtered using *confirmed* field in *language* table. +This field indicates how many users had selected the language. diff --git a/frontend/modules/languageReport/TagInputAsset.php b/frontend/modules/languageReport/TagInputAsset.php new file mode 100644 index 0000000..1a4a400 --- /dev/null +++ b/frontend/modules/languageReport/TagInputAsset.php @@ -0,0 +1,37 @@ + + * @since 2.0 + */ +class TypeAheadAsset extends AssetBundle { + + public $sourcePath = '@bower/typeahead.js/dist'; + public $js = [ + 'typeahead.bundle.js', + ]; + public $depends = [ + 'yii\bootstrap\BootstrapAsset', + 'yii\bootstrap\BootstrapPluginAsset', + ]; + +} diff --git a/frontend/modules/languageReport/assets/css/language.css b/frontend/modules/languageReport/assets/css/language.css new file mode 100644 index 0000000..126ff0b --- /dev/null +++ b/frontend/modules/languageReport/assets/css/language.css @@ -0,0 +1,84 @@ +.languageReportWidget { + width: 750px; +} +.languageReportWidget span.language { + +} +.languageReportWidget span.language { + font-style: italic; +} + +.languageReportWidget span.reporterArea { + display: inline-block; + position: relative; +} + +div.fader { + position: fixed; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + z-index: 100; +} + +div.reporterForm { + width: 350px; + display: none; + position: absolute; + padding: 15px; + border-radius: 10px; + background: white; + top: 30px; + z-index: 101; +} + +div.reporterForm .bubble-arrow { + position:absolute; left:25px; top:-10px; + display:block; + width: 0px; + height: 0px; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-bottom: 10px solid #fff; +} + +div.reporterForm input{ + width: 100%; +} +div.reporterForm .bootstrap-tagsinput{ + width: 100%; +} + +.tt-dropdown-menu { + width: 422px; + margin-top: 2px; + padding: 8px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} + +.tt-suggestion { + padding: 3px 20px; + font-size: 18px; + line-height: 24px; +} + +.tt-suggestion.tt-cursor { + color: #fff; + background-color: #0097cf; + +} + +.tt-hint { + opacity: 0.5 !important; +} + +.tt-suggestion p { + margin: 0; +} diff --git a/frontend/modules/languageReport/assets/js/language.js b/frontend/modules/languageReport/assets/js/language.js new file mode 100644 index 0000000..7d15e68 --- /dev/null +++ b/frontend/modules/languageReport/assets/js/language.js @@ -0,0 +1,80 @@ +$(document).ready(function() { + var langWidget = { + init: function() { + var that = this; + var langnames = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + prefetch: { + url: '/languageReport/default/languages', + filter: function(list) { + return $.map(list, function(cityname) { + return {name: cityname}; + }); + }, + ttl: 60 + } + }); + langnames.initialize(); + $('#language-input').tagsinput({ + typeaheadjs: { + name: 'langnames', + displayKey: 'name', + valueKey: 'name', + source: langnames.ttAdapter() + } + }); + + $(document).on('click', 'div.fader', function() { + that.close(); + }); + + $('a.reportLanguage').click(function(event) { + event.preventDefault(); + event.stopPropagation(); + that.open(); + return false; + }); + + $('#cancelReport').click(function() { + that.close(); + }); + + $('#sendReport').click(function() { + that.save(); + }); + }, + open: function() { + $('
') + .addClass('fader') + .hide() + .appendTo('body') + .fadeIn(); + $('.reporterForm').fadeIn(); + }, + save: function() { + var data = { + id: $('#sendReport').data('id'), + language: $('#language-input').val() + }; + $.get('/languageReport/default/report', data, function(r) { + $('#recordLanguage').html(r.languages); + alert(r.message); + $('.reporterForm').fadeOut(function() { + $('.reporterArea').remove(); + }); + }).fail(function() { + alert('Avast! Server is unreachable now like a far land, ya land lubber!'); + $('.reporterForm').fadeOut(); + }); + this.close(); + }, + close: function() { + $('div.fader').fadeOut(function() { + $('div.fader').remove(); + }); + $('.reporterForm').fadeOut(); + } + }; + langWidget.init(); +}); diff --git a/frontend/modules/languageReport/components/LanguageReportAddEvent.php b/frontend/modules/languageReport/components/LanguageReportAddEvent.php new file mode 100644 index 0000000..5f1e32f --- /dev/null +++ b/frontend/modules/languageReport/components/LanguageReportAddEvent.php @@ -0,0 +1,16 @@ +response->format = Response::FORMAT_JSON; + if (!\Yii::$app->request->isAjax) { + return false; + } + return parent::beforeAction($action); + } + + /** + * Gets the list of already reported languages + * @return String[] + */ + public function actionLanguages() { + return Language::find()->select('language')->createCommand()->queryColumn(); + } + + /** + * Report handling action. + * @param int $id + * @param string $language Comma separated list of languages + * @return array + */ + public function actionReport($id, $language) { + $recordId = intval($id); + $languages = explode(',', $language); + $languages = array_map('trim', $languages); + $languages = array_map('strtolower', $languages); + + $result = $this->_validate($recordId, $languages); + if ($result !== true) { + return $result; + } + return $this->_saveReport($recordId, $languages); + } + + /** + * @param $recordId + * @param $languages + * @return array + */ + private function _saveReport($recordId, $languages) { + $transaction = \Yii::$app->db->beginTransaction(); + $recordLanguages = ""; + $result = Language::saveLanguages($languages); + + if ($result) { + $recordLanguages = TorrentLanguage::defineLanguages($recordId, $languages); + + if ($recordLanguages === false) { + $result = false; + } + + if (!TorrentLanguage::saveLanguages($recordId, $recordLanguages)) { + $result = false; + } + + $this->_createLanguageReportAddEvent($recordId, $recordLanguages); + } + + if ($result) { + $lang = new LanguageReport(); + $lang->record_id = $recordId; + $lang->user_id = \Yii::$app->user->id; + $lang->language = join(',', $languages); + if (!$lang->save()) { + $result = false; + } + } + + if ($result) { + $transaction->commit(); + return $this->_sendSuccessResponse("We have got your vote!", ['languages' => join(', ', $recordLanguages)]); + } + + $transaction->rollback(); + return $this->_sendErrorResponse("Arrr! Can't get yer vote. Try again later, lad!"); + } + + /** + * @param $recordId + * @param $languages + * @return bool|string + */ + private function _validate($recordId, $languages) { + if (!\Yii::$app->user->getId()) { + return $this->_sendErrorResponse("C'mon, lad, ya've got to authorize"); + } + if (!count($languages)) { + return $this->_sendErrorResponse("Report can\'t be blank! It\'s like grog without rum!"); + } + if (!$this->_validateRecordId($recordId)) { + return $this->_sendErrorResponse("Hey-hey-hey, fella, take it easy… No need to abuse our confidence."); + } + if (!$recordId || !\Yii::$app->request->isAjax) { + return $this->_sendErrorResponse("Hey-hey-hey, fella, take it easy… No need to abuse our confidence."); + } + if ($this->_checkVote($recordId)) { + return $this->_sendErrorResponse("C'mon, lad, ya've already voted"); + } + return true; + } + + /** + * @param $recordId + * @return bool + */ + private function _validateRecordId($recordId) { + $recordModel = \Yii::$app->getModule("complain")->recordModel; + if (empty($recordId) || !$recordModel::findOne($recordId)) { + return false; + } + return true; + } + + /** + * @param $recordId + * @return bool + */ + private function _checkVote($recordId) { + return LanguageReport::checkUserVote(\Yii::$app->user->id, $recordId); + } + + private function _sendErrorResponse($message, $data = []) { + return $this->_sendResponse('error', $message, $data); + } + + private function _sendSuccessResponse($message, $data = []) { + return $this->_sendResponse('success', $message, $data); + } + + private function _sendResponse($type, $message, $data) { + return ArrayHelper::merge( + [ + $type => true, + 'message' => \Yii::t("complain", $message) + ], $data); + } + + /** + * @param $recordId int + * @param $languages string + */ + private function _createLanguageReportAddEvent($recordId, $languages) { + $event = new LanguageReportAddEvent(); + $event->recordId = $recordId; + $event->languages = $languages; + $event->userId = \Yii::$app->user->id; + + Event::trigger(LanguageReportModule::className(), LanguageReportModule::EVENT_LANGUAGEREPORT_ADD, $event); + } + +} diff --git a/frontend/modules/languageReport/migrations/m150219_133947_language_table.php b/frontend/modules/languageReport/migrations/m150219_133947_language_table.php new file mode 100644 index 0000000..2ea9151 --- /dev/null +++ b/frontend/modules/languageReport/migrations/m150219_133947_language_table.php @@ -0,0 +1,23 @@ +createTable('{{%language}}', [ + 'id' => Schema::TYPE_PK, + 'language' => Schema::TYPE_STRING . " NOT NULL", + 'confirmed' => Schema::TYPE_INTEGER . " NOT NULL DEFAULT 1", + ], $tableOptions); + $this->createIndex('language_unique_language', '{{%language}}', 'language', true); + } + + public function down() { + $this->dropTable('{{%language}}'); + } + +} diff --git a/frontend/modules/languageReport/migrations/m150219_134354_language_report.php b/frontend/modules/languageReport/migrations/m150219_134354_language_report.php new file mode 100644 index 0000000..094f113 --- /dev/null +++ b/frontend/modules/languageReport/migrations/m150219_134354_language_report.php @@ -0,0 +1,27 @@ +createTable('{{%language_report}}', [ + 'id' => Schema::TYPE_PK, + 'record_id' => Schema::TYPE_INTEGER . ' NOT NULL', + 'user_id' => Schema::TYPE_INTEGER . ' NULL DEFAULT NULL', + 'language' => Schema::TYPE_STRING . ' NOT NULL', + 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + + $this->createIndex('language_report_record_id', '{{%language_report}}', 'record_id'); + $this->createIndex('language_report_user_id', '{{%language_report}}', 'user_id'); + } + + public function safeDown() { + $this->dropTable('{{%language_report}}'); + } + +} diff --git a/frontend/modules/languageReport/migrations/m150219_223825_torrent_language.php b/frontend/modules/languageReport/migrations/m150219_223825_torrent_language.php new file mode 100644 index 0000000..b65198c --- /dev/null +++ b/frontend/modules/languageReport/migrations/m150219_223825_torrent_language.php @@ -0,0 +1,24 @@ +createTable('{{%torrent_language}}', [ + 'id' => Schema::TYPE_INTEGER . ' NOT NULL PRIMARY KEY', + 'languages' => Schema::TYPE_STRING . ' NOT NULL', + 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + + $this->createIndex('torrent_language_created_at', '{{%torrent_language}}', 'created_at'); + } + + public function safeDown() { + $this->dropTable('{{%torrent_language}}'); + } + +} diff --git a/frontend/modules/languageReport/models/Language.php b/frontend/modules/languageReport/models/Language.php new file mode 100644 index 0000000..d6e07e6 --- /dev/null +++ b/frontend/modules/languageReport/models/Language.php @@ -0,0 +1,69 @@ +db->quoteTableName(self::tableName()) . " (`language`, `confirmed`) " + . "VALUES " . join(',', $inserts) . " " + . "ON DUPLICATE KEY UPDATE `confirmed` = `confirmed` + 1"; + + try { + Yii::$app->db->createCommand($sql, $params)->execute(); + } catch (\Exception $e) { + + } + return true; + } + + /** + * @inheritdoc + */ + public static function tableName() { + return 'language'; + } + + /** + * @inheritdoc + */ + public function rules() { + return [ + [['language'], 'required'], + [['language'], 'string', 'max' => 255] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() { + return [ + 'id' => Yii::t('languageReport', 'ID'), + 'language' => Yii::t('languageReport', 'Language'), + ]; + } + +} diff --git a/frontend/modules/languageReport/models/LanguageReport.php b/frontend/modules/languageReport/models/LanguageReport.php new file mode 100644 index 0000000..83926a5 --- /dev/null +++ b/frontend/modules/languageReport/models/LanguageReport.php @@ -0,0 +1,69 @@ +where(['user_id' => $userId, 'record_id' => $recordId])->count(); + } + + /** + * @inheritdoc + */ + public function behaviors() { + return [ + 'timestamp' => [ + 'class' => TimestampBehavior::className(), + 'attributes' => [ + ActiveRecord::EVENT_BEFORE_INSERT => ['created_at'], + ] + ], + ]; + } + + /** + * @inheritdoc + */ + public static function tableName() { + return 'language_report'; + } + + /** + * @inheritdoc + */ + public function rules() { + return [ + [['record_id', 'language'], 'required'], + [['record_id', 'user_id', 'created_at'], 'integer'], + [['language'], 'string'], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() { + return [ + 'id' => Yii::t('languageReport', 'ID'), + 'record_id' => Yii::t('languageReport', 'Record ID'), + 'user_id' => Yii::t('languageReport', 'User ID'), + 'language' => Yii::t('languageReport', 'Language'), + 'created_at' => Yii::t('languageReport', 'Created At'), + ]; + } + +} diff --git a/frontend/modules/languageReport/models/TorrentLanguage.php b/frontend/modules/languageReport/models/TorrentLanguage.php new file mode 100644 index 0000000..8ea90b5 --- /dev/null +++ b/frontend/modules/languageReport/models/TorrentLanguage.php @@ -0,0 +1,118 @@ + [ + 'class' => TimestampBehavior::className(), + 'attributes' => [ + ActiveRecord::EVENT_BEFORE_INSERT => ['created_at'], + ] + ], + ]; + } + + /** + * @inheritdoc + */ + public static function tableName() { + return 'torrent_language'; + } + + public static function defineLanguages($recordId, $newLanguages) { + $reports = LanguageReport::find()->where(['record_id' => $recordId])->all(); + $totalReports = count($reports); + + $languages = []; + + foreach ($reports as $report) { + $reportedLanguage = split(',', $report->language); + foreach ($reportedLanguage as $language) { + if (isset($languages[$language])) { + $languages[$language] ++; + } else { + $languages[$language] = 1; + } + } + } + + foreach ($newLanguages as $language) { + if (isset($languages[$language])) { + $languages[$language] ++; + } else { + $languages[$language] = 1; + } + } + + $maxReports = max($languages); + $result = []; + + foreach ($languages as $language => $reportCount) { + if (self::THRESHOLD * $maxReports / 100 <= $reportCount) { + $result[] = mb_convert_case($language, MB_CASE_TITLE); + } + } + + return $result; + } + + public static function saveLanguages($recordId, $languages) { + $languages = join(', ', $languages); + + $item = self::findOne([$recordId]); + + if (!$item) { + $item = new TorrentLanguage(); + $item->id = $recordId; + } + $item->languages = $languages; + return $item->save(); + } + + /** + * @inheritdoc + */ + public function rules() { + return [ + [['languages'], 'required'], + [['created_at', 'updated_at'], 'integer'], + [['languages'], 'string', 'max' => 255] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() { + return [ + 'id' => Yii::t('languageReport', 'ID'), + 'languages' => Yii::t('languageReport', 'Languages'), + 'created_at' => Yii::t('languageReport', 'Created At'), + 'updated_at' => Yii::t('languageReport', 'Updated At'), + ]; + } + +} diff --git a/frontend/modules/languageReport/widgets/LanguageReportWidget.php b/frontend/modules/languageReport/widgets/LanguageReportWidget.php new file mode 100644 index 0000000..416ba8e --- /dev/null +++ b/frontend/modules/languageReport/widgets/LanguageReportWidget.php @@ -0,0 +1,59 @@ +_validateRecordId()) { + $this->_initError = true; + } + $this->_registerClientScript(); + } + + /** + * @return string + */ + public function run() { + if (!$this->_initError) { + return $this->render('index', [ + 'recordId' => $this->recordId, + ]); + } + return \Yii::t("languageReport", "Unknown recordId"); + } + + /** + * @return bool + */ + private function _validateRecordId() { + $recordModel = \Yii::$app->getModule("languageReport")->recordModel; + if (empty($this->recordId) || !$recordModel::findOne($this->recordId)) { + return false; + } + return true; + } + + /** + * Register widget client scripts. + */ + private function _registerClientScript() { + $view = $this->getView(); + Asset::register($view); + TagInputAsset::register($view); + TypeAheadAsset::register($view); + } + +} diff --git a/frontend/modules/languageReport/widgets/views/index.php b/frontend/modules/languageReport/widgets/views/index.php new file mode 100644 index 0000000..44f178c --- /dev/null +++ b/frontend/modules/languageReport/widgets/views/index.php @@ -0,0 +1,34 @@ +languages; +} + +$showLanguageEditForm = (bool) Yii::$app->user->getId() && !LanguageReport::checkUserVote(Yii::$app->user->getId(), $recordId); +?> +
+ Language: + + + + + + '#', 'class' => 'dashed red reportLanguage']) ?> + + +
+ + + + +
+
+ +
+
\ No newline at end of file diff --git a/frontend/themes/newopb/modules/torrent/views/default/view.php b/frontend/themes/newopb/modules/torrent/views/default/view.php index eaa4057..0e69e86 100644 --- a/frontend/themes/newopb/modules/torrent/views/default/view.php +++ b/frontend/themes/newopb/modules/torrent/views/default/view.php @@ -6,7 +6,7 @@ use frontend\modules\comment\widgets\CommentWidget; use frontend\modules\rating\widgets\RatingWidget; use frontend\modules\complain\widgets\ComplainWidget; - +use frontend\modules\languageReport\widgets\LanguageReportWidget; /* @var $this yii\web\View */ /* @var $model frontend\modules\torrent\models\Torrent */ @@ -32,6 +32,9 @@

Files: getFilesDataProvider()->getCount(); ?>

Size: formatter->asShortSize($model->size); ?>

+
+ $model->id]) ?> +

Seeders: seeders, 0, '.', ' '); ?>

Leechers: leechers, 0, '.', ' '); ?>