diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..1669168 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory" : "vendor/bower" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7146c02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# yii console command +/yii + +# phpstorm project files +.idea + +# netbeans project files +nbproject + +# zend studio for eclipse project files +.buildpath +.project +.settings + +# windows thumbnail cache +Thumbs.db + +# composer vendor dir +/vendor + +# composer itself is not needed +composer.phar + +# Mac DS_Store Files +.DS_Store + +# phpunit itself is not needed +phpunit.phar +# local phpunit config +/phpunit.xml + +/storage +common/config/main-local.php +common/config/params-local.php +backend/config/main-local.php +backend/config/params-local.php +backend/assets/* +frontend/config/main-local.php +frontend/config/params-local.php +frontend/assets/* \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..17d3b8c --- /dev/null +++ b/.htaccess @@ -0,0 +1,75 @@ +AddDefaultCharset utf-8 + + + + Options +FollowSymlinks + + RewriteEngine On + + + + + + RewriteBase / + # deal with admin first + + + + RewriteRule ^storage/(.*)?$ /storage/$1 [L,PT] + + RewriteCond %{REQUEST_URI} ^/(admin) + + + + RewriteRule ^admin/assets/(.*)$ backend/web/assets/$1 [L] + + RewriteRule ^admin/css/(.*)$ backend/web/css/$1 [L] + + RewriteRule ^admin/js/(.*)$ backend/web/js/$1 [L] + + RewriteRule ^admin/images/(.*)$ backend/web/images/$1 [L] + + RewriteRule ^admin/fonts/(.*)$ backend/web/fonts/$1 [L] + + + + + + RewriteCond %{REQUEST_URI} !^/backend/web/(assets|css|js|images|fonts)/ + + RewriteCond %{REQUEST_URI} ^/(admin) + + RewriteRule ^.*$ backend/web/index.php [L] + + + + RewriteCond %{REQUEST_URI} ^/(assets|css) + + RewriteRule ^assets/(.*)$ frontend/web/assets/$1 [L] + + RewriteRule ^css/(.*)$ frontend/web/css/$1 [L] + + RewriteRule ^js/(.*)$ frontend/web/js/$1 [L] + + RewriteRule ^images/(.*)$ frontend/web/images/$1 [L] + + RewriteRule ^fonts/(.*)$ frontend/web/fonts/$1 [L] + + + RewriteCond %{REQUEST_URI} !^/(frontend|backend)/web/(assets|css|js|images|fonts)/ + + RewriteCond %{REQUEST_URI} !index.php + + RewriteCond %{REQUEST_FILENAME} !-f [OR] + + RewriteCond %{REQUEST_FILENAME} !-d + + RewriteRule ^.*$ frontend/web/index.php [L] + + + +#для возможности загрузки файлов парсера + + php_value upload_max_filesize 20M + php_value post_max_size 30M + \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e98f03d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..45e56ad --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +Yii 2 Advanced Project Template +=============================== + +Yii 2 Advanced Project Template is a skeleton [Yii 2](http://www.yiiframework.com/) application best for +developing complex Web applications with multiple tiers. + +The template includes three tiers: front end, back end, and console, each of which +is a separate Yii application. + +The template is designed to work in a team development environment. It supports +deploying the application in different environments. + +Documentation is at [docs/guide/README.md](docs/guide/README.md). + +[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-app-advanced/v/stable.png)](https://packagist.org/packages/yiisoft/yii2-app-advanced) +[![Total Downloads](https://poser.pugx.org/yiisoft/yii2-app-advanced/downloads.png)](https://packagist.org/packages/yiisoft/yii2-app-advanced) +[![Build Status](https://travis-ci.org/yiisoft/yii2-app-advanced.svg?branch=master)](https://travis-ci.org/yiisoft/yii2-app-advanced) + +DIRECTORY STRUCTURE +------------------- + +``` +common + config/ contains shared configurations + mail/ contains view files for e-mails + models/ contains model classes used in both backend and frontend +console + config/ contains console configurations + controllers/ contains console controllers (commands) + migrations/ contains database migrations + models/ contains console-specific model classes + runtime/ contains files generated during runtime +backend + assets/ contains application assets such as JavaScript and CSS + config/ contains backend configurations + controllers/ contains Web controller classes + models/ contains backend-specific model classes + runtime/ contains files generated during runtime + views/ contains view files for the Web application + web/ contains the entry script and Web resources +frontend + assets/ contains application assets such as JavaScript and CSS + config/ contains frontend configurations + controllers/ contains Web controller classes + models/ contains frontend-specific model classes + runtime/ contains files generated during runtime + views/ contains view files for the Web application + web/ contains the entry script and Web resources + widgets/ contains frontend widgets +vendor/ contains dependent 3rd-party packages +environments/ contains environment-based overrides +tests contains various tests for the advanced application + codeception/ contains tests developed with Codeception PHP Testing Framework +``` diff --git a/backend/config/.gitignore b/backend/config/.gitignore new file mode 100644 index 0000000..20da318 --- /dev/null +++ b/backend/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php \ No newline at end of file diff --git a/backend/config/bootstrap.php b/backend/config/bootstrap.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/backend/config/bootstrap.php @@ -0,0 +1 @@ + 'app-backend', + 'basePath' => dirname(__DIR__), + 'layout' => 'admin', + 'controllerNamespace' => 'backend\controllers', + 'bootstrap' => ['log'], + 'modules' => [ + 'rubrication' => [ + 'class' => 'common\modules\rubrication\Module', + 'types' => [ + 'string' => 'Strings', + 'float' => 'Floating number', + 'int' => 'Integer number', + 'link' => 'Web-link', + ] + ], + 'product' => [ + 'class' => 'common\modules\product\Module' + ], + ], + 'components' => [ + 'user' => [ + 'identityClass' => 'common\models\User', + 'enableAutoLogin' => true, + ], + 'log' => [ + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + 'errorHandler' => [ + 'errorAction' => 'site/error', + ], + 'request'=>[ + 'cookieValidationKey' => 'j4iuot9u5894e7tu8reyh78g9y54sy7i', + 'csrfParam' => '_backendCSRF', + + 'class' => 'common\components\Request', + + 'web'=> '/backend/web', + + 'adminUrl' => '/admin' + + ], + + ], + 'params' => $params, +]; diff --git a/backend/config/params.php b/backend/config/params.php new file mode 100644 index 0000000..7f754b9 --- /dev/null +++ b/backend/config/params.php @@ -0,0 +1,4 @@ + 'admin@example.com', +]; diff --git a/backend/controllers/BlogController.php b/backend/controllers/BlogController.php new file mode 100644 index 0000000..1963d1b --- /dev/null +++ b/backend/controllers/BlogController.php @@ -0,0 +1,131 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ]; + } + + /** + * Lists all Blog models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new BlogSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a single Blog model. + * @param integer $id + * @return mixed + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new Blog model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new Blog(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + + Fields::saveFieldData(Yii::$app->request->post('Fields'), $model->blog_id, Blog::className(), 'ru'); + + return $this->redirect(['view', 'id' => $model->blog_id]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } + } + + /** + * Updates an existing Blog model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + Fields::saveFieldData(Yii::$app->request->post('Fields'), $model->blog_id, Blog::className(), 'ru'); + return $this->redirect(['view', 'id' => $model->blog_id]); + } else { + return $this->render('update', [ + 'model' => $model, + ]); + } + } + + /** + * Deletes an existing Blog model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the Blog model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return Blog the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Blog::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } +} diff --git a/backend/controllers/SiteController.php b/backend/controllers/SiteController.php new file mode 100644 index 0000000..90d2445 --- /dev/null +++ b/backend/controllers/SiteController.php @@ -0,0 +1,95 @@ + [ + 'class' => AccessControl::className(), + 'rules' => [ + [ + 'actions' => ['login', 'error'], + 'allow' => true, + ], + [ + 'actions' => ['logout', 'index'], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'error' => [ + 'class' => 'yii\web\ErrorAction', + ], + ]; + } + + public function actionIndex() + { + $blog = new Blog(); + $post = \Yii::$app->request->post(); + if($blog->load($post)) { + $blog->save(); + } + return $this->render('index',[ + 'blog' => $blog + ]); + } + + public function actionLogin() + { + $this->layout = '/none'; + + if (!\Yii::$app->user->isGuest) { + return $this->goHome(); + } + + $model = new LoginForm(); + if ($model->load(Yii::$app->request->post()) && $model->login()) { + return $this->goBack(); + } else { + return $this->render('login', [ + 'model' => $model, + ]); + } + } + + public function actionLogout() + { + Yii::$app->user->logout(); + + return $this->goHome(); + } +} diff --git a/backend/models/.gitkeep b/backend/models/.gitkeep new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/backend/models/.gitkeep @@ -0,0 +1 @@ +* diff --git a/backend/runtime/.gitignore b/backend/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/backend/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/backend/views/blog/_form.php b/backend/views/blog/_form.php new file mode 100644 index 0000000..7f35383 --- /dev/null +++ b/backend/views/blog/_form.php @@ -0,0 +1,61 @@ + + +
+ + + + field($model, 'name')->textInput(['maxlength' => true]) ?> + + field($model, 'link')->textInput(['maxlength' => true]) ?> + + field($model, 'description')->widget(CKEditor::className(), + [ + 'editorOptions' => ElFinder::ckeditorOptions('elfinder',[ + 'preset' => 'full', //разработанны стандартные настройки basic, standard, full данную возможность не обязательно использовать + 'inline' => false, //по умолчанию false]), + 'filebrowserUploadUrl'=>Yii::$app->getUrlManager()->createUrl('file/uploader/images-upload') + ] + ) + ]) ?> + + $model, + 'field'=>'cover', + 'size' => [ + [ + 'width'=>340, + 'height'=>260, + ] + ], + 'multi'=>true, + 'gallery' =>$model->cover, + 'name' => 'Загрузить миниатюру статьи' + ]); + ?> + + 'education', + 'item_id' => $model->blog_id, + 'model' => 'common\models\Blog', + 'language' => 'ru', + ]); ?> +
+ isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
+ + + +
diff --git a/backend/views/blog/_search.php b/backend/views/blog/_search.php new file mode 100644 index 0000000..364656f --- /dev/null +++ b/backend/views/blog/_search.php @@ -0,0 +1,43 @@ + + + diff --git a/backend/views/blog/create.php b/backend/views/blog/create.php new file mode 100644 index 0000000..a321524 --- /dev/null +++ b/backend/views/blog/create.php @@ -0,0 +1,21 @@ +title = 'Create Blog'; +$this->params['breadcrumbs'][] = ['label' => 'Blogs', 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/backend/views/blog/index.php b/backend/views/blog/index.php new file mode 100644 index 0000000..be76474 --- /dev/null +++ b/backend/views/blog/index.php @@ -0,0 +1,40 @@ +title = 'Blogs'; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ render('_search', ['model' => $searchModel]); ?> + +

+ 'btn btn-success']) ?> +

+ $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + + 'blog_id', + 'user_id', + 'name', + 'link', + 'date_add', + // 'user_add_id', + // 'view_count', + // 'description:ntext', + // 'cover', + + ['class' => 'yii\grid\ActionColumn'], + ], + ]); ?> +
diff --git a/backend/views/blog/update.php b/backend/views/blog/update.php new file mode 100644 index 0000000..606398e --- /dev/null +++ b/backend/views/blog/update.php @@ -0,0 +1,21 @@ +title = 'Update Blog: ' . ' ' . $model->name; +$this->params['breadcrumbs'][] = ['label' => 'Blogs', 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->blog_id]]; +$this->params['breadcrumbs'][] = 'Update'; +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/backend/views/blog/view.php b/backend/views/blog/view.php new file mode 100644 index 0000000..f7ac37d --- /dev/null +++ b/backend/views/blog/view.php @@ -0,0 +1,43 @@ +title = $model->name; +$this->params['breadcrumbs'][] = ['label' => 'Blogs', 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ +

+ $model->blog_id], ['class' => 'btn btn-primary']) ?> + $model->blog_id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => 'Are you sure you want to delete this item?', + 'method' => 'post', + ], + ]) ?> +

+ + $model, + 'attributes' => [ + 'blog_id', + 'user_id', + 'name', + 'link', + 'date_add', + 'user_add_id', + 'view_count', + 'description:ntext', + 'cover', + ], + ]) ?> + +
diff --git a/backend/views/layouts/admin.php b/backend/views/layouts/admin.php new file mode 100644 index 0000000..23b018a --- /dev/null +++ b/backend/views/layouts/admin.php @@ -0,0 +1,35 @@ +beginContent('@app/views/layouts/main.php'); +?> + + render('header') ?> + + render('main-sidebar') ?> + + +
+
+ isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], + ]) ?> + + + +
+
+ render('footer') ?> + + + + + render('control-sidebar') ?> + + +
+ +endContent() ?> \ No newline at end of file diff --git a/backend/views/layouts/control-sidebar.php b/backend/views/layouts/control-sidebar.php new file mode 100644 index 0000000..7e34b3f --- /dev/null +++ b/backend/views/layouts/control-sidebar.php @@ -0,0 +1,188 @@ + \ No newline at end of file diff --git a/backend/views/layouts/footer.php b/backend/views/layouts/footer.php new file mode 100644 index 0000000..ad38e44 --- /dev/null +++ b/backend/views/layouts/footer.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/backend/views/layouts/header.php b/backend/views/layouts/header.php new file mode 100644 index 0000000..8a97ed1 --- /dev/null +++ b/backend/views/layouts/header.php @@ -0,0 +1,260 @@ +
+ + + + +
\ No newline at end of file diff --git a/backend/views/layouts/main-sidebar.php b/backend/views/layouts/main-sidebar.php new file mode 100644 index 0000000..12efa63 --- /dev/null +++ b/backend/views/layouts/main-sidebar.php @@ -0,0 +1,54 @@ + + \ No newline at end of file diff --git a/backend/views/layouts/main-sidebar1.php b/backend/views/layouts/main-sidebar1.php new file mode 100644 index 0000000..675e21c --- /dev/null +++ b/backend/views/layouts/main-sidebar1.php @@ -0,0 +1,118 @@ + + \ No newline at end of file diff --git a/backend/views/layouts/main.php b/backend/views/layouts/main.php new file mode 100644 index 0000000..cb4d7d8 --- /dev/null +++ b/backend/views/layouts/main.php @@ -0,0 +1,34 @@ + +beginPage() ?> + + + + + + + <?= Html::encode($this->title) ?> + head() ?> + + +beginBody() ?> +
+ + + +
+ +endBody() ?> + + +endPage() ?> diff --git a/backend/views/layouts/none.php b/backend/views/layouts/none.php new file mode 100644 index 0000000..aae13c7 --- /dev/null +++ b/backend/views/layouts/none.php @@ -0,0 +1,9 @@ +beginContent('@app/views/layouts/main.php'); +?> + + + +endContent() ?> \ No newline at end of file diff --git a/backend/views/site/error.php b/backend/views/site/error.php new file mode 100644 index 0000000..0ba2574 --- /dev/null +++ b/backend/views/site/error.php @@ -0,0 +1,27 @@ +title = $name; +?> +
+ +

title) ?>

+ +
+ +
+ +

+ The above error occurred while the Web server was processing your request. +

+

+ Please contact us if you think this is a server error. Thank you. +

+ +
diff --git a/backend/views/site/index.php b/backend/views/site/index.php new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/backend/views/site/index.php diff --git a/backend/views/site/login.php b/backend/views/site/login.php new file mode 100644 index 0000000..20f3f78 --- /dev/null +++ b/backend/views/site/login.php @@ -0,0 +1,35 @@ +title = 'Login'; +$this->params['breadcrumbs'][] = $this->title; +?> +
+

title) ?>

+ +

Please fill out the following fields to login:

+ +
+
+ 'login-form']); ?> + + field($model, 'username')->textInput(['autofocus' => true]) ?> + + field($model, 'password')->passwordInput() ?> + + field($model, 'rememberMe')->checkbox() ?> + +
+ 'btn btn-primary', 'name' => 'login-button']) ?> +
+ + +
+
+
diff --git a/backend/web/.gitignore b/backend/web/.gitignore new file mode 100644 index 0000000..25c74e6 --- /dev/null +++ b/backend/web/.gitignore @@ -0,0 +1,2 @@ +/index.php +/index-test.php diff --git a/backend/web/assets/.gitignore b/backend/web/assets/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/web/css/site.css b/backend/web/css/site.css new file mode 100644 index 0000000..324663d --- /dev/null +++ b/backend/web/css/site.css @@ -0,0 +1,105 @@ +html, +body { + height: 100%; +} + +.wrap { + min-height: 100%; + height: auto; + margin: 0 auto -60px; + padding: 0 0 60px; +} + +.wrap > .container { + padding: 70px 15px 20px; +} + +.footer { + height: 60px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + padding-top: 20px; +} + +.jumbotron { + text-align: center; + background-color: transparent; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + +.not-set { + color: #c55; + font-style: italic; +} + +/* add sorting icons to gridview sort links */ +a.asc:after, a.desc:after { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + padding-left: 5px; +} + +a.asc:after { + content: /*"\e113"*/ "\e151"; +} + +a.desc:after { + content: /*"\e114"*/ "\e152"; +} + +.sort-numerical a.asc:after { + content: "\e153"; +} + +.sort-numerical a.desc:after { + content: "\e154"; +} + +.sort-ordinal a.asc:after { + content: "\e155"; +} + +.sort-ordinal a.desc:after { + content: "\e156"; +} + +.grid-view td { + white-space: nowrap; +} + +.grid-view .filters input, +.grid-view .filters select { + min-width: 50px; +} + +.hint-block { + display: block; + margin-top: 5px; + color: #999; +} + +.error-summary { + color: #a94442; + background: #fdf7f7; + border-left: 3px solid #eed3d7; + padding: 10px 20px; + margin: 0 0 15px 0; +} + +/* align the logout "link" (button in form) of the navbar */ +.nav > li > form { + padding: 8px; +} + +.nav > li > form > button:hover { + text-decoration: none; +} diff --git a/backend/web/favicon.ico b/backend/web/favicon.ico new file mode 100644 index 0000000..580ed73 Binary files /dev/null and b/backend/web/favicon.ico differ diff --git a/backend/web/js/fieldWidget.js b/backend/web/js/fieldWidget.js new file mode 100644 index 0000000..3d860ef --- /dev/null +++ b/backend/web/js/fieldWidget.js @@ -0,0 +1,24 @@ +$(function(){ + $.each($('.delete-field-item'), function(index, value) { + var container = $(value).parents('.field_list').first(); + var count = $(container).find('.form-group').length; + if(count <= 1) { + $(container).find('.delete-field-item').addClass('hidden'); + } + }); + $(document).on('click', '.delete-field-item', function(){ + var container = $(this).parents('.field_list').first(); + $(this).parent('.form-group').remove(); + var count = $(container).find('.form-group').length; + if(count <= 1) { + $(container).find('.delete-field-item').addClass('hidden'); + } + }); + $(document).on('click', '[class*=add_field_w]', function() { + var container = $(this).siblings('.field_list').first(); + var count = $(container).find('.form-group').length; + if(count > 1) { + $(container).find('.delete-field-item').removeClass('hidden'); + } + }); +}); \ No newline at end of file diff --git a/backend/web/robots.txt b/backend/web/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/backend/web/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/common/behaviors/ShowImage.php b/common/behaviors/ShowImage.php new file mode 100644 index 0000000..4960eea --- /dev/null +++ b/common/behaviors/ShowImage.php @@ -0,0 +1,43 @@ + 'getSlug' + ]; + } + + public function getSlug( $event ) + { + if ( empty( $this->owner->{$this->out_attribute} ) ) { + $this->owner->{$this->out_attribute} = $this->generateSlug( $this->owner->{$this->in_attribute} ); + } else { + $this->owner->{$this->out_attribute} = $this->generateSlug( $this->owner->{$this->out_attribute} ); + } + } + + private function generateSlug( $slug ) + { + $slug = $this->slugify( $slug ); + if ( $this->checkUniqueSlug( $slug ) ) { + return $slug; + } else { + for ( $suffix = 2; !$this->checkUniqueSlug( $new_slug = $slug . '-' . $suffix ); $suffix++ ) {} + return $new_slug; + } + } + + private function slugify( $slug ) + { + if ( $this->translit ) { + return yii\helpers\Inflector::slug( TransliteratorHelper::process( $slug ), '-', true ); + } else { + return $this->slug( $slug, '-', true ); + } + } + + private function slug( $string, $replacement = '-', $lowercase = true ) + { + $string = preg_replace( '/[^\p{L}\p{Nd}]+/u', $replacement, $string ); + $string = trim( $string, $replacement ); + return $lowercase ? strtolower( $string ) : $string; + } + + private function checkUniqueSlug( $slug ) + { + $pk = $this->owner->primaryKey(); + $pk = $pk[0]; + + $condition = $this->out_attribute . ' = :out_attribute'; + $params = [ ':out_attribute' => $slug ]; + if ( !$this->owner->isNewRecord ) { + $condition .= ' and ' . $pk . ' != :pk'; + $params[':pk'] = $this->owner->{$pk}; + } + + return !$this->owner->find() + ->where( $condition, $params ) + ->one(); + } + +} \ No newline at end of file diff --git a/common/components/LangRequest.php b/common/components/LangRequest.php new file mode 100644 index 0000000..9b5ab3f --- /dev/null +++ b/common/components/LangRequest.php @@ -0,0 +1,99 @@ +_lang_url === null) + { + $this->_lang_url = $this->getUrl(); + + $url_list = explode ('/', $this->_lang_url); + + $lang_url = isset ($url_list[1]) ? $url_list[1] : null; + + Language::setCurrent($lang_url); + + if ($lang_url !== null && $lang_url === Language::getCurrent()->language_code + && strpos($this->_lang_url, Language::getCurrent()->language_code) === 1) + { + $this->_lang_url = substr ($this->_lang_url, strlen (Language::getCurrent()->language_code) + 1); + } + } + + return $this->_lang_url; + } + + protected function resolvePathInfo() + { + $pathInfo = $this->getLangUrl(); + + if (($pos = strpos ($pathInfo, '?')) !== false) + { + $pathInfo = substr ($pathInfo, 0, $pos); + } + + $pathInfo = urldecode ($pathInfo); + + // try to encode in UTF8 if not so + // http://w3.org/International/questions/qa-forms-utf-8.html + if (! preg_match ('%^(?: + [\x09\x0A\x0D\x20-\x7E] # ASCII + | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte + | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte + | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates + | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 + )*$%xs', $pathInfo) + ) + { + $pathInfo = utf8_encode($pathInfo); + } + + $scriptUrl = $this->getScriptUrl(); + + $baseUrl = $this->getBaseUrl(); + + if (strpos($pathInfo, $scriptUrl) === 0) + { + $pathInfo = substr($pathInfo, strlen($scriptUrl)); + } + else if ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) + { + $pathInfo = substr($pathInfo, strlen($baseUrl)); + } + elseif (isset ($_SERVER['PHP_SELF']) && strpos ($_SERVER['PHP_SELF'], $scriptUrl) === 0) + { + $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); + } + else + { + throw new InvalidConfigException('Unable to determine the path info of the current request.'); + } + + if ($pathInfo[0] === '/') + { + $pathInfo = substr ($pathInfo, 1); + } + + return (string) $pathInfo; + } +} \ No newline at end of file diff --git a/common/components/LangUrlManager.php b/common/components/LangUrlManager.php new file mode 100644 index 0000000..2170b02 --- /dev/null +++ b/common/components/LangUrlManager.php @@ -0,0 +1,37 @@ +is_default == 1 ? $url : '/'.$lang_code->lang_code.$url); + } +} \ No newline at end of file diff --git a/common/components/Request.php b/common/components/Request.php new file mode 100644 index 0000000..f3a4f37 --- /dev/null +++ b/common/components/Request.php @@ -0,0 +1,40 @@ +web, "", parent::getBaseUrl()) . $this->adminUrl; + + } + + + + public function resolvePathInfo() + + { + + if ($this->getUrl() === $this->adminUrl) { + + return ""; + + } else { + + return parent::resolvePathInfo(); + + } + + } + +} \ No newline at end of file diff --git a/common/components/artboxtree/ArtboxTreeBehavior.php b/common/components/artboxtree/ArtboxTreeBehavior.php new file mode 100644 index 0000000..a5dc4b8 --- /dev/null +++ b/common/components/artboxtree/ArtboxTreeBehavior.php @@ -0,0 +1,370 @@ + $keyNameDepth; + + /** + * @var string + */ + public $delimiter = '|'; + + /** + * @var ActiveRecord|self|null + */ + protected $entity; + + /** + * @param ActiveRecord $owner + * @throws Exception + */ + public function attach($owner) + { + parent::attach($owner); + if ($this->keyNameId === null) { + $primaryKey = $owner->primaryKey(); + if (!isset($primaryKey[0])) { + throw new Exception('"' . $owner->className() . '" must have a primary key.'); + } + $this->keyNameId = $primaryKey[0]; + } + } + + public function events() + { + return [ + // @todo Use beforeSave for automatic set MP-params + ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate', + ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert', + ]; + } + + /* + * Main methods + */ + + /* + * get one parent + * use AL-method + */ + public function getParent() { + return $this->getParentAL(); + } + + /* + * get all parents + * use MP-method + */ + public function getParents() { + return $this->getParentsMP(); + } + + /* + * get one-level children items + * use AL-method + */ + public function getChildren() { + return $this->getChildrenAL(); + } + + /* + * get all-level children items + * use MP-method + */ + public function getAllChildren($depth = null) { + return $this->getAllChildrenMP($depth); + } + + + /* + * ================================ + * MP-methods + * ================================ + */ + + /* + * Full-path (use MP-method) + */ + public function getParentsMP($depth = null) { + $path = $this->getParentPath(); + if ($path !== null) { + $paths = str_replace(['{', '}'], '', explode(',', $path)); + if (!$this->primaryKeyMode) { + $path = null; + $paths = array_map( + function ($value) use (&$path) { + return $path = ($path !== null ? $path . ',' : '') . $value; + }, + $paths + ); + } + if ($depth !== null) { + $paths = array_slice($paths, -$depth); + } + } else { + $paths = []; + } + + $tableName = $this->owner->tableName(); + $condition = ['and']; + if ($this->primaryKeyMode) { + $condition[] = ["{$tableName}.[[{$this->keyNameId}]]" => $paths]; + } else { + $condition[] = ["{$tableName}.[[{$this->keyNamePath}]]" => $paths]; + } + + $query = $this->owner->find() + ->andWhere($condition) + ->andWhere($this->treeCondition()) + ->addOrderBy(["{$tableName}.[[{$this->keyNamePath}]]" => SORT_ASC]); + $query->multiple = true; + + return $query; + } + + /** + * @param bool $asArray = false + * @return null|string|array + */ + public function getParentPath($asArray = false) + { + return static::getParentPathInternal($this->owner->getAttribute($this->keyNamePath), $asArray); + } + + public function getAllChildrenMP($depth = null) + { + $tableName = $this->owner->tableName(); + $path = $this->owner->getAttribute($this->keyNamePath); + $query = $this->owner->find() + ->andWhere(['@>', "{$tableName}.[[{$this->keyNamePath}]]", $this->getLike($path), false]); + + + if ($depth > 0) { + $query->andWhere(['<=', "{$tableName}.[[{$this->keyNameDepth}]]", $this->owner->getAttribute($this->keyNameDepth) + $depth]); + } + + $orderBy = []; + $orderBy["{$tableName}.[[{$this->keyNameDepth}]]"] = SORT_ASC; + $orderBy["{$tableName}.[[{$this->keyNameId}]]"] = SORT_ASC; + + $query + ->andWhere($this->treeCondition()) + ->addOrderBy($orderBy); + $query->multiple = true; + + return $query; + } + + /* + * ================================ + * AL methods + * ================================ + */ + + /* + * Parent entity (use AL-method) + * @return \yii\db\ActiveRecord + */ + public function getParentAL() { + $parent_id = $this->owner->getAttribute($this->keyNameParentId); + if (empty($parent_id)) + return null; + return $this->owner->find()->where([$this->keyNameId => $parent_id, $this->keyNameGroup => $this->owner->getAttribute($this->keyNameGroup)])->one(); + } + + /* + * Get parents by AL-method + * @return array + */ + public function getParentsAL() { + $parent_id = $this->owner->getAttribute($this->keyNameParentId); + if ($parent_id == 0) { + return []; + } + + $parent = $this->owner; + $parents = []; + while(true) { + $parent = $parent->getParentAL(); + if (is_null($parent)) + break; + $parents[] = $parent; + } + + return array_reverse($parents); + } + + /* + * Children entities (one-step) (use AL-method) + * @return ActiveQuery + */ + public function getChildrenAL() { + return $this->owner->find()->where([$this->keyNameParentId => $this->owner->getAttribute($this->keyNameId), $this->keyNameGroup => $this->owner->getAttribute($this->keyNameGroup)]); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /** + * @param array $changedAttributes + * @throws Exception + */ + protected function _rebuildChildren($changedAttributes) + { + $path = isset($changedAttributes[$this->keyNamePath]) ? $changedAttributes[$this->keyNamePath] : $this->owner->getAttribute($this->keyNamePath); + $update = []; + $condition = [ + 'and', + ['@>', "[[{$this->keyNamePath}]]", $path, false], + ]; + if ($this->keyNameGroup !== null) { + $group = isset($changedAttributes[$this->keyNameGroup]) ? $changedAttributes[$this->keyNameGroup] : $this->owner->getAttribute($this->keyNameGroup); + $condition[] = [$this->keyNameGroup => $group]; + } + $params = []; + + if (isset($changedAttributes[$this->keyNamePath])) { + $substringExpr = $this->substringExpression( + "[[{$this->keyNamePath}]]", + 'array_length(:pathOld) + 1', + "array_length([[{$this->keyNamePath}]]) - array_length(:pathOld)" + ); + $update[$this->keyNamePath] = new Expression($this->concatExpression([':pathNew', $substringExpr])); + $params[':pathOld'] = $path; + $params[':pathNew'] = $this->owner->getAttribute($this->keyNamePath); + } + + if ($this->keyNameGroup !== null && isset($changedAttributes[$this->keyNameGroup])) { + $update[$this->keyNameGroup] = $this->owner->getAttribute($this->keyNameGroup); + } + + if ($this->keyNameDepth !== null && isset($changedAttributes[$this->keyNameDepth])) { + $delta = $this->owner->getAttribute($this->keyNameDepth) - $changedAttributes[$this->keyNameDepth]; + $update[$this->keyNameDepth] = new Expression("[[{$this->keyNameDepth}]]" . sprintf('%+d', $delta)); + } + if (!empty($update)) { + $this->owner->updateAll($update, $condition, $params); + } + } + + /** + * @param string $path + * @param string $delimiter + * @param bool $asArray = false + * @return null|string|array + */ + protected static function getParentPathInternal($path, $asArray = false) + { + $path = str_replace(['{', '}'], '', explode(',', $path)); + array_pop($path); + if ($asArray) { + return $path; + } + return count($path) > 0 ? implode(',', $path) : null; + } + + protected function toLike($path) { + return strtr($path . ',', ['%' => '\%', '_' => '\_', '\\' => '\\\\']) . '%'; + } + + protected function concatExpression($items) + { + if ($this->owner->getDb()->driverName === 'sqlite' || $this->owner->getDb()->driverName === 'pgsql') { + return implode(' || ', $items); + } + return 'CONCAT(' . implode(',', $items) . ')'; + } + + protected function substringExpression($string, $from, $length) + { + if ($this->owner->getDb()->driverName === 'sqlite') { + return "SUBSTR({$string}, {$from}, {$length})"; + } + return "SUBSTRING({$string}, {$from}, {$length})"; + } + + // ======================================================= + public function afterInsert() { + $this->withSave(); + $this->owner->updateAttributes([$this->keyNamePath => $this->owner->getAttribute($this->keyNamePath), $this->keyNameDepth => $this->owner->getAttribute($this->keyNameDepth)]); + } + + public function beforeUpdate() + { + if ($this->owner->getIsNewRecord()) { + throw new NotSupportedException('Method "' . $this->owner->className() . '::insert" is not supported for inserting new entitys.'); + } + $this->withSave(); + } + + protected function withSave() { + $id = $this->owner->getAttribute($this->keyNameId); + $parent_id = $this->owner->getAttribute($this->keyNameParentId); + + if (is_null($parent_id)) { + $parent_id = 0; + } + + // check parent_id value is changed! + /*if ($this->owner->getOldAttribute($this->keyNameParentId) == $parent_id) { + return; + }*/ + + // rebuild parents entities + if ($parent_id == 0) { + $depth = 0; + $path = [intval($id)]; + } else { + $parents = $this->getParentsAL(); + $path = []; + $depth = 0; + foreach ($parents as $entity) { + $path[] = $entity->getAttribute($this->keyNameId); + $depth++; + } + $path[] = intval($id); + } + + $path = '{'. implode(',', $path) .'}'; + + // rebuild children entities (recurcive) +// $this->_rebuildChildren([ +// $this->keyNamePath => $path +// ]); + + $this->owner->setAttribute('path_int', $path); +// $this->owner->setAttribute($this->keyNamePath, $path); + $this->owner->setAttribute($this->keyNameDepth, $depth); + } + + public function _recursiveRebuildChildren() { + $children = $this->getChildrenAL()->all(); + $root_path = explode(',', $this->owner->getAttribute($this->keyNamePath)); + $root_depth = $this->owner->getAttribute($this->keyNameDepth); + + /** @var $child ActiveRecord */ + foreach ($children as $child) { + $path = $root_path; + $path[] = $child->getAttribute($this->keyNameId); + $depth = $root_depth + 1; + + $child->_recursiveRebuildChildren(); + } + } +} \ No newline at end of file diff --git a/common/components/artboxtree/ArtboxTreeHelper.php b/common/components/artboxtree/ArtboxTreeHelper.php new file mode 100644 index 0000000..d5da465 --- /dev/null +++ b/common/components/artboxtree/ArtboxTreeHelper.php @@ -0,0 +1,30 @@ +depth+1) . $value; + $result[$key] = $row; + if (!empty($item['children'])) { + self::_recursiveTreeMap($result, $item['children'], $from, $to, $symbol); + } + } + } +} \ No newline at end of file diff --git a/common/components/artboxtree/ArtboxTreeQueryTrait.php b/common/components/artboxtree/ArtboxTreeQueryTrait.php new file mode 100644 index 0000000..90891ca --- /dev/null +++ b/common/components/artboxtree/ArtboxTreeQueryTrait.php @@ -0,0 +1,95 @@ +modelClass; + self::$model = new $class; + } + return self::$model; + } + + public function getTree($group, $cached = true) { + if ($cached && isset(self::$cache_tree[$group])) + return self::$cache_tree[$group]; + + $model = $this->getModel(); + $data = $this->andWhere([$model->keyNameGroup => $group])->all(); + if (empty($data)) + return []; + + self::$cache_tree[$group] = $this->buildTree($data); + + return self::$cache_tree[$group]; + } + + private function _recursiveRebuild($tree, $parentPath = null, $depth = 0) { + $model = $this->getModel(); + + foreach ($tree as $row) { + $path = (is_null($parentPath) ? '' : $parentPath . $model->delimiter) . $row['item']->getAttribute($model->keyNameId); + $row['item']->setAttribute($model->keyNamePath, $path); + $row['item']->setAttribute($model->keyNameDepth, $depth); + $row['item']->save(); + if (!empty($row['children'])) { + $this->_recursiveRebuild($row['children'], $path, $depth+1); + } + } + } + + /** + * @param int $group + */ + public function rebuildMP($group) { + $tree = $this->getTree($group); + + $this->_recursiveRebuild($tree); + } + + protected function buildTree(array $data, $parentId = 0) { + $model = $this->getModel(); + + $result = []; + foreach ($data as $element) { + if ($element[$model->keyNameParentId] == $parentId) { + $children = $this->buildTree($data, $element[$model->keyNameId]); + $result[] = [ + 'item' => $element, + 'children' => $children + ]; + } + } + return $result; + } + + public function normalizeTreeData(array $data, $parentId = null) + { + $model = $this->getModel(); + + $result = []; + foreach ($data as $element) { + if ($element[$model->keyNameParentId] == $parentId) { + $result[] = $element; + $children = $this->normalizeTreeData($data, $element[$model->keyNameId]); + if ($children) { + $result = array_merge($result, $children); + } + } + } + return $result; + } +} \ No newline at end of file diff --git a/common/components/artboxtree/ArtboxTreeWidget.php b/common/components/artboxtree/ArtboxTreeWidget.php new file mode 100644 index 0000000..fafa4a6 --- /dev/null +++ b/common/components/artboxtree/ArtboxTreeWidget.php @@ -0,0 +1,138 @@ +dataProvider === null) { + throw new InvalidConfigException('The "dataProvider" property must be set.'); + } + if ($this->keyNameId === null) { + throw new InvalidConfigException('The "keyNameId" property must be set.'); + } + if ($this->formatter == null) { + $this->formatter = Yii::$app->getFormatter(); + } elseif (is_array($this->formatter)) { + $this->formatter = Yii::createObject($this->formatter); + } + if (!$this->formatter instanceof Formatter) { + throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); + } + } + + /** + * Runs the widget. + */ + public function run() + { + if (!empty($this->assetBundle) && class_exists($this->assetBundle)) { + $view = $this->getView(); + $assetBundle = $this->assetBundle; + $assetBundle::register($view); + } + if ($this->dataProvider->getCount() == 0) { + return $this->renderEmptyResult(); + } + + parent::run(); + } + + protected function renderEmptyResult() { + return empty($this->emptyResult) ? Yii::t('artbox', 'TreeViewEmptyResult') : Yii::t('artbox', $this->emptyResult); + } + + /** + * Normalize tree data + * @param array $data + * @param string $parentId + * @return array + */ + protected function _normalizeTreeData(array $data, $parentId = null) { + $result = []; + foreach ($data as $element) { + if ($element[$this->keyNameParentId] == $parentId) { + $result[] = $element; + $children = $this->_normalizeTreeData($data, $element[$this->keyNameId]); + if ($children) { + $result = array_merge($result, $children); + } + } + } + return $result; + } + + /** + * Hierarchy tree data + * @param array $data + * @param string $parentId + * @return array + */ + protected function _hierarchyTreeData(array $data, $parentId = null) { + $result = []; + foreach ($data as $element) { + if ($element[$this->keyNameParentId] == $parentId) { + $children = $this->_hierarchyTreeData($data, $element[$this->keyNameId]); + $result[] = [ + 'item' => $element, + 'children' => $children + ]; + } + } + return $result; + } +} \ No newline at end of file diff --git a/common/components/artboxtree/treegrid/TreeGridColumn.php b/common/components/artboxtree/treegrid/TreeGridColumn.php new file mode 100644 index 0000000..f7e2132 --- /dev/null +++ b/common/components/artboxtree/treegrid/TreeGridColumn.php @@ -0,0 +1,250 @@ + + */ +class TreeGridColumn extends Object { + + /** + * @var TreeGrid the grid view object that owns this column. + */ + public $grid; + + /** + * @var string the header cell content. Note that it will not be HTML-encoded. + */ + public $header; + + /** + * @var string the footer cell content. Note that it will not be HTML-encoded. + */ + public $footer; + + /** + * @var callable This is a callable that will be used to generate the content of each cell. + * The signature of the function should be the following: `function ($model, $key, $index, $column)`. + * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered + * and `$column` is a reference to the [[TreeColumn]] object. + */ + public $content; + + /** + * @var boolean whether this column is visible. Defaults to true. + */ + public $visible = true; + + /** + * @var array the HTML attributes for the column group tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + + /** + * @var array the HTML attributes for the header cell tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + + /** + * @var array|\Closure the HTML attributes for the data cell tag. This can either be an array of + * attributes or an anonymous function ([[Closure]]) that returns such an array. + * The signature of the function should be the following: `function ($model, $key, $index, $column)`. + * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered + * and `$column` is a reference to the [[Column]] object. + * A function may be used to assign different attributes to different rows based on the data in that row. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $contentOptions = []; + + /** + * @var array the HTML attributes for the footer cell tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $footerOptions = []; + + /** + * @var string the attribute name associated with this column. When neither [[content]] nor [[value]] + * is specified, the value of the specified attribute will be retrieved from each data model and displayed. + * + * Also, if [[label]] is not specified, the label associated with the attribute will be displayed. + */ + public $attribute; + + /** + * @var string label to be displayed in the [[header|header cell]] and also to be used as the sorting + * link label when sorting is enabled for this column. + * If it is not set and the models provided by the GridViews data provider are instances + * of [[\yii\db\ActiveRecord]], the label will be determined using [[\yii\db\ActiveRecord::getAttributeLabel()]]. + * Otherwise [[\yii\helpers\Inflector::camel2words()]] will be used to get a label. + */ + public $label; + + /** + * @var boolean whether the header label should be HTML-encoded. + * @see label + */ + public $encodeLabel = true; + + /** + * @var string|\Closure an anonymous function or a string that is used to determine the value to display in the current column. + * + * If this is an anonymous function, it will be called for each row and the return value will be used as the value to + * display for every data model. The signature of this function should be: `function ($model, $key, $index, $column)`. + * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered + * and `$column` is a reference to the [[DataColumn]] object. + * + * You may also set this property to a string representing the attribute name to be displayed in this column. + * This can be used when the attribute to be displayed is different from the [[attribute]] that is used for + * sorting and filtering. + * + * If this is not set, `$model[$attribute]` will be used to obtain the value, where `$attribute` is the value of [[attribute]]. + */ + public $value; + + /** + * @var string|array in which format should the value of each data model be displayed as (e.g. `"raw"`, `"text"`, `"html"`, + * `['date', 'php:Y-m-d']`). Supported formats are determined by the [[GridView::formatter|formatter]] used by + * the [[GridView]]. Default format is "text" which will format the value as an HTML-encoded plain text when + * [[\yii\i18n\Formatter]] is used as the [[GridView::$formatter|formatter]] of the GridView. + */ + public $format = 'text'; + + /** + * Renders the header cell. + */ + public function renderHeaderCell() + { + return Html::tag('th', $this->renderHeaderCellContent(), $this->headerOptions); + } + + /** + * Renders the footer cell. + */ + public function renderFooterCell() + { + return Html::tag('td', $this->renderFooterCellContent(), $this->footerOptions); + } + + /** + * Renders a data cell. + * @param mixed $model the data model being rendered + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data item among the item array returned by [[GridView::dataProvider]]. + * @return string the rendering result + */ + public function renderDataCell($model, $key, $index, $is_first = false, $symbol = '–') + { + if ($this->contentOptions instanceof Closure) { + $options = call_user_func($this->contentOptions, $model, $key, $index, $this); + } else { + $options = $this->contentOptions; + } + return Html::tag('td', ($is_first ? str_repeat($symbol, $model->depth) : '') . $this->renderDataCellContent($model, $key, $index), $options); + } + + /** + * Renders the header cell content. + * The default implementation simply renders [[header]]. + * This method may be overridden to customize the rendering of the header cell. + * @return string the rendering result + */ + protected function renderHeaderCellContent() + { + if ($this->header !== null || $this->label === null && $this->attribute === null) { + return trim($this->header) !== '' ? $this->header : $this->grid->emptyCell; + } + + $provider = $this->grid->dataProvider; + + if ($this->label === null) { + if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQueryInterface) { + /* @var $model Model */ + $model = new $provider->query->modelClass; + $label = $model->getAttributeLabel($this->attribute); + } else { + $models = $provider->getModels(); + if (($model = reset($models)) instanceof Model) { + /* @var $model Model */ + $label = $model->getAttributeLabel($this->attribute); + } else { + $label = Inflector::camel2words($this->attribute); + } + } + } else { + $label = $this->label; + } + + return $this->encodeLabel ? Html::encode($label) : $label; + } + + /** + * Renders the footer cell content. + * The default implementation simply renders [[footer]]. + * This method may be overridden to customize the rendering of the footer cell. + * @return string the rendering result + */ + protected function renderFooterCellContent() + { + return trim($this->footer) !== '' ? $this->footer : $this->grid->emptyCell; + } + + /** + * Renders the data cell content. + * @param mixed $model the data model + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. + * @return string the rendering result + */ + protected function renderDataCellContent($model, $key, $index) + { + if ($this->content === null) { + return $this->grid->formatter->format($this->getDataCellValue($model, $key, $index), $this->format); + } else { + if ($this->content !== null) { + return call_user_func($this->content, $model, $key, $index, $this); + } else { + return $this->grid->emptyCell; + } + } + + + } + + /** + * Returns the data cell value. + * @param mixed $model the data model + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. + * @return string the data cell value + */ + public function getDataCellValue($model, $key, $index) + { + if ($this->value !== null) { + if (is_string($this->value)) { + return ArrayHelper::getValue($model, $this->value); + } else { + return call_user_func($this->value, $model, $key, $index, $this); + } + } elseif ($this->attribute !== null) { + return ArrayHelper::getValue($model, $this->attribute); + } + return null; + } + +} \ No newline at end of file diff --git a/common/components/artboxtree/treegrid/TreeGridWidget.php b/common/components/artboxtree/treegrid/TreeGridWidget.php new file mode 100644 index 0000000..908ba69 --- /dev/null +++ b/common/components/artboxtree/treegrid/TreeGridWidget.php @@ -0,0 +1,277 @@ + 'table table-striped table-bordered']; + + /** + * @var array The plugin options + */ + public $pluginOptions = []; + + /** + * @var boolean whether to show the grid view if [[dataProvider]] returns no data. + */ + public $showOnEmpty = true; + + public $rowOptions = []; + + /** + * @var Closure an anonymous function that is called once BEFORE rendering each data model. + * It should have the similar signature as [[rowOptions]]. The return result of the function + * will be rendered directly. + */ + public $beforeRow; + + /** + * @var Closure an anonymous function that is called once AFTER rendering each data model. + * It should have the similar signature as [[rowOptions]]. The return result of the function + * will be rendered directly. + */ + public $afterRow; + + /** + * @var boolean whether to show the header section of the grid table. + */ + public $showHeader = true; + + /** + * @var array the HTML attributes for the table header row. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerRowOptions = []; + + /** + * @var boolean whether to show the footer section of the grid table. + */ + public $showFooter = false; + + /** + * @var string the HTML display when the content of a cell is empty + */ + public $emptyCell = ' '; + + public $levelSymbol = '–'; + + /** + * Init the widget object. + */ + public function init() { + parent::init(); + + $this->initColumns(); + } + + /** + * Runs the widget. + */ + public function run() { + $run = parent::run(); + if (!is_null($run)) + return $run; + + if ($this->showOnEmpty || $this->dataProvider->getCount() > 0) { + $pagination = $this->dataProvider->getPagination(); + $pagination->setPageSize($this->dataProvider->getTotalCount()); + + $header = $this->showHeader ? $this->renderTableHeader() : false; + $body = $this->renderItems(); + $footer = $this->showFooter ? $this->renderTableFooter() : false; + + $content = array_filter([ + $header, + $body, + $footer + ]); + + return Html::tag('table', implode("\n", $content), $this->options); + } else { + return $this->renderEmptyResult(); + } + } + + /** + * Renders the table header. + * @return string the rendering result. + */ + public function renderTableHeader() + { + $cells = []; + foreach ($this->columns as $column) { + /* @var $column TreeGridColumn */ + $cells[] = $column->renderHeaderCell(); + } + $content = Html::tag('tr', implode('', $cells), $this->headerRowOptions); + return "\n" . $content . "\n"; + } + + /** + * Renders the table footer. + * @return string the rendering result. + */ + public function renderTableFooter() + { + $cells = []; + foreach ($this->columns as $column) { + /* @var $column TreeGridColumn */ + $cells[] = $column->renderFooterCell(); + } + $content = Html::tag('tr', implode('', $cells), $this->footerRowOptions); + return "\n" . $content . "\n"; + } + + /** + * Renders the data models for the grid view. + */ + public function renderItems() + { + $rows = []; + $models = array_values($this->dataProvider->getModels()); + $keys = $this->dataProvider->getKeys(); + $models = TaxOption::find()->normalizeTreeData($models, $this->rootParentId); + foreach ($models as $index => $model) { + $key = $keys[$index]; + if ($this->beforeRow !== null) { + $row = call_user_func($this->beforeRow, $model, $key, $index, $this); + if (!empty($row)) { + $rows[] = $row; + } + } + + $rows[] = $this->renderTableRow($model, $key, $index); + + if ($this->afterRow !== null) { + $row = call_user_func($this->afterRow, $model, $key, $index, $this); + if (!empty($row)) { + $rows[] = $row; + } + } + } + + if (empty($rows)) { + $colspan = count($this->columns); + return "" . $this->renderEmpty() . ""; + } else { + return implode("\n", $rows); + } + } + + /** + * Renders a table row with the given data model and key. + * @param mixed $model the data model to be rendered + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the model array returned by [[dataProvider]]. + * @return string the rendering result + */ + public function renderTableRow($model, $key, $index) + { + $cells = []; + /* @var $column TreeGridColumn */ + $i = 0; + foreach ($this->columns as $column) { + $cells[] = $column->renderDataCell($model, $key, $index, $i == 0, $this->levelSymbol); + $i++; + } + if ($this->rowOptions instanceof Closure) { + $options = call_user_func($this->rowOptions, $model, $key, $index, $this); + } else { + $options = $this->rowOptions; + } + $options['data-key'] = is_array($key) ? json_encode($key) : (string) $key; + + $id = ArrayHelper::getValue($model, $this->keyNameId); + Html::addCssClass($options, "treegrid-$id"); + + $parentId = ArrayHelper::getValue($model, $this->keyNameParentId); + if ($parentId) { + Html::addCssClass($options, "treegrid-parent-$parentId"); + } + + return Html::tag('tr', implode('', $cells), $options); + } + + /** + * Creates column objects and initializes them. + */ + protected function initColumns() + { + if (empty($this->columns)) { + $this->guessColumns(); + } + foreach ($this->columns as $i => $column) { + if (is_string($column)) { + $column = $this->createDataColumn($column); + } else { + $column = Yii::createObject(array_merge([ + 'class' => $this->dataColumnClass ? : TreeGridColumn::className(), + 'grid' => $this, + ], $column)); + } + if (!$column->visible) { + unset($this->columns[$i]); + continue; + } + $this->columns[$i] = $column; + } + } + + /** + * Creates a [[DataColumn]] object based on a string in the format of "attribute:format:label". + * @param string $text the column specification string + * @return DataColumn the column instance + * @throws InvalidConfigException if the column specification is invalid + */ + protected function createDataColumn($text) + { + if (!preg_match('/^([^:]+)(:(\w*))?(:(.*))?$/', $text, $matches)) { + throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"'); + } + + return Yii::createObject([ + 'class' => $this->dataColumnClass ? : TreeGridColumn::className(), + 'grid' => $this, + 'attribute' => $matches[1], + 'format' => isset($matches[3]) ? $matches[3] : 'text', + 'label' => isset($matches[5]) ? $matches[5] : null, + ]); + } + + /** + * This function tries to guess the columns to show from the given data + * if [[columns]] are not explicitly specified. + */ + protected function guessColumns() + { + $models = $this->dataProvider->getModels(); + $model = reset($models); + if (is_array($model) || is_object($model)) { + foreach ($model as $name => $value) { + $this->columns[] = $name; + } + } + } +} \ No newline at end of file diff --git a/common/components/artboxtree/treelist/TreeListWidget.php b/common/components/artboxtree/treelist/TreeListWidget.php new file mode 100644 index 0000000..868e82e --- /dev/null +++ b/common/components/artboxtree/treelist/TreeListWidget.php @@ -0,0 +1,62 @@ +_hierarchyTreeData(array_values($this->dataProvider->getModels()), $this->rootParentId); + return $this->renderTreelist($models); + } + + protected function renderTreelist($models) { + foreach ($models as $index => $model) { + $row = $this->renderTreelistItem($model['item']); + $children = empty($model['children']) ? '' : $this->renderTreelist($model['children']); + $output[] = '
  • '. $row . $children .'
  • '; + } + + if (!empty($output)) + return ''; + } + + protected function renderTreelistItem($model) + { + $options = []; + $id = ArrayHelper::getValue($model, $this->keyNameId); + Html::addCssClass($options, "treelistitem-$id"); + + $parent_id = ArrayHelper::getValue($model, $this->keyNameParentId); + if ($parent_id) { + Html::addCssClass($options, "treelistitem-parent-$parent_id"); + } + +// if (is_string($this->value)) { +// return ArrayHelper::getValue($model, $this->value); +// } else { +// return call_user_func($this->value, $model, $key, $index, $this); +// } + + return Html::tag('span', ArrayHelper::getValue($model, $this->displayField), $options); + } +} \ No newline at end of file diff --git a/common/components/artboxtree/treemenu/TreeMenuWidget.php b/common/components/artboxtree/treemenu/TreeMenuWidget.php new file mode 100644 index 0000000..381cb91 --- /dev/null +++ b/common/components/artboxtree/treemenu/TreeMenuWidget.php @@ -0,0 +1,62 @@ +_hierarchyTreeData(array_values($this->dataProvider->getModels()), $this->rootParentId); + return $this->renderTreelist($models); + } + + protected function renderTreelist($models) { + foreach ($models as $index => $model) { + $row = $this->renderTreelistItem($model['item']); + $children = empty($model['children']) ? '' : $this->renderTreelist($model['children']); + $output[] = '
  • '. $row . $children .'
  • '; + } + + if (!empty($output)) + return ''; + } + + protected function renderTreelistItem($model) + { + $options = []; + $id = ArrayHelper::getValue($model, $this->keyNameId); + Html::addCssClass($options, "treelistitem-$id"); + + $parent_id = ArrayHelper::getValue($model, $this->keyNameParentId); + if ($parent_id) { + Html::addCssClass($options, "treelistitem-parent-$parent_id"); + } + +// if (is_string($this->value)) { +// return ArrayHelper::getValue($model, $this->value); +// } else { +// return call_user_func($this->value, $model, $key, $index, $this); +// } + + return Html::tag('span', ArrayHelper::getValue($model, $this->displayField), $options); + } +} \ No newline at end of file diff --git a/common/config/.gitignore b/common/config/.gitignore new file mode 100644 index 0000000..97c0f01 --- /dev/null +++ b/common/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php diff --git a/common/config/bootstrap.php b/common/config/bootstrap.php new file mode 100644 index 0000000..fb64fc7 --- /dev/null +++ b/common/config/bootstrap.php @@ -0,0 +1,6 @@ + 'ru-RU', + 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', + 'controllerMap' => [ + 'elfinder' => [ + 'class' => 'mihaildev\elfinder\Controller', + 'access' => ['@'], //глобальный доступ к фаил менеджеру @ - для авторизорованных , ? - для гостей , чтоб открыть всем ['@', '?'] + 'disabledCommands' => ['netmount'], //отключение ненужных команд https://github.com/Studio-42/elFinder/wiki/Client-configuration-options#commands + 'roots' => [ + [ + 'class' => 'mihaildev\elfinder\UserPath', + 'path' => '../../storage/user_{id}', + 'name' => 'My Documents' + ], + ], + 'watermark' => [ + 'source' => __DIR__.'/logo.png', // Path to Water mark image + 'marginRight' => 5, // Margin right pixel + 'marginBottom' => 5, // Margin bottom pixel + 'quality' => 95, // JPEG image save quality + 'transparency' => 70, // Water mark image transparency ( other than PNG ) + 'targetType' => IMG_GIF|IMG_JPG|IMG_PNG|IMG_WBMP, // Target image formats ( bit-field ) + 'targetMinPixel' => 200 // Target image minimum pixel size + ] + ], + 'artbox-comment' => [ + 'class' => \common\modules\comment\Controller::className(), + ], + ], + 'components' => [ + 'assetManager' => [ + 'bundles' => [ + 'yii\web\JqueryAsset' =>[ + 'jsOptions' => ['position' => \yii\web\View::POS_HEAD] + ] + ], + ], + 'cache' => [ + 'class' => 'yii\caching\FileCache', + ], + 'urlManager' => [ + 'enablePrettyUrl' => true, + 'showScriptName' => false, + 'rules' => [ + 'module///' => '//', + ] + ], + 'i18n' => [ + 'translations' => [ + '*' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'basePath' => '@common/translation', + 'fileMap' => [ + 'app' => 'app.php', + 'app/error' => 'error.php', + ], + ], + 'app' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'basePath' => '@common/translation', + 'fileMap' => [ + 'app' => 'app.php', + 'app/error' => 'error.php', + ], + ], + ], + ], + ], + + 'modules' => [ + 'file' => [ + 'class' => 'common\modules\file\Module', + ], + 'relation' => [ + 'class' => 'common\modules\relation\Module', + 'relations' => [ + 'product_categories' => [ + 'name' => Yii::t('product', 'Categories'), + 'field' => 'categories', + 'entity1' => [ + 'model' => '\common\modules\product\models\Product', + 'label' => 'Product', + 'listField' => 'fullname', + 'key' => 'product_id', + 'linked_key' => 'product_id', + ], + 'entity2' => [ + 'model' => '\common\modules\rubrication\models\TaxOption', + 'label' => 'Category', + 'listField' => 'ValueRenderFlash', + 'key' => 'tax_option_id', + 'linked_key' => 'category_id', + 'where' => [ + 'tax_group_id' => 1 + ], + 'hierarchy' => [ + 'key' => 'tax_option_id', + 'parentKey' => 'parent_id', + ] + ], + 'via' => [ + 'model' => '\common\modules\product\models\ProductCategory', + ] + ], + 'relation_categories' => [ + 'name' => Yii::t('relation', 'Relation categories'), + 'field' => 'categories', + 'entity1' => [ + 'model' => '\common\modules\product\models\Product', + 'label' => 'Product', + 'listField' => 'fullname', + 'key' => 'product_id', + 'linked_key' => 'product_id', + ], + 'entity2' => [ + 'model' => '\common\modules\rubrication\models\TaxOption', + 'label' => 'Category', + 'listField' => 'ValueRenderFlash', + 'key' => 'tax_option_id', + 'linked_key' => 'category_id', + 'where' => [ + 'tax_group_id' => 1 + ] + ], + 'via' => [ + 'model' => '\common\modules\relation\models\Relation', + 'alias' => 'alias', + ] + ], + 'tax_option_to_group' => [ + 'name' => 'Options-Groups', + 'field' => 'tax_option_to_group', + 'linked_table' => 'tax_option_to_group', + 'entity1' => [ + 'label' => 'Option', + 'listField' => 'ValueRenderFlash', + 'model' => '\common\modules\rubrication\models\TaxOption', + 'key' => 'tax_option_id', + 'linked_key' => 'tax_option_id', + ], + 'entity2' => [ + 'label' => 'Group', + 'listField' => 'name', + 'model' => '\common\modules\rubrication\models\TaxGroup', + 'key' => 'tax_group_id', + 'linked_key' => 'tax_group_id', + ], + 'via' => [ + 'model' => 'common\modules\rubrication\models\TaxOptionToGroup', + 'alias' => 'alias', + ] + ], + 'tax_option_to_option' => [ + 'name' => 'Options-Options', + 'field' => 'tax_option_to_option', + 'entity1' => [ + 'label' => 'Option', + 'listField' => 'ValueRenderFlash', + 'model' => '\common\modules\rubrication\models\TaxOption', + 'key' => 'tax_option_id', + 'linked_key' => 'tax_option1_id', + ], + 'entity2' => [ + 'label' => 'Option', + 'listField' => 'ValueRenderFlash', + 'model' => '\common\modules\rubrication\models\TaxOption', + 'key' => 'tax_option_id', + 'linked_key' => 'tax_option2_id', + ], + 'via' => [ + 'model' => 'common\modules\rubrication\models\TaxOptionRelation', + 'alias' => 'alias', + ] + ], + 'brand_cats' => [ + 'name' => 'Категории производителей', + 'field' => 'tax_option_to_option', + 'entity1' => [ + 'label' => 'Бренд', + 'listField' => 'ValueRenderFlash', + 'model' => '\common\modules\rubrication\models\TaxOption', + 'key' => 'tax_option_id', + 'linked_key' => 'tax_option1_id', + ], + 'entity2' => [ + 'label' => 'Категория', + 'listField' => 'ValueRenderFlash', + 'model' => '\common\modules\rubrication\models\TaxOption', + 'key' => 'tax_option_id', + 'linked_key' => 'tax_option2_id', + ], + 'via' => [ + 'model' => 'common\modules\rubrication\models\TaxOptionRelation', + 'alias' => 'alias', + ] + ] + ] + ], + 'comment' => [ + 'class' => 'common\modules\comment\Module', + 'useRbac' => true, + 'rbac' => [ + 'rules' => [ + \common\modules\comment\rbac\ArtboxCommentCreateRule::className(), + \common\modules\comment\rbac\ArtboxCommentDeleteRule::className(), + \common\modules\comment\rbac\ArtboxCommentUpdateRule::className(), + \common\modules\comment\rbac\ArtboxCommentUpdateOwnRule::className(), + \common\modules\comment\rbac\ArtboxCommentDeleteOwnRule::className(), + ], + 'permissions' => [ + [ + 'name' => common\modules\comment\Permissions::CREATE, + 'description' => 'Can create comments', + 'ruleName' =>(new \common\modules\comment\rbac\ArtboxCommentCreateRule())->name, + ], + [ + 'name' => common\modules\comment\Permissions::UPDATE, + 'description' => 'Can update comments', + 'ruleName' =>(new \common\modules\comment\rbac\ArtboxCommentUpdateRule())->name, + ], + [ + 'name' => common\modules\comment\Permissions::DELETE, + 'description' => 'Can delete comments', + 'ruleName' =>(new \common\modules\comment\rbac\ArtboxCommentDeleteRule())->name, + ], + [ + 'name' => common\modules\comment\Permissions::UPDATE_OWN, + 'description' => 'Can update own comments', + 'ruleName' =>(new \common\modules\comment\rbac\ArtboxCommentUpdateOwnRule())->name, + ], + [ + 'name' => common\modules\comment\Permissions::DELETE_OWN, + 'description' => 'Can delete own comments', + 'ruleName' =>(new \common\modules\comment\rbac\ArtboxCommentDeleteOwnRule())->name, + ], + ], + ], + + ], + ], + 'language' => 'ru-RU' +]; diff --git a/common/config/params.php b/common/config/params.php new file mode 100644 index 0000000..4ec9ba6 --- /dev/null +++ b/common/config/params.php @@ -0,0 +1,6 @@ + 'admin@example.com', + 'supportEmail' => 'support@example.com', + 'user.passwordResetTokenExpire' => 3600, +]; diff --git a/common/mail/layouts/html.php b/common/mail/layouts/html.php new file mode 100644 index 0000000..bddbc61 --- /dev/null +++ b/common/mail/layouts/html.php @@ -0,0 +1,22 @@ + +beginPage() ?> + + + + + <?= Html::encode($this->title) ?> + head() ?> + + + beginBody() ?> + + endBody() ?> + + +endPage() ?> diff --git a/common/mail/layouts/text.php b/common/mail/layouts/text.php new file mode 100644 index 0000000..7087cea --- /dev/null +++ b/common/mail/layouts/text.php @@ -0,0 +1,12 @@ + +beginPage() ?> +beginBody() ?> + +endBody() ?> +endPage() ?> diff --git a/common/mail/passwordResetToken-html.php b/common/mail/passwordResetToken-html.php new file mode 100644 index 0000000..f3daf49 --- /dev/null +++ b/common/mail/passwordResetToken-html.php @@ -0,0 +1,15 @@ +urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]); +?> +
    +

    Hello username) ?>,

    + +

    Follow the link below to reset your password:

    + +

    +
    diff --git a/common/mail/passwordResetToken-text.php b/common/mail/passwordResetToken-text.php new file mode 100644 index 0000000..244c0cb --- /dev/null +++ b/common/mail/passwordResetToken-text.php @@ -0,0 +1,12 @@ +urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]); +?> +Hello username ?>, + +Follow the link below to reset your password: + + diff --git a/common/models/Blog.php b/common/models/Blog.php new file mode 100644 index 0000000..1547b6b --- /dev/null +++ b/common/models/Blog.php @@ -0,0 +1,107 @@ + BlameableBehavior::className(), + 'createdByAttribute' => 'user_id', + 'updatedByAttribute' => false, + ], + [ + 'class' => TimestampBehavior::className(), + 'createdAtAttribute' => 'date_add', + 'updatedAtAttribute' => false, + 'value' => new Expression('NOW()'), + ], + 'slug' => [ + 'class' => 'common\behaviors\Slug', + 'in_attribute' => 'name', + 'out_attribute' => 'link', + 'translit' => true + ] + ]; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [ + [ 'name', 'description' ], + 'required', + ], + [ + [ 'description' ], + 'string', + ], + [ + [ + 'name', + 'link', + 'cover', + ], + 'string', + 'max' => 255, + ], + ]; + } + + public function getDateCreate(){ + return date('Y-m-d',strtotime($this->date_add)); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'blog_id' => Yii::t('app', 'Blog ID'), + 'user_id' => Yii::t('app', 'User ID'), + 'name' => Yii::t('app', 'Название'), + 'link' => Yii::t('app', 'URL'), + 'date_add' => Yii::t('app', 'Дата добавления'), + 'user_add_id' => Yii::t('app', 'User Add ID'), + 'view_count' => Yii::t('app', 'Количество просмотров'), + 'description' => Yii::t('app', 'Описание'), + 'cover' => Yii::t('app', 'Фото главное'), + ]; + } + } diff --git a/common/models/BlogSearch.php b/common/models/BlogSearch.php new file mode 100644 index 0000000..8faadac --- /dev/null +++ b/common/models/BlogSearch.php @@ -0,0 +1,135 @@ + date('Y-m-d', 0), + ], + [ + [ + 'date_add_to', + ], + 'default', + 'value' => date('Y-m-d'), + ], + ]; + } + + /** + * @inheritdoc + */ + public function scenarios() + { + // bypass scenarios() implementation in the parent class + return Model::scenarios(); + } + + /** + * Creates data provider instance with search query applied + * + * @param array $params + * + * @return ActiveDataProvider + */ + public function search($params) + { + $query = Blog::find(); + + // add conditions that should always apply here + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + + $this->load($params); + + if(!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + $query->andWhere([ 'user_id' => \Yii::$app->user->getId() ]); + + // grid filtering conditions + $query->andFilterWhere([ + 'blog_id' => $this->blog_id, + 'date_add' => $this->date_add, + 'user_add_id' => $this->user_add_id, + 'view_count' => $this->view_count, + ]); + + $query->andFilterWhere([ + 'between', + 'date_add', + $this->date_add_from, + (new \DateTime($this->date_add_to))->modify('+1 day')->format('Y-m-d') + ]); + + $query->andFilterWhere([ + 'like', + 'name', + $this->name, + ]) + ->andFilterWhere([ + 'like', + 'description', + $this->description, + ]) + ->andFilterWhere([ + 'like', + 'cover', + $this->cover, + ]); + + return $dataProvider; + } + + } diff --git a/common/models/Fields.php b/common/models/Fields.php new file mode 100644 index 0000000..dcd60fc --- /dev/null +++ b/common/models/Fields.php @@ -0,0 +1,155 @@ + 255] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'table_name' => 'Model Name', + 'table_id' => 'Model ID', + 'value' => 'Value', + 'field_name' => 'Field Name', + 'language' => 'Language', + ]; + } + + public static function getData($id, $model, $type){ + $data = ArrayHelper::toArray(self::find()->where(['table_id'=>$id, 'table_name'=>$model, 'field_type'=>$type])->all()); + $result = []; + for($i=0; $i < count($data); $i ++){ + $result[$data[$i]['parent_key']][$data[$i]['field_name']] = $data[$i]['value']; + } + + return $result; + } + + + /** + * @param $post - array with field data + * @param $table_id - row id in model table + * @param $table_name - madel table name + * @param $language - language id + */ + + public static function saveFieldData($post,$table_id,$table_name, $language){ + + self::deleteAll(['table_id'=>$table_id, 'table_name'=>$table_name, 'language' => $language, 'field_type' => array_keys($post)]); + + if($post){ + + + foreach($post as $k => $field){ + + + + foreach($field as $parent_key => $row){ + + foreach($row as $key => $value){ + + $field_model = new Fields(); + $field_model->field_name = array_keys($value)[0]; + $field_model->value = $value[array_keys($value)[0]]; + $field_model->table_name = $table_name; + $field_model->table_id = $table_id; + $field_model->field_type = $k; + $field_model->language = 'ru'; + $field_model->parent_key = $parent_key; + $field_model->key = $key; + $field_model->save(); + } + + } + } + } + } + + + /** + * @param $post - array with field data + * @param $table_id - row id in model table + * @param $table_name - madel table name + * @param $language - language id + */ + + public static function saveFieldVideoData($post,$table_id,$table_name, $language){ + + self::deleteAll(['table_id'=>$table_id, 'table_name'=>$table_name, 'language' => $language, 'field_type' => array_keys($post)]); + + if($post){ + + + foreach($post as $k => $field){ + + + + foreach($field as $parent_key => $row){ + + foreach($row as $key => $value){ + + preg_match('/src=\"(.[^"]*)\"/', $value[array_keys($value)[0]], $video_url); + + if(isset($video_url[1]) && !empty($video_url[1])){ + + $field_model = new Fields(); + $field_model->field_name = array_keys($value)[0]; + $field_model->value = $video_url[1].'?showinfo=0&autoplay=0'; + $field_model->table_name = $table_name; + $field_model->table_id = $table_id; + $field_model->field_type = $k; + $field_model->language = 'ru'; + $field_model->parent_key = $parent_key; + $field_model->key = $key; + $field_model->save(); + + } + + + } + + } + } + } + } +} diff --git a/common/models/LoginForm.php b/common/models/LoginForm.php new file mode 100644 index 0000000..afc1c23 --- /dev/null +++ b/common/models/LoginForm.php @@ -0,0 +1,78 @@ +hasErrors()) { + $user = $this->getUser(); + if (!$user || !$user->validatePassword($this->password)) { + $this->addError($attribute, 'Incorrect username or password.'); + } + } + } + + /** + * Logs in a user using the provided username and password. + * + * @return boolean whether the user is logged in successfully + */ + public function login() + { + if ($this->validate()) { + return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); + } else { + return false; + } + } + + /** + * Finds user by [[username]] + * + * @return User|null + */ + protected function getUser() + { + if ($this->_user === null) { + $this->_user = User::findByUsername($this->username); + } + + return $this->_user; + } +} diff --git a/common/models/User.php b/common/models/User.php new file mode 100644 index 0000000..ce78fcd --- /dev/null +++ b/common/models/User.php @@ -0,0 +1,188 @@ + self::STATUS_ACTIVE], + ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]], + ]; + } + + /** + * @inheritdoc + */ + public static function findIdentity($id) + { + return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]); + } + + /** + * @inheritdoc + */ + public static function findIdentityByAccessToken($token, $type = null) + { + throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); + } + + /** + * Finds user by username + * + * @param string $username + * @return static|null + */ + public static function findByUsername($username) + { + return static::findOne(['username' => $username, 'status' => self::STATUS_ACTIVE]); + } + + /** + * Finds user by password reset token + * + * @param string $token password reset token + * @return static|null + */ + public static function findByPasswordResetToken($token) + { + if (!static::isPasswordResetTokenValid($token)) { + return null; + } + + return static::findOne([ + 'password_reset_token' => $token, + 'status' => self::STATUS_ACTIVE, + ]); + } + + /** + * Finds out if password reset token is valid + * + * @param string $token password reset token + * @return boolean + */ + public static function isPasswordResetTokenValid($token) + { + if (empty($token)) { + return false; + } + + $timestamp = (int) substr($token, strrpos($token, '_') + 1); + $expire = Yii::$app->params['user.passwordResetTokenExpire']; + return $timestamp + $expire >= time(); + } + + /** + * @inheritdoc + */ + public function getId() + { + return $this->getPrimaryKey(); + } + + /** + * @inheritdoc + */ + public function getAuthKey() + { + return $this->auth_key; + } + + /** + * @inheritdoc + */ + public function validateAuthKey($authKey) + { + return $this->getAuthKey() === $authKey; + } + + /** + * Validates password + * + * @param string $password password to validate + * @return boolean if password provided is valid for current user + */ + public function validatePassword($password) + { + return Yii::$app->security->validatePassword($password, $this->password_hash); + } + + /** + * Generates password hash from password and sets it to the model + * + * @param string $password + */ + public function setPassword($password) + { + $this->password_hash = Yii::$app->security->generatePasswordHash($password); + } + + /** + * Generates "remember me" authentication key + */ + public function generateAuthKey() + { + $this->auth_key = Yii::$app->security->generateRandomString(); + } + + /** + * Generates new password reset token + */ + public function generatePasswordResetToken() + { + $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time(); + } + + /** + * Removes password reset token + */ + public function removePasswordResetToken() + { + $this->password_reset_token = null; + } +} diff --git a/common/modules/blog/Module.php b/common/modules/blog/Module.php new file mode 100644 index 0000000..8752f7e --- /dev/null +++ b/common/modules/blog/Module.php @@ -0,0 +1,15 @@ + [[0 => property(свойство обьекта), ... дополнительные + * настройки]], ...[]] + * + */ + public $attributes; + + /** + * События + * + * События на которые должно срабатывать поведение. Задается ассоциативный массив, в котором ключ - событие + * связанного обьекта, а значение - метод, который вызывается при этом событии + * + * @return array [key(event) => val(method)] + * + */ + public function events() + { + return [ + ActiveRecord::EVENT_BEFORE_INSERT => 'autocomplete', + ActiveRecord::EVENT_BEFORE_UPDATE => 'autocomplete', + ]; + } + + /** + * События + * + * События на которые должно срабатывать поведение. Задается ассоциативный массив, в котором ключ - событие + * связанного обьекта, а значение - метод, который вызывается при этом событии + * Доступные автозаполнения: + * ['translit' => ['prop1', ... 'prop2']], + * где prop - свойство подлежащее транслитерации + * ['repeat' => [[string 'prop1', string 'target1', boolean 'skipFilled', int 'count', boolean 'truncate', string 'suffix'], ...[]], + * где prop - свойство для преобразования, + * target - свойство с которого взять данные, + * count - число для преобразования, + * skipFilled - пропустить непустые, + * truncate - true - обрезать по словам, false - по символам, + * suffix - суффикс, который добавить после обрезки + * + * @param mixed $event Yii обьект свойста https://github.com/yiisoft/yii2/blob/master/docs/guide-ru/concept-events.md + * + */ + public function autocomplete($event) + { + if(!empty($this->attributes['translit'])) { + foreach($this->attributes['translit'] as $translit) { + if($this->owner->hasAttribute($translit)) { + $this->owner->$translit = Tools::translit($this->owner->$translit); + } + } + } + if(!empty($this->attributes['repeat'])) { + foreach($this->attributes['repeat'] as $repeat) { + if(is_array($repeat) && $this->owner->hasAttribute($repeat[0]) && $this->owner->hasAttribute($repeat[1]) && is_int($repeat[3]) && (empty($this->owner->$repeat[0]) || $repeat[2])) { + $suffix = $repeat[5]?:''; + $truncate = $repeat[4]?'truncateWords':'truncate'; + $this->owner->$repeat[0] = StringHelper::$truncate($this->owner->$repeat[1], $repeat[3], $suffix); + } + } + } + } +} \ No newline at end of file diff --git a/common/modules/blog/config.php b/common/modules/blog/config.php new file mode 100644 index 0000000..f528ffb --- /dev/null +++ b/common/modules/blog/config.php @@ -0,0 +1,9 @@ + [ + + ], + 'params' => [ + 'test' => 'Hello', + ], +]; diff --git a/common/modules/blog/controllers/AjaxController.php b/common/modules/blog/controllers/AjaxController.php new file mode 100644 index 0000000..8da349f --- /dev/null +++ b/common/modules/blog/controllers/AjaxController.php @@ -0,0 +1,133 @@ +request->getIsAjax()) { + //throw new ForbiddenHttpException('Permission denied'); + } + + if(!parent::beforeAction($action)) { + return false; + } + + return true; + } + + public function actionCategoryForm($language_id, $widget_id) + { + $model = Language::find()->where(['>=', 'language_id', 1])->andWhere(['status' => 1, 'language_id' => $language_id])->one(); + if(!$model) { + throw new NotFoundHttpException('Language not found'); + } + $category_lang = new ArticleCategoryLang(); + return $this->renderAjax('_category_form', ['model' => $model, 'category_lang' => $category_lang, 'widget_id' => $widget_id]); + } + + public function actionArticleForm($language_id, $widget_id) + { + $model = Language::find()->where(['>=', 'language_id', 1])->andWhere(['status' => 1, 'language_id' => $language_id])->one(); + if(!$model) { + throw new NotFoundHttpException('Language not found'); + } + $article_lang = new ArticleLang(); + return $this->renderAjax('_article_form', ['model' => $model, 'article_lang' => $article_lang, 'widget_id' => $widget_id]); + } + + public function actionArticleMediaForm($language_id, $widget_id, $type) + { + $model = Language::find()->where(['>=', 'language_id', 1])->andWhere(['status' => 1, 'language_id' => $language_id])->one(); + if(!$model) { + throw new NotFoundHttpException('Language not found'); + } + if(!in_array($type, ['full', 'preview'])) { + throw new InvalidParamException('Type must only be full/preview'); + } + $article_lang = new ArticleMedia(); + return $this->renderAjax('_article_media_form', ['model' => $model, 'article_lang' => $article_lang, 'widget_id' => $widget_id, 'type' => $type]); + } + + public function actionArticleCategoryMediaForm($language_id, $widget_id, $type) + { + $model = Language::find()->where(['>=', 'language_id', 1])->andWhere(['status' => 1, 'language_id' => $language_id])->one(); + if(!$model) { + throw new NotFoundHttpException('Language not found'); + } + if(!in_array($type, ['full', 'preview'])) { + throw new InvalidParamException('Type must only be full/preview'); + } + $article_lang = new ArticleCategoryMedia(); + return $this->renderAjax('_article_media_form', ['model' => $model, 'article_lang' => $article_lang, 'widget_id' => $widget_id, 'type' => $type]); + } + + public function actionRemoveImage() + { + $post = \Yii::$app->request->post(); + if(!empty($post['article_media_id'])) { + $article_media = ArticleMedia::findOne($post['article_media_id']); + if($post['remove_media']) { + $media = $article_media->media->delete(); + } + if(!empty($article_media)) { + $article_media->delete(); + } + return true; + } else { + return false; + } + } + + public function actionRemoveCategoryImage() + { + $post = \Yii::$app->request->post(); + if(!empty($post['category_media_id'])) { + $category_media = ArticleCategoryMedia::findOne($post['category_media_id']); + if($post['remove_media']) { + $media = $category_media->media->delete(); + } + if(!empty($category_media)) { + $category_media->delete(); + } + return true; + } else { + return false; + } + } + + public function actionRemoveImageCategory() + { + $post = \Yii::$app->request->post(); + if(!empty($post['category_media_id'])) { + $category_media = ArticleCategoryMedia::findOne($post['category_media_id']); + if($post['remove_media']) { + $media = $category_media->media->delete(); + } + if(!empty($category_media)) { + $category_media->delete(); + } + return true; + } else { + return false; + } + } + + public function actionMultilangForm($model, $ajaxView, $widget_id, $language_id = NULL) + { + $model = new $model(['language_id' => $language_id]); + return $this->renderAjax($ajaxView, ['model' => $model, 'widget_id' => $widget_id]); + } + +} diff --git a/common/modules/blog/controllers/ArticleController.php b/common/modules/blog/controllers/ArticleController.php new file mode 100644 index 0000000..5061555 --- /dev/null +++ b/common/modules/blog/controllers/ArticleController.php @@ -0,0 +1,228 @@ + Article::find(), + 'pagination' => [ + 'pageSize' => 1, + ], + ]); + return $this->render('index', ['dataProvider' => $dataProvider]); + } + + public function actionCreate() + { + $article_langs = array(); + $article = new Article(); + $default_lang = Language::getDefaultLang(); + $images = array(); + $images[$default_lang->language_id]['full'] = new ArticleMedia(['scenario' => ArticleMedia::SCENARIO_FULL]); + $images[$default_lang->language_id]['preview'] = new ArticleMedia(['scenario' => ArticleMedia::SCENARIO_PREVIEW]); + $images[0]['additional'] = new ArticleMedia(['scenario' => ArticleMedia::SCENARIO_ADDITIONAL]); + $article->loadDefaultValues(); + $langs = Language::getActiveLanguages(); + $isValid = false; + if(!empty(\Yii::$app->request->post())) { + $isValid = true; + $article->load(\Yii::$app->request->post()); + $article->user_id = \Yii::$app->user->getId(); + $isValid = $article->validate(); + foreach(\Yii::$app->request->post()['ArticleMedia'] as $lang => $value) { + foreach($value as $type => $fields) { + $images[$lang][$type] = new ArticleMedia(['scenario' => $type]); + $images[$lang][$type]->type = $type; + $images[$lang][$type]->language_id = $lang; + $images[$lang][$type]->imageFile = UploadedFile::getInstance($images[$lang][$type], "[{$lang}][{$type}]imageFile"); + $isValid = $images[$lang][$type]->validate(['imageFile']) && $isValid; + } + } + $images[0]['additional']->language_id = 0; + $images[0]['additional']->type = 'additional'; + $images[0]['additional']->imageFile = UploadedFile::getInstances($images[0]['additional'], "[0][additional]imageFile"); + if(empty(\Yii::$app->request->post()['ArticleLang'])) { + $article_langs[$default_lang->language_id] = new ArticleLang(); + $isValid = ArticleLang::validateMultiple($article_langs) && $isValid; + } else { + foreach(\Yii::$app->request->post()['ArticleLang'] as $index => $article_lang) { + $article_langs[$index] = new ArticleLang(); + } + ArticleLang::loadMultiple($article_langs, \Yii::$app->request->post()); + $isValid = ArticleLang::validateMultiple($article_langs) && $isValid; + } + } else { + $article_langs[$default_lang->language_id] = new ArticleLang(); + } + if($isValid) { + $article->save(false); + $article_categories = \Yii::$app->request->post('Article')['articleCategoriesArray']; + if(!empty($article_categories)) { + foreach($article_categories as $article_category) { + $articletocategory[$article_category] = new ArticleToCategory(); + $articletocategory[$article_category]->article_category_id = $article_category; + $articletocategory[$article_category]->link('article', $article); + } + } + $first = 1; + foreach($images as $lang => $value) { + foreach($value as $type => $fields) { + $images[$lang][$type]->upload($article->article_id); + if($first && $type != 'additional') { + $media_clone = clone $images[$lang][$type]; + $media_clone->setIsNewRecord(true); + unset($media_clone->article_media_id); + $media_clone->language_id = 0; + $media_clone->upload($article->article_id); + unset($media_clone); + $first = 0; + } + } + } + $first = 1; + foreach($article_langs as $article_lang) { + if($first) { + $article_lang_clone = clone $article_lang; + $article_lang_clone->language_id = 0; + $article_lang_clone->link('article', $article); + unset($article_lang_clone); + } + $article_lang->link('article', $article); + $first = 0; + } + echo "ok"; + //$this->redirect('index'); + } else { + return $this->render('create', [ + 'article_langs' => $article_langs, + 'article' => $article, + 'langs' => $langs, + 'images' => $images + ]); + } + } + + public function actionUpdate($id) + { + $article = Article::findOne($id); + $imagestack = $article->getArticleMedia()->all(); + $images = []; + $images[0]['additional'][0] = new ArticleMedia(['scenario' => ArticleMedia::SCENARIO_ADDITIONAL]); + $images[0]['additional'][0]->type = 'additional'; + $images[0]['additional'][0]->language_id = 0; + foreach($imagestack as $image) { + if(in_array($image->type, ['full', 'preview'])) { + $images[$image->language_id][$image->type] = $image; + $images[$image->language_id][$image->type]->scenario = $image->type; + } else { + $images[$image->language_id][$image->type][$image->article_media_id] = $image; + $images[$image->language_id][$image->type][$image->article_media_id]->scenario = $image->type; + } + } + foreach($images as $lang => $value) { + $images[$lang]['additional'][0] = new ArticleMedia(['scenario' => ArticleMedia::SCENARIO_ADDITIONAL]); + } + $article_langs = $article->getArticleLangs()->where(['>=', 'language_id', '1'])->indexBy('language_id')->all(); + $langs = Language::getActiveLanguages(); + $default_lang = Language::getDefaultLang(); + $isValid = false; + if(!empty(\Yii::$app->request->post())) { + $isValid = true; + $article->load(\Yii::$app->request->post()); + ArticleToCategory::deleteAll(['article_id' => $article->article_id]); + $article_categories = \Yii::$app->request->post('Article')['articleCategoriesArray']; + if(!empty($article_categories)) { + foreach($article_categories as $article_category) { + $articletocategory[$article_category] = new ArticleToCategory(); + $articletocategory[$article_category]->article_category_id = $article_category; + $articletocategory[$article_category]->link('article', $article); + } + } + $isValid = $article->validate(); + $images[0]['additional'][0]->type = 'additional'; + $images[0]['additional'][0]->language_id = 0; + $images[0]['additional'][0]->imageFile = UploadedFile::getInstances($images[0]['additional'][0], "[0][additional]imageFile"); + $isValid = $images[0]['additional'][0]->validate(['imageFile']) && $isValid; + foreach(\Yii::$app->request->post()['ArticleMedia'] as $lang => $value) { + foreach($value as $type => $fields) { + if(!in_array($type, ['full', 'preview'])) continue; + $images[$lang][$type] = new ArticleMedia(['scenario' => $type]); + $images[$lang][$type]->language_id = $lang; + $images[$lang][$type]->type = $type; + $images[$lang][$type]->imageFile = UploadedFile::getInstance($images[$lang][$type], "[{$lang}][{$type}]imageFile"); + $isValid = $images[$lang][$type]->validate(['imageFile']) && $isValid; + } + } + if(empty(\Yii::$app->request->post()['ArticleLang'])) { + $isValid = ArticleLang::validateMultiple($article_langs) && $isValid; + } else { + foreach(\Yii::$app->request->post()['ArticleLang'] as $index => $article_lang) { + if (!array_key_exists($index, $article_langs)) { + $article_langs[$index] = new ArticleLang(); + $article_langs[$index]->article_id = $article->article_id; + } + } + ArticleLang::loadMultiple($article_langs, \Yii::$app->request->post()); + $isValid = ArticleLang::validateMultiple($article_langs) && $isValid; + } + } + if($isValid) { + $article->save(false); + foreach($images as $lang => $value) { + foreach($value as $type => $fields) { + if($type == 'additional') { + $images[$lang][$type][0]->upload($article->id); + } else { + if(!empty($images[$lang][$type]->imageFile)) { + $images[$lang][$type]->replace($article->article_id); + } + } + } + } + foreach($article_langs as $article_lang) { + $article_lang->save(false); + } + echo "ok"; + //$this->redirect('index'); + } else { + return $this->render('update', [ + 'article_langs' => $article_langs, + 'article' => $article, + 'langs' => $langs, + 'images' => $images + ]); + } + } + + public function actionDelete($id) + { + $this->findModel($id)->delete(); + return $this->redirect(['index']); + } + + protected function findModel($id) + { + if (($model = Article::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } +} diff --git a/common/modules/blog/controllers/CategoryController.php b/common/modules/blog/controllers/CategoryController.php new file mode 100644 index 0000000..7759416 --- /dev/null +++ b/common/modules/blog/controllers/CategoryController.php @@ -0,0 +1,219 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['post'] + ] + ] + ]; + } + + public function actionIndex() + { + $dataProvider = new ActiveDataProvider([ + 'query' => ArticleCategory::find(), + 'pagination' => [ + 'pageSize' => 1, + ], + ]); + return $this->render('index', ['dataProvider' => $dataProvider]); + } + + public function actionCreate() + { + $category_langs = array(); + $category = new ArticleCategory(); + $default_lang = Language::getDefaultLang(); + $images = array(); + $images[$default_lang->language_id]['full'] = new ArticleCategoryMedia(['scenario' => ArticleCategoryMedia::SCENARIO_FULL]); + $images[$default_lang->language_id]['preview'] = new ArticleCategoryMedia(['scenario' => ArticleCategoryMedia::SCENARIO_PREVIEW]); + $images[0]['additional'] = new ArticleCategoryMedia(['scenario' => ArticleCategoryMedia::SCENARIO_ADDITIONAL]); + $category->loadDefaultValues(); + $langs = Language::getActiveLanguages(); + $isValid = false; + if(!empty(\Yii::$app->request->post())) { + $isValid = true; + $category->load(\Yii::$app->request->post()); + $isValid = $category->validate(); + foreach(\Yii::$app->request->post()['ArticleCategoryMedia'] as $lang => $value) { + foreach($value as $type => $fields) { + $images[$lang][$type] = new ArticleCategoryMedia(['scenario' => $type]); + $images[$lang][$type]->type = $type; + $images[$lang][$type]->language_id = $lang; + $images[$lang][$type]->imageFile = UploadedFile::getInstance($images[$lang][$type], "[{$lang}][{$type}]imageFile"); + $isValid = $images[$lang][$type]->validate(['imageFile']) && $isValid; + } + } + $images[0]['additional']->language_id = 0; + $images[0]['additional']->type = 'additional'; + $images[0]['additional']->imageFile = UploadedFile::getInstances($images[0]['additional'], "[0][additional]imageFile"); + if(empty(\Yii::$app->request->post()['ArticleCategoryLang'])) { + $category_langs[$default_lang->language_id] = new ArticleCategoryLang(); + $isValid = ArticleCategoryLang::validateMultiple($category_langs) && $isValid; + } else { + foreach(\Yii::$app->request->post()['ArticleCategoryLang'] as $index => $category_lang) { + $category_langs[$index] = new ArticleCategoryLang(); + } + ArticleCategoryLang::loadMultiple($category_langs, \Yii::$app->request->post()); + $isValid = ArticleCategoryLang::validateMultiple($category_langs) && $isValid; + } + } else { + $category_langs[$default_lang->language_id] = new ArticleCategoryLang(); + } + if($isValid) { + $category->save(false); + $first = 1; + foreach($images as $lang => $value) { + foreach($value as $type => $fields) { + $images[$lang][$type]->upload($category->article_category_id); + if($first && $type != 'additional') { + $media_clone = clone $images[$lang][$type]; + $media_clone->setIsNewRecord(true); + unset($media_clone->article_category_media_id); + $media_clone->language_id = 0; + $media_clone->upload($category->article_category_id); + unset($media_clone); + $first = 0; + } + } + } + $first = 1; + foreach($category_langs as $category_lang) { + if($first) { + $category_lang_clone = clone $category_lang; + $category_lang_clone->language_id = 0; + $category_lang_clone->link('category', $category); + unset($category_lang_clone); + } + $category_lang->link('category', $category); + $first = 0; + } + echo "ok"; + //$this->redirect('index'); + } else { + return $this->render('create', [ + 'category_langs' => $category_langs, + 'category' => $category, + 'langs' => $langs, + 'images' => $images + ]); + } + } + + public function actionUpdate($id) + { + $category = ArticleCategory::findOne($id); + $imagestack = $category->getArticleCategoryMedia()->all(); + $images = []; + $images[0]['additional'][0] = new ArticleCategoryMedia(['scenario' => ArticleCategoryMedia::SCENARIO_ADDITIONAL]); + $images[0]['additional'][0]->type = 'additional'; + $images[0]['additional'][0]->language_id = 0; + foreach($imagestack as $image) { + if(in_array($image->type, ['full', 'preview'])) { + $images[$image->language_id][$image->type] = $image; + $images[$image->language_id][$image->type]->scenario = $image->type; + } else { + $images[$image->language_id][$image->type][$image->article_category_media_id] = $image; + $images[$image->language_id][$image->type][$image->article_category_media_id]->scenario = $image->type; + } + } + foreach($images as $lang => $value) { + $images[$lang]['additional'][0] = new ArticleCategoryMedia(['scenario' => ArticleCategoryMedia::SCENARIO_ADDITIONAL]); + } + $category_langs = $category->getArticleCategoryLangs()->where(['>=', 'language_id', '1'])->indexBy('language_id')->all(); + $langs = Language::getActiveLanguages(); + $default_lang = Language::getDefaultLang(); + $isValid = false; + if(!empty(\Yii::$app->request->post())) { + $isValid = true; + $category->load(\Yii::$app->request->post()); + $isValid = $category->validate(); + $images[0]['additional'][0]->type = 'additional'; + $images[0]['additional'][0]->language_id = 0; + $images[0]['additional'][0]->imageFile = UploadedFile::getInstances($images[0]['additional'][0], "[0][additional]imageFile"); + $isValid = $images[0]['additional'][0]->validate(['imageFile']) && $isValid; + foreach(\Yii::$app->request->post()['ArticleCategoryMedia'] as $lang => $value) { + foreach($value as $type => $fields) { + if(!in_array($type, ['full', 'preview'])) continue; + $images[$lang][$type] = new ArticleCategoryMedia(['scenario' => $type]); + $images[$lang][$type]->language_id = $lang; + $images[$lang][$type]->type = $type; + $images[$lang][$type]->imageFile = UploadedFile::getInstance($images[$lang][$type], "[{$lang}][{$type}]imageFile"); + $isValid = $images[$lang][$type]->validate(['imageFile']) && $isValid; + } + } + if(empty(\Yii::$app->request->post()['ArticleCategoryLang'])) { + $isValid = ArticleCategoryLang::validateMultiple($category_langs) && $isValid; + } else { + foreach(\Yii::$app->request->post()['ArticleCategoryLang'] as $index => $category_lang) { + if(!array_key_exists($index, $category_langs)) { + $category_langs[$index] = new ArticleCategoryLang(); + $category_langs[$index]->article_category_id = $category->article_category_id; + } + } + ArticleCategoryLang::loadMultiple($category_langs, \Yii::$app->request->post()); + $isValid = ArticleCategoryLang::validateMultiple($category_langs) && $isValid; + } + } + if($isValid) { + $category->save(false); + foreach($images as $lang => $value) { + foreach($value as $type => $fields) { + if($type == 'additional') { + $images[$lang][$type][0]->upload($category->article_category_id); + } else { + if(!empty($images[$lang][$type]->imageFile)) { + $images[$lang][$type]->replace($category->article_category_id); + } + } + } + } + foreach($category_langs as $category_lang) { + $category_lang->save(false); + } + echo "ok"; + //$this->redirect('index'); + } else { + return $this->render('update', [ + 'category_langs' => $category_langs, + 'category' => $category, + 'langs' => $langs, + 'images' => $images + ]); + } + } + + public function actionDelete($id) + { + $this->findModel($id)->delete(); + return $this->redirect(['index']); + } + + protected function findModel($id) + { + if (($model = ArticleCategory::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } +} diff --git a/common/modules/blog/controllers/DefaultController.php b/common/modules/blog/controllers/DefaultController.php new file mode 100644 index 0000000..3e2294c --- /dev/null +++ b/common/modules/blog/controllers/DefaultController.php @@ -0,0 +1,12 @@ +render('index'); + } +} diff --git a/common/modules/blog/controllers/MediaController.php b/common/modules/blog/controllers/MediaController.php new file mode 100644 index 0000000..e1d91f5 --- /dev/null +++ b/common/modules/blog/controllers/MediaController.php @@ -0,0 +1,45 @@ +request->isPost) { + $model->imageFile = UploadedFile::getInstance($model, 'imageFile'); + if($model->upload()) { + return true; + } else { + return false; + } + } + return $this->render('index', ['model' => $model]); + } + + public function actionCreate() + { + + } + + public function actionUpdate($id) + { + + } + + public function actionDelete($id) + { + $model = Media::findOne($id); + return $model->delete(); + } + + protected function findModel($id) + { + + } +} diff --git a/common/modules/blog/controllers/TestController.php b/common/modules/blog/controllers/TestController.php new file mode 100644 index 0000000..5f77485 --- /dev/null +++ b/common/modules/blog/controllers/TestController.php @@ -0,0 +1,17 @@ +language_id] = new ArticleLang(); + $model[3] = new ArticleLang(); + return $this->render('index', ['model' => $model]); + } +} \ No newline at end of file diff --git a/common/modules/blog/models/Article.php b/common/modules/blog/models/Article.php new file mode 100644 index 0000000..85584b1 --- /dev/null +++ b/common/modules/blog/models/Article.php @@ -0,0 +1,163 @@ + Autocomplete::className(), + 'attributes' => [ + 'translit' => ['code'], + ] + ] + ]; + } + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['sort', 'article_pid', 'status', 'comment', 'vote'], 'integer'], + [['date_add', 'date_update'], 'safe'], + [['code'], 'required'], + [['code', 'tag'], 'string'] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'article_id' => Yii::t('app', 'ID'), + 'sort' => Yii::t('app', 'Sort'), + 'date_add' => Yii::t('app', 'Create At'), + 'date_update' => Yii::t('app', 'Update At'), + 'code' => Yii::t('app', 'Code'), + 'user_id' => Yii::t('app', 'Author'), + 'tag' => Yii::t('app', 'Tags'), + 'article_pid' => Yii::t('app', 'Parent ID'), + 'status' => Yii::t('app', 'Active'), + 'comment' => Yii::t('app', 'Comments'), + 'vote' => Yii::t('app', 'Voting'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getParent() + { + return $this->hasOne(Article::className(), ['article_id' => 'article_pid']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticles() + { + return $this->hasMany(Article::className(), ['article_pid' => 'article_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getUser() + { + return $this->hasOne(User::className(), ['id' => 'user_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleLangs() + { + return $this->hasMany(ArticleLang::className(), ['article_id' => 'article_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleMedia() + { + return $this->hasMany(ArticleMedia::className(), ['article_id' => 'article_id']); + } + + public function getMedia() + { + return $this->hasMany(Media::className(), ['article_id' => 'media_id'])->via('articleMedia'); + } + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleToCategories() + { + return $this->hasMany(ArticleToCategory::className(), ['article_id' => 'article_id']); + } + + public function getArticleCategories() + { + return $this->hasMany(ArticleCategory::className(), ['article_category_id' => 'article_category_id'])->viaTable('article_to_category', ['article_id' => 'article_category_id']); + } + + public static function findArticleDropdown($id) + { + $query = new Query(); + return $query->select(['l.name', 'a.article_id']) + ->from(['article a']) + ->leftJoin(['article_lang l'], 'a.article_id = l.article_id') + ->where(['l.language_id' => 0, 'a.status' => 1]) + ->andWhere(['not', ['a.article_id' => $id]]) + ->indexBy('article_id') + ->column(); + } + + public function getArticleCategoriesArray() + { + return $this->getArticleToCategories()->select('article_category_id')->column(); + } + +} diff --git a/common/modules/blog/models/ArticleCategory.php b/common/modules/blog/models/ArticleCategory.php new file mode 100644 index 0000000..f91721f --- /dev/null +++ b/common/modules/blog/models/ArticleCategory.php @@ -0,0 +1,133 @@ + Autocomplete::className(), + 'attributes' => [ + 'translit' => ['code'], + ] + ] + ]; + } + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['status', 'sort', 'article_category_pid'], 'integer'], + [['code'], 'required'], + [['code', 'tag'], 'string'], + [['date_add', 'date_update'], 'safe'], + [['status'], 'boolean'], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'article_category_id' => Yii::t('app', 'ID'), + 'status' => Yii::t('app', 'Active'), + 'sort' => Yii::t('app', 'Sort'), + 'code' => Yii::t('app', 'Code'), + 'date_add' => Yii::t('app', 'Created At'), + 'date_update' => Yii::t('app', 'Updated At'), + 'tag' => Yii::t('app', 'Tags'), + 'article_category_pid' => Yii::t('app', 'Parent ID'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticles() + { + return $this->hasMany(Article::className(), ['article_id' => 'article_id'])->viaTable('article_to_category', ['article_category_id' => 'article_category_id']) ; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getParent() + { + return $this->hasOne(ArticleCategory::className(), ['article_category_id' => 'article_category_pid']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleCategories() + { + return $this->hasMany(ArticleCategory::className(), ['article_category_pid' => 'article_category_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleCategoryLangs() + { + return $this->hasMany(ArticleCategoryLang::className(), ['article_category_id' => 'article_category_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticleCategoryMedia() + { + return $this->hasMany(ArticleCategoryMedia::className(), ['article_category_id' => 'article_category_id']); + } + + public static function findArticleCategoryDropdown($id) + { + $query = new Query(); + return $query->select(['l.name', 'c.article_category_id']) + ->from(['article_category c']) + ->leftJoin(['article_category_lang l'], 'c.article_category_id = l.article_category_id') + ->where(['l.language_id' => 0, 'c.status' => 1]) + ->andWhere(['not', ['c.article_category_id' => $id]]) + ->indexBy('article_category_id') + ->column(); + } + +} diff --git a/common/modules/blog/models/ArticleCategoryLang.php b/common/modules/blog/models/ArticleCategoryLang.php new file mode 100644 index 0000000..1851437 --- /dev/null +++ b/common/modules/blog/models/ArticleCategoryLang.php @@ -0,0 +1,102 @@ + Autocomplete::className(), + 'attributes' => [ + 'repeat' => [['preview', 'text', false, 5, true, '...']], + ] + ] + ]; + } + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['language_id', 'article_category_id'], 'integer'], + [['text', 'name'], 'required'], + [['text', 'preview', 'seo_url', 'name', 'meta_title', 'meta_descr', 'meta_keyword', 'h1_tag', 'tag'], 'string'], + ['seo_url', function($attribute, $params) { + $pattern = "/^[a-zA-Z\d_-]+$/"; + if(!preg_match($pattern, $this->$attribute)) { + $this->addError($attribute, Yii::t('app', "Pattern doesn't match.")); + } + }] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'article_category_language_id' => Yii::t('app', 'ID'), + 'language_id' => Yii::t('app', 'Lang ID'), + 'article_category_id' => Yii::t('app', 'Category ID'), + 'text' => Yii::t('app', 'Text'), + 'preview' => Yii::t('app', 'Preview'), + 'seo_url' => Yii::t('app', 'Seo Url'), + 'name' => Yii::t('app', 'Name'), + 'meta_title' => Yii::t('app', 'Meta Title'), + 'meta_descr' => Yii::t('app', 'Meta Descr'), + 'meta_keyword' => Yii::t('app', 'Meta Keywords'), + 'h1_tag' => Yii::t('app', 'H1 Tag'), + 'tag' => Yii::t('app', 'Tags'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getCategory() + { + return $this->hasOne(ArticleCategory::className(), ['article_category_id' => 'article_category_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getLang() + { + return $this->hasOne(Language::className(), ['language_id' => 'language_id']); + } +} diff --git a/common/modules/blog/models/ArticleCategoryMedia.php b/common/modules/blog/models/ArticleCategoryMedia.php new file mode 100644 index 0000000..a923910 --- /dev/null +++ b/common/modules/blog/models/ArticleCategoryMedia.php @@ -0,0 +1,162 @@ + 10], + [['imageFile'], 'file', 'extensions' => 'png, gif, jpg, jpeg', 'skipOnEmpty' => true, 'on' => self::SCENARIO_FULL], + [['imageFile'], 'file', 'extensions' => 'png, gif, jpg, jpeg', 'skipOnEmpty' => true, 'on' => self::SCENARIO_PREVIEW], + [['imageFile'], 'file', 'extensions' => 'png, gif, jpg, jpeg', 'skipOnEmpty' => true, 'maxFiles' => 10, 'on' => self::SCENARIO_ADDITIONAL] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'article_category_media_id' => Yii::t('app', 'ID'), + 'article_category_id' => Yii::t('app', 'Category ID'), + 'media_id' => Yii::t('app', 'Media ID'), + 'media_alt' => Yii::t('app', 'Media Alt'), + 'media_title' => Yii::t('app', 'Media Title'), + 'media_caption' => Yii::t('app', 'Media Caption'), + 'type' => Yii::t('app', 'Type'), + 'imageFile' => Yii::t('app', 'Image File'), + 'language_id' => Yii::t('app', 'Language ID'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getCategory() + { + return $this->hasOne(ArticleCategory::className(), ['article_category_id' => 'article_category_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getMedia() + { + return $this->hasOne(Media::className(), ['media_id' => 'media_id']); + } + + public function upload($category_id) + { + $this->article_category_id = $category_id; + if(is_array($this->imageFile)) { + $ok = true; + foreach($this->imageFile as $image) { + $media_category = clone $this; + $media = new Media(); + $media->imageFile = $image; + $media->upload(); + $media_category->media_id = $media->media_id; + $ok = $media_category->save() && $ok; + unset($media_category); + } + return $ok; + } elseif(!empty($this->imageFile)) { + $media = new Media(); + $media->imageFile = $this->imageFile; + $media->upload(); + $this->media_id = $media->media_id; + return $this->save(); + } + } + + public function replace($category_id, $removeMedia = false) + { + $this->article_category_id = $category_id; + if($removeMedia) { + $category_media = ArticleCategoryMedia::find()->select('media_id')->where(['article_category_id' => $this->article_category_id, 'type' => $this->type])->column(); + $media = array(); + foreach($category_media as $media_id) { + $media[] = Media::findOne(['media_id' => $media_id]); + } + $media = array_unique($media); + foreach($media as $one_media) { + if($one_media instanceof Media) { + $one_media->delete(); + } + } + unset($media); + unset($category_media); + } + if(is_array($this->imageFile)) { + $ok = true; + foreach($this->imageFile as $image) { + $media_category = clone $this; + $media = new Media(); + $media->imageFile = $image; + $media->upload(); + $media_category->media_id = $media->media_id; + $ok = $media_category->save() && $ok; + unset($media_category); + } + return $ok; + } elseif(!empty($this->imageFile)) { + ArticleCategoryMedia::deleteAll(['category_id' => $this->article_category_id, 'type' => $this->type]); + $media = new Media(); + $media->imageFile = $this->imageFile; + $media->upload(); + $this->media_id = $media->media_id; + $this->setIsNewRecord(true); + return $this->save(); + } + } + +} diff --git a/common/modules/blog/models/ArticleLang.php b/common/modules/blog/models/ArticleLang.php new file mode 100644 index 0000000..378b266 --- /dev/null +++ b/common/modules/blog/models/ArticleLang.php @@ -0,0 +1,85 @@ + Yii::t('app', 'ID'), + 'language_id' => Yii::t('app', 'Lang ID'), + 'article_id' => Yii::t('app', 'Article ID'), + 'text' => Yii::t('app', 'Text'), + 'seo_url' => Yii::t('app', 'Seo Url'), + 'name' => Yii::t('app', 'Name'), + 'preview' => Yii::t('app', 'Preview'), + 'meta_title' => Yii::t('app', 'Meta Title'), + 'meta_descr' => Yii::t('app', 'Meta Descr'), + 'meta_keyword' => Yii::t('app', 'Meta Keywords'), + 'h1_tag' => Yii::t('app', 'H1 Tag'), + 'tag' => Yii::t('app', 'Tags'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticle() + { + return $this->hasOne(Article::className(), ['article_id' => 'article_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getLang() + { + return $this->hasOne(Language::className(), ['language_id' => 'language_id']); + } +} diff --git a/common/modules/blog/models/ArticleMedia.php b/common/modules/blog/models/ArticleMedia.php new file mode 100644 index 0000000..42ccfa0 --- /dev/null +++ b/common/modules/blog/models/ArticleMedia.php @@ -0,0 +1,161 @@ + 10], + [['imageFile'], 'file', 'extensions' => 'png, gif, jpg, jpeg', 'skipOnEmpty' => true, 'on' => self::SCENARIO_FULL], + [['imageFile'], 'file', 'extensions' => 'png, gif, jpg, jpeg', 'skipOnEmpty' => true, 'on' => self::SCENARIO_PREVIEW], + [['imageFile'], 'file', 'extensions' => 'png, gif, jpg, jpeg', 'skipOnEmpty' => true, 'maxFiles' => 10, 'on' => self::SCENARIO_ADDITIONAL] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'article_media_id' => Yii::t('app', 'ID'), + 'article_id' => Yii::t('app', 'Article ID'), + 'media_id' => Yii::t('app', 'Media ID'), + 'type' => Yii::t('app', 'Type'), + 'media_alt' => Yii::t('app', 'Media Alt'), + 'media_title' => Yii::t('app', 'Media Title'), + 'media_caption' => Yii::t('app', 'Media Caption'), + 'imageFile' => Yii::t('app', 'Image File'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticle() + { + return $this->hasOne(Article::className(), ['article_id' => 'article_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getMedia() + { + return $this->hasOne(Media::className(), ['media_id' => 'media_id']); + } + + public function upload($article_id) + { + $this->article_id = $article_id; + if(is_array($this->imageFile)) { + $ok = true; + foreach($this->imageFile as $image) { + $media_article = clone $this; + $media = new Media(); + $media->imageFile = $image; + $media->upload(); + $media_article->media_id = $media->media_id; + $ok = $media_article->save() && $ok; + unset($media_article); + } + return $ok; + } elseif(!empty($this->imageFile)) { + $media = new Media(); + $media->imageFile = $this->imageFile; + $media->upload(); + $this->media_id = $media->media_id; + return $this->save(); + } + } + + public function replace($article_id, $removeMedia = false) + { + $this->article_id = $article_id; + if($removeMedia && !$this->getIsNewRecord()) { + $article_media = ArticleMedia::find()->select('media_id')->where(['article_id' => $this->article_id, 'type' => $this->type, 'language_id' => $this->language_id])->column(); + $media = array(); + foreach($article_media as $media_id) { + $media[] = Media::findOne(['media_id' => $media_id]); + } + $media = array_unique($media); + foreach($media as $one_media) { + if($one_media instanceof Media) { + $one_media->delete(); + } + } + unset($media); + unset($article_media); + } + if(is_array($this->imageFile)) { + $ok = true; + foreach($this->imageFile as $image) { + $media_article = clone $this; + $media = new Media(); + $media->imageFile = $image; + $media->upload(); + $media_article->media_id = $media->media_id; + $ok = $media_article->save() && $ok; + unset($media_article); + } + return $ok; + } elseif(!empty($this->imageFile)) { + ArticleMedia::deleteAll(['article_id' => $this->article_id, 'type' => $this->type, 'language_id' => $this->language_id]); + $media = new Media(); + $media->imageFile = $this->imageFile; + $media->upload(); + $this->media_id = $media->media_id; + $this->setIsNewRecord(true); + return $this->save(); + } + } + +} diff --git a/common/modules/blog/models/ArticleToCategory.php b/common/modules/blog/models/ArticleToCategory.php new file mode 100644 index 0000000..f665d84 --- /dev/null +++ b/common/modules/blog/models/ArticleToCategory.php @@ -0,0 +1,62 @@ + Yii::t('app', 'Article ID'), + 'article_category_id' => Yii::t('app', 'Category ID'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getArticle() + { + return $this->hasOne(Article::className(), ['article_id' => 'article_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getCategory() + { + return $this->hasOne(ArticleCategory::className(), ['article_category_id' => 'article_category_id']); + } +} diff --git a/common/modules/blog/views/ajax/_article_form.php b/common/modules/blog/views/ajax/_article_form.php new file mode 100644 index 0000000..47e23d4 --- /dev/null +++ b/common/modules/blog/views/ajax/_article_form.php @@ -0,0 +1,38 @@ + +
    + + $model, 'attribute' => "[$model->language_id]language_id"]))->label(false)->hiddenInput(['value' => $model->language_id]) ?> + + $model, 'attribute' => "[$model->language_id]text", 'form' => $form]))->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ]]); ?> + + $model, 'attribute' => "[$model->language_id]preview", 'form' => $form]))->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ]]); ?> + + $model, 'attribute' => "[$model->language_id]seo_url"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]name"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]meta_title"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]meta_descr"]))->textarea() ?> + + $model, 'attribute' => "[$model->language_id]meta_keyword"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]h1_tag"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]tag"]))->textInput() ?> + +
    +end(); + } +?> diff --git a/common/modules/blog/views/ajax/_article_form_test.php b/common/modules/blog/views/ajax/_article_form_test.php new file mode 100644 index 0000000..fbadb64 --- /dev/null +++ b/common/modules/blog/views/ajax/_article_form_test.php @@ -0,0 +1,38 @@ + +
    + + $model, 'attribute' => "[$model->language_id]language_id"]))->label(false)->hiddenInput(['value' => $model->language_id]) ?> + + $model, 'attribute' => "[$model->language_id]text", 'form' => $form]))->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ]]); ?> + + $model, 'attribute' => "[$model->language_id]preview", 'form' => $form]))->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ]]); ?> + + $model, 'attribute' => "[$model->language_id]seo_url"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]name"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]meta_title"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]meta_descr"]))->textarea() ?> + + $model, 'attribute' => "[$model->language_id]meta_keyword"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]h1_tag"]))->textInput() ?> + + $model, 'attribute' => "[$model->language_id]tag"]))->textInput() ?> + +
    +end(); + } +?> diff --git a/common/modules/blog/views/ajax/_article_media_form.php b/common/modules/blog/views/ajax/_article_media_form.php new file mode 100644 index 0000000..8ff29f3 --- /dev/null +++ b/common/modules/blog/views/ajax/_article_media_form.php @@ -0,0 +1,17 @@ + +
    + + $article_lang, 'attribute' => "[$model->language_id][$type]language_id"]))->label(false)->hiddenInput(['value' => $model->language_id]) ?> + + $article_lang, 'attribute' => "[$model->language_id][$type]imageFile"]))->fileInput(['class' => 'image_inputs_field']) ?> + +
    +end(); +?> diff --git a/common/modules/blog/views/ajax/_category_form.php b/common/modules/blog/views/ajax/_category_form.php new file mode 100644 index 0000000..521317b --- /dev/null +++ b/common/modules/blog/views/ajax/_category_form.php @@ -0,0 +1,33 @@ + +
    + + $category_lang, 'attribute' => "[$model->language_id]language_id"]))->label(false)->hiddenInput(['value' => $model->language_id]) ?> + + $category_lang, 'attribute' => "[$model->language_id]text", 'form' => $form]))->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ]]); ?> + + $category_lang, 'attribute' => "[$model->language_id]preview", 'form' => $form]))->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ]]); ?> + + $category_lang, 'attribute' => "[$model->language_id]seo_url"]))->textInput() ?> + + $category_lang, 'attribute' => "[$model->language_id]name"]))->textInput() ?> + + $category_lang, 'attribute' => "[$model->language_id]meta_title"]))->textInput() ?> + + $category_lang, 'attribute' => "[$model->language_id]meta_descr"]))->textarea() ?> + + $category_lang, 'attribute' => "[$model->language_id]meta_keyword"]))->textInput() ?> + + $category_lang, 'attribute' => "[$model->language_id]h1_tag"]))->textInput() ?> + + $category_lang, 'attribute' => "[$model->language_id]tag"]))->textInput() ?> + +
    +end(); +?> diff --git a/common/modules/blog/views/article/_form.php b/common/modules/blog/views/article/_form.php new file mode 100644 index 0000000..30eb077 --- /dev/null +++ b/common/modules/blog/views/article/_form.php @@ -0,0 +1,172 @@ + +
    + + ['enctype' => 'multipart/form-data']]); ?> + + field($article, 'code')->hint(Yii::t('app', 'Insensitive latin non-space'))->textInput() ?> + + field($article, 'tag')->hint(Yii::t('app', 'Comma-separated'))->textInput() ?> + + field($article, 'sort')->input('number') ?> + + field($article, 'article_pid') + ->dropDownList(Article::findArticleDropdown($article->article_id), ['prompt' => Yii::t('app', 'Select parent')]) ?> + + field($article, 'articleCategoriesArray') + ->dropDownList(ArticleCategory::findArticleCategoryDropdown(NULL), ['multiple' => 'multiple'])->label(\Yii::t('app', 'Article Categories Array')); ?> + + field($article, 'status')->checkbox() ?> + + +
    +
    + Url::to(['/blog/ajax/article-media-form?type=full']), + 'form' => $form, + 'data_langs' => $article->getIsNewRecord()?$images:ArticleMedia::find()->where(['article_id' => $article->article_id, 'type' => 'full'])->indexBy('language_id')->all() + ]); + $first = 1; + foreach($images as $lang => $value) { + if(!array_key_exists('full', $value)) continue; + ?> +
    + field($images[$lang]['full'], "[{$lang}][full]language_id")->label(false)->hiddenInput(['value' => $lang]); + echo $form->field($images[$lang]['full'], "[{$lang}][full]imageFile")->fileInput(['class' => 'image_inputs_field']); + if(!empty($images[$lang]['full']->article_media_id)) { + echo "media->hash}/original.{$images[$lang]['full']->media->extension}' width='100' class='image_inputs_prev'>"; + } + ?> +
    + end(); + ?> +
    +
    + Url::to(['/blog/ajax/article-media-form?type=preview']), + 'form' => $form, + 'data_langs' => $article->getIsNewRecord()?$images:ArticleMedia::find()->where(['article_id' => $article->article_id, 'type' => 'preview'])->indexBy('language_id')->all() + ]); + $first = 1; + foreach($images as $lang => $value) { + if(!array_key_exists('preview', $value)) continue; + ?> +
    + field($images[$lang]['preview'], "[{$lang}][preview]language_id")->label(false)->hiddenInput(['value' => $lang]); + echo $form->field($images[$lang]['preview'], "[{$lang}][preview]imageFile")->fileInput(['class' => 'image_inputs_field']); + if(!empty($images[$lang]['preview']->article_media_id)) { + echo "media->hash}/original.{$images[$lang]['preview']->media->extension}' width='100' class='image_inputs_prev'>"; + } + ?> +
    + end(); + ?> +
    +
    + field(is_array($images[0]['additional'])?$images[0]['additional'][0]:$images[0]['additional'], "[0][additional]imageFile[]")->fileInput(['multiple' => 'multiple', 'class' => 'image_inputs_field']); + if(is_array($images[0]['additional']) && count($images[0]['additional']) > 1) { + foreach($images[0]['additional'] as $onefield => $oneimage) { + if($onefield) { + ?> + + +
    +
    + +
    + + $article_langs, + 'form' => $form, + 'ajaxView' => '@common/modules/blog/views/ajax/_article_form', + ]); + /* + $multilang = Multilang::begin(['ajaxpath' => Url::to(['/blog/ajax/article-form']), 'form' => $form, 'data_langs' => $article_langs]); + ?> + $article_lang) { + ?> +
    + + field($article_langs[$index], "[$index]language_id")->label(false)->hiddenInput(['value' => $index]) ?> + + field($article_langs[$index], "[$index]text")->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ], ]); ?> + + field($article_langs[$index], "[$index]preview")->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ], ]); ?> + + field($article_langs[$index], "[$index]seo_url")->textInput() ?> + + field($article_langs[$index], "[$index]name")->textInput() ?> + + field($article_langs[$index], "[$index]meta_title")->textInput() ?> + + field($article_langs[$index], "[$index]meta_descr")->textarea(); ?> + + field($article_langs[$index], "[$index]meta_keywords")->textInput() ?> + + field($article_langs[$index], "[$index]h1_tag")->textInput() ?> + + field($article_langs[$index], "[$index]tags")->textInput() ?> + +
    + + end(); + */ + ?> + +
    + isNewRecord ? Yii::t('app', 'Create') : Yii::t('app', 'Update'), ['class' => $article->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
    + + + +
    + \ No newline at end of file diff --git a/common/modules/blog/views/article/create.php b/common/modules/blog/views/article/create.php new file mode 100644 index 0000000..6cd9da6 --- /dev/null +++ b/common/modules/blog/views/article/create.php @@ -0,0 +1,19 @@ +title = Yii::t('app', 'Article create'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Articles'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'article_langs' => $article_langs, + 'article' => $article, + 'langs' => $langs, + 'images' => $images + ]) ?> + +
    diff --git a/common/modules/blog/views/article/index.php b/common/modules/blog/views/article/index.php new file mode 100644 index 0000000..ce83f97 --- /dev/null +++ b/common/modules/blog/views/article/index.php @@ -0,0 +1,32 @@ + $dataProvider, + 'columns' => [ + 'article_id', + 'code', + 'date_add', + [ + 'value' => function($data) { + return $data->user->firstname.' '.$data->user->lastname; + }, + 'header' => Yii::t('app', 'Author') + ], + [ + 'class' => Column::className(), + 'header' => Yii::t('app', 'Name'), + 'content' => function($model, $key, $index, $column) { + return $model->getArticleLangs()->where(['language_id' => Language::getDefaultLang()->language_id])->one()->name; + } + ], + [ + 'class' => ActionColumn::className(), + 'template' => '{update} {delete}' + ] + ] +]); \ No newline at end of file diff --git a/common/modules/blog/views/article/update.php b/common/modules/blog/views/article/update.php new file mode 100644 index 0000000..4458a2b --- /dev/null +++ b/common/modules/blog/views/article/update.php @@ -0,0 +1,18 @@ +title = Yii::t('app', 'Article update'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Articles'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + render('_form', [ + 'article_langs' => $article_langs, + 'article' => $article, + 'langs' => $langs, + 'images' => $images + ]) ?> + +
    diff --git a/common/modules/blog/views/category/_form.php b/common/modules/blog/views/category/_form.php new file mode 100644 index 0000000..001da73 --- /dev/null +++ b/common/modules/blog/views/category/_form.php @@ -0,0 +1,158 @@ + +
    + + ['enctype' => 'multipart/form-data']]); ?> + + field($category, 'code')->hint(Yii::t('app', 'Insensitive latin non-space'))->textInput() ?> + + field($category, 'tag')->hint(Yii::t('app', 'Comma-separated'))->textInput() ?> + + field($category, 'sort')->input('number') ?> + + field($category, 'article_category_pid') + ->dropDownList(ArticleCategory::findArticleCategoryDropdown($category->article_category_id), ['prompt' => Yii::t('app', 'Select parent')]) ?> + + field($category, 'status')->checkbox() ?> + + +
    +
    + Url::to(['/blog/ajax/article-category-media-form?type=full']), + 'form' => $form, + 'data_langs' => $category->getIsNewRecord()?$images:ArticleCategoryMedia::find()->where(['article_category_id' => $category->article_category_id, 'type' => 'full'])->indexBy('language_id')->all() + ]); + $first = 1; + foreach($images as $lang => $value) { + if(!array_key_exists('full', $value)) continue; + ?> +
    + field($images[$lang]['full'], "[{$lang}][full]language_id")->label(false)->hiddenInput(['value' => $lang]); + echo $form->field($images[$lang]['full'], "[{$lang}][full]imageFile")->fileInput(['class' => 'image_inputs_field']); + if(!empty($images[$lang]['full']->article_category_media_id)) { + echo "media->hash}/original.{$images[$lang]['full']->media->extension}' width='100' class='image_inputs_prev'>"; + } + ?> +
    + end(); + ?> +
    +
    + Url::to(['/blog/ajax/article-category-media-form?type=preview']), + 'form' => $form, + 'data_langs' => $category->getIsNewRecord()?$images:ArticleCategoryMedia::find()->where(['article_category_id' => $category->article_category_id, 'type' => 'preview'])->indexBy('language_id')->all() + ]); + $first = 1; + foreach($images as $lang => $value) { + if(!array_key_exists('preview', $value)) continue; + ?> +
    + field($images[$lang]['preview'], "[{$lang}][preview]language_id")->label(false)->hiddenInput(['value' => $lang]); + echo $form->field($images[$lang]['preview'], "[{$lang}][preview]imageFile")->fileInput(['class' => 'image_inputs_field']); + if(!empty($images[$lang]['preview']->article_category_media_id)) { + echo "media->hash}/original.{$images[$lang]['preview']->media->extension}' width='100' class='image_inputs_prev'>"; + } + ?> +
    + end(); + ?> +
    +
    + field(is_array($images[0]['additional'])?$images[0]['additional'][0]:$images[0]['additional'], "[0][additional]imageFile[]")->fileInput(['multiple' => 'multiple', 'class' => 'image_inputs_field']); + if(is_array($images[0]['additional']) && count($images[0]['additional']) > 1) { + foreach($images[0]['additional'] as $onefield => $oneimage) { + if($onefield) { + ?> + + +
    +
    + +
    + + Url::to(['/blog/ajax/category-form']), 'form' => $form, 'data_langs' => $category_langs]) + ?> + $category_lang) { + ?> +
    + field($category_langs[$index], "[$index]language_id")->label(false)->hiddenInput(['value' => $index]) ?> + + field($category_langs[$index], "[$index]text")->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ], ]); ?> + + field($category_langs[$index], "[$index]preview")->widget(CKEditor::className(),['editorOptions' => [ 'preset' => 'full', 'inline' => false, ], ]); ?> + + field($category_langs[$index], "[$index]seo_url")->textInput() ?> + + field($category_langs[$index], "[$index]name")->textInput() ?> + + field($category_langs[$index], "[$index]meta_title")->textInput() ?> + + field($category_langs[$index], "[$index]meta_descr")->textarea(); ?> + + field($category_langs[$index], "[$index]meta_keyword")->textInput() ?> + + field($category_langs[$index], "[$index]h1_tag")->textInput() ?> + + field($category_langs[$index], "[$index]tag")->textInput() ?> + +
    + + end(); + ?> + +
    + isNewRecord ? Yii::t('app', 'Create') : Yii::t('app', 'Update'), ['class' => $category->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
    + + + +
    + \ No newline at end of file diff --git a/common/modules/blog/views/category/create.php b/common/modules/blog/views/category/create.php new file mode 100644 index 0000000..265a30d --- /dev/null +++ b/common/modules/blog/views/category/create.php @@ -0,0 +1,19 @@ +title = Yii::t('app', 'Category create'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Categories'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'category_langs' => $category_langs, + 'category' => $category, + 'langs' => $langs, + 'images' => $images + ]) ?> + +
    diff --git a/common/modules/blog/views/category/index.php b/common/modules/blog/views/category/index.php new file mode 100644 index 0000000..f5e1d9e --- /dev/null +++ b/common/modules/blog/views/category/index.php @@ -0,0 +1,26 @@ + $dataProvider, + 'columns' => [ + 'article_category_id', + 'code', + 'date_add', + 'date_update', + [ + 'class' => Column::className(), + 'header' => Yii::t('app', 'Name'), + 'content' => function($model, $key, $index, $column) { + return $model->getArticleCategoryLangs()->orderBy(['language_id' => 'ASC'])->one()->name; + } + ], + [ + 'class' => ActionColumn::className(), + 'template' => '{update} {delete}' + ] + ] +]); \ No newline at end of file diff --git a/common/modules/blog/views/category/update.php b/common/modules/blog/views/category/update.php new file mode 100644 index 0000000..353c73e --- /dev/null +++ b/common/modules/blog/views/category/update.php @@ -0,0 +1,18 @@ +title = Yii::t('app', 'Update category'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Categories'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + render('_form', [ + 'category_langs' => $category_langs, + 'category' => $category, + 'langs' => $langs, + 'images' => $images + ]) ?> + +
    diff --git a/common/modules/blog/views/default/index.php b/common/modules/blog/views/default/index.php new file mode 100644 index 0000000..17d1a81 --- /dev/null +++ b/common/modules/blog/views/default/index.php @@ -0,0 +1,7 @@ +value); \ No newline at end of file diff --git a/common/modules/blog/views/media/index.php b/common/modules/blog/views/media/index.php new file mode 100644 index 0000000..b931b6b --- /dev/null +++ b/common/modules/blog/views/media/index.php @@ -0,0 +1,37 @@ + ['enctype' => 'multipart/form-data']]); + +echo $form->field($model, 'imageFile')->fileInput(['multiple' => 'multiple']); + +?> + your image +
    + 'btn btn-success']) ?> +
    + + diff --git a/common/modules/blog/views/test/index.php b/common/modules/blog/views/test/index.php new file mode 100644 index 0000000..7244adf --- /dev/null +++ b/common/modules/blog/views/test/index.php @@ -0,0 +1,11 @@ + $model, + 'form' => $form, + 'ajaxView' => '@common/modules/blog/views/ajax/_article_form_test', +]); +$form->end(); \ No newline at end of file diff --git a/common/modules/comment/Controller.php b/common/modules/comment/Controller.php new file mode 100644 index 0000000..d0ce56f --- /dev/null +++ b/common/modules/comment/Controller.php @@ -0,0 +1,96 @@ + [ + 'class' => \yii\filters\VerbFilter::className(), + 'actions' => [ + '*' => ['post'], + ], + ], + ]; + } + + public function actionDelete() + { + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + $post = \Yii::$app->request->post('Comment'); + if(!empty($post['comment_id'])) { + if($model = \common\modules\comment\models\Comment::findOne($post['comment_id'])) { + /** + * @var \common\modules\comment\models\Comment $model + */ + $model->scenario = is_int(\Yii::$app->user->getId()) ? \common\modules\comment\models\Comment::SCENARIO_USER : \common\modules\comment\models\Comment::SCENARIO_GUEST; + if($model->deleteComment()) { + \Yii::$app->response->data = ['text' => 'Comment marked as deleted and will be check by administrators']; + } else { + \Yii::$app->response->data = ['error' => $model->hasErrors('comment_id')?$model->getFirstError('comment_id'):'Cannot delete message']; + } + }else { + \Yii::$app->response->data = ['error' => 'Comment not found']; + }; + } else { + \Yii::$app->response->data = ['error' => 'Missing comment_id']; + } + \Yii::$app->response->send(); + } + + public function actionUpdate() + { + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + $post = \Yii::$app->request->post(); + if(!empty($post['Comment']['comment_id'])) { + if($model = \common\modules\comment\models\Comment::findOne($post['Comment']['comment_id'])) { + /** + * @var \common\modules\comment\models\Comment $model + */ + $model->scenario = is_int(\Yii::$app->user->getId()) ? \common\modules\comment\models\Comment::SCENARIO_USER : \common\modules\comment\models\Comment::SCENARIO_GUEST; + $model->load($post); + if(empty($post['Comment']['comment_pid'])) { + $model->comment_pid = null; + } + if($model->updateComment()) { + \Yii::$app->response->data = ['text' => 'Comment successfully updated']; + } else { + \Yii::$app->response->data = ['error' => $model->hasErrors()?$model->getFirstErrors():'Cannot update message']; + } + }else { + \Yii::$app->response->data = ['error' => 'Comment not found']; + }; + } else { + \Yii::$app->response->data = ['error' => 'Missing comment_id']; + } + \Yii::$app->response->send(); + } + + public function actionForm() + { + $post = \Yii::$app->request->post('Comment'); + if(!empty($post['comment_id'])) { + $model = \common\modules\comment\models\Comment::find()->where(['comment_id' => $post['comment_id']])->with('parent', 'author')->one(); + if($model) { + /** + * @var \common\modules\comment\models\Comment $model + */ + $model->scenario = is_int(\Yii::$app->user->getId()) ? \common\modules\comment\models\Comment::SCENARIO_USER : \common\modules\comment\models\Comment::SCENARIO_GUEST; + if($model->checkUpdate()) { + return $this->renderAjax('@common/modules/comment/views/comment_form', [ + 'model' => $model, + ]); + } else { + \Yii::$app->response->data = ['error' => 'You are not able to update this comment']; + } + }else { + \Yii::$app->response->data = ['error' => 'Comment not found']; + }; + } else { + \Yii::$app->response->data = ['error' => 'Missing comment_id']; + } + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + \Yii::$app->response->send(); + } + } \ No newline at end of file diff --git a/common/modules/comment/Module.php b/common/modules/comment/Module.php new file mode 100644 index 0000000..6a18e72 --- /dev/null +++ b/common/modules/comment/Module.php @@ -0,0 +1,74 @@ + + * [ + * 'rules' => [ + * \full\namapaced\ClassName, + * \another\one\ClassName, + * ], + * 'permissions' => [ + * [ + * 'name' => stringName, + * 'description' => descriptionText, + * 'ruleName' => (new \full\namespaced\ClassName())->name (optional) + * ], + * [ + * 'name' => stringName2, + * 'description' => descriptionText2, + * 'ruleName' => (new \another\one\ClassName())->name (optional) + * ], + * ] + * ] + * + * + * @var array + * @see \common\modules\comment\commands\RbacController + */ + public $rbac = []; + + /** + * @var \yii\db\Connection Connection to the db + */ + public $db = null; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if(\Yii::$app instanceof \yii\console\Application) { + $this->controllerNamespace = 'common\modules\comment\commands'; + } + if($this->db === null) { + $this->db = \Yii::$app->db; + } elseif(!$this->db instanceof \yii\db\Connection) { + throw new \yii\base\InvalidConfigException('Конфиг db обязан наследоваться от'.\yii\db\Connection::className()); + } + } + } \ No newline at end of file diff --git a/common/modules/comment/Permissions.php b/common/modules/comment/Permissions.php new file mode 100644 index 0000000..965f0f4 --- /dev/null +++ b/common/modules/comment/Permissions.php @@ -0,0 +1,12 @@ +controller->module; + if(!$module->useRbac) { + throw new \yii\base\InvalidConfigException('Please set useRbac config to TRUE in your module configs'); + } + $auth = \Yii::$app->getAuthManager(); + if(!$auth instanceof \yii\rbac\ManagerInterface) { + throw new \yii\base\InvalidConfigException('ManagerInterface is not configured'); + } + if(!empty($module->rbac['rules'])) { + foreach($module->rbac['rules'] as $rule) { + $rule_model = new $rule(); + echo "Creating rule: ".$rule_model->name."\n"; + if($auth->add($rule_model)) { + echo "Successful\n"; + } else { + echo "Failed\n"; + } + unset($rule_model); + } + } + if(!empty($module->rbac['permissions'])) { + foreach($module->rbac['permissions'] as $permission) { + echo "Creating permission: ".$permission['name']."\n"; + if($auth->add(new \yii\rbac\Permission($permission))) { + echo "Successful\n"; + } else { + echo "Failed\n"; + } + } + } + } + + public function actionUninstall() + { + /** + * @var \common\modules\comment\Module $module + */ + $module = \Yii::$app->controller->module; + if(!$module->useRbac) { + throw new \yii\base\InvalidConfigException('Please set useRbac config to TRUE in your module configs'); + } + $auth = \Yii::$app->getAuthManager(); + if(!$auth instanceof \yii\rbac\ManagerInterface) { + throw new \yii\base\InvalidConfigException('ManagerInterface is not configured'); + } + if(!empty($module->rbac['rules'])) { + foreach($module->rbac['rules'] as $rule) { + $rule_model = new $rule(); + echo "Removing rule: ".$rule_model->name."\n"; + if($auth->remove($rule_model)) { + echo "Successful\n"; + } else { + echo "Failed\n"; + } + unset($rule_model); + } + } + if(!empty($module->rbac['permissions'])) { + foreach($module->rbac['permissions'] as $permission) { + echo "Removing permission: ".$permission['name']."\n"; + if($auth->remove(new \yii\rbac\Permission($permission))) { + echo "Successful\n"; + } else { + echo "Failed\n"; + } + } + } + } + + } \ No newline at end of file diff --git a/common/modules/comment/interfaces/CommentInterface.php b/common/modules/comment/interfaces/CommentInterface.php new file mode 100644 index 0000000..471dddf --- /dev/null +++ b/common/modules/comment/interfaces/CommentInterface.php @@ -0,0 +1,11 @@ + 1, + ], + [ + [ 'comment_pid' ], + 'exist', + 'targetAttribute' => 'comment_id', + 'filter' => [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ], + ], + ]; + } + + public function scenarios() + { + return [ + self::SCENARIO_GUEST => [ + 'user_name', + 'user_email', + 'text', + 'comment_pid', + ], + self::SCENARIO_USER => [ + 'text', + 'comment_pid', + ], + ]; + } + + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + [ + 'class' => \yii\behaviors\TimestampBehavior::className(), + 'createdAtAttribute' => 'date_add', + 'updatedAtAttribute' => 'date_update', + 'value' => new \yii\db\Expression('NOW()'), + ], + ]; + } + + public static function tableName() + { + return '{{%comment}}'; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'text' => \Yii::t('app', 'Комментарий'), + 'user_name' => \Yii::t('app', 'Имя'), + 'user_email' => \Yii::t('app', 'Email'), + ]; + } + + public function getGuestComment() + { + return $this->guestComment; + } + + public function setGuestComment($value) + { + $this->guestComment = $value; + } + + /** + * @param string $model + * @param integer $model_id + * + * @return ActiveQuery + */ + public function getComments($model, $model_id) + { + return $this->find() + ->where([ + 'comment.model' => $model, + 'comment.model_id' => $model_id, + 'comment.status' => 1, + ]); + } + + public function postComment() + { + if($this->checkCreate()) { + if($this->insert()) { + $this->clearSafe(); + return true; + } else { + return false; + } + } else { + $this->addError('comment_id', 'You can`t post comment here'); + return false; + } + } + + public function updateComment() + { + if($this->checkUpdate()) { + if(empty( $this->comment_id )) { + $this->addError('comment_id', 'Comment ID not found'); + return false; + } else { + if($this->update()) { + $this->clearSafe(); + return true; + } else { + return false; + } + } + } else { + $this->addError('comment_id', 'You can`t update this post'); + return false; + } + } + + public function deleteComment() + { + if($this->checkDelete()) { + if(empty( $this->comment_id )) { + $this->addError('comment_id', 'Comment ID not found'); + return false; + } else { + if($this->status == self::STATUS_DELETED) { + return false; + } + $this->status = self::STATUS_DELETED; + if($this->update()) { + $this->clearSafe(); + return true; + } else { + return false; + } + } + } else { + $this->addError('comment_id', 'You can`t delete this post'); + return false; + } + } + + public function checkCreate() + { + if($this->getGuestComment()) { + return true; + } else { + return \Yii::$app->user->can(\common\modules\comment\Permissions::CREATE, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]); + } + } + + public function checkUpdate() + { + if($this->scenario == self::SCENARIO_GUEST) { + return false; + } else { + return \Yii::$app->user->can(\common\modules\comment\Permissions::UPDATE, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]) || \Yii::$app->user->can(\common\modules\comment\Permissions::UPDATE_OWN, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]); + } + } + + public function checkDelete() + { + if($this->scenario == self::SCENARIO_GUEST) { + return false; + } else { + return \Yii::$app->user->can(\common\modules\comment\Permissions::DELETE, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]) || \Yii::$app->user->can(\common\modules\comment\Permissions::DELETE_OWN, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]); + } + } + + protected function clearSafe($setNew = true) + { + $safe = $this->safeAttributes(); + $count = count($safe); + $values = array_fill(0, $count, NULL); + $result = array_combine($safe, $values); + $this->setAttributes($result); + $this->setIsNewRecord($setNew); + } + + public function getParent() + { + return $this->hasOne(self::className(), [ 'comment_id' => 'comment_pid' ]); + } + + public function getAuthor() + { + // if($this->user_id != NULL) { + return $this->hasOne(\common\models\User::className(), [ 'id' => 'user_id' ]); + // } else { + // return ['firstname' => $this->user_name, 'email' => $this->user_email]; + // } + } + + public function checkRating() + { + $rating = $this->hasOne(\common\modules\comment\models\Rating::className(), [ + 'model_id' => 'comment_id', + ]) + ->andWhere([ + 'model' => $this->className(), + ]) + ->one(); + if(!$rating instanceof \common\modules\comment\models\Rating) { + $rating = new \common\modules\comment\models\Rating([ + 'model' => $this->className(), + 'model_id' => $this->comment_id, + 'user_id' => $this->user_id, + ]); + $rating->save(); + } + } + + public function getRating() + { + $this->checkRating(); + return $this->hasOne(\common\modules\comment\models\Rating::className(), [ + 'model_id' => 'comment_id', + ]) + ->andWhere([ 'model' => $this->className() ]); + } + + public function hasRating($return = true) + { + $rating = $this->hasOne(\common\modules\comment\models\Rating::className(), [ + 'model_id' => 'comment_id', + ]) + ->andWhere([ 'model' => $this->className() ]) + ->andWhere([ + 'not', + [ 'value' => NULL ], + ]) + ->one(); + if($return) { + return $rating; + } else { + return $rating ? true : false; + } + } + + } diff --git a/common/modules/comment/models/CommentProject.php b/common/modules/comment/models/CommentProject.php new file mode 100644 index 0000000..7cc3793 --- /dev/null +++ b/common/modules/comment/models/CommentProject.php @@ -0,0 +1,328 @@ + 0, + ], + [ + [ + 'budget_currency', + ], + 'default', + 'value' => 3, + ], + [ + [ 'budget_currency' ], + 'exist', + 'targetClass' => Currency::className(), + 'targetAttribute' => 'currency_id', + ], + [ + [ + 'files', + ], + 'string', + ], + [ + [ + 'file', + ], + 'safe', + ], + [ + [ 'status' ], + 'default', + 'value' => 1, + ], + ]; + } + + public function scenarios() + { + return [ + self::SCENARIO_USER => [ + 'text', + 'budget_from', + 'budget_to', + 'term_from', + 'term_to', + 'file', + ], + self::SCENARIO_GUEST => [ + + ], + ]; + } + + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + [ + 'class' => \yii\behaviors\TimestampBehavior::className(), + 'createdAtAttribute' => 'date_add', + 'updatedAtAttribute' => 'date_update', + 'value' => new \yii\db\Expression('NOW()'), + ], + ]; + } + + public static function tableName() + { + return '{{%comment_project}}'; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'text' => \Yii::t('app', 'Текст ответа'), + 'budget_from' => \Yii::t('app', 'от'), + 'budget_to' => \Yii::t('app', 'до'), + 'term_from' => \Yii::t('app', 'от'), + 'term_to' => \Yii::t('app', 'до'), + ]; + } + + public function getGuestComment() + { + return $this->guestComment; + } + +// public function setGuestComment($value) +// { +// $this->guestComment = $value; +// } + + /** + * @param string $model + * @param integer $model_id + * + * @return ActiveQuery + */ + public function getComments($model, $model_id) + { + return $this->find() + ->where([ + 'comment_project.model' => $model, + 'comment_project.model_id' => $model_id, + 'comment_project.status' => 1, + ]); + } + + public function postComment() + { + if($this->checkCreate()) { + if(!empty(\Yii::$app->request->post($this->formName())['anonymous'])) { + $this->status = self::STATUS_ANONYMOUS; + } + $this->file = UploadedFile::getInstances($this, 'file'); + if(!empty($this->file)) { + $file_id = []; + if(is_array($this->file)){ + foreach($this->file as $file){ + if($file instanceof UploadedFile){ + $file_model = new File(); + $file_id[] = $file_model->saveFile($file); + } + } + } else { + if($this->file instanceof UploadedFile){ + $file_model = new File(); + $file_id[] = $file_model->saveFile($this->file); + } + } + $this->files = json_encode($file_id); + } + if($this->insert()) { + $this->clearSafe(); + return true; + } else { + return false; + } + } else { + $this->addError('comment_id', 'You can`t post comment here'); + return false; + } + } + + public function updateComment() + { + if($this->checkUpdate()) { + if(empty( $this->comment_id )) { + $this->addError('comment_id', 'Comment ID not found'); + return false; + } else { + if($this->update()) { + $this->clearSafe(); + return true; + } else { + return false; + } + } + } else { + $this->addError('comment_id', 'You can`t update this post'); + return false; + } + } + + public function deleteComment() + { + if($this->checkDelete()) { + if(empty( $this->comment_id )) { + $this->addError('comment_id', 'Comment ID not found'); + return false; + } else { + if($this->status == self::STATUS_DELETED) { + return false; + } + $this->status = self::STATUS_DELETED; + if($this->update()) { + $this->clearSafe(); + return true; + } else { + return false; + } + } + } else { + $this->addError('comment_id', 'You can`t delete this post'); + return false; + } + } + + public function checkCreate() + { + if($this->getGuestComment()) { + return true; + } else { + return \Yii::$app->user->can(\common\modules\comment\Permissions::CREATE, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]); + } + } + + public function checkUpdate() + { + if($this->scenario == self::SCENARIO_GUEST) { + return false; + } else { + return \Yii::$app->user->can(\common\modules\comment\Permissions::UPDATE, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]) || \Yii::$app->user->can(\common\modules\comment\Permissions::UPDATE_OWN, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]); + } + } + + public function checkDelete() + { + if($this->scenario == self::SCENARIO_GUEST) { + return false; + } else { + return \Yii::$app->user->can(\common\modules\comment\Permissions::DELETE, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]) || \Yii::$app->user->can(\common\modules\comment\Permissions::DELETE_OWN, [ + 'model' => $this->model, + 'model_id' => $this->model_id, + ]); + } + } + + protected function clearSafe($setNew = true) + { + $safe = $this->safeAttributes(); + $count = count($safe); + $values = array_fill(0, $count, NULL); + $result = array_combine($safe, $values); + $this->setAttributes($result); + $this->setIsNewRecord($setNew); + } + + public function getAuthor() + { + // if($this->user_id != NULL) { + return $this->hasOne(\common\models\User::className(), [ 'id' => 'user_id' ]); + // } else { + // return ['firstname' => $this->user_name, 'email' => $this->user_email]; + // } + } + + } diff --git a/common/modules/comment/models/Rating.php b/common/modules/comment/models/Rating.php new file mode 100644 index 0000000..3222732 --- /dev/null +++ b/common/modules/comment/models/Rating.php @@ -0,0 +1,64 @@ + Yii::t('app', 'Rating ID'), + 'date_add' => Yii::t('app', 'Date Add'), + 'date_update' => Yii::t('app', 'Date Update'), + 'user_id' => Yii::t('app', 'User ID'), + 'entity' => Yii::t('app', 'Entity'), + 'value' => Yii::t('app', 'Value'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getUser() + { + return $this->hasOne(\common\models\User::className(), ['id' => 'user_id']); + } +} diff --git a/common/modules/comment/rbac/ArtboxCommentCreateRule.php b/common/modules/comment/rbac/ArtboxCommentCreateRule.php new file mode 100644 index 0000000..6f481fc --- /dev/null +++ b/common/modules/comment/rbac/ArtboxCommentCreateRule.php @@ -0,0 +1,17 @@ +'+data.text+'

    '); + $(container).remove(); + } else { + $(container).prepend('

    '+data.error+'

    ') + } + } + ); + } + }); + + $(document).on('click', '.artbox_comment_reply', function() { + var container = $(this).parents('.artbox_comment_container').first(); + var comment_id = $(container).data('comment_id'); + var form_name = $(container).data('form_name'); + var author = $(container).find('.artbox_comment_author').first().text(); + var comment_form = $('.artbox_comment_form').first(); + var offset = $(comment_form).offset(); + var reply_block = $(comment_form).find('.artbox_comment_reply_block').first(); + $(reply_block).empty(); + $(reply_block).append(''); + $(reply_block).append('

    '+author+'

    '); + $('html, body').animate({ + scrollTop: offset.top - 50, + }); + }); + + $(document).on('click', '.artbox_comment_reply_author', function() { + $(this).parents('.artbox_comment_reply_block').first().empty(); + }); + + $(document).on('click', '.artbox_comment_update', function() { + $(this).removeClass('artbox_comment_update'); + $(this).text('Сохранить'); + $(this).addClass('artbox_comment_update_submit'); + var container = $(this).parents('.artbox_comment_container').first(); + var comment_id = $(container).data('comment_id'); + var form_name = $(container).data('form_name'); + var text = $(container).find('.artbox_comment_text'); + var object = {}; + object[form_name] = {comment_id: comment_id}; + $.post( + '/artbox-comment/form', + object, + function(data, textStatus, jqXHR) { + $(text).hide(); + $(text).after( + '
    ' + + data + + '
    ' + ); + } + ); + }); + + $(document).on('click', '.artbox_comment_update_reply', function() { + $(this).remove(); + }); + + $(document).on('click', '.artbox_comment_update_submit', function(e) { + e.preventDefault(); + var container = $(this).parents('.artbox_comment_container').first(); + var edit = $(container).find('.artbox_comment_text_edit').first(); + $.post( + '/artbox-comment/update', + $(edit).find('form').serialize(), + function(data) { + if(!data.error) { + location.reload(true); + } + } + ) + }); +}); \ No newline at end of file diff --git a/common/modules/comment/resources/delete-ico.png b/common/modules/comment/resources/delete-ico.png new file mode 100644 index 0000000..52c3718 Binary files /dev/null and b/common/modules/comment/resources/delete-ico.png differ diff --git a/common/modules/comment/views/comment_form.php b/common/modules/comment/views/comment_form.php new file mode 100644 index 0000000..717dcc1 --- /dev/null +++ b/common/modules/comment/views/comment_form.php @@ -0,0 +1,38 @@ + +field($model, 'comment_id') + ->label(false) + ->hiddenInput(); + if(!empty( $model->parent )) { + ?> +
    + "; + if(!empty( $model->parent->author )) { + echo $model->parent->author->firstname . ' ' . $model->parent->author->lastname; + } else { + echo $model->parent->user_name; + } + echo ""; + echo $form->field($model, 'comment_pid') + ->label(false) + ->hiddenInput(); + ?> +
    + field($model, 'text') + ->label(false) + ->textarea(); +?> +end(); +?> \ No newline at end of file diff --git a/common/modules/comment/widgets/CommentWidget.php b/common/modules/comment/widgets/CommentWidget.php new file mode 100644 index 0000000..52c316f --- /dev/null +++ b/common/modules/comment/widgets/CommentWidget.php @@ -0,0 +1,229 @@ + 'div', + 'view' => 'list-comment', + 'class' => 'test-class', + ]; + + /** + * @var array Options sent to success part + */ + public $success_options = [ + 'tag' => 'div', + 'content' => NULL, + 'class' => 'test-class-success', + ]; + + /** + * @var array Options sent to form part + */ + public $form_options = [ + 'tag' => 'div', + 'view' => 'form-comment', + 'class' => 'test-class-form', + ]; + + /** + * @var bool Indicates whether any successful action happened + */ + protected $isSuccess = false; + + public $success_text = 'Comment successfully added'; + + /** + * @var string $model Model, to which comments attached + */ + public $model; + + /** + * @var integer $model_id Model id, to which comments attached + */ + public $model_id; + + /** + * @var string Template of the widget. You may use {success}, {form}, {list} + * to render particular parts. You are also able to use common HTML here. + */ + public $template = "{success}\n{form}\n{list}"; + + /** + * @var array Widget options + */ + public $options = [ ]; + + /** + * @var \yii\data\DataProviderInterface Data provider of comments + */ + public $dataProvider; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + \common\modules\comment\assets\CommentAsset::register($this->view); + if(is_string($this->comment_class)) { + $this->comment_class = new $this->comment_class($this->class_options); + } else { + throw new \yii\base\InvalidConfigException(__CLASS__ . '->comment_class must be defined as object full class name string.'); + } + if(!empty( $this->rating_class ) && is_string($this->rating_class)) { + $this->rating_class = new $this->rating_class($this->rating_options); + } elseif(!empty( $this->rating_class )) { + throw new \yii\base\InvalidConfigException(__CLASS__ . '->rating_class must be defined as object full class name string.'); + } + $this->comment_class->model = $this->model; + $this->comment_class->model_id = $this->model_id; + $this->createDataProvider(); + $this->process(); + ob_start(); + } + + /** + * @inheritdoc + * @return string + */ + public function run() + { + $content = ob_get_clean(); + $this->createParts(); + return $this->renderWidget(); + } + + public function createParts() + { + if($this->display_comment_success && $this->isSuccess) { + $tag = ArrayHelper::remove($this->success_options, 'tag', 'div'); + if(is_callable($this->success_options[ 'content' ])) { + $result = call_user_func(ArrayHelper::remove($this->success_options, 'content'), $this->success_text); + } elseif($this->success_options[ 'content' ] != NULL) { + $result = Html::encode(ArrayHelper::remove($this->success_options, 'content', $this->success_text)); + } else { + $result = Html::encode($this->success_text); + } + $this->parts[ 'success' ] = Html::tag($tag, $result, $this->success_options); + unset( $tag, $result ); + } + + if($this->display_comment_list) { + $tag = ArrayHelper::remove($this->list_options, 'tag', 'div'); + $view = ArrayHelper::remove($this->list_options, 'view'); + $this->parts[ 'list' ] = Html::tag($tag, $this->renderItems($view), $this->list_options); + } + + if($this->display_comment_form) { + $tag = ArrayHelper::remove($this->form_options, 'tag', 'div'); + $view = ArrayHelper::remove($this->form_options, 'view'); + $this->parts[ 'form' ] = Html::tag($tag, $this->renderForm($view), $this->form_options); + } + } + + public function createDataProvider() + { + $this->dataProvider = new \yii\data\ActiveDataProvider([ + 'query' => $this->comment_class->getComments($this->model, $this->model_id), + 'pagination' => [ + 'pageSize' => 10, + ], + ]); + } + + public function renderItems($view) + { + if(empty( $view )) { + throw new \yii\base\InvalidConfigException("list_options[view] must be set"); + } + return $this->render($view, [ 'dataProvider' => $this->dataProvider ]); + } + + public function renderForm($view) + { + if(empty( $view )) { + throw new \yii\base\InvalidConfigException("form_options[view] must be set"); + } + return $this->render($view, [ + 'model' => $this->comment_class, + 'rating' => $this->rating_class, + 'user' => \Yii::$app->user->identity, + 'dataProvider' => $this->dataProvider, + ]); + } + + public function renderWidget() + { + $template = $this->template; + $parts = $this->parts; + $options = $this->options; + $template = preg_replace('/{success}/', ArrayHelper::remove($parts, 'success', ''), $template); + $template = preg_replace('/{list}/', ArrayHelper::remove($parts, 'list', ''), $template); + $template = preg_replace('/{form}/', ArrayHelper::remove($parts, 'form', ''), $template); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + return Html::tag($tag, $template, $options); + } + + public function process() + { + $data = \Yii::$app->request->post(); + if($this->comment_class->load($data) && $this->comment_class->postComment()) { + if(is_object($this->rating_class) && $this->comment_class->rating->load($data) && $this->comment_class->rating->save()) { + $this->isSuccess = true; + } + } + } + } \ No newline at end of file diff --git a/common/modules/comment/widgets/views/_project_comment_view.php b/common/modules/comment/widgets/views/_project_comment_view.php new file mode 100644 index 0000000..8e3c04f --- /dev/null +++ b/common/modules/comment/widgets/views/_project_comment_view.php @@ -0,0 +1,113 @@ +user_id )) { + $user = User::find() + ->where([ 'id' => $model->user_id ]) + ->with('userInfo') + ->one(); + } +?> +
    +
    +
    + userInfo->image) ?> +
    +
    +
    +
      + userInfo->social_fb )) { + echo '
    • '.Html::a(Html::img('/images/ico-fb.png'), $user->userInfo->social_fb, ['target' => '_blank']).'
    • '; + } + ?> + userInfo->social_t )) { + echo '
    • '.Html::a(Html::img('/images/ico-tw.png'), $user->userInfo->social_t, ['target' => '_blank']).'
    • '; + } + ?> + userInfo->social_in )) { + echo '
    • '.Html::a(Html::img('/images/ico-in.png'), $user->userInfo->social_in, ['target' => '_blank']).'
    • '; + } + ?> + userInfo->social_vk )) { + echo '
    • '.Html::a(Html::img('/images/ico-vk.png'), $user->userInfo->social_vk, ['target' => '_blank']).'
    • '; + } + ?> +
    +
    +
    +
      +
    • +
      userInfo->view_count ?>
      +
    • +
    • +
      Статус: userInfo->busy)?'Свободен':'Занят') ?> +
      +
    • +
    • +
      + На сайте: 1г. 8 мес. +
      +
    • +
    • +
      Последний визит:
      2 дня назад +
      +
    • +
    + Посмотреть профиль +
    +
    +
    +
    +
    +
    +
    2000 грн
    +
    3 ДНЯ
    +
    +
    +
    +
    Петер Цумтор
    +
    + + +
    + 30 отзывов +
    +
    +

    1.1 Строительная площадка расположена по адресу: г. Киев.

    +

    1.2 Существующий объект представляет собой помещение общей площадью ориентировочно – 140 м2.

    +

    1.3. Цель проекта состоит в проведении внутренних общестроительных и отделочных работ.

    +

    1.4. При разработке методов строительства и выборе материалов, используемых в настоящем проекте, необходимо учитывать климатические условия, характерные для г. Киева.

    +

    1.5. Требования к проектированию и производству работ определяются следующими документами:

    +

    - Техническим заданием.

    +

    - Строительными нормами и правилами.

    +

    Все проектные решения и все разделы рабочего проекта должны быть согласованы с Заказчиком в объеме, необходимом для последующей сдачи инженерных систем и коммуникаций.

    +
    + +
    + + +
    diff --git a/common/modules/comment/widgets/views/form-comment.php b/common/modules/comment/widgets/views/form-comment.php new file mode 100644 index 0000000..9262ebc --- /dev/null +++ b/common/modules/comment/widgets/views/form-comment.php @@ -0,0 +1,70 @@ + +
    Комментарии: totalCount ?>
    + +
    + + field($rating, 'value') + ->label(false) + ->radioList([ + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + ]); + + if($model->scenario == $model::SCENARIO_GUEST) { + echo $form->field($model, 'user_name', [ + 'options' => [ + 'class' => 'input-blocks-comm', + ], + 'inputOptions' => [ + 'class' => 'custom-input-4', + ], + ]) + ->textInput(); + echo $form->field($model, 'user_email', [ + 'options' => [ + 'class' => 'input-blocks-comm', + ], + 'inputOptions' => [ + 'class' => 'custom-input-4', + ], + ]) + ->textInput(); + } + + ?> +
    + field($model, 'text', [ + 'options' => [ + 'class' => 'input-blocks-comm area-comm', + ], + 'inputOptions' => [ + 'class' => 'custom-area-4', + ], + ]) + ->textarea(); + ?> +
    + +
    + end(); + ?> +
    \ No newline at end of file diff --git a/common/modules/comment/widgets/views/form-project-comment.php b/common/modules/comment/widgets/views/form-project-comment.php new file mode 100644 index 0000000..240d241 --- /dev/null +++ b/common/modules/comment/widgets/views/form-project-comment.php @@ -0,0 +1,101 @@ + + +
    +
    +
    +
    Добавить ответ
    +
    + [ 'class' => 'resformsfile MultiFile-intercepted', 'enctype' => 'multipart/form-data' ] ]); + ?> +
    +
    +
    + userInfo->image) ?> +
    +
    + name ?> +
    +
    +
    +
    Стоимость
    +
    + field($model, 'budget_from', [ + 'template' => "{input}\n{error}", + 'options' => [ 'tag' => 'span' ], + ]) + ->input('number', [ 'placeholder' => $model->getAttributeLabel('budget_from') ]) ?> + field($model, 'budget_to', [ + 'template' => "{input}\n{error}", + 'options' => [ 'tag' => 'span' ], + ]) + ->input('number', [ 'placeholder' => $model->getAttributeLabel('budget_to') ]) ?> + field($model, 'budget_currency', [ + 'template' => "{input}
    \n{error}", + 'options' => [ 'class' => 'blocks-check-list-wrapp check-valuta' ], + ]) + ->dropDownList($currencies) ?> +
    +
    +
    +
    + Сроки(в днях) +
    +
    + field($model, 'term_from', [ + 'template' => "{input}\n{error}", + 'options' => [ 'tag' => 'span' ], + ]) + ->input('number', [ 'placeholder' => $model->getAttributeLabel('term_from') ]) ?> + field($model, 'term_to', [ + 'template' => "{input}\n{error}", + 'options' => [ 'tag' => 'span' ], + ]) + ->input('number', [ 'placeholder' => $model->getAttributeLabel('term_to') ]) ?> +
    +
    +
    + field($model, 'text', [ + 'template' => "{input}\n{error}", + 'options' => [ 'class' => 'form-tender-txt style' ], + ]) + ->textarea([ 'placeholder' => $model->getAttributeLabel('text') ]) ?> +
    + field($model, 'file[]') + ->fileInput([ 'class' => 'multi' ]) + ->label(false) ?> + Прикрепить файл +
    Максимальный размер
    файла 5 МБ
    +
    +
    + 'get-project-new' ]); + echo Html::submitInput('ответить анонимно', [ + 'class' => 'get-list-new', + 'name' => 'CommentProject[anonymous]', + ]); + echo Html::a('Очистить', [ + 'tender/view', + 'tender_id' => \Yii::$app->request->get('tender_id'), + '#' => 'w1', + ]); + ?> +
    + end(); + ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/common/modules/comment/widgets/views/list-comment.php b/common/modules/comment/widgets/views/list-comment.php new file mode 100644 index 0000000..4f37738 --- /dev/null +++ b/common/modules/comment/widgets/views/list-comment.php @@ -0,0 +1,12 @@ + $dataProvider, + 'itemView' => 'project_comment_view', + 'itemOptions' => [ + 'tag' => false, + ], + 'summary' => '', +]); \ No newline at end of file diff --git a/common/modules/comment/widgets/views/list-project-comment.php b/common/modules/comment/widgets/views/list-project-comment.php new file mode 100644 index 0000000..f592675 --- /dev/null +++ b/common/modules/comment/widgets/views/list-project-comment.php @@ -0,0 +1,22 @@ + +
    +
    +
    Предложения проектантов
    +
    + $dataProvider, + 'itemView' => '_project_comment_view', + 'itemOptions' => [ + 'class' => 'tender-offer-proj-blocks style', + ], + 'summary' => '', + ]); + ?> +
    +
    +
    \ No newline at end of file diff --git a/common/modules/comment/widgets/views/project_comment_view.php b/common/modules/comment/widgets/views/project_comment_view.php new file mode 100644 index 0000000..c5cf8a4 --- /dev/null +++ b/common/modules/comment/widgets/views/project_comment_view.php @@ -0,0 +1,84 @@ +user_id )) { + $user = User::find() + ->where([ 'id' => $model->user_id ]) + ->with('userInfo') + ->one(); + } +?> +
    +
    +
    + userInfo->image )) { + echo ""; + } else { + echo ""; + } + ?> +
    +
    +
    + ' . $user->firstname . ' ' . $user->lastname . '', [ + 'performer/common', + 'performer_id' => $user->id, + ]); + } else { + echo '' . $model->user_name . '' . '(Гость)'; + } + ?> +
    +
    + hasRating()) { + ?> +
    + + + + +
    + +
    + +
    + +
    + text) ?> +
    +
    + +
    +
      +
    • Ответить
    • +
    • Редактировать
    • +
    • Удалить
    • +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/common/modules/file/FileUploadAsset.php b/common/modules/file/FileUploadAsset.php new file mode 100644 index 0000000..7ad2791 --- /dev/null +++ b/common/modules/file/FileUploadAsset.php @@ -0,0 +1,43 @@ + + * @since 2.0 + */ +class FileUploadAsset extends AssetBundle +{ + + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->sourcePath = __DIR__.'/assets'; + } + + public $css = [ + 'css/jquery.fileupload.css', + 'css/fileupload/style.css' + ]; + + public $js = [ + 'js/vendor/jquery.ui.widget.js', + 'js/jquery.iframe-transport.js', + 'js/jquery.fileupload.js' + ]; +} diff --git a/common/modules/file/Module.php b/common/modules/file/Module.php new file mode 100644 index 0000000..f0ac4c1 --- /dev/null +++ b/common/modules/file/Module.php @@ -0,0 +1,15 @@ +').prop('href', options.postMessage)[0], + target = loc.protocol + '//' + loc.host, + xhrUpload = options.xhr().upload; + return { + send: function (_, completeCallback) { + counter += 1; + var message = { + id: 'postmessage-transport-' + counter + }, + eventName = 'message.' + message.id; + iframe = $( + '' + ).bind('load', function () { + $.each(names, function (i, name) { + message[name] = options[name]; + }); + message.dataType = message.dataType.replace('postmessage ', ''); + $(window).bind(eventName, function (e) { + e = e.originalEvent; + var data = e.data, + ev; + if (e.origin === target && data.id === message.id) { + if (data.type === 'progress') { + ev = document.createEvent('Event'); + ev.initEvent(data.type, false, true); + $.extend(ev, data); + xhrUpload.dispatchEvent(ev); + } else { + completeCallback( + data.status, + data.statusText, + {postmessage: data.result}, + data.headers + ); + iframe.remove(); + $(window).unbind(eventName); + } + } + }); + iframe[0].contentWindow.postMessage( + message, + target + ); + }).appendTo(document.body); + }, + abort: function () { + if (iframe) { + iframe.remove(); + } + } + }; + } + }); + +})); diff --git a/common/modules/file/assets/js/cors/jquery.xdr-transport.js b/common/modules/file/assets/js/cors/jquery.xdr-transport.js new file mode 100644 index 0000000..5b9c6ca --- /dev/null +++ b/common/modules/file/assets/js/cors/jquery.xdr-transport.js @@ -0,0 +1,89 @@ +/* + * jQuery XDomainRequest Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Based on Julian Aubourg's ajaxHooks xdr.js: + * https://github.com/jaubourg/ajaxHooks/ + */ + +/* global define, require, window, XDomainRequest */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + if (window.XDomainRequest && !$.support.cors) { + $.ajaxTransport(function (s) { + if (s.crossDomain && s.async) { + if (s.timeout) { + s.xdrTimeout = s.timeout; + delete s.timeout; + } + var xdr; + return { + send: function (headers, completeCallback) { + var addParamChar = /\?/.test(s.url) ? '&' : '?'; + function callback(status, statusText, responses, responseHeaders) { + xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; + xdr = null; + completeCallback(status, statusText, responses, responseHeaders); + } + xdr = new XDomainRequest(); + // XDomainRequest only supports GET and POST: + if (s.type === 'DELETE') { + s.url = s.url + addParamChar + '_method=DELETE'; + s.type = 'POST'; + } else if (s.type === 'PUT') { + s.url = s.url + addParamChar + '_method=PUT'; + s.type = 'POST'; + } else if (s.type === 'PATCH') { + s.url = s.url + addParamChar + '_method=PATCH'; + s.type = 'POST'; + } + xdr.open(s.type, s.url); + xdr.onload = function () { + callback( + 200, + 'OK', + {text: xdr.responseText}, + 'Content-Type: ' + xdr.contentType + ); + }; + xdr.onerror = function () { + callback(404, 'Not Found'); + }; + if (s.xdrTimeout) { + xdr.ontimeout = function () { + callback(0, 'timeout'); + }; + xdr.timeout = s.xdrTimeout; + } + xdr.send((s.hasContent && s.data) || null); + }, + abort: function () { + if (xdr) { + xdr.onerror = $.noop(); + xdr.abort(); + } + } + }; + } + }); + } +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-angular.js b/common/modules/file/assets/js/jquery.fileupload-angular.js new file mode 100644 index 0000000..f7ba07b --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-angular.js @@ -0,0 +1,425 @@ +/* + * jQuery File Upload AngularJS Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, angular */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'angular', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else { + factory(); + } +}(function () { + 'use strict'; + + angular.module('blueimp.fileupload', []) + + // The fileUpload service provides configuration options + // for the fileUpload directive and default handlers for + // File Upload events: + .provider('fileUpload', function () { + var scopeEvalAsync = function (expression) { + var scope = angular.element(this) + .fileupload('option', 'scope'); + // Schedule a new $digest cycle if not already inside of one + // and evaluate the given expression: + scope.$evalAsync(expression); + }, + addFileMethods = function (scope, data) { + var files = data.files, + file = files[0]; + angular.forEach(files, function (file, index) { + file._index = index; + file.$state = function () { + return data.state(); + }; + file.$processing = function () { + return data.processing(); + }; + file.$progress = function () { + return data.progress(); + }; + file.$response = function () { + return data.response(); + }; + }); + file.$submit = function () { + if (!file.error) { + return data.submit(); + } + }; + file.$cancel = function () { + return data.abort(); + }; + }, + $config; + $config = this.defaults = { + handleResponse: function (e, data) { + var files = data.result && data.result.files; + if (files) { + data.scope.replace(data.files, files); + } else if (data.errorThrown || + data.textStatus === 'error') { + data.files[0].error = data.errorThrown || + data.textStatus; + } + }, + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var scope = data.scope, + filesCopy = []; + angular.forEach(data.files, function (file) { + filesCopy.push(file); + }); + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + var method = scope.option('prependFiles') ? + 'unshift' : 'push'; + Array.prototype[method].apply(scope.queue, data.files); + }); + data.process(function () { + return scope.process(data); + }).always(function () { + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + scope.replace(filesCopy, data.files); + }); + }).then(function () { + if ((scope.option('autoUpload') || + data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }); + }, + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this; + data.scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this, + scope = data.scope; + if (data.errorThrown === 'abort') { + scope.clear(data.files); + return; + } + scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + stop: scopeEvalAsync, + processstart: scopeEvalAsync, + processstop: scopeEvalAsync, + getNumberOfFiles: function () { + var scope = this.scope; + return scope.queue.length - scope.processing(); + }, + dataType: 'json', + autoUpload: false + }; + this.$get = [ + function () { + return { + defaults: $config + }; + } + ]; + }) + + // Format byte numbers to readable presentations: + .provider('formatFileSizeFilter', function () { + var $config = { + // Byte units following the IEC format + // http://en.wikipedia.org/wiki/Kilobyte + units: [ + {size: 1000000000, suffix: ' GB'}, + {size: 1000000, suffix: ' MB'}, + {size: 1000, suffix: ' KB'} + ] + }; + this.defaults = $config; + this.$get = function () { + return function (bytes) { + if (!angular.isNumber(bytes)) { + return ''; + } + var unit = true, + i = 0, + prefix, + suffix; + while (unit) { + unit = $config.units[i]; + prefix = unit.prefix || ''; + suffix = unit.suffix || ''; + if (i === $config.units.length - 1 || bytes >= unit.size) { + return prefix + (bytes / unit.size).toFixed(2) + suffix; + } + i += 1; + } + }; + }; + }) + + // The FileUploadController initializes the fileupload widget and + // provides scope methods to control the File Upload functionality: + .controller('FileUploadController', [ + '$scope', '$element', '$attrs', '$window', 'fileUpload', + function ($scope, $element, $attrs, $window, fileUpload) { + var uploadMethods = { + progress: function () { + return $element.fileupload('progress'); + }, + active: function () { + return $element.fileupload('active'); + }, + option: function (option, data) { + if (arguments.length === 1) { + return $element.fileupload('option', option); + } + $element.fileupload('option', option, data); + }, + add: function (data) { + return $element.fileupload('add', data); + }, + send: function (data) { + return $element.fileupload('send', data); + }, + process: function (data) { + return $element.fileupload('process', data); + }, + processing: function (data) { + return $element.fileupload('processing', data); + } + }; + $scope.disabled = !$window.jQuery.support.fileInput; + $scope.queue = $scope.queue || []; + $scope.clear = function (files) { + var queue = this.queue, + i = queue.length, + file = files, + length = 1; + if (angular.isArray(files)) { + file = files[0]; + length = files.length; + } + while (i) { + i -= 1; + if (queue[i] === file) { + return queue.splice(i, length); + } + } + }; + $scope.replace = function (oldFiles, newFiles) { + var queue = this.queue, + file = oldFiles[0], + i, + j; + for (i = 0; i < queue.length; i += 1) { + if (queue[i] === file) { + for (j = 0; j < newFiles.length; j += 1) { + queue[i + j] = newFiles[j]; + } + return; + } + } + }; + $scope.applyOnQueue = function (method) { + var list = this.queue.slice(0), + i, + file; + for (i = 0; i < list.length; i += 1) { + file = list[i]; + if (file[method]) { + file[method](); + } + } + }; + $scope.submit = function () { + this.applyOnQueue('$submit'); + }; + $scope.cancel = function () { + this.applyOnQueue('$cancel'); + }; + // Add upload methods to the scope: + angular.extend($scope, uploadMethods); + // The fileupload widget will initialize with + // the options provided via "data-"-parameters, + // as well as those given via options object: + $element.fileupload(angular.extend( + {scope: $scope}, + fileUpload.defaults + )).on('fileuploadadd', function (e, data) { + data.scope = $scope; + }).on('fileuploadfail', function (e, data) { + if (data.errorThrown === 'abort') { + return; + } + if (data.dataType && + data.dataType.indexOf('json') === data.dataType.length - 4) { + try { + data.result = angular.fromJson(data.jqXHR.responseText); + } catch (ignore) {} + } + }).on([ + 'fileuploadadd', + 'fileuploadsubmit', + 'fileuploadsend', + 'fileuploaddone', + 'fileuploadfail', + 'fileuploadalways', + 'fileuploadprogress', + 'fileuploadprogressall', + 'fileuploadstart', + 'fileuploadstop', + 'fileuploadchange', + 'fileuploadpaste', + 'fileuploaddrop', + 'fileuploaddragover', + 'fileuploadchunksend', + 'fileuploadchunkdone', + 'fileuploadchunkfail', + 'fileuploadchunkalways', + 'fileuploadprocessstart', + 'fileuploadprocess', + 'fileuploadprocessdone', + 'fileuploadprocessfail', + 'fileuploadprocessalways', + 'fileuploadprocessstop' + ].join(' '), function (e, data) { + $scope.$parent.$applyAsync(function () { + if ($scope.$emit(e.type, data).defaultPrevented) { + e.preventDefault(); + } + }); + }).on('remove', function () { + // Remove upload methods from the scope, + // when the widget is removed: + var method; + for (method in uploadMethods) { + if (uploadMethods.hasOwnProperty(method)) { + delete $scope[method]; + } + } + }); + // Observe option changes: + $scope.$watch( + $attrs.fileUpload, + function (newOptions) { + if (newOptions) { + $element.fileupload('option', newOptions); + } + } + ); + } + ]) + + // Provide File Upload progress feedback: + .controller('FileUploadProgressController', [ + '$scope', '$attrs', '$parse', + function ($scope, $attrs, $parse) { + var fn = $parse($attrs.fileUploadProgress), + update = function () { + var progress = fn($scope); + if (!progress || !progress.total) { + return; + } + $scope.num = Math.floor( + progress.loaded / progress.total * 100 + ); + }; + update(); + $scope.$watch( + $attrs.fileUploadProgress + '.loaded', + function (newValue, oldValue) { + if (newValue !== oldValue) { + update(); + } + } + ); + } + ]) + + // Display File Upload previews: + .controller('FileUploadPreviewController', [ + '$scope', '$element', '$attrs', + function ($scope, $element, $attrs) { + $scope.$watch( + $attrs.fileUploadPreview + '.preview', + function (preview) { + $element.empty(); + if (preview) { + $element.append(preview); + } + } + ); + } + ]) + + .directive('fileUpload', function () { + return { + controller: 'FileUploadController', + scope: true + }; + }) + + .directive('fileUploadProgress', function () { + return { + controller: 'FileUploadProgressController', + scope: true + }; + }) + + .directive('fileUploadPreview', function () { + return { + controller: 'FileUploadPreviewController' + }; + }) + + // Enhance the HTML5 download attribute to + // allow drag&drop of files to the desktop: + .directive('download', function () { + return function (scope, elm) { + elm.on('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [ + 'application/octet-stream', + elm.prop('download'), + elm.prop('href') + ].join(':') + ); + } catch (ignore) {} + }); + }; + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-audio.js b/common/modules/file/assets/js/jquery.fileupload-audio.js new file mode 100644 index 0000000..1a746f9 --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-audio.js @@ -0,0 +1,112 @@ +/* + * jQuery File Upload Audio Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('load-image') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadAudio', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableAudioPreview' + }, + { + action: 'setAudio', + name: '@audioPreviewName', + disabled: '@disableAudioPreview' + } + ); + + // The File Upload Audio Preview plugin extends the fileupload widget + // with audio preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of audio files to load, + // matched against the file type: + loadAudioFileTypes: /^audio\/.*$/ + }, + + _audioElement: document.createElement('audio'), + + processActions: { + + // Loads the audio file given via data.files and data.index + // as audio element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadAudio: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + audio; + if (this._audioElement.canPlayType && + this._audioElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + audio = this._audioElement.cloneNode(false); + audio.src = url; + audio.controls = true; + data.audio = audio; + return data; + } + } + return data; + }, + + // Sets the audio element as a property of the file object: + setAudio: function (data, options) { + if (data.audio && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.audio; + } + return data; + } + + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-image.js b/common/modules/file/assets/js/jquery.fileupload-image.js new file mode 100644 index 0000000..0b91fbb --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-image.js @@ -0,0 +1,321 @@ +/* + * jQuery File Upload Image Preview & Resize Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, Blob */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-exif', + 'load-image-ios', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('load-image') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + disableImageHead: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifSub: '@', + disableExifGps: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + forceResize: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + quality: '@imageQuality', + type: '@imageType', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + }, + { + action: 'deleteImageReferences', + disabled: '@disableImageReferencesDeletion' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Defines the image orientation (1-8) or takes the orientation + // value from Exif data if set to true: + imageOrientation: false, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element, if the browser supports the File API. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + )) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + options = $.extend({canvas: true}, options); + var that = this, + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if (newImg && (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize)) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + // Prevent orienting the same image twice: + if (data.orientation) { + delete options.orientation; + } else { + data.orientation = options.orientation; + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\.\w+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-jquery-ui.js b/common/modules/file/assets/js/jquery.fileupload-jquery-ui.js new file mode 100644 index 0000000..4f239fa --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-jquery-ui.js @@ -0,0 +1,155 @@ +/* + * jQuery File Upload jQuery UI Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', './jquery.fileupload-ui'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + processdone: function (e, data) { + data.context.find('.start').button('enable'); + }, + progress: function (e, data) { + if (data.context) { + data.context.find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + } + }, + progressall: function (e, data) { + var $this = $(this); + $this.find('.fileupload-progress') + .find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ).end() + .find('.progress-extended').each(function () { + $(this).html( + ($this.data('blueimp-fileupload') || + $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + }); + } + }, + + _renderUpload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.progress').empty().progressbar(); + node.find('.start').button({ + icons: {primary: 'ui-icon-circle-arrow-e'}, + text: showIconText + }); + node.find('.cancel').button({ + icons: {primary: 'ui-icon-cancel'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _renderDownload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.delete').button({ + icons: {primary: 'ui-icon-trash'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _startHandler: function (e) { + $(e.currentTarget).button('disable'); + this._super(e); + }, + + _transition: function (node) { + var deferred = $.Deferred(); + if (node.hasClass('fade')) { + node.fadeToggle( + this.options.transitionDuration, + this.options.transitionEasing, + function () { + deferred.resolveWith(node); + } + ); + } else { + deferred.resolveWith(node); + } + return deferred; + }, + + _create: function () { + this._super(); + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button({icons: {primary: 'ui-icon-plusthick'}}) + .append(input); + }) + .end().find('.start') + .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) + .end().find('.cancel') + .button({icons: {primary: 'ui-icon-cancel'}}) + .end().find('.delete') + .button({icons: {primary: 'ui-icon-trash'}}) + .end().find('.progress').progressbar(); + }, + + _destroy: function () { + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button('destroy') + .append(input); + }) + .end().find('.start') + .button('destroy') + .end().find('.cancel') + .button('destroy') + .end().find('.delete') + .button('destroy') + .end().find('.progress').progressbar('destroy'); + this._super(); + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-process.js b/common/modules/file/assets/js/jquery.fileupload-process.js new file mode 100644 index 0000000..ce914df --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-process.js @@ -0,0 +1,175 @@ +/* + * jQuery File Upload Processing Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data, originalData) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + if (originalData.errorThrown) { + return $.Deferred() + .rejectWith(that, [originalData]).promise(); + } + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.pipe(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return that._processFile(opts, data); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.pipe(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-ui.js b/common/modules/file/assets/js/jquery.fileupload-ui.js new file mode 100644 index 0000000..8154218 --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-ui.js @@ -0,0 +1,710 @@ +/* + * jQuery File Upload User Interface Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'tmpl', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('tmpl') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.tmpl + ); + } +}(function ($, tmpl) { + 'use strict'; + + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The ID of the upload template: + uploadTemplateId: 'template-upload', + // The ID of the download template: + downloadTemplateId: 'template-download', + // The container for the list of files. If undefined, it is set to + // an element with class "files" inside of the widget element: + filesContainer: undefined, + // By default, files are appended to the files container. + // Set the following option to true, to prepend files instead: + prependFiles: false, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // Error and info messages: + messages: { + unknownError: 'Unknown error' + }, + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children() + .not('.processing').length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + that = $this.data('blueimp-fileupload') || + $this.data('fileupload'), + options = that.options; + data.context = that._renderUpload(data.files) + .data('data', data) + .addClass('processing'); + options.filesContainer[ + options.prependFiles ? 'prepend' : 'append' + ](data.context); + that._forceReflow(data.context); + that._transition(data.context); + data.process(function () { + return $this.fileupload('process', data); + }).always(function () { + data.context.each(function (index) { + $(this).find('.size').text( + that._formatFileSize(data.files[index].size) + ); + }).removeClass('processing'); + that._renderPreviews(data); + }).done(function () { + data.context.find('.start').prop('disabled', false); + if ((that._trigger('added', e, data) !== false) && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }).fail(function () { + if (data.files.error) { + data.context.each(function (index) { + var error = data.files[index].error; + if (error) { + $(this).find('.error').text(error); + } + }); + } + }); + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context + .find('.progress').addClass( + !$.support.transition && 'progress-animated' + ) + .attr('aria-valuenow', 100) + .children().first().css( + 'width', + '100%' + ); + } + return that._trigger('sent', e, data); + }, + // Callback for successful uploads: + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + getFilesFromResponse = data.getFilesFromResponse || + that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + var file = files[index] || + {error: 'Empty file upload result'}; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + }); + } else { + template = that._renderDownload(files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer); + that._forceReflow(template); + deferred = that._addFinishedDeferreds(); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown || + data.i18n('unknownError'); + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + } else { + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + $(this).remove(); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }); + } else if (data.errorThrown !== 'abort') { + data.context = that._renderUpload(data.files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer) + .data('data', data); + that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); + that._transition(data.context).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } else { + that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var progress = Math.floor(data.loaded / data.total * 100); + if (data.context) { + data.context.each(function () { + $(this).find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + progress = Math.floor(data.loaded / data.total * 100), + globalProgressNode = $this.find('.fileupload-progress'), + extendedProgressNode = globalProgressNode + .find('.progress-extended'); + if (extendedProgressNode.length) { + extendedProgressNode.html( + ($this.data('blueimp-fileupload') || $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + } + globalProgressNode + .find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + that._resetFinishedDeferreds(); + that._transition($(this).find('.fileupload-progress')).done( + function () { + that._trigger('started', e); + } + ); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()) + .done(function () { + that._trigger('stopped', e); + }); + that._transition($(this).find('.fileupload-progress')).done( + function () { + $(this).find('.progress') + .attr('aria-valuenow', '0') + .children().first().css('width', '0%'); + $(this).find('.progress-extended').html(' '); + deferred.resolve(); + } + ); + }, + processstart: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).addClass('fileupload-processing'); + }, + processstop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).removeClass('fileupload-processing'); + }, + // Callback for file deletion: + destroy: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done( + function () { + $(this).remove(); + that._trigger('destroyed', e, data); + } + ); + }; + if (data.url) { + data.dataType = data.dataType || that.options.dataType; + $.ajax(data).done(removeNode).fail(function () { + that._trigger('destroyfailed', e, data); + }); + } else { + removeNode(); + } + } + }, + + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + if (!deferred) { + deferred = $.Deferred(); + } + this._finishedUploads.push(deferred); + return deferred; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = link.prop('download'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (ignore) {} + }); + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _formatBitrate: function (bits) { + if (typeof bits !== 'number') { + return ''; + } + if (bits >= 1000000000) { + return (bits / 1000000000).toFixed(2) + ' Gbit/s'; + } + if (bits >= 1000000) { + return (bits / 1000000).toFixed(2) + ' Mbit/s'; + } + if (bits >= 1000) { + return (bits / 1000).toFixed(2) + ' kbit/s'; + } + return bits.toFixed(2) + ' bit/s'; + }, + + _formatTime: function (seconds) { + var date = new Date(seconds * 1000), + days = Math.floor(seconds / 86400); + days = days ? days + 'd ' : ''; + return days + + ('0' + date.getUTCHours()).slice(-2) + ':' + + ('0' + date.getUTCMinutes()).slice(-2) + ':' + + ('0' + date.getUTCSeconds()).slice(-2); + }, + + _formatPercentage: function (floatValue) { + return (floatValue * 100).toFixed(2) + ' %'; + }, + + _renderExtendedProgress: function (data) { + return this._formatBitrate(data.bitrate) + ' | ' + + this._formatTime( + (data.total - data.loaded) * 8 / data.bitrate + ) + ' | ' + + this._formatPercentage( + data.loaded / data.total + ) + ' | ' + + this._formatFileSize(data.loaded) + ' / ' + + this._formatFileSize(data.total); + }, + + _renderTemplate: function (func, files) { + if (!func) { + return $(); + } + var result = func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + }); + if (result instanceof $) { + return result; + } + return $(this.options.templatesContainer).html(result).children(); + }, + + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).append(data.files[index].preview); + }); + }, + + _renderUpload: function (files) { + return this._renderTemplate( + this.options.uploadTemplate, + files + ); + }, + + _renderDownload: function (files) { + return this._renderTemplate( + this.options.downloadTemplate, + files + ).find('a[download]').each(this._enableDragToDesktop).end(); + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'); + button.prop('disabled', true); + if (data && data.submit) { + data.submit(); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var template = $(e.currentTarget) + .closest('.template-upload,.template-download'), + data = template.data('data') || {}; + data.context = data.context || template; + if (data.abort) { + data.abort(); + } else { + data.errorThrown = 'abort'; + this._trigger('fail', e, data); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget); + this._trigger('destroy', e, $.extend({ + context: button.closest('.template-download'), + type: 'DELETE' + }, button.data())); + }, + + _forceReflow: function (node) { + return $.support.transition && node.length && + node[0].offsetWidth; + }, + + _transition: function (node) { + var dfd = $.Deferred(); + if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { + node.bind( + $.support.transition.end, + function (e) { + // Make sure we don't respond to other transitions events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.unbind($.support.transition.end); + dfd.resolveWith(node); + } + } + ).toggleClass('in'); + } else { + node.toggleClass('in'); + dfd.resolveWith(node); + } + return dfd; + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { + e.preventDefault(); + filesList.find('.start').click(); + } + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { + e.preventDefault(); + filesList.find('.cancel').click(); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { + e.preventDefault(); + filesList.find('.toggle:checked') + .closest('.template-download') + .find('.delete').click(); + fileUploadButtonBar.find('.toggle') + .prop('checked', false); + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList.find('.toggle').prop( + 'checked', + $(e.currentTarget).is(':checked') + ); + } + }); + }, + + _destroyButtonBarEventHandlers: function () { + this._off( + this.element.find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off( + this.element.find('.fileupload-buttonbar .toggle'), + 'change.' + ); + }, + + _initEventHandlers: function () { + this._super(); + this._on(this.options.filesContainer, { + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); + this._initButtonBarEventHandlers(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._off(this.options.filesContainer, 'click'); + this._super(); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', false) + .parent().removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', true) + .parent().addClass('disabled'); + }, + + _initTemplates: function () { + var options = this.options; + options.templatesContainer = this.document[0].createElement( + options.filesContainer.prop('nodeName') + ); + if (tmpl) { + if (options.uploadTemplateId) { + options.uploadTemplate = tmpl(options.uploadTemplateId); + } + if (options.downloadTemplateId) { + options.downloadTemplate = tmpl(options.downloadTemplateId); + } + } + }, + + _initFilesContainer: function () { + var options = this.options; + if (options.filesContainer === undefined) { + options.filesContainer = this.element.find('.files'); + } else if (!(options.filesContainer instanceof $)) { + options.filesContainer = $(options.filesContainer); + } + }, + + _initSpecialOptions: function () { + this._super(); + this._initFilesContainer(); + this._initTemplates(); + }, + + _create: function () { + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); + } + }, + + enable: function () { + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } + }, + + disable: function () { + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-validate.js b/common/modules/file/assets/js/jquery.fileupload-validate.js new file mode 100644 index 0000000..d6f754c --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-validate.js @@ -0,0 +1,122 @@ +/* + * jQuery File Upload Validation Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, require, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + fileSize; + if (options.minFileSize || options.maxFileSize) { + fileSize = file.size; + } + if ($.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (fileSize > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(fileSize) === 'number' && + fileSize < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload-video.js b/common/modules/file/assets/js/jquery.fileupload-video.js new file mode 100644 index 0000000..8067ca1 --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload-video.js @@ -0,0 +1,112 @@ +/* + * jQuery File Upload Video Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('load-image') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadVideo', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableVideoPreview' + }, + { + action: 'setVideo', + name: '@videoPreviewName', + disabled: '@disableVideoPreview' + } + ); + + // The File Upload Video Preview plugin extends the fileupload widget + // with video preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of video files to load, + // matched against the file type: + loadVideoFileTypes: /^video\/.*$/ + }, + + _videoElement: document.createElement('video'), + + processActions: { + + // Loads the video file given via data.files and data.index + // as video element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadVideo: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + video; + if (this._videoElement.canPlayType && + this._videoElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + video = this._videoElement.cloneNode(false); + video.src = url; + video.controls = true; + data.video = video; + return data; + } + } + return data; + }, + + // Sets the video element as a property of the file object: + setVideo: function (data, options) { + if (data.video && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.video; + } + return data; + } + + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.fileupload.js b/common/modules/file/assets/js/jquery.fileupload.js new file mode 100644 index 0000000..91b7254 --- /dev/null +++ b/common/modules/file/assets/js/jquery.fileupload.js @@ -0,0 +1,1477 @@ +/* + * jQuery File Upload Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery.ui.widget' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./vendor/jquery.ui.widget') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('').prop('disabled')); + + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false, + timeout: 0 + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(paramName, options.blob, file.name); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).pipe( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.pipe(send); + } else { + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options)) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; + }); + return result; + }, + + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; + $('
    ').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.focus(); + } + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + successHandler = function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }, + dirReader, entries = []; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data); + } + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + data = this.element.data(); + // Initialize options set via HTML5 data-attributes: + $.each( + this.element[0].attributes, + function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data); + jqXHR.then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/common/modules/file/assets/js/jquery.iframe-transport.js b/common/modules/file/assets/js/jquery.iframe-transport.js new file mode 100644 index 0000000..a7d34e0 --- /dev/null +++ b/common/modules/file/assets/js/jquery.iframe-transport.js @@ -0,0 +1,217 @@ +/* + * jQuery Iframe Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, require, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0; + + // The iframe transport accepts four additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s), + // can be a string or an array of strings. + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + // options.initialIframeSrc: the URL of the initial iframe src, + // by default set to "javascript:false;" + $.ajaxTransport('iframe', function (options) { + if (options.async) { + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6: + /*jshint scripturl: true */ + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + /*jshint scripturl: false */ + form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + form = $('
    '); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('') + .appendTo(form); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + // Remove the HTML5 form attribute from the input(s): + options.fileInput.removeAttr('form'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + // Restore the original name and form properties: + $(input) + .prop('name', clone.prop('name')) + .attr('form', clone.attr('form')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', initialIframeSrc); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && $.parseJSON($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); + +})); diff --git a/common/modules/file/assets/js/vendor/jquery.ui.widget.js b/common/modules/file/assets/js/vendor/jquery.ui.widget.js new file mode 100644 index 0000000..e08df3f --- /dev/null +++ b/common/modules/file/assets/js/vendor/jquery.ui.widget.js @@ -0,0 +1,572 @@ +/*! jQuery UI - v1.11.4+CommonJS - 2015-08-28 +* http://jqueryui.com +* Includes: widget.js +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + + } else if ( typeof exports === "object" ) { + + // Node/CommonJS + factory( require( "jquery" ) ); + + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { +/*! + * jQuery UI Widget 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + + +var widget_uuid = 0, + widget_slice = Array.prototype.slice; + +$.cleanData = (function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; (elem = elems[i]) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; +})( $.cleanData ); + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; +}; + +$.widget.extend = function( target ) { + var input = widget_slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = widget_slice.call( arguments, 1 ), + returnValue = this; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat(args) ); + } + + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widget_uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled", !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + } + + return this; + }, + + enable: function() { + return this._setOptions({ disabled: false }); + }, + disable: function() { + return this._setOptions({ disabled: true }); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +var widget = $.widget; + + + +})); diff --git a/common/modules/file/config.php b/common/modules/file/config.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/common/modules/file/config.php @@ -0,0 +1,3 @@ +$w){ + return true; + }else if($height >$h) { + return true; + } + return false; + } + + + private function getUserPath(){ + if(isset(Yii::$app->user->id)){ + return 'user_'.Yii::$app->user->id; + }else { + return 'guest'; + } + } + + private function resizeImg($w, $h, $imageAlias,$imageAliasSave){ + $img = Image::getImagine()->open(Yii::getAlias($imageAlias)); + + $size = $img->getSize(); + + $width = $size->getWidth(); + $height = $size->getHeight(); + + $e_width = $w/$h; + $e_height = $h/$w; + + $e1_width = $width/$height; + $e1_height = $height/$width; + + + + if($e_width<$e1_width){ + + $new_width = $width*($e_width/$e1_width); + + $y = 0; + $x = $width/ 2-($new_width/2); + $width = $new_width; + + }else { + + $new_height = $height*($e_height/$e1_height); + $x = 0; + $y = $height/2-($new_height/2); + $height = $new_height; + } + + + + + Image::crop($imageAlias, $width, $height,[$x,$y]) + ->save(Yii::getAlias($imageAliasSave), ['quality' => + 100]); + + + $imagine = new Imagine(); + $imagine->open($imageAliasSave) + ->resize(new Box($w, $h)) + ->save($imageAliasSave, array('flatten' => false)); + + + + } + + private function deleteImages($old_img){ + + if(!empty($old_img) && file_exists($_SERVER['DOCUMENT_ROOT'].$old_img)){ + + $rootDir = explode("/", $old_img); + + $row = $_SERVER['DOCUMENT_ROOT'].'/'.$rootDir[1].'/'.$rootDir[2].'/'.$rootDir[3].'/'; + + $allFiles = scandir($row); + + $allFiles = array_slice($allFiles, 2); + + foreach($allFiles as $oldFile){ + + unlink($row.$oldFile); + + } + + } + } + + public function actionDeleteImage(){ + + $this->enableCsrfValidation = false; + + $request = Yii::$app->request->post(); + + if($request){ + if ($request['old_img']) { + $this->deleteImages($request['old_img']); + } + if(isset($request['action']) && $request['action']=='save'){ + $object = str_replace('-', '\\',$request['model']); + $model = new $object; + $model = $model->findOne($request['id']); + $model->$request['field'] = $request['new_url']; + $model->save(); + } + } + + } + + + public function actionDownloadPhoto() + { + + $model = new ImageSizerForm(); + + $request = Yii::$app->request->post(); + + if ($request) { + + $model->multi = isset($request['multi'])? 1 : 0; + + $model->file = UploadedFile::getInstance($model, 'file'); + + $md5_file = md5_file($model->file->tempName).rand(1, 1000); + + $imgDir = Yii::getAlias('@storage/'.$this->getUserPath().'/'.$md5_file.'/'); + + $imageOrigAlias = Yii::getAlias($imgDir.'original'.'.'.$model->file->extension); + + if(!is_dir($imgDir)) { + mkdir($imgDir, 0755, true); + } + + $model->file->saveAs($imageOrigAlias); + + + if(isset($request['size'] )){ + + $request['size'] = ArrayHelper::toArray(json_decode($request['size'])); + + foreach($request['size'] as $size){ + if($size['width'] && $size['height']){ + + $imageAlias = Yii::getAlias($imgDir.$size['width'].'x'.$size['height'].'.'.$model->file->extension); + + $imageLink = '/storage/'.$this->getUserPath().'/'.$md5_file.'/'.$size['width'].'x'.$size['height'].'.'.$model->file->extension; + + $this->resizeImg($size['width'], $size['height'], $imageOrigAlias,$imageAlias); + + } + } + + } else { + + $imageLink = '/storage/'.$this->getUserPath().'/'.$md5_file.'/'.'original'.'.'.$model->file->extension; + + } + + + if($model->multi){ + $view = $this->renderPartial('/_gallery_item', [ + 'item' => ['image'=>$imageLink], + 'field'=>$request['field'] + ]); + return json_encode(['link'=>$imageLink, + 'view' =>$view, + + ]); + + + } else { + $view = $this->renderPartial('/_one_item', [ + 'item' => ['image'=>$imageLink], + 'field'=>$request['field'] + ]); + return json_encode(['link'=>$imageLink, + 'view' =>$view, + ]); + } + + + } + } + + + public function getex($filename) { + return end(explode(".", $filename)); + } + + + public function actionImagesUpload(){ + + if($_FILES['upload']) + { + if (($_FILES['upload'] == "none") OR (empty($_FILES['upload']['name'])) ) + { + $message = "Вы не выбрали файл"; + } + else if ($_FILES['upload']["size"] == 0 OR $_FILES['upload']["size"] > 2050000) + { + $message = "Размер файла не соответствует нормам"; + } + else if (($_FILES['upload']["type"] != "image/jpeg") AND ($_FILES['upload']["type"] != "image/jpeg") AND ($_FILES['upload']["type"] != "image/png") AND ($_FILES['upload']['type'] != 'image/gif')) + { + $message = "Допускается загрузка только картинок JPG и PNG."; + } + else if (!is_uploaded_file($_FILES['upload']["tmp_name"])) + { + $message = "Что-то пошло не так. Попытайтесь загрузить файл ещё раз."; + } + else{ + $name =$_FILES['upload']['name'].'.'.$this->getex($_FILES['upload']['name']); + + $path = "../../storage/".$this->getUserPath()."/images/"; + if(!is_dir($path)) { + mkdir($path, 0755, true); + } + + + + move_uploaded_file($_FILES['upload']['tmp_name'], $path.$name); + + $full_path = '/storage/'.$this->getUserPath().'/images/'.$name; + + $message = "Файл ".$_FILES['upload']['name']." загружен"; + + + } + $callback = $_REQUEST['CKEditorFuncNum']; + echo ''; + } + } + + +} \ No newline at end of file diff --git a/common/modules/file/models/ImageSizerForm.php b/common/modules/file/models/ImageSizerForm.php new file mode 100644 index 0000000..4f141fb --- /dev/null +++ b/common/modules/file/models/ImageSizerForm.php @@ -0,0 +1,38 @@ + 255], + [['model', 'form',], 'string'], + [['file','img','price_list'], 'file'], + ]; + } +} \ No newline at end of file diff --git a/common/modules/file/views/_gallery_item.php b/common/modules/file/views/_gallery_item.php new file mode 100644 index 0000000..f04a44d --- /dev/null +++ b/common/modules/file/views/_gallery_item.php @@ -0,0 +1,9 @@ + + +
    + + +
    \ No newline at end of file diff --git a/common/modules/file/views/_one_item.php b/common/modules/file/views/_one_item.php new file mode 100644 index 0000000..4534483 --- /dev/null +++ b/common/modules/file/views/_one_item.php @@ -0,0 +1,17 @@ + + +
    + +
    + + +
    + + 'thumbnail']) ?> + +
    + diff --git a/common/modules/file/widgets/ImageUploader.php b/common/modules/file/widgets/ImageUploader.php new file mode 100644 index 0000000..48d7787 --- /dev/null +++ b/common/modules/file/widgets/ImageUploader.php @@ -0,0 +1,63 @@ +render('image_sizer', + [ + 'model'=>$this->model, + 'size' => $this->size, + 'field' => $this->field, + 'height' => $this->height, + 'width' => $this->width, + 'multi' => $this->multi, + 'name' => $this->name, + 'remover' => $this->remover + ]); + + } + + public function getGallery(){ + if($this->gallery){ + $array = explode(",", $this->gallery); + if(count($array) > 1){ + array_pop($array); + } + return $array; + } else { + return array(); + } + + } + +} \ No newline at end of file diff --git a/common/modules/file/widgets/views/image_sizer.php b/common/modules/file/widgets/views/image_sizer.php new file mode 100644 index 0000000..510f883 --- /dev/null +++ b/common/modules/file/widgets/views/image_sizer.php @@ -0,0 +1,203 @@ +tableSchema->primaryKey[0]; + +?> +
    + + + + "{$field}_picture_link"]) ?> + + + + + + +
    +
    + + $field): ?> + + + + $field):?> + $field),'#',['class'=>'thumbnail']) ?> + +
    +
    + +
    +
    + + + + + $field, 'data-url'=>Yii::$app->getUrlManager()->createUrl('file/uploader/download-photo')]);?> + +
    + + + + + + + + + + + + + + + + + + $field, 'data-url'=>Yii::$app->getUrlManager()->createUrl('file/uploader/download-photo'), 'multiple'=> 'multiple' ]);?> + + + "{$field}_picture_link"]) ?> + + + + +
    + context->getGallery() as $image){ + echo $this->render('@common/modules/file/views/_gallery_item', [ 'item' => ['image'=>$image]]); + } + ?> +
    + + + diff --git a/common/modules/product/Module.php b/common/modules/product/Module.php new file mode 100644 index 0000000..4cc65a6 --- /dev/null +++ b/common/modules/product/Module.php @@ -0,0 +1,24 @@ + [ + 'category_group' => 1, + 'brand_group' => 2, + ], +]; \ No newline at end of file diff --git a/common/modules/product/controllers/DefaultController.php b/common/modules/product/controllers/DefaultController.php new file mode 100644 index 0000000..609a4c4 --- /dev/null +++ b/common/modules/product/controllers/DefaultController.php @@ -0,0 +1,20 @@ +render('index'); + } +} diff --git a/common/modules/product/controllers/ManageController.php b/common/modules/product/controllers/ManageController.php new file mode 100644 index 0000000..fdfe194 --- /dev/null +++ b/common/modules/product/controllers/ManageController.php @@ -0,0 +1,124 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ]; + } + + /** + * Lists all Product models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new ProductSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a single Product model. + * @param integer $id + * @return mixed + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new Product model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new Product(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->product_id]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } + } + + /** + * Updates an existing Product model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->product_id]); + } else { + return $this->render('update', [ + 'model' => $model, + ]); + } + } + + /** + * Deletes an existing Product model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the Product model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return Product the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Product::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } +} diff --git a/common/modules/product/helpers/ProductHelper.php b/common/modules/product/helpers/ProductHelper.php new file mode 100644 index 0000000..fc2b49c --- /dev/null +++ b/common/modules/product/helpers/ProductHelper.php @@ -0,0 +1,24 @@ +getModule('product')->params['category_group']; + } + + public static function getBrandGroupId() { + return \Yii::$app->getModule('product')->params['brand_group']; + } + + public static function getCategories() { + return TaxOption::find()->getTree(self::getCategoryGroupId()); + } + + public static function getBrands() { + return TaxOption::find()->where(['tax_group_id' => self::getBrandGroupId()]); + } +} \ No newline at end of file diff --git a/common/modules/product/models/Product.php b/common/modules/product/models/Product.php new file mode 100644 index 0000000..1fbc8f7 --- /dev/null +++ b/common/modules/product/models/Product.php @@ -0,0 +1,106 @@ + relationBehavior::className(), + 'relations' => [ + 'product_categories' => 'entity1' // Product category + ] + ], + ]; + } + + /** + * @inheritdoc + */ + public static function tableName() + { + return '{{%product}}'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['tax_brand_id'], 'integer'], + [['name'], 'string', 'max' => 150], + [['categories'], 'safe'], +// [['product_id'], 'exist', 'skipOnError' => true, 'targetClass' => Product::className(), 'targetAttribute' => ['product_id' => 'product_id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'product_id' => Yii::t('product', 'Product ID'), + 'name' => Yii::t('product', 'Name'), + 'tax_brand_id' => Yii::t('product', 'Brand'), + 'brand' => Yii::t('product', 'Brand'), + 'categories' => Yii::t('product', 'Categories'), // relation behavior field + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getBrand() + { + return $this->hasOne(TaxOption::className(), ['tax_option_id' => 'tax_brand_id']); + } + + public function getFullName() + { + return $this->brandname .' '. $this->name; + } + + public function getBrandName() + { + return $this->getBrand()->one()->valueRenderHTML; + } + + public function getCategory() { + /** @var ActiveQuery $categories */ + $categories = $this->getRelations('product_categories'); + $count = $categories->count(); + if ($count == 0) + return 'None'; + return $categories->one()->ValueRenderFlash . ($count > 1 ? ' + '. $count : ''); + } + + /** + * @inheritdoc + * @return ProductQuery the active query used by this AR class. + */ + public static function find() + { + return new ProductQuery(get_called_class()); + } +} diff --git a/common/modules/product/models/ProductCategory.php b/common/modules/product/models/ProductCategory.php new file mode 100644 index 0000000..a5d1245 --- /dev/null +++ b/common/modules/product/models/ProductCategory.php @@ -0,0 +1,60 @@ + true, 'targetClass' => TaxOption::className(), 'targetAttribute' => ['dev_category_id' => 'tax_option_id']], + [['product_id'], 'exist', 'skipOnError' => true, 'targetClass' => Product::className(), 'targetAttribute' => ['product_id' => 'product_id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'product_id' => Yii::t('product', 'Product'), + 'dev_category_id' => Yii::t('product', 'Category'), + ]; + } + + public function getProduct() { + return $this->getEntity1(); + } + + public function getCategory() { + return $this->getEntity2(); + } +} diff --git a/common/modules/product/models/ProductQuery.php b/common/modules/product/models/ProductQuery.php new file mode 100644 index 0000000..373c74b --- /dev/null +++ b/common/modules/product/models/ProductQuery.php @@ -0,0 +1,34 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * @inheritdoc + * @return Product[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + * @return Product|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} diff --git a/common/modules/product/models/ProductSearch.php b/common/modules/product/models/ProductSearch.php new file mode 100644 index 0000000..4d2c42d --- /dev/null +++ b/common/modules/product/models/ProductSearch.php @@ -0,0 +1,70 @@ + $query, + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'tax_brand_id' => $this->tax_brand_id, + 'product_id' => $this->product_id, + ]); + + $query->andFilterWhere(['like', 'name', $this->name]); + + return $dataProvider; + } +} diff --git a/common/modules/product/views/default/index.php b/common/modules/product/views/default/index.php new file mode 100644 index 0000000..218cc44 --- /dev/null +++ b/common/modules/product/views/default/index.php @@ -0,0 +1,12 @@ +
    +

    context->action->uniqueId ?>

    +

    + This is the view content for action "context->action->id ?>". + The action belongs to the controller "context) ?>" + in the "context->module->id ?>" module. +

    +

    + You may customize this page by editing the following file:
    + +

    +
    diff --git a/common/modules/product/views/manage/_form.php b/common/modules/product/views/manage/_form.php new file mode 100644 index 0000000..f1233cb --- /dev/null +++ b/common/modules/product/views/manage/_form.php @@ -0,0 +1,43 @@ + + +
    + + + + field($model, 'name')->textInput(['maxlength' => true]) ?> + + field($model, 'tax_brand_id')->dropDownList( + ArrayHelper::map(ProductHelper::getBrands()->all(), 'tax_option_id', 'ValueRenderFlash'), + [ + 'prompt' => Yii::t('product', 'Select brand') + ] + ) ?> + categories); + ?> + field($model, 'categories')->dropDownList( + ArtboxTreeHelper::treeMap(ProductHelper::getCategories(), 'tax_option_id', 'ValueRenderFlash'), + [ +// 'prompt' => Yii::t('product', 'Select category'), + 'multiple' => true + ] + ) ?> + +
    + isNewRecord ? Yii::t('product', 'Create') : Yii::t('product', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
    + + + +
    diff --git a/common/modules/product/views/manage/_search.php b/common/modules/product/views/manage/_search.php new file mode 100644 index 0000000..f051c6a --- /dev/null +++ b/common/modules/product/views/manage/_search.php @@ -0,0 +1,31 @@ + + + diff --git a/common/modules/product/views/manage/create.php b/common/modules/product/views/manage/create.php new file mode 100644 index 0000000..e5f7902 --- /dev/null +++ b/common/modules/product/views/manage/create.php @@ -0,0 +1,21 @@ +title = Yii::t('product', 'Create Product'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('product', 'Products'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + ]) ?> + +
    diff --git a/common/modules/product/views/manage/index.php b/common/modules/product/views/manage/index.php new file mode 100644 index 0000000..82254a2 --- /dev/null +++ b/common/modules/product/views/manage/index.php @@ -0,0 +1,34 @@ +title = Yii::t('product', 'Products'); +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + render('_search', ['model' => $searchModel]); ?> + +

    + 'btn btn-success']) ?> +

    + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + +// 'product_id', + 'fullname', + 'brandname', + 'category', + + ['class' => 'yii\grid\ActionColumn'], + ], + ]); ?> +
    diff --git a/common/modules/product/views/manage/update.php b/common/modules/product/views/manage/update.php new file mode 100644 index 0000000..9ff3851 --- /dev/null +++ b/common/modules/product/views/manage/update.php @@ -0,0 +1,23 @@ +title = Yii::t('product', 'Update {modelClass}: ', [ + 'modelClass' => 'Product', +]) . ' ' . $model->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('product', 'Products'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->product_id]]; +$this->params['breadcrumbs'][] = Yii::t('product', 'Update'); +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + ]) ?> + +
    diff --git a/common/modules/product/views/manage/view.php b/common/modules/product/views/manage/view.php new file mode 100644 index 0000000..e6d9c7c --- /dev/null +++ b/common/modules/product/views/manage/view.php @@ -0,0 +1,37 @@ +title = $model->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('product', 'Products'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + +

    + $model->product_id], ['class' => 'btn btn-primary']) ?> + $model->product_id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('product', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> +

    + + $model, + 'attributes' => [ + 'name', + 'tax_brand_id', + 'product_id', + ], + ]) ?> + +
    diff --git a/common/modules/relation/Module.php b/common/modules/relation/Module.php new file mode 100644 index 0000000..b4f79b1 --- /dev/null +++ b/common/modules/relation/Module.php @@ -0,0 +1,40 @@ +render('index'); + } +} diff --git a/common/modules/relation/controllers/ManageController.php b/common/modules/relation/controllers/ManageController.php new file mode 100644 index 0000000..2b797e4 --- /dev/null +++ b/common/modules/relation/controllers/ManageController.php @@ -0,0 +1,214 @@ + $relation) { + $list[] = [ + 'key' => $key, + 'name' => $relation['name'], + 'entity1_label' => $relation['entity1']['label'], + 'entity1_model' => $relation['entity1']['model'], + 'entity2_label' => $relation['entity2']['label'], + 'entity2_model' => $relation['entity2']['model'], + ]; + } + return $this->render('relations', [ + 'relations' => $list + ]); + } + /** + * Renders the pars view for + * @return string + */ + public function actionPars($relation) + { + $relation_key = strtolower($relation); + $relation = relationHelper::getRelation($relation_key); + + $dataProvider = new ActiveDataProvider([ + 'query' => $relation['via']['model']::find(), + ]); + + return $this->render('pars', [ + 'dataProvider' => $dataProvider, + 'relation_key' => $relation_key, + 'relation' => $relation, + ]); + } + + public function actionCreate($relation) { + $relation_key = strtolower($relation); + $relation = relationHelper::getRelation($relation_key); + + $model = new $relation['via']['model']; + + $query1 = $relation['entity1']['model']::find(); + if (!empty($relation['entity1']['where'])) + $query1->where($relation['entity1']['where']); + + $query2 = $relation['entity2']['model']::find(); + if (!empty($relation['entity2']['where'])) + $query2->where($relation['entity2']['where']); + + if ($model->load(Yii::$app->request->post())) { + $model->save(); + return $this->redirect(['pars', 'relation' => $relation_key]); +// return $this->redirect(['update', 'id' => $model->{$relation['entity1']['linked_key']}. ':' .$model->{$relation['entity2']['linked_key']}]); + } else { + return $this->render('create', [ + 'model' => $model, + 'items1' => $query1->all(), + 'items2' => $query2->all(), + 'relation_key' => $relation_key, + 'relation' => $relation, + ]); + } + } + + /** + * Updates an existing TaxGroup model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + */ + public function actionUpdate($relation, $id) { + $relation_key = strtolower($relation); + $relation = relationHelper::getRelation($relation_key); + + list($id1, $id2) = explode(':', $id); + + $model = $this->findModel($relation_key, $id1, $id2); + + $query1 = $relation['entity1']['model']::find(); + if (!empty($relation['entity1']['where'])) + $query1->where($relation['entity1']['where']); + + $query2 = $relation['entity2']['model']::find(); + if (!empty($relation['entity2']['where'])) + $query2->where($relation['entity2']['where']); + + if ($model->load(Yii::$app->request->post())) { + $connection = Yii::$app->getDb(); + $transaction = $connection->beginTransaction(); + try { + // Delete links from viaTable + $connection->createCommand() + ->update + ( + $relation['linked_table'], + [ + $relation['entity1']['linked_key'] => $model->getAttribute($relation['entity1']['linked_key']), + $relation['entity2']['linked_key'] => $model->getAttribute($relation['entity2']['linked_key']) + ], + $this->getWhere($relation_key, $id1, $id2) + ) + ->execute(); + $transaction->commit(); + } catch (Exception $ex) { + $transaction->rollback(); + throw $ex; + } + + return $this->redirect(['pars', 'relation' => $relation_key]); + } else { + return $this->render('update', [ + 'model' => $model, + 'items1' => $query1->all(), + 'items2' => $query2->all(), + 'relation_key' => $relation_key, + 'relation' => $relation, + ]); + } + } + + /** + * Deletes an existing TaxGroup model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + */ + public function actionDelete($relation, $id) + { + $relation_key = strtolower($relation); + $relation = relationHelper::getRelation($relation_key); + + list($id1, $id2) = explode(':', $id); + + $connection = Yii::$app->getDb(); + $transaction = $connection->beginTransaction(); + try { + // Delete links from viaTable + $connection->createCommand() + ->delete + ( + $relation['linked_table'], + $this->getWhere($relation_key, $id1, $id2) + ) + ->execute(); + $transaction->commit(); + } catch (Exception $ex) { + $transaction->rollback(); + throw $ex; + } + + return $this->redirect(['pars', 'relation' => $relation_key]); + } + + /** + * Finds the based model for relation on its primaries keys value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param string $relation + * @param integer $id1 + * @param integer $id2 + * @return ActiveRecord the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($relation, $id1, $id2) + { + $relation_key = strtolower($relation); + $relation = relationHelper::getRelation($relation_key); + if (($model = $relation['via']['model']::findOne($this->getWhere($relation_key, $id1, $id2))) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } + + protected function getWhere($relation_key, $id1, $id2) { + $relation = relationHelper::getRelation($relation_key); + // @todo Just think - if you need to search keys in the reverse order + $where = [ + $relation['entity1']['linked_key'] => $id1, + $relation['entity2']['linked_key'] => $id2, + ]; + if (!empty($relation['alias'])) { + $where[$relation['alias']] = $relation_key; + } + return $where; + } + +} diff --git a/common/modules/relation/models/Relation.php b/common/modules/relation/models/Relation.php new file mode 100644 index 0000000..bfe287f --- /dev/null +++ b/common/modules/relation/models/Relation.php @@ -0,0 +1,78 @@ + 50] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'alias' => Yii::t('relation', 'Alias'), + 'entity1_id' => Yii::t('relation', 'Entity1 ID'), + 'entity2_id' => Yii::t('relation', 'Entity2 ID'), + 'entity1' => Yii::t('relation', 'Entity1 ID'), + 'entity2' => Yii::t('relation', 'Entity2 ID'), + ]; + } + + /** + * @inheritdoc + * @return RelationQuery the active query used by this AR class. + */ + public static function find() + { + return new RelationQuery(get_called_class()); + } + + public function getRelationSection() { + return relationHelper::getRelation($this->alias); + } + + public function getEntity1() { + return $this->getEntity('entity1'); + } + + public function getEntity2() { + return $this->getEntity('entity2'); + } + + protected function getEntity($entity) { + $relation = $this->getRelationSection(); + if (!$relation) + return; + return $this->hasOne($relation[$entity]['model']::className(), [$relation[$entity]['key'] => $relation[$entity]['linked_key']]); + } +} diff --git a/common/modules/relation/models/RelationQuery.php b/common/modules/relation/models/RelationQuery.php new file mode 100644 index 0000000..9a04c33 --- /dev/null +++ b/common/modules/relation/models/RelationQuery.php @@ -0,0 +1,35 @@ +andWhere('[[status]]=1'); + return $this; + }*/ + + /** + * @inheritdoc + * @return Relation[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + * @return Relation|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} \ No newline at end of file diff --git a/common/modules/relation/relationBehavior.php b/common/modules/relation/relationBehavior.php new file mode 100644 index 0000000..3f83836 --- /dev/null +++ b/common/modules/relation/relationBehavior.php @@ -0,0 +1,224 @@ +relations as $relation_key => &$relation) { + if (is_string($relation)) { + // Get data from module's data + $relation_entity = $relation; + $relation = $this->_getRelationParams($relation_key); + $relation['inner'] = $relation[$relation_entity]; + $relation['outer'] = $relation[$relation_entity == 'entity1' ? 'entity2' : 'entity1']; + $relation['linked_table'] = $relation['via']['model']::tableName(); + } + $this->fields[$relation['field']] = $relation_key; + } + } + + /* + * Events for auto-drive relations data + */ + public function events() + { + return [ + ActiveRecord::EVENT_AFTER_INSERT => 'relationsAfterSave', + ActiveRecord::EVENT_AFTER_UPDATE => 'relationsAfterSave', + ActiveRecord::EVENT_BEFORE_DELETE => 'relationBeforeDelete', + ]; + } + + public function relationsAfterSave($insert) { + if (is_array($modelPrimaryKey = $this->owner->getPrimaryKey())) { + throw new ErrorException('This behavior does not support composite primary keys'); + } + foreach ($this->relations as $relation_key => $relation) { + if (empty($relation['field'])) + continue; + $values = $this->{$relation['field']}; + + /** @var ActiveRecord $model */ + $model = new $relation['inner']['model']; + + $connection = $model::getDb(); + $transaction = $connection->beginTransaction(); + + // @todo Refix to ActiveRecord format + try { + // Delete all links from viaTable + $connection->createCommand() + ->delete + ( + $relation['linked_table'], + [$relation['inner']['linked_key'] => $this->owner->{$relation['inner']['key']}] + ) + ->execute(); + + if (!empty($values)) { + foreach($values as $value) { + $connection->createCommand() + ->insert + ( + $relation['linked_table'], + [ + $relation['inner']['linked_key'] => $this->owner->{$relation['inner']['key']}, + $relation['outer']['linked_key'] => $value + ] + ) + ->execute(); + } + } + $transaction->commit(); + +// $model->link($relation_key, ) + } catch (Exception $ex) { + var_dump($relation_key, $relation);exit; + $transaction->rollback(); + throw $ex; + } + } + } + + public function relationBeforeDelete() { + if (is_array($modelPrimaryKey = $this->owner->getPrimaryKey())) { + throw new ErrorException('This behavior does not support composite primary keys'); + } + foreach ($this->relations as $relation_key => $relation) { + if (empty($relation['field'])) + continue; + $values = $this->{$relation['field']}; + + /** @var ActiveRecord $model */ + $model = new $relation['inner']['model']; + + $connection = $model::getDb(); + $transaction = $connection->beginTransaction(); + + // @todo Refix to ActiveRecord format + try { + // Delete all links from viaTable + $connection->createCommand() + ->delete + ( + $relation['linked_table'], + [$relation['inner']['linked_key'] => $this->owner->{$relation['inner']['key']}] + ) + ->execute(); + $transaction->commit(); + } catch (Exception $ex) { + $transaction->rollback(); + throw $ex; + } + } + } + + /** + * Get related data for $relation + * @params string $relation Relation key + */ + public function getRelations($relation) { + $relation = $this->_getRelation($relation); + return + $this->owner + ->hasMany($relation['outer']['model'], [$relation['outer']['key'] => $relation['outer']['linked_key']]) + ->viaTable($relation['linked_table'], [$relation['inner']['linked_key'] => $relation['inner']['key']]); + } + + /* + * Get relation params for $relation + * @param string $relation Relation key + */ + protected function _getRelation($relation) { + $relation = strtolower($relation); + return isset($this->relations[$relation]) ? $this->relations[$relation] : null; + } + + /** + * Return relation data from main app config + * @params string $section Relations key + */ + protected function _getRelationParams($section) { + $relation = relationHelper::getRelation($section); + if (!$relation) + throw new Exception('Relation "' . $section . '" not set on this application.'); + return $relation; + } + + protected function _getRelationNameByField($field) { + return isset($this->fields[$field]) ? $this->fields[$field] : null; + } + + protected function _getRelationByField($field) { + return ( isset($this->fields[$field]) && isset($this->relations[$this->fields[$field]]) ) ? $this->relations[$this->fields[$field]] : null; + } + + /** + * @inheritdoc + */ + public function canGetProperty($name, $checkVars = true) + { + return true; + } + + /** + * @inheritdoc + */ + public function canSetProperty($name, $checkVars = true) + { + return array_key_exists($name, $this->fields) ? + true : parent::canSetProperty($name, $checkVars = true); + } + + /** + * @inheritdoc + */ + public function __set($name, $value) { + if (isset($this->fields[$name])) { + $this->values[$name] = $value; + } + } + + /** + * @inheritdoc + */ + public function __get($name) { + if (isset($this->values[$name])) { + return $this->values[$name]; + } else { + $relation_key = $this->_getRelationNameByField($name); + if (!$relation_key) + return; + + return $this->getRelations($relation_key); + } + } +} \ No newline at end of file diff --git a/common/modules/relation/relationHelper.php b/common/modules/relation/relationHelper.php new file mode 100644 index 0000000..bdb6991 --- /dev/null +++ b/common/modules/relation/relationHelper.php @@ -0,0 +1,60 @@ +getModule('relation'); + + if (!is_array($module->relations)) + return []; + + return $module->relations; + } + + /** + * Return one relation for key $name + * @param string $name + * @return string (@todo refix to relationOject) + */ + public static function getRelation($name) { + $name = strtolower($name); + if (isset(self::$relations[$name])) { + return self::$relations[$name]; + } + $relations = self::getRelations(); + if (isset($relations[$name])) { + self::$relations[$name] = self::prepareRelation($relations[$name]); + } else { + self::$relations[$name] = null; + } + return self::$relations[$name]; + } + + + private static function prepareRelation($relation) { + if (!isset($relation['linked_table']) && isset($relation['via']['model'])) { + $relation['linked_table'] = $relation['via']['model']::tableName(); + } + return $relation; + } + + /** + * @param string $name + * @return bool + */ + public static function issetRelation($name) { + $relations = self::getRelations(); + return isset($relations[$name]); + } +} \ No newline at end of file diff --git a/common/modules/relation/relationObject.php b/common/modules/relation/relationObject.php new file mode 100644 index 0000000..9dce6fa --- /dev/null +++ b/common/modules/relation/relationObject.php @@ -0,0 +1,29 @@ +_getRelationParams($relation_key); + $relation['inner'] = $relation[$relation_entity]; + $relation['outer'] = $relation[$relation_entity == 'entity1' ? 'entity2' : 'entity1']; + $relation['linked_table'] = $relation['via']['model']::tableName(); + } + + $this->moduleRelations = relationHelper::getRelations(); + + + } +} \ No newline at end of file diff --git a/common/modules/relation/relationQueryTrait.php b/common/modules/relation/relationQueryTrait.php new file mode 100644 index 0000000..822556c --- /dev/null +++ b/common/modules/relation/relationQueryTrait.php @@ -0,0 +1,27 @@ +modelClass; + self::$model = new $class; + } + return self::$model; + } + + public function getRelations($relationKey) { + + } +} \ No newline at end of file diff --git a/common/modules/relation/views/default/index.php b/common/modules/relation/views/default/index.php new file mode 100644 index 0000000..e8f0a30 --- /dev/null +++ b/common/modules/relation/views/default/index.php @@ -0,0 +1,12 @@ +
    +

    context->action->uniqueId ?>

    +

    + This is the view content for action "context->action->id ?>". + The action belongs to the controller "context) ?>" + in the "context->module->id ?>" module. +

    +

    + You may customize this page by editing the following file:
    + +

    +
    diff --git a/common/modules/relation/views/manage/_form.php b/common/modules/relation/views/manage/_form.php new file mode 100644 index 0000000..0c047f0 --- /dev/null +++ b/common/modules/relation/views/manage/_form.php @@ -0,0 +1,36 @@ + + +
    + + + + field($model, $relation['entity1']['linked_key'])->dropDownList( + ArrayHelper::map($items1, $relation['entity1']['key'], $relation['entity1']['listField']), + [ + 'prompt' => Yii::t('relation', 'Select value'), + ] + ) ?> + + field($model, $relation['entity2']['linked_key'])->dropDownList( + ArrayHelper::map($items2, $relation['entity2']['key'], $relation['entity2']['listField']), + [ + 'prompt' => Yii::t('relation', 'Select value'), + ] + ) ?> + +
    + isNewRecord ? Yii::t('relation', 'Create') : Yii::t('relation', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
    + + + +
    diff --git a/common/modules/relation/views/manage/create.php b/common/modules/relation/views/manage/create.php new file mode 100644 index 0000000..743430c --- /dev/null +++ b/common/modules/relation/views/manage/create.php @@ -0,0 +1,25 @@ +title = Yii::t('relation', $relation['name']); +$this->params['breadcrumbs'][] = ['label' => Yii::t('relation', 'Relations'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('relation', $relation['name']), 'url' => ['/relation/manage/pars', 'relation' => $relation_key]]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + 'items1' => $items1, + 'items2' => $items2, + 'relation_key' => $relation_key, + 'relation' => $relation, + ]) ?> + +
    diff --git a/common/modules/relation/views/manage/pars.php b/common/modules/relation/views/manage/pars.php new file mode 100644 index 0000000..c82accf --- /dev/null +++ b/common/modules/relation/views/manage/pars.php @@ -0,0 +1,54 @@ +title = Yii::t('relation', 'Relation items for {relation}', ['relation' => $relation['name']]); +$this->params['breadcrumbs'][] = ['label' => Yii::t('relation', 'Relations'), 'url' => ['/relation/manage']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + +

    + 'btn btn-success']) ?> +

    + + $dataProvider, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + 'alias', + 'entity1.'. $relation['entity1']['listField'], + 'entity2.'. $relation['entity2']['listField'], + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {delete}', + 'buttons' => [ + 'update' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('relation', 'Edit par'), + ]); + }, + 'delete' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('relation', 'Delete par'), + ]); + }, + ], + 'urlCreator' => function ($action, $model, $key, $index) use ($relation, $relation_key) { + if ($action === 'update') { + $url ='/admin/relation/manage/update?relation='. $relation_key .'&id='. $model->{$relation['entity1']['linked_key']} .':'. $model->{$relation['entity2']['linked_key']}; + return $url; + } + if ($action === 'delete') { + $url ='/admin/relation/manage/delete?relation='. $relation_key .'&id='. $model->{$relation['entity1']['linked_key']} .':'. $model->{$relation['entity2']['linked_key']}; + return $url; + } + } + ], + ], + ]); ?> +
    diff --git a/common/modules/relation/views/manage/relations.php b/common/modules/relation/views/manage/relations.php new file mode 100644 index 0000000..46950ab --- /dev/null +++ b/common/modules/relation/views/manage/relations.php @@ -0,0 +1,53 @@ +title = Yii::t('relation', 'Relations'); +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + $relations, + 'sort' => [ + 'attributes' => ['name', 'key', 'entity1_label', 'entity2_label'], + ], + 'pagination' => [ + 'pageSize' => 10, + ], + ]); + ?> + + $dataProvider, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + 'name', + 'key', + 'entity1_label', + 'entity2_label', + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{view}', + 'buttons' => [ + 'view' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('relation', 'View pars'), + ]); + }, + ], + 'urlCreator' => function ($action, $model) { + if ($action === 'view') { + $url ='/admin/relation/manage/pars?relation='. $model['key']; + return $url; + } + } + ], + ], + ]); ?> +
    diff --git a/common/modules/relation/views/manage/update.php b/common/modules/relation/views/manage/update.php new file mode 100644 index 0000000..743430c --- /dev/null +++ b/common/modules/relation/views/manage/update.php @@ -0,0 +1,25 @@ +title = Yii::t('relation', $relation['name']); +$this->params['breadcrumbs'][] = ['label' => Yii::t('relation', 'Relations'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('relation', $relation['name']), 'url' => ['/relation/manage/pars', 'relation' => $relation_key]]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + 'items1' => $items1, + 'items2' => $items2, + 'relation_key' => $relation_key, + 'relation' => $relation, + ]) ?> + +
    diff --git a/common/modules/rubrication/Module.php b/common/modules/rubrication/Module.php new file mode 100644 index 0000000..ffc888c --- /dev/null +++ b/common/modules/rubrication/Module.php @@ -0,0 +1,28 @@ +render('index'); + } +} diff --git a/common/modules/rubrication/controllers/TaxGroupController.php b/common/modules/rubrication/controllers/TaxGroupController.php new file mode 100644 index 0000000..f112d9c --- /dev/null +++ b/common/modules/rubrication/controllers/TaxGroupController.php @@ -0,0 +1,152 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ]; + } + + /** + * Lists all TaxGroup models. + * @return mixed + */ + public function actionIndex() + { + $dataProvider = new ActiveDataProvider([ + 'query' => TaxGroup::find(), + ]); + + return $this->render('index', [ + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a single TaxGroup model. + * @param integer $id + * @return mixed + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new TaxGroup model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new TaxGroup(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->tax_group_id]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } + } + + /** + * Updates an existing TaxGroup model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->tax_group_id]); + } else { + return $this->render('update', [ + 'model' => $model, + ]); + } + } + + /** + * Deletes an existing TaxGroup model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /* + * Group-relations + */ + public function actionRelation($id, $relations = ['tax_option_to_group', 'tax_option_to_option']) { + $group = $this->findModel($id); + $items = []; + + foreach($relations as $relation) { + $rows = $group->getRelations($relation)->all(); + $items = array_merge($items, $rows); + } + + return $this->render('relations', [ + 'items' => $items, + 'group' => $group, + ]); + } + + /* + * Rebuilp MP-params for group options + */ + public function actionRebuild($id) { + TaxOption::find()->rebuildMP($id); + + return $this->redirect(['index']); + } + + /** + * Finds the TaxGroup model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return TaxGroup the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = TaxGroup::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } +} diff --git a/common/modules/rubrication/controllers/TaxOptionController.php b/common/modules/rubrication/controllers/TaxOptionController.php new file mode 100644 index 0000000..3ca7b29 --- /dev/null +++ b/common/modules/rubrication/controllers/TaxOptionController.php @@ -0,0 +1,171 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ]; + } + + /** + * Lists all TaxOption models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new TaxOptionSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + $group = TaxGroup::findOne(Yii::$app->request->queryParams['group']); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + 'group' => $group, + ]); + } + + /** + * Displays a single TaxOption model. + * @param string $id + * @return mixed + */ + public function actionView($id) + { + $model = $this->findModel($id); + $group = TaxGroup::findOne($model->tax_group_id); + return $this->render('view', [ + 'model' => $model, + 'group' => $group, + ]); + } + + /** + * Creates a new TaxOption model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new TaxOption(); + $group = TaxGroup::findOne(Yii::$app->request->queryParams['group']); + $valueModelName = $this->getValueModelName($group); + $valueModel = new $valueModelName; + + if ($model->load(Yii::$app->request->post()) && $valueModel->load(Yii::$app->request->post())) { + $model->save(); + + $valueModel->tax_option_id = $model->tax_option_id; + $valueModel->save(); + + $model->default_value = $valueModel->tax_value_id; + $model->save(); + + return is_null(Yii::$app->request->post('create_and_new')) ? $this->redirect(['view', 'id' => $model->tax_option_id]) : $this->redirect(array_merge(['create'], Yii::$app->request->queryParams)); + } else { + $model->tax_group_id = $group->tax_group_id; + if (!empty(Yii::$app->request->queryParams['parent'])) { + $model->parent_id = Yii::$app->request->queryParams['parent']; + } + return $this->render('create', [ + 'model' => $model, + 'group' => $group, + 'valueModel' => $valueModel, + ]); + } + } + + /** + * Updates an existing TaxOption model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param string $id + * @return mixed + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + $group = TaxGroup::findOne($model->tax_group_id); + $valueModelName = $this->getValueModelName($group); + $valueModel = $valueModelName::findOne($model->default_value); + + if ($model->load(Yii::$app->request->post()) && $valueModel->load(Yii::$app->request->post())) { + $model->save(); + $valueModel->tax_option_id = $model->tax_option_id; + $valueModel->save(); + + $model->default_value = $valueModel->tax_value_id; + $model->save(); + + TaxOption::find()->rebuildMP($model->tax_group_id); + + return $this->redirect(['view', 'id' => $model->tax_option_id]); + } else { + return $this->render('update', [ + 'model' => $model, + 'group' => $group, + 'valueModel' => $valueModel, + ]); + } + } + + /** + * Deletes an existing TaxOption model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param string $id + * @return mixed + */ + public function actionDelete($id) + { + $model = $this->findModel($id); + $group_id = $model->tax_group_id; + + $model->delete(); + + return $this->redirect(['index', 'group' => $group_id]); + } + + /** + * Finds the TaxOption model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param string $id + * @return TaxOption the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = TaxOption::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } + + protected function getValueModelName($group) { + $valueClass = '\common\modules\rubrication\models\TaxValue'. ucfirst($group->module); + return class_exists($valueClass) ? $valueClass : FALSE; + } +} diff --git a/common/modules/rubrication/helpers/RubricationHelper.php b/common/modules/rubrication/helpers/RubricationHelper.php new file mode 100644 index 0000000..39e7cbb --- /dev/null +++ b/common/modules/rubrication/helpers/RubricationHelper.php @@ -0,0 +1,43 @@ + Yii::t('order', 'Invisible'), + 1 => Yii::t('order', 'Active'), + ]; + } + + /** + * @inheritdoc + * Returns sort-interval of appropriate options and others + * @return array. + */ + static public function SortArray($low = 0, $high = 100) { + return range($low, $high); + } + + static public function OptionTypes() { + if (!is_null(self::$types)) { + return self::$types; + } + + $module = \Yii::$app->getModule('rubrication'); + + if (!is_array($module->types)) + return []; + + return $module->types; + + } +} \ No newline at end of file diff --git a/common/modules/rubrication/models/TaxGroup.php b/common/modules/rubrication/models/TaxGroup.php new file mode 100644 index 0000000..d9df989 --- /dev/null +++ b/common/modules/rubrication/models/TaxGroup.php @@ -0,0 +1,116 @@ + relationBehavior::className(), + 'relations' => [ +// 'tax_option_to_group' => 'entity2', + ] + ], + ]; + } + + /** + * @inheritdoc + */ + public static function tableName() + { + return '{{%tax_group}}'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['alias', 'name', 'module'], 'required'], + [['description', 'settings'], 'string'], + [['hierarchical'], 'boolean'], + [['alias', 'module'], 'string', 'max' => 50], + [['name'], 'string', 'max' => 255], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_group_id' => 'Tax Group ID', + 'alias' => 'Alias', + 'name' => 'Name', + 'description' => 'Description', + 'module' => 'Module', + 'hierarchical' => 'Hierarchical', +// 'settings' => 'Settings', + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxGroupToGroups() + { + return $this->hasMany(TaxGroupToGroup::className(), ['tax_group1_id' => 'tax_group_id'])->inverseOf('taxGroup1'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxGroupToGroups0() + { + return $this->hasMany(TaxGroupToGroup::className(), ['tax_group2_id' => 'tax_group_id'])->inverseOf('taxGroup2'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOptions() + { + return $this->hasMany(TaxOption::className(), ['tax_group_id' => 'tax_group_id'])->inverseOf('taxGroup'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOptionToGroups() + { + return $this->hasMany(TaxOptionToGroup::className(), ['tax_group_id' => 'tax_group_id'])->inverseOf('taxGroup'); + } + + public function getValueModelName($full_path = true) { + $valueClass = 'TaxValue'. ucfirst($this->module); + $fullClass = '\common\modules\rubrication\models\\'. $valueClass; + return class_exists($fullClass) ? $full_path ? $fullClass : $valueClass : FALSE; + } +} diff --git a/common/modules/rubrication/models/TaxGroupToGroup.php b/common/modules/rubrication/models/TaxGroupToGroup.php new file mode 100644 index 0000000..6d3fe73 --- /dev/null +++ b/common/modules/rubrication/models/TaxGroupToGroup.php @@ -0,0 +1,69 @@ + 50] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_group1_id' => Yii::t('app', 'Tax Group1 ID'), + 'tax_group2_id' => Yii::t('app', 'Tax Group2 ID'), + 'alias' => Yii::t('app', 'Alias'), + 'sort' => Yii::t('app', 'Sort'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxGroup1() + { + return $this->hasOne(TaxGroup::className(), ['tax_group_id' => 'tax_group1_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxGroup2() + { + return $this->hasOne(TaxGroup::className(), ['tax_group_id' => 'tax_group2_id']); + } +} diff --git a/common/modules/rubrication/models/TaxOption.php b/common/modules/rubrication/models/TaxOption.php new file mode 100644 index 0000000..d25262b --- /dev/null +++ b/common/modules/rubrication/models/TaxOption.php @@ -0,0 +1,232 @@ + [ + 'class' => ArtboxTreeBehavior::className(), + 'keyNameGroup' => 'tax_group_id', + ], + 'slug' => [ + 'class' => 'common\behaviors\Slug', + 'in_attribute' => 'ValueRenderFlash', + 'out_attribute' => 'alias', + 'translit' => true + ], + ]; + } + + public function events() { + return $this->events(); + return [ +// ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', + ]; + } + + /** + * @inheritdoc + */ + public static function tableName() + { + return '{{%tax_option}}'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['tax_group_id'], 'required'], + [['tax_group_id', 'parent_id', 'sort', 'default_value'], 'integer'], + [['alias'], 'string', 'max' => 50], + [['tax_group_id'], 'exist', 'skipOnError' => true, 'targetClass' => TaxGroup::className(), 'targetAttribute' => ['tax_group_id' => 'tax_group_id']], +// [['parent_id'], 'exist', 'skipOnError' => true, 'targetClass' => TaxOption::className(), 'targetAttribute' => ['parent_id' => 'tax_option_id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_option_id' => Yii::t('app', 'Tax Option ID'), + 'tax_group_id' => Yii::t('app', 'Tax Group ID'), + 'parent_id' => Yii::t('app', 'Parent ID'), + 'alias' => Yii::t('app', 'Alias'), + 'sort' => Yii::t('app', 'Sort'), + 'default_value' => Yii::t('app', 'Default Value'), + ]; + } + + public static function find() { + return new TaxOptionQuery(get_called_class()); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxEntityRelations() + { + return $this->hasMany(TaxEntityRelation::className(), ['tax_option_id' => 'tax_option_id'])->inverseOf('taxOption'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getGroup() + { + return $this->getTaxGroup(); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxGroup() + { + return $this->hasOne(TaxGroup::className(), ['tax_group_id' => 'tax_group_id'])->inverseOf('taxOptions'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOptions() + { + return $this->hasMany(TaxOption::className(), ['parent_id' => 'tax_option_id'])->inverseOf('parent'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOptionToGroups() + { + return $this->hasMany(TaxOptionToGroup::className(), ['tax_option_id' => 'tax_option_id'])->inverseOf('taxOption'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOptionToOptions() + { + return $this->hasMany(TaxOptionToOption::className(), ['tax_option1_id' => 'tax_option_id'])->inverseOf('taxOption1'); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOptionToOptions0() + { + return $this->hasMany(TaxOptionToOption::className(), ['tax_option2_id' => 'tax_option_id'])->inverseOf('taxOption2'); + } + + /** + */ + public function getValue() + { + $valueClass = $this->getValueModelName(); + if ($valueClass) { + return $this->hasOne($valueClass, ['tax_value_id' => 'default_value', 'tax_option_id' => 'tax_option_id'])->inverseOf('taxOption'); + } + } + + /** + */ + public function getValueRenderFlash() + { + $valueClass = $this->getValueModelName(); + $value = $this->getValue()->one(); + if ($valueClass && method_exists($valueClass, 'getValueRenderFlash')) { + return $valueClass::getValueRenderFlash($value); + } elseif(!empty($value)) { + return $value->value; + } else { + return null; + } + } + + /** + */ + public function getValueRenderHTML() + { + $valueClass = $this->getValueModelName(); + $value = $this->getValue()->one(); + if ($valueClass && method_exists($valueClass, 'getValueRenderHTML')) { + return $valueClass::getValueRenderHTML($value); + } else { + return $value->value; + } + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getValues() + { + if ($valueClass = $this->getValueModelName()) + return $this->hasMany($valueClass, ['tax_option_id' => 'tax_option_id'])->inverseOf('taxOption')->cascadeOnDelete(); + } + + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + + if (empty($this->parent_id)) + $this->parent_id = 0; + + + + return true; + } + return false; + } + +// public function beforeDelete() { +// if ( ($model = $this->getValueModelName()) !== FALSE ) { +// +// } +// } + + private function getValueModelName() { + $group = $this->getTaxGroup()->one(); + $valueClass = '\common\modules\rubrication\models\TaxValue'. ucfirst($group->module); + return class_exists($valueClass) ? $valueClass : FALSE; + } +} diff --git a/common/modules/rubrication/models/TaxOptionQuery.php b/common/modules/rubrication/models/TaxOptionQuery.php new file mode 100644 index 0000000..5113f13 --- /dev/null +++ b/common/modules/rubrication/models/TaxOptionQuery.php @@ -0,0 +1,37 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * @inheritdoc + * @return TaxOption[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + * @return TaxOption|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} diff --git a/common/modules/rubrication/models/TaxOptionRelation.php b/common/modules/rubrication/models/TaxOptionRelation.php new file mode 100644 index 0000000..f80a7f1 --- /dev/null +++ b/common/modules/rubrication/models/TaxOptionRelation.php @@ -0,0 +1,50 @@ + 50] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_option1_id' => Yii::t('product', 'Tax Option1 ID'), + 'tax_option2_id' => Yii::t('product', 'Tax Option2 ID'), + 'alias' => Yii::t('product', 'Alias'), + 'sort' => Yii::t('product', 'Sort'), + ]; + } +} diff --git a/common/modules/rubrication/models/TaxOptionSearch.php b/common/modules/rubrication/models/TaxOptionSearch.php new file mode 100644 index 0000000..adfb6bc --- /dev/null +++ b/common/modules/rubrication/models/TaxOptionSearch.php @@ -0,0 +1,85 @@ +tax_group_id = intval($params['group']); +// $group = TaxGroup::findOne($this->tax_group_id); + unset($params['group']); + } +// if (!empty($params['value_render_flat'])) { +// $this->value_render_flat = trim($params['value_render_flat']); +// } + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + +// if (!empty($group)) { +// $query->joinWith('Value'); +// } + + // grid filtering conditions + $query->andFilterWhere([ + 'tax_option_id' => $this->tax_option_id, + 'tax_group_id' => $this->tax_group_id, + 'parent_id' => $this->parent_id, + 'sort' => $this->sort, + ]); + + $query->andFilterWhere(['like', 'alias', $this->alias]); + $query->andFilterWhere(['like', 'tax_value_string.value', $this->default_value]); + + $query->orderBy(['path_int' => SORT_ASC, 'depth' => SORT_ASC, 'sort' => SORT_ASC]); + + return $dataProvider; + } +} diff --git a/common/modules/rubrication/models/TaxValueInt.php b/common/modules/rubrication/models/TaxValueInt.php new file mode 100644 index 0000000..1fa9ea2 --- /dev/null +++ b/common/modules/rubrication/models/TaxValueInt.php @@ -0,0 +1,57 @@ + true, 'targetClass' => TaxOption::className(), 'targetAttribute' => ['tax_option_id' => 'tax_option_id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_value_id' => Yii::t('app', 'Tax Value ID'), + 'tax_option_id' => Yii::t('app', 'Tax Option ID'), + 'value' => Yii::t('app', 'Value'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOption() + { + return $this->hasOne(TaxOption::className(), ['tax_option_id' => 'tax_option_id'])->inverseOf('taxValues'); + } +} diff --git a/common/modules/rubrication/models/TaxValueLink.php b/common/modules/rubrication/models/TaxValueLink.php new file mode 100644 index 0000000..f2eb859 --- /dev/null +++ b/common/modules/rubrication/models/TaxValueLink.php @@ -0,0 +1,69 @@ + 150], + [['link'], 'string', 'max' => 255], + [['tax_option_id'], 'exist', 'skipOnError' => true, 'targetClass' => TaxOption::className(), 'targetAttribute' => ['tax_option_id' => 'tax_option_id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_value_id' => Yii::t('app', 'Tax Value ID'), + 'tax_option_id' => Yii::t('app', 'Tax Option ID'), + 'name' => Yii::t('app', 'Name'), + 'link' => Yii::t('app', 'Link'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOption() + { + return $this->hasOne(TaxOption::className(), ['tax_option_id' => 'tax_option_id'])->inverseOf('taxValueLinks'); + } + + public static function getValueRenderFlash($value) { + return $value->name; + } + + public static function getValueRenderHTML($value) { + return Html::a($value->name, $value->link); + } +} diff --git a/common/modules/rubrication/models/TaxValueString.php b/common/modules/rubrication/models/TaxValueString.php new file mode 100644 index 0000000..d9f617a --- /dev/null +++ b/common/modules/rubrication/models/TaxValueString.php @@ -0,0 +1,58 @@ + 255], + [['tax_option_id'], 'exist', 'skipOnError' => true, 'targetClass' => TaxOption::className(), 'targetAttribute' => ['tax_option_id' => 'tax_option_id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tax_value_id' => Yii::t('rubrication', 'Tax Value ID'), + 'tax_option_id' => Yii::t('rubrication', 'Tax Option ID'), + 'value' => Yii::t('rubrication', 'Value'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getTaxOption() + { + return $this->hasOne(TaxOption::className(), ['tax_option_id' => 'tax_option_id'])->inverseOf('taxValueStrings'); + } +} diff --git a/common/modules/rubrication/views/default/index.php b/common/modules/rubrication/views/default/index.php new file mode 100644 index 0000000..6b05acc --- /dev/null +++ b/common/modules/rubrication/views/default/index.php @@ -0,0 +1,12 @@ +
    +

    context->action->uniqueId ?>

    +

    + This is the view content for action "context->action->id ?>". + The action belongs to the controller "context) ?>" + in the "context->module->id ?>" module. +

    +

    + You may customize this page by editing the following file:
    + +

    +
    diff --git a/common/modules/rubrication/views/tax-group/_form.php b/common/modules/rubrication/views/tax-group/_form.php new file mode 100644 index 0000000..b593821 --- /dev/null +++ b/common/modules/rubrication/views/tax-group/_form.php @@ -0,0 +1,34 @@ + + +
    + + + + field($model, 'alias')->textInput(['maxlength' => true]) ?> + + field($model, 'name')->textInput(['maxlength' => true]) ?> + + field($model, 'description')->textarea(['rows' => 6]) ?> + + field($model, 'module')->dropDownList(RubricationHelper::OptionTypes(), [ + 'promtp' => Yii::t('rubrication', 'Select module'), + ]) ?> + + field($model, 'hierarchical')->checkbox() ?> + +
    + isNewRecord ? Yii::t('rubrication', 'Create') : Yii::t('rubrication', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
    + + + +
    diff --git a/common/modules/rubrication/views/tax-group/create.php b/common/modules/rubrication/views/tax-group/create.php new file mode 100644 index 0000000..e671dd1 --- /dev/null +++ b/common/modules/rubrication/views/tax-group/create.php @@ -0,0 +1,21 @@ +title = Yii::t('rubrication', 'Create Tax Group'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Tax Groups'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + ]) ?> + +
    diff --git a/common/modules/rubrication/views/tax-group/index.php b/common/modules/rubrication/views/tax-group/index.php new file mode 100644 index 0000000..5e266ad --- /dev/null +++ b/common/modules/rubrication/views/tax-group/index.php @@ -0,0 +1,72 @@ +title = Yii::t('rubrication', 'Groups'); +$this->params['breadcrumbs'][] = $this->title; +?> + +
    + +

    title) ?>

    +

    + 'btn btn-success']) ?> +

    + + $dataProvider, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + + 'name', + 'alias', + 'description:ntext', + 'module', + 'hierarchical:boolean', + // 'settings:ntext', + + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {options} {relations} {delete} {rebuild}', + 'buttons' => [ + 'options' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('rubrication', 'Options'), + ]); + }, + 'relations' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('rubrication', 'Relations'), + ]); + }, + 'rebuild' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('rubrication', 'Rebuild cache'), + ]); + } + ], + 'urlCreator' => function ($action, $model, $key, $index) { + if ($action === 'options') { + $url ='/admin/rubrication/tax-option?group='.$model->tax_group_id; + return $url; + } elseif ($action === 'relations') { + $url ='/admin/rubrication/tax-group/relation&id='.$model->tax_group_id; + return $url; + } elseif ($action === 'update') { + $url ='/admin/rubrication/tax-group/update?id='.$model->tax_group_id; + return $url; + } elseif ($action === 'delete') { + $url ='/admin/rubrication/tax-group/delete?id='.$model->tax_group_id; + return $url; + } elseif ($action === 'rebuild') { + $url ='/admin/rubrication/tax-group/rebuild?id='.$model->tax_group_id; + return $url; + } + } + ], + ], + ]); ?> +
    + diff --git a/common/modules/rubrication/views/tax-group/relations.php b/common/modules/rubrication/views/tax-group/relations.php new file mode 100644 index 0000000..2c0c884 --- /dev/null +++ b/common/modules/rubrication/views/tax-group/relations.php @@ -0,0 +1,58 @@ +title = Yii::t('relation', 'Relation for Group "{group}"', ['group' => $group->name]); +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Tax Groups'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + + 'btn btn-success']) ?> + + + $items, + 'sort' => [ + 'attributes' => ['name', 'alias', 'entity1_label', 'entity2_label'], + ], + 'pagination' => [ + 'pageSize' => 10, + ], + ]); + ?> + + $dataProvider, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + 'name', + 'alias', + 'entity1_label', + 'entity2_label', + /*[ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{view}', + 'buttons' => [ + 'view' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('relation', 'View pars'), + ]); + }, + ], + 'urlCreator' => function ($action, $model, $key, $index) { + if ($action === 'view') { + $url ='/admin/relation/manage/pars?relation='. $model['key']; + return $url; + } + } + ],*/ + ], + ]); ?> +
    diff --git a/common/modules/rubrication/views/tax-group/update.php b/common/modules/rubrication/views/tax-group/update.php new file mode 100644 index 0000000..7f3feaa --- /dev/null +++ b/common/modules/rubrication/views/tax-group/update.php @@ -0,0 +1,23 @@ +title = Yii::t('rubrication', 'Update {modelClass}: ', [ + 'modelClass' => 'Tax Group', +]) . ' ' . $model->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Groups'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->tax_group_id]]; +$this->params['breadcrumbs'][] = Yii::t('rubrication', 'Update'); +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + ]) ?> + +
    diff --git a/common/modules/rubrication/views/tax-group/view.php b/common/modules/rubrication/views/tax-group/view.php new file mode 100644 index 0000000..3074048 --- /dev/null +++ b/common/modules/rubrication/views/tax-group/view.php @@ -0,0 +1,42 @@ +title = $model->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Tax Groups'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + +

    + $model->tax_group_id], ['class' => 'btn btn-primary']) ?> + $model->tax_group_id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('rubrication', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> + tax_group_id], ['class' => 'btn btn-success']) ?> +

    + + $model, + 'attributes' => [ + 'tax_group_id', + 'alias', + 'name', + 'description:ntext', + 'module', + 'hierarchical:boolean', + 'settings:ntext', + ], + ]) ?> + +
    diff --git a/common/modules/rubrication/views/tax-option/_form.php b/common/modules/rubrication/views/tax-option/_form.php new file mode 100644 index 0000000..f29cdea --- /dev/null +++ b/common/modules/rubrication/views/tax-option/_form.php @@ -0,0 +1,62 @@ + + +
    + + + tax_group_id)) :?> + field($model, 'tax_group_id')->dropDownList( + ArrayHelper::map(TaxOption::find()->all(), 'tax_group_id', 'name'), + [ + 'prompt' => Yii::t('rubrication', 'Select group'), + ] + ) ?> + + field($model, 'tax_group_id')->hiddenInput()->label('') ?> + + + module .'.php')?> + + field($model, 'alias')->textInput(['maxlength' => true]) ?> + + hierarchical) :?> + getTree($model->tax_group_id); + ?> + field($model, 'parent_id')->dropDownList( + ArtboxTreeHelper::treeMap($tree, 'tax_option_id', 'ValueRenderFlash', '.'), + [ + 'prompt' => Yii::t('rubrication', 'Root level'), + 'options' => [ + $model->tax_option_id => ['disabled' => true] + ] + ] + ) ?> + + field($model, 'parent_id')->hiddenInput()->label('') ?> + + + field($model, 'sort')->dropDownList(RubricationHelper::SortArray()) ?> + +
    + isNewRecord ? Yii::t('rubrication', 'Create') : Yii::t('rubrication', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> + isNewRecord) :?> + 'create_and_new', 'class' => 'btn btn-primary']) ?> + +
    + + + +
    diff --git a/common/modules/rubrication/views/tax-option/_search.php b/common/modules/rubrication/views/tax-option/_search.php new file mode 100644 index 0000000..145b33d --- /dev/null +++ b/common/modules/rubrication/views/tax-option/_search.php @@ -0,0 +1,37 @@ + + + diff --git a/common/modules/rubrication/views/tax-option/create.php b/common/modules/rubrication/views/tax-option/create.php new file mode 100644 index 0000000..71c91d6 --- /dev/null +++ b/common/modules/rubrication/views/tax-option/create.php @@ -0,0 +1,25 @@ +title = Yii::t('rubrication', 'Create Tax Option'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Groups'), 'url' => ['tax-group/index']]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', $group->name), 'url' => ['index', 'group' => $group->tax_group_id]]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', Yii::t('rubrication', 'Options of {name}', ['name' => $group->name])), 'url' => ['index', 'group' => $group->tax_group_id]]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + 'group' => $group, + 'valueModel' => $valueModel, + ]) ?> + +
    diff --git a/common/modules/rubrication/views/tax-option/index.php b/common/modules/rubrication/views/tax-option/index.php new file mode 100644 index 0000000..986b02e --- /dev/null +++ b/common/modules/rubrication/views/tax-option/index.php @@ -0,0 +1,137 @@ +title = Yii::t('rubrication', 'Options for group "{group}"', ['group' => $group->name]); +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Groups'), 'url' => ['tax-group/index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + render('_search', ['model' => $searchModel]); ?> + +

    + tax_group_id], ['class' => 'btn btn-success']) ?> +

    + +hierarchical) :?> + $dataProvider, +// 'filterModel' => $searchModel, + 'columns' => [ + [ + 'label'=> Yii::t('rubrication', 'Value'), + 'content'=>function($data){ + return str_repeat('-', $data->depth) .' '. $data->ValueRenderFlash; + } + ], + 'alias', + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{view} {update} {delete} {synonim}', + 'buttons' => [ + 'synonim' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('rubrication', 'Synonims'), + ]); + }, + ], + 'urlCreator' => function ($action, $model, $key, $index) { + if ($action === 'view') { + $url = '/admin/rubrication/tax-option/view?id=' . $model->tax_option_id; + return $url; + } elseif ($action === 'update') { + $url ='/admin/rubrication/tax-option/update?id='.$model->tax_option_id; + return $url; + } elseif ($action === 'delete') { + $url ='/admin/rubrication/tax-option/delete?id='.$model->tax_option_id; + return $url; + } elseif ($action === 'synonim') { + $url ='/admin/rubrication/tax-synonim/delete?id='.$model->tax_option_id; + return $url; + } + } + ], + ], + ]);?> + $dataProvider, + 'keyNameId' => 'tax_option_id', + 'keyNameParentId' => 'parent_id', + 'rootParentId' => 0, + 'columns' => [ + [ + 'attribute' => 'valueRenderHTML', + 'label' => Yii::t('rubrication', 'Value'), + 'format' => 'html' + ], + 'alias', + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {delete} {synonim}', + 'buttons' => [ + 'synonim' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('rubrication', 'Synonims'), + ]); + }, + ], + 'urlCreator' => function ($action, $model, $key, $index) { + if ($action === 'update') { + $url ='/admin/rubrication/tax-option/update?id='.$model->tax_option_id; + return $url; + } elseif ($action === 'delete') { + $url ='/admin/rubrication/tax-option/delete?id='.$model->tax_option_id; + return $url; + } elseif ($action === 'synonim') { + $url ='/admin/rubrication/tax-synonim/delete?id='.$model->tax_option_id; + return $url; + } + } + ], + ] + ]); */?> + + $dataProvider, +// 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + 'valueRenderHTML', + 'alias', + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {delete} {synonim}', + 'buttons' => [ + 'synonim' => function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('rubrication', 'Synonims'), + ]); + }, + ], + 'urlCreator' => function ($action, $model, $key, $index) { + if ($action === 'update') { + $url ='/admin/rubrication/tax-option/update?id='.$model->tax_option_id; + return $url; + } elseif ($action === 'delete') { + $url ='/admin/rubrication/tax-option/delete?id='.$model->tax_option_id; + return $url; + } elseif ($action === 'synonim') { + $url ='/admin/rubrication/tax-synonim/delete?id='.$model->tax_option_id; + return $url; + } + } + ], + ], + ]);?> + +
    diff --git a/common/modules/rubrication/views/tax-option/update.php b/common/modules/rubrication/views/tax-option/update.php new file mode 100644 index 0000000..5c7164f --- /dev/null +++ b/common/modules/rubrication/views/tax-option/update.php @@ -0,0 +1,26 @@ +title = Yii::t('rubrication', 'Update {modelClass}: ', [ + 'modelClass' => 'Tax Option', +]) . ' ' . $model->tax_option_id; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Groups'), 'url' => ['tax-group/index']]; +$this->params['breadcrumbs'][] = ['label' => $group->name, 'url' => ['view', 'id' => $group->tax_group_id]]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', Yii::t('rubrication', 'Options of {name}', ['name' => $group->name])), 'url' => ['index', 'group' => $group->tax_group_id]]; +$this->params['breadcrumbs'][] = Yii::t('rubrication', 'Update'); +?> +
    + +

    title) ?>

    + + render('_form', [ + 'model' => $model, + 'group' => $group, + 'valueModel' => $valueModel, + ]) ?> + +
    diff --git a/common/modules/rubrication/views/tax-option/value/_fields_int.php b/common/modules/rubrication/views/tax-option/value/_fields_int.php new file mode 100644 index 0000000..6a35b2f --- /dev/null +++ b/common/modules/rubrication/views/tax-option/value/_fields_int.php @@ -0,0 +1 @@ +field($valueModel, 'value')->textInput() ?> \ No newline at end of file diff --git a/common/modules/rubrication/views/tax-option/value/_fields_link.php b/common/modules/rubrication/views/tax-option/value/_fields_link.php new file mode 100644 index 0000000..faebf0c --- /dev/null +++ b/common/modules/rubrication/views/tax-option/value/_fields_link.php @@ -0,0 +1,2 @@ +field($valueModel, 'name')->textInput(['maxlength' => true]) ?> +field($valueModel, 'link')->textInput(['maxlength' => true]) ?> \ No newline at end of file diff --git a/common/modules/rubrication/views/tax-option/value/_fields_string.php b/common/modules/rubrication/views/tax-option/value/_fields_string.php new file mode 100644 index 0000000..6a35b2f --- /dev/null +++ b/common/modules/rubrication/views/tax-option/value/_fields_string.php @@ -0,0 +1 @@ +field($valueModel, 'value')->textInput() ?> \ No newline at end of file diff --git a/common/modules/rubrication/views/tax-option/view.php b/common/modules/rubrication/views/tax-option/view.php new file mode 100644 index 0000000..5cdc66e --- /dev/null +++ b/common/modules/rubrication/views/tax-option/view.php @@ -0,0 +1,46 @@ +title = $model->valueRenderFlash; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', 'Groups'), 'url' => ['tax-group/index']]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', $group->name), 'url' => ['index', 'group' => $group->tax_group_id]]; +$this->params['breadcrumbs'][] = ['label' => Yii::t('rubrication', Yii::t('rubrication', 'Options of {name}', ['name' => $group->name])), 'url' => ['index', 'group' => $group->tax_group_id]]; +$this->params['breadcrumbs'][] = $this->title; +?> +
    + +

    title) ?>

    + +

    + $model->tax_option_id], ['class' => 'btn btn-primary']) ?> + $model->tax_option_id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('rubrication', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> + tax_group_id], ['class' => 'btn btn-success']) ?> + parent_id)) :?> + $model->parent->ValueRenderFlash]), ['tax-option/create?group='. $model->tax_group_id .'&parent='. $model->parent->tax_option_id], ['class' => 'btn btn-success']) ?> + +

    + + $model, + 'attributes' => [ + 'tax_option_id', + 'ValueRenderFlash', + 'alias', + 'parent.ValueRenderFlash', + 'group.name', + 'sort', + ], + ]) ?> + +
    diff --git a/common/widgets/Alert.php b/common/widgets/Alert.php new file mode 100644 index 0000000..8f1e590 --- /dev/null +++ b/common/widgets/Alert.php @@ -0,0 +1,79 @@ +session->setFlash('error', 'This is the message'); + * \Yii::$app->session->setFlash('success', 'This is the message'); + * \Yii::$app->session->setFlash('info', 'This is the message'); + * ``` + * + * Multiple messages could be set as follows: + * + * ```php + * \Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']); + * ``` + * + * @author Kartik Visweswaran + * @author Alexander Makarov + */ +class Alert extends \yii\bootstrap\Widget +{ + /** + * @var array the alert types configuration for the flash messages. + * This array is setup as $key => $value, where: + * - $key is the name of the session flash variable + * - $value is the bootstrap alert type (i.e. danger, success, info, warning) + */ + public $alertTypes = [ + 'error' => 'alert-danger', + 'danger' => 'alert-danger', + 'success' => 'alert-success', + 'info' => 'alert-info', + 'warning' => 'alert-warning' + ]; + + /** + * @var array the options for rendering the close button tag. + */ + public $closeButton = []; + + public function init() + { + parent::init(); + + $session = \Yii::$app->session; + $flashes = $session->getAllFlashes(); + $appendCss = isset($this->options['class']) ? ' ' . $this->options['class'] : ''; + + foreach ($flashes as $type => $data) { + if (isset($this->alertTypes[$type])) { + $data = (array) $data; + foreach ($data as $i => $message) { + /* initialize css class for each alert box */ + $this->options['class'] = $this->alertTypes[$type] . $appendCss; + + /* assign unique id to each alert box */ + $this->options['id'] = $this->getId() . '-' . $type . '-' . $i; + + echo \yii\bootstrap\Alert::widget([ + 'body' => $message, + 'closeButton' => $this->closeButton, + 'options' => $this->options, + ]); + } + + $session->removeFlash($type); + } + } + } +} diff --git a/common/widgets/FieldEditor.php b/common/widgets/FieldEditor.php new file mode 100644 index 0000000..62261a4 --- /dev/null +++ b/common/widgets/FieldEditor.php @@ -0,0 +1,48 @@ +item_id && $this->model){ + $widgetData = $this->findModel(); + } else { + $widgetData= [new Fields()]; + } + + return $this->render($this->template.'_field',['model'=>ArrayHelper::toArray($widgetData)]); + } + + protected function findModel() + { + + if (($model = Fields::find()->where([ + 'table_id'=>$this->item_id, + 'table_name'=>$this->model, + 'field_type'=>$this->template, + 'language'=>$this->language, + ])->all())) { + + return $model; + + } else { + return [new Fields()]; + } + } +} \ No newline at end of file diff --git a/common/widgets/views/education_field.php b/common/widgets/views/education_field.php new file mode 100644 index 0000000..ca6ad41 --- /dev/null +++ b/common/widgets/views/education_field.php @@ -0,0 +1,83 @@ + +
    + +
    Образование
    + +
    + + + + 'form-group', + 'id' => isset( $model[ $i ][ 'parent_key' ] ) ? $model[ $i ][ 'parent_key' ] : 0, + ]) ?> +
    + + +
    +
    + + +
    +
    + + +
    + + + + + +
    +

    добавить еще

    +
    + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0090d0f --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "yiisoft/yii2-app-advanced", + "description": "Yii 2 Advanced Project Template", + "keywords": ["yii2", "framework", "advanced", "project template"], + "homepage": "http://www.yiiframework.com/", + "type": "project", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "minimum-stability": "dev", + "require": { + "php": ">=5.4.0", + "yiisoft/yii2": ">=2.0.6", + "yiisoft/yii2-bootstrap": "*", + "yiisoft/yii2-swiftmailer": "*", + "dmstr/yii2-adminlte-asset": "2.*", + "yiisoft/yii2-jui": "^2.0", + "kartik-v/yii2-widget-select2": "@dev", + "mihaildev/yii2-ckeditor": "*", + "developeruz/yii2-db-rbac": "*", + "nodge/yii2-eauth": "*", + "yiisoft/yii2-imagine": "^2.0", + "mihaildev/yii2-elfinder": "^1.1", + "kartik-v/yii2-widget-colorinput": "*", + "2amigos/yii2-transliterator-helper": "*", + "rmrevin/yii2-comments": "1.4.*", + "bower-asset/admin-lte": "*", + "FortAwesome/Font-Awesome": "*" + }, + "require-dev": { + "yiisoft/yii2-codeception": "*", + "yiisoft/yii2-debug": "*", + "yiisoft/yii2-gii": "*", + "yiisoft/yii2-faker": "*" + }, + "config": { + "process-timeout": 1800 + }, + "extra": { + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e30a2eb --- /dev/null +++ b/composer.lock @@ -0,0 +1,1969 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "7543c4377d8fa28fcee5a5d9e3aabcb8", + "content-hash": "7c7bea2b7f00d81aab75adce39789723", + "packages": [ + { + "name": "2amigos/yii2-transliterator-helper", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/2amigos/yii2-transliterator-helper.git", + "reference": "1e4284351f4250a8f2ce553ea4f420fcbb424309" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/2amigos/yii2-transliterator-helper/zipball/1e4284351f4250a8f2ce553ea4f420fcbb424309", + "reference": "1e4284351f4250a8f2ce553ea4f420fcbb424309", + "shasum": "" + }, + "require": { + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "autoload": { + "psr-4": { + "dosamigos\\transliterator\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Antonio Ramirez", + "email": "ramirez.cobos@gmail.com", + "homepage": "http://www.ramirezcobos.com" + } + ], + "description": "Transliterator Helper for Yii2.", + "keywords": [ + "extension", + "helper", + "transliterator", + "yii" + ], + "time": "2014-06-23 14:01:30" + }, + { + "name": "almasaeed2010/adminlte", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/almasaeed2010/AdminLTE.git", + "reference": "1ee281b3b99e8d8cccdc72fb8437c6888149cb46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/almasaeed2010/AdminLTE/zipball/1ee281b3b99e8d8cccdc72fb8437c6888149cb46", + "reference": "1ee281b3b99e8d8cccdc72fb8437c6888149cb46", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Abdullah Almsaeed", + "email": "support@almsaeedstudio.com" + } + ], + "description": "AdminLTE - admin control panel and dashboard that's based on Bootstrap 3", + "homepage": "http://almsaeedstudio.com/", + "keywords": [ + "JS", + "admin", + "back-end", + "css", + "less", + "responsive", + "template", + "theme", + "web" + ], + "time": "2015-10-23 14:50:49" + }, + { + "name": "bower-asset/admin-lte", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/almasaeed2010/AdminLTE.git", + "reference": "fe147c9b2188bc3e4c651ca24581a6710d5421ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/almasaeed2010/AdminLTE/zipball/fe147c9b2188bc3e4c651ca24581a6710d5421ff", + "reference": "fe147c9b2188bc3e4c651ca24581a6710d5421ff", + "shasum": "" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "index2.html", + "dist/css/AdminLTE.css", + "dist/js/app.js", + "build/less/AdminLTE.less" + ], + "bower-asset-ignore": [ + "/.*", + "node_modules", + "bower_components", + "composer.json", + "documentation" + ] + }, + "license": [ + "MIT" + ], + "description": "Admin dashboard and control panel template", + "keywords": [ + "admin", + "backend", + "bootstrap", + "css", + "html", + "js", + "responsive", + "template", + "theme" + ], + "time": "2016-03-14 01:14:03" + }, + { + "name": "bower-asset/bootstrap", + "version": "v3.3.5", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "16b48259a62f576e52c903c476bd42b90ab22482" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/16b48259a62f576e52c903c476bd42b90ab22482", + "reference": "16b48259a62f576e52c903c476bd42b90ab22482", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.9.1" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "less/bootstrap.less", + "dist/js/bootstrap.js" + ], + "bower-asset-ignore": [ + "/.*", + "_config.yml", + "CNAME", + "composer.json", + "CONTRIBUTING.md", + "docs", + "js/tests", + "test-infra" + ] + }, + "license": [ + "MIT" + ], + "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", + "keywords": [ + "css", + "framework", + "front-end", + "js", + "less", + "mobile-first", + "responsive", + "web" + ] + }, + { + "name": "bower-asset/fontawesome", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/FortAwesome/Font-Awesome.git", + "reference": "fddd2c240452e6c8990c4ef75e0265b455aa7968" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FortAwesome/Font-Awesome/zipball/fddd2c240452e6c8990c4ef75e0265b455aa7968", + "reference": "fddd2c240452e6c8990c4ef75e0265b455aa7968", + "shasum": "" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "less/font-awesome.less", + "scss/font-awesome.scss" + ], + "bower-asset-ignore": [ + "*/.*", + "*.json", + "src", + "*.yml", + "Gemfile", + "Gemfile.lock", + "*.md" + ] + }, + "license": [ + "OFL-1.1", + "MIT", + "CC-BY-3.0" + ], + "description": "Font Awesome" + }, + { + "name": "bower-asset/jquery", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/jquery/jquery-dist.git", + "reference": "788eaba2f83e7b7445c7a83a50c81c0704423874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/788eaba2f83e7b7445c7a83a50c81c0704423874", + "reference": "788eaba2f83e7b7445c7a83a50c81c0704423874", + "shasum": "" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "dist/jquery.js", + "bower-asset-ignore": [ + "package.json" + ] + }, + "license": [ + "MIT" + ], + "keywords": [ + "browser", + "javascript", + "jquery", + "library" + ] + }, + { + "name": "bower-asset/jquery-ui", + "version": "1.11.4", + "source": { + "type": "git", + "url": "https://github.com/components/jqueryui.git", + "reference": "c34f8dbf3ba57b3784b93f26119f436c0e8288e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/components/jqueryui/zipball/c34f8dbf3ba57b3784b93f26119f436c0e8288e1", + "reference": "c34f8dbf3ba57b3784b93f26119f436c0e8288e1", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.6" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "jquery-ui.js" + ], + "bower-asset-ignore": [] + } + }, + { + "name": "bower-asset/jquery.inputmask", + "version": "3.2.7", + "source": { + "type": "git", + "url": "https://github.com/RobinHerbots/jquery.inputmask.git", + "reference": "5a72c563b502b8e05958a524cdfffafe9987be38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/5a72c563b502b8e05958a524cdfffafe9987be38", + "reference": "5a72c563b502b8e05958a524cdfffafe9987be38", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.7" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "./dist/inputmask/inputmask.js" + ], + "bower-asset-ignore": [ + "**/*", + "!dist/*", + "!dist/inputmask/*", + "!dist/min/*", + "!dist/min/inputmask/*", + "!extra/bindings/*", + "!extra/dependencyLibs/*", + "!extra/phone-codes/*" + ] + }, + "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.6", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/jquery-pjax.git", + "reference": "60728da6ade5879e807a49ce59ef9a72039b8978" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/60728da6ade5879e807a49ce59ef9a72039b8978", + "reference": "60728da6ade5879e807a49ce59ef9a72039b8978", + "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", + "CONTRIBUTING.md", + "vendor/", + "script/", + "test/" + ] + }, + "license": [ + "MIT" + ] + }, + { + "name": "cebe/markdown", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/cebe/markdown.git", + "reference": "e4499350d8a94c4c693a7e784295eff7a717ae67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cebe/markdown/zipball/e4499350d8a94c4c693a7e784295eff7a717ae67", + "reference": "e4499350d8a94c4c693a7e784295eff7a717ae67", + "shasum": "" + }, + "require": { + "lib-pcre": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "cebe/indent": "*", + "facebook/xhprof": "*@dev", + "phpunit/phpunit": "4.1.*" + }, + "bin": [ + "bin/markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.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": "2016-02-09 22:09:46" + }, + { + "name": "cebe/yii2-gravatar", + "version": "1.1", + "target-dir": "cebe/gravatar", + "source": { + "type": "git", + "url": "https://github.com/cebe/yii2-gravatar.git", + "reference": "c9c01bd14c9bdee9e5ae1ef1aad23f80c182c057" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cebe/yii2-gravatar/zipball/c9c01bd14c9bdee9e5ae1ef1aad23f80c182c057", + "reference": "c9c01bd14c9bdee9e5ae1ef1aad23f80c182c057", + "shasum": "" + }, + "require": { + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "autoload": { + "psr-0": { + "cebe\\gravatar\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Core framework development" + } + ], + "description": "Gravatar Widget for Yii 2", + "keywords": [ + "gravatar", + "yii" + ], + "time": "2013-12-10 17:49:58" + }, + { + "name": "developeruz/yii2-db-rbac", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/developeruz/yii2-db-rbac.git", + "reference": "28c1b0ebcc45b6365af6f1e9949b4d9cfeaebf1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/developeruz/yii2-db-rbac/zipball/28c1b0ebcc45b6365af6f1e9949b4d9cfeaebf1b", + "reference": "28c1b0ebcc45b6365af6f1e9949b4d9cfeaebf1b", + "shasum": "" + }, + "require": { + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "autoload": { + "psr-4": { + "developeruz\\db_rbac\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Elvira Sheina", + "email": "elleuz@gmail.com", + "homepage": "http://developer.uz" + } + ], + "description": "Dynamic control of access rights in YII2", + "keywords": [ + "rbac", + "yii" + ], + "time": "2015-10-03 05:56:47" + }, + { + "name": "dmstr/yii2-adminlte-asset", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/dmstr/yii2-adminlte-asset.git", + "reference": "c842a15ceef4e903f70ac927ec3246e6d53e1148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dmstr/yii2-adminlte-asset/zipball/c842a15ceef4e903f70ac927ec3246e6d53e1148", + "reference": "c842a15ceef4e903f70ac927ec3246e6d53e1148", + "shasum": "" + }, + "require": { + "almasaeed2010/adminlte": "~2.0", + "cebe/yii2-gravatar": "1.*", + "rmrevin/yii2-fontawesome": "~2.9", + "yiisoft/yii2": "2.*", + "yiisoft/yii2-bootstrap": "2.*" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "dmstr\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tobias Munk", + "email": "tobias@diemeisterei.de" + }, + { + "name": "Evgeniy Tkachenko", + "email": "et.coder@gmail.com" + } + ], + "description": "Backend theme for Yii2 Framework", + "keywords": [ + "AdminLTE", + "extension", + "yii2" + ], + "time": "2015-11-06 10:35:36" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.7.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "ae1828d955112356f7677c465f94f7deb7d27a40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/ae1828d955112356f7677c465f94f7deb7d27a40", + "reference": "ae1828d955112356f7677c465f94f7deb7d27a40", + "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" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2015-08-05 01:03:42" + }, + { + "name": "fortawesome/font-awesome", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/FortAwesome/Font-Awesome.git", + "reference": "03fd1951e930d4d4754204fdc532ca493e924a1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FortAwesome/Font-Awesome/zipball/03fd1951e930d4d4754204fdc532ca493e924a1b", + "reference": "03fd1951e930d4d4754204fdc532ca493e924a1b", + "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": "2016-03-10 18:47:19" + }, + { + "name": "imagine/imagine", + "version": "0.5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/avalanche123/Imagine.git", + "reference": "343580fceed1f89220481ac98480e92f47d91e6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/avalanche123/Imagine/zipball/343580fceed1f89220481ac98480e92f47d91e6c", + "reference": "343580fceed1f89220481ac98480e92f47d91e6c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "sami/sami": "dev-master" + }, + "suggest": { + "ext-gd": "to use the GD implementation", + "ext-gmagick": "to use the Gmagick implementation", + "ext-imagick": "to use the Imagick implementation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "0.5-dev" + } + }, + "autoload": { + "psr-0": { + "Imagine": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bulat Shakirzyanov", + "email": "mallluhuct@gmail.com", + "homepage": "http://avalanche123.com" + } + ], + "description": "Image processing for PHP 5.3", + "homepage": "http://imagine.readthedocs.org/", + "keywords": [ + "drawing", + "graphics", + "image manipulation", + "image processing" + ], + "time": "2014-06-13 10:54:04" + }, + { + "name": "kartik-v/yii2-krajee-base", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/kartik-v/yii2-krajee-base.git", + "reference": "3e491e51ed742663b239cd6e0b7f76d403bed7e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kartik-v/yii2-krajee-base/zipball/3e491e51ed742663b239cd6e0b7f76d403bed7e1", + "reference": "3e491e51ed742663b239cd6e0b7f76d403bed7e1", + "shasum": "" + }, + "require": { + "yiisoft/yii2-bootstrap": "@dev" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "kartik\\base\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kartik Visweswaran", + "email": "kartikv2@gmail.com", + "homepage": "http://www.krajee.com/" + } + ], + "description": "Base library and foundation components for all Yii2 Krajee extensions.", + "homepage": "https://github.com/kartik-v/yii2-krajee-base", + "keywords": [ + "base", + "extension", + "foundation", + "krajee", + "widget", + "yii2" + ], + "time": "2016-03-03 12:24:13" + }, + { + "name": "kartik-v/yii2-widget-colorinput", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/kartik-v/yii2-widget-colorinput.git", + "reference": "18537fcdab0f5491d5eebff8e2464ef6a616ee4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kartik-v/yii2-widget-colorinput/zipball/18537fcdab0f5491d5eebff8e2464ef6a616ee4c", + "reference": "18537fcdab0f5491d5eebff8e2464ef6a616ee4c", + "shasum": "" + }, + "require": { + "kartik-v/yii2-krajee-base": "*" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "kartik\\color\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD 3-Clause" + ], + "authors": [ + { + "name": "Kartik Visweswaran", + "email": "kartikv2@gmail.com", + "homepage": "http://www.krajee.com/" + } + ], + "description": "An enhanced Yii 2 widget encapsulating the HTML 5 color input (sub repo split from yii2-widgets)", + "homepage": "https://github.com/kartik-v/yii2-widget-colorinput", + "keywords": [ + "HTML5", + "color", + "extension", + "form", + "input", + "jquery", + "plugin", + "widget", + "yii2" + ], + "time": "2016-02-02 14:28:12" + }, + { + "name": "kartik-v/yii2-widget-select2", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/kartik-v/yii2-widget-select2.git", + "reference": "cb2a5992cb96bd2939e30ec1c76eba418d6a30af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kartik-v/yii2-widget-select2/zipball/cb2a5992cb96bd2939e30ec1c76eba418d6a30af", + "reference": "cb2a5992cb96bd2939e30ec1c76eba418d6a30af", + "shasum": "" + }, + "require": { + "kartik-v/yii2-krajee-base": "~1.7" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "kartik\\select2\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kartik Visweswaran", + "email": "kartikv2@gmail.com", + "homepage": "http://www.krajee.com/" + } + ], + "description": "Enhanced Yii2 wrapper for the Select2 jQuery plugin (sub repo split from yii2-widgets).", + "homepage": "https://github.com/kartik-v/yii2-widget-select2", + "keywords": [ + "dropdown", + "extension", + "form", + "jquery", + "plugin", + "select2", + "widget", + "yii2" + ], + "time": "2016-03-10 11:33:59" + }, + { + "name": "lusitanian/oauth", + "version": "v0.3.6", + "source": { + "type": "git", + "url": "https://github.com/Lusitanian/PHPoAuthLib.git", + "reference": "4ce8c488971410233eb3b1e6d9ac4e81debb41d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Lusitanian/PHPoAuthLib/zipball/4ce8c488971410233eb3b1e6d9ac4e81debb41d5", + "reference": "4ce8c488971410233eb3b1e6d9ac4e81debb41d5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*", + "predis/predis": "0.8.*@dev", + "symfony/http-foundation": "~2.1" + }, + "suggest": { + "ext-openssl": "Allows for usage of secure connections with the stream-based HTTP client.", + "predis/predis": "Allows using the Redis storage backend.", + "symfony/http-foundation": "Allows using the Symfony Session storage backend." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "psr-0": { + "OAuth": "src", + "OAuth\\Unit": "tests" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Desberg", + "email": "david@daviddesberg.com" + }, + { + "name": "Pieter Hordijk", + "email": "info@pieterhordijk.com" + } + ], + "description": "PHP 5.3+ oAuth 1/2 Library", + "keywords": [ + "Authentication", + "authorization", + "oauth", + "security" + ], + "time": "2015-09-09 06:43:02" + }, + { + "name": "mihaildev/yii2-ckeditor", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/MihailDev/yii2-ckeditor.git", + "reference": "d20aa7f6bcf610fee226d6eb15212a279875bf87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MihailDev/yii2-ckeditor/zipball/d20aa7f6bcf610fee226d6eb15212a279875bf87", + "reference": "d20aa7f6bcf610fee226d6eb15212a279875bf87", + "shasum": "" + }, + "require": { + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "extra": { + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "mihaildev\\ckeditor\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Mihail", + "email": "mihail.kucher@gmail.com", + "homepage": "https://github.com/MihailDev", + "role": "Developer" + } + ], + "description": "Yii2 CKEditor", + "homepage": "https://github.com/MihailDev/yii2-ckeditor", + "keywords": [ + "CKEditor", + "editor", + "wysiwyg", + "yii" + ], + "time": "2014-11-19 22:04:08" + }, + { + "name": "mihaildev/yii2-elfinder", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/MihailDev/yii2-elfinder.git", + "reference": "64b42572dec94e5c2d0d1630bce8292848a98f4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MihailDev/yii2-elfinder/zipball/64b42572dec94e5c2d0d1630bce8292848a98f4b", + "reference": "64b42572dec94e5c2d0d1630bce8292848a98f4b", + "shasum": "" + }, + "require": { + "yiisoft/yii2-jui": "*" + }, + "type": "yii2-extension", + "extra": { + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "mihaildev\\elfinder\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Mihail", + "email": "mihail.kucher@gmail.com", + "homepage": "https://github.com/MihailDev", + "role": "Developer" + } + ], + "description": "Yii2 ElFinder", + "homepage": "https://github.com/MihailDev/yii2-elfinder", + "keywords": [ + "elfinder", + "filemanager", + "yii" + ], + "time": "2015-07-03 07:08:52" + }, + { + "name": "nodge/lightopenid", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/Nodge/LightOpenID.git", + "reference": "a5492cc0c932c557b7e9b54a6e5bbd85cc5fa041" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nodge/LightOpenID/zipball/a5492cc0c932c557b7e9b54a6e5bbd85cc5fa041", + "reference": "a5492cc0c932c557b7e9b54a6e5bbd85cc5fa041", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "classmap": [ + "openid.php", + "provider/provider.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT License" + ], + "authors": [ + { + "name": "Mewp", + "homepage": "http://code.google.com/p/lightopenid/" + }, + { + "name": "Ignat Ignatov", + "homepage": "https://github.com/iignatov/LightOpenID" + } + ], + "description": "Lightweight PHP5 library for easy OpenID authentication.", + "homepage": "https://github.com/Nodge/LightOpenID", + "keywords": [ + "Authentication", + "OpenId" + ], + "time": "2013-08-31 16:48:56" + }, + { + "name": "nodge/yii2-eauth", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Nodge/yii2-eauth.git", + "reference": "f45efd95e3853db33153cc1b856d1f648d221938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nodge/yii2-eauth/zipball/f45efd95e3853db33153cc1b856d1f648d221938", + "reference": "f45efd95e3853db33153cc1b856d1f648d221938", + "shasum": "" + }, + "require": { + "lib-curl": "*", + "lusitanian/oauth": "~0.3.0", + "nodge/lightopenid": "~1.1.0", + "php": ">=5.4.0", + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "extra": { + "bootstrap": "nodge\\eauth\\Bootstrap" + }, + "autoload": { + "psr-4": { + "nodge\\eauth\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "New BSD License" + ], + "authors": [ + { + "name": "Maxim Zemskov", + "email": "nodge@yandex.ru", + "homepage": "http://nodge.ru/" + } + ], + "description": "Yii2 EAuth Extension. EAuth allows to authenticate users with accounts on other websites (Google, Facebook, Twitter, etc).", + "homepage": "https://github.com/Nodge/yii2-eauth", + "keywords": [ + "Authentication", + "OpenId", + "eauth", + "extension", + "oauth", + "yii2" + ], + "time": "2016-01-13 18:15:48" + }, + { + "name": "rmrevin/yii2-comments", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/rmrevin/yii2-comments.git", + "reference": "c68ddf276fe24ece0173781a8f7391a2cf7e6a12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rmrevin/yii2-comments/zipball/c68ddf276fe24ece0173781a8f7391a2cf7e6a12", + "reference": "c68ddf276fe24ece0173781a8f7391a2cf7e6a12", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "rmrevin/yii2-fontawesome": "~2.10", + "yiisoft/yii2": "2.0.*" + }, + "type": "yii2-extension", + "extra": { + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "rmrevin\\yii\\module\\Comments\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Revin", + "email": "xgismox@gmail.com", + "homepage": "http://rmrevin.ru/" + } + ], + "description": "Comments module for Yii2", + "keywords": [ + "comment", + "module", + "widget", + "yii" + ], + "time": "2016-03-01 13:18:41" + }, + { + "name": "rmrevin/yii2-fontawesome", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/rmrevin/yii2-fontawesome.git", + "reference": "2efbfacb22be59f373d11a7e3dfa9213e2ba18a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rmrevin/yii2-fontawesome/zipball/2efbfacb22be59f373d11a7e3dfa9213e2ba18a9", + "reference": "2efbfacb22be59f373d11a7e3dfa9213e2ba18a9", + "shasum": "" + }, + "require": { + "bower-asset/fontawesome": "4.5.*", + "php": ">=5.4.0", + "yiisoft/yii2": "2.0.*" + }, + "type": "yii2-extension", + "extra": { + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "rmrevin\\yii\\fontawesome\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Revin Roman", + "email": "roman@rmrevin.com", + "homepage": "https://rmrevin.com/" + } + ], + "description": "Asset Bundle for Yii2 with Font Awesome", + "keywords": [ + "asset", + "awesome", + "bundle", + "font", + "yii" + ], + "time": "2015-11-26 15:24:53" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "fffbc0e2a7e376dbb0a4b5f2ff6847330f20ccf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/fffbc0e2a7e376dbb0a4b5f2ff6847330f20ccf9", + "reference": "fffbc0e2a7e376dbb0a4b5f2ff6847330f20ccf9", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "mockery/mockery": "~0.9.1,<0.9.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.4-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "http://swiftmailer.org", + "keywords": [ + "email", + "mail", + "mailer" + ], + "time": "2016-01-03 15:42:47" + }, + { + "name": "yiisoft/yii2", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-framework.git", + "reference": "88ceadba3fc19ec90f72b6ba895347b7fecd3dfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/88ceadba3fc19ec90f72b6ba895347b7fecd3dfb", + "reference": "88ceadba3fc19ec90f72b6ba895347b7fecd3dfb", + "shasum": "" + }, + "require": { + "bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable", + "bower-asset/jquery.inputmask": "~3.2.2", + "bower-asset/punycode": "1.3.*", + "bower-asset/yii2-pjax": "~2.0.1", + "cebe/markdown": "~1.0.0 | ~1.1.0", + "ext-ctype": "*", + "ext-mbstring": "*", + "ezyang/htmlpurifier": "~4.6", + "lib-pcre": "*", + "php": ">=5.4.0", + "yiisoft/yii2-composer": "~2.0.4" + }, + "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" + }, + { + "name": "Dmitry Naumenko", + "email": "d.naumenko.a@gmail.com", + "role": "Core framework development" + } + ], + "description": "Yii PHP Framework Version 2", + "homepage": "http://www.yiiframework.com/", + "keywords": [ + "framework", + "yii2" + ], + "time": "2016-03-14 12:11:50" + }, + { + "name": "yiisoft/yii2-bootstrap", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-bootstrap.git", + "reference": "4133d6b26f48615de38ea1ec04eb00c4b7057bc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-bootstrap/zipball/4133d6b26f48615de38ea1ec04eb00c4b7057bc9", + "reference": "4133d6b26f48615de38ea1ec04eb00c4b7057bc9", + "shasum": "" + }, + "require": { + "bower-asset/bootstrap": "3.3.* | 3.2.* | 3.1.*", + "yiisoft/yii2": ">=2.0.6" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + }, + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "yii\\bootstrap\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The Twitter Bootstrap extension for the Yii framework", + "keywords": [ + "bootstrap", + "yii2" + ], + "time": "2016-03-04 00:48:53" + }, + { + "name": "yiisoft/yii2-composer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-composer.git", + "reference": "d33d1046a5951f2f7823fe343f28ddc58b3421a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/d33d1046a5951f2f7823fe343f28ddc58b3421a4", + "reference": "d33d1046a5951f2f7823fe343f28ddc58b3421a4", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.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": "2016-02-06 01:03:32" + }, + { + "name": "yiisoft/yii2-imagine", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-imagine.git", + "reference": "d87e6a0d1adfd6fa5ef49c18228b2fc9bca04299" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-imagine/zipball/d87e6a0d1adfd6fa5ef49c18228b2fc9bca04299", + "reference": "d87e6a0d1adfd6fa5ef49c18228b2fc9bca04299", + "shasum": "" + }, + "require": { + "imagine/imagine": "0.5.*", + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\imagine\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Antonio Ramirez", + "email": "amigo.cobos@gmail.com" + } + ], + "description": "The Imagine integration for the Yii framework", + "keywords": [ + "helper", + "image", + "imagine", + "yii2" + ], + "time": "2016-02-21 23:09:41" + }, + { + "name": "yiisoft/yii2-jui", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-jui.git", + "reference": "1425ab29929dd195f468d3c4eb340ab509f28b83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-jui/zipball/1425ab29929dd195f468d3c4eb340ab509f28b83", + "reference": "1425ab29929dd195f468d3c4eb340ab509f28b83", + "shasum": "" + }, + "require": { + "bower-asset/jquery-ui": "1.11.*@stable", + "yiisoft/yii2": ">=2.0.4" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + }, + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "yii\\jui\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The Jquery UI extension for the Yii framework", + "keywords": [ + "jQuery UI", + "yii2" + ], + "time": "2015-12-24 06:23:53" + }, + { + "name": "yiisoft/yii2-swiftmailer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-swiftmailer.git", + "reference": "2cca1bb86444a6438b0720f8c60120a400982366" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-swiftmailer/zipball/2cca1bb86444a6438b0720f8c60120a400982366", + "reference": "2cca1bb86444a6438b0720f8c60120a400982366", + "shasum": "" + }, + "require": { + "swiftmailer/swiftmailer": "~5.0", + "yiisoft/yii2": ">=2.0.4" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\swiftmailer\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com" + } + ], + "description": "The SwiftMailer integration for the Yii framework", + "keywords": [ + "email", + "mail", + "mailer", + "swift", + "swiftmailer", + "yii2" + ], + "time": "2015-12-07 11:40:31" + } + ], + "packages-dev": [ + { + "name": "bower-asset/typeahead.js", + "version": "v0.11.1", + "source": { + "type": "git", + "url": "https://github.com/twitter/typeahead.js.git", + "reference": "588440f66559714280628a4f9799f0c4eb880a4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twitter/typeahead.js/zipball/588440f66559714280628a4f9799f0c4eb880a4a", + "reference": "588440f66559714280628a4f9799f0c4eb880a4a", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.7" + }, + "require-dev": { + "bower-asset/jasmine-ajax": "~1.3.1", + "bower-asset/jasmine-jquery": "~1.5.2", + "bower-asset/jquery": "~1.7" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "dist/typeahead.bundle.js" + } + }, + { + "name": "fzaninotto/faker", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "588da2fff1b9da3acb9b6e1d7395e820102bde01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/588da2fff1b9da3acb9b6e1d7395e820102bde01", + "reference": "588da2fff1b9da3acb9b6e1d7395e820102bde01", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "ext-intl": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "time": "2016-03-10 07:12:46" + }, + { + "name": "phpspec/php-diff", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton", + "role": "Original developer" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "time": "2013-11-01 13:02:21" + }, + { + "name": "yiisoft/yii2-codeception", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-codeception.git", + "reference": "ee239c244fd011f11c8827f97ebe7600d03f1841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-codeception/zipball/ee239c244fd011f11c8827f97ebe7600d03f1841", + "reference": "ee239c244fd011f11c8827f97ebe7600d03f1841", + "shasum": "" + }, + "require": { + "yiisoft/yii2": ">=2.0.4" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\codeception\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Mark Jebri", + "email": "mark.github@yandex.ru" + } + ], + "description": "The Codeception integration for the Yii framework", + "keywords": [ + "codeception", + "yii2" + ], + "time": "2015-11-20 08:52:21" + }, + { + "name": "yiisoft/yii2-debug", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-debug.git", + "reference": "b258732b10a706d3fa41829f13be727e49dabd09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/b258732b10a706d3fa41829f13be727e49dabd09", + "reference": "b258732b10a706d3fa41829f13be727e49dabd09", + "shasum": "" + }, + "require": { + "yiisoft/yii2": ">=2.0.4", + "yiisoft/yii2-bootstrap": "*" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\debug\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The debugger extension for the Yii framework", + "keywords": [ + "debug", + "debugger", + "yii2" + ], + "time": "2016-03-14 21:47:38" + }, + { + "name": "yiisoft/yii2-faker", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-faker.git", + "reference": "186c77214e0a4b75f10380b4e6e5f82788922cb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-faker/zipball/186c77214e0a4b75f10380b4e6e5f82788922cb7", + "reference": "186c77214e0a4b75f10380b4e6e5f82788922cb7", + "shasum": "" + }, + "require": { + "fzaninotto/faker": "~1.4", + "yiisoft/yii2": "*" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\faker\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Mark Jebri", + "email": "mark.github@yandex.ru" + } + ], + "description": "Fixture generator. The Faker integration for the Yii framework.", + "keywords": [ + "Fixture", + "faker", + "yii2" + ], + "time": "2015-12-18 01:52:12" + }, + { + "name": "yiisoft/yii2-gii", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-gii.git", + "reference": "ce42838abcbef076ebaf46147671d518ae69d028" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-gii/zipball/ce42838abcbef076ebaf46147671d518ae69d028", + "reference": "ce42838abcbef076ebaf46147671d518ae69d028", + "shasum": "" + }, + "require": { + "bower-asset/typeahead.js": "0.10.* | ~0.11.0", + "phpspec/php-diff": ">=1.0.2", + "yiisoft/yii2": ">=2.0.4", + "yiisoft/yii2-bootstrap": "~2.0" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + }, + "asset-installer-paths": { + "npm-asset-library": "vendor/npm", + "bower-asset-library": "vendor/bower" + } + }, + "autoload": { + "psr-4": { + "yii\\gii\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The Gii extension for the Yii framework", + "keywords": [ + "code generator", + "gii", + "yii2" + ], + "time": "2016-02-21 20:39:29" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "kartik-v/yii2-widget-select2": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [] +} diff --git a/console/config/.gitignore b/console/config/.gitignore new file mode 100644 index 0000000..20da318 --- /dev/null +++ b/console/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php \ No newline at end of file diff --git a/console/config/bootstrap.php b/console/config/bootstrap.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/console/config/bootstrap.php @@ -0,0 +1 @@ + 'app-console', + 'basePath' => dirname(__DIR__), + 'bootstrap' => ['log'], + 'controllerNamespace' => 'console\controllers', + 'components' => [ + 'log' => [ + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + ], + 'params' => $params, +]; diff --git a/console/config/params.php b/console/config/params.php new file mode 100644 index 0000000..7f754b9 --- /dev/null +++ b/console/config/params.php @@ -0,0 +1,4 @@ + 'admin@example.com', +]; diff --git a/console/controllers/.gitkeep b/console/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/console/controllers/.gitkeep diff --git a/console/migrations/m130524_201442_init.php b/console/migrations/m130524_201442_init.php new file mode 100644 index 0000000..6b649f4 --- /dev/null +++ b/console/migrations/m130524_201442_init.php @@ -0,0 +1,33 @@ +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('{{%user}}', [ + 'id' => $this->primaryKey(), + 'username' => $this->string()->notNull()->unique(), + 'auth_key' => $this->string(32)->notNull(), + 'password_hash' => $this->string()->notNull(), + 'password_reset_token' => $this->string()->unique(), + 'email' => $this->string()->notNull()->unique(), + + 'status' => $this->smallInteger()->notNull()->defaultValue(10), + 'created_at' => $this->integer()->notNull(), + 'updated_at' => $this->integer()->notNull(), + ], $tableOptions); + } + + public function down() + { + $this->dropTable('{{%user}}'); + } +} diff --git a/console/migrations/m160126_071717_rubrication.php b/console/migrations/m160126_071717_rubrication.php new file mode 100644 index 0000000..200cb49 --- /dev/null +++ b/console/migrations/m160126_071717_rubrication.php @@ -0,0 +1,135 @@ +db->driverName === 'mysql') { + // Only for MySQL + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + + // @todo https://habrahabr.ru/post/138947/ + } elseif ($this->db->driverName === 'pgsql') { + // Only for PostgreSQL + // @todo use intarray field for tax_options + } + $this->createTable('{{%tax_group}}', [ + 'tax_group_id' => $this->primaryKey(), + 'alias' => $this->string(50)->notNull(), + 'name' => $this->string(255)->notNull(), + 'description' => $this->text(), + 'module' => $this->string(50)->notNull(), + 'hierarchical' => $this->boolean()->notNull()->defaultValue(false), + 'settings' => $this->text() + ], $tableOptions); + + $this->createTable('{{%tax_option}}', [ + 'tax_option_id' => $this->bigPrimaryKey(), + 'tax_group_id' => $this->integer()->notNull(), + 'parent_id' => $this->integer()->notNull()->defaultValue(0), + 'path' => $this->string(), + 'depth' => $this->integer(), + 'sort' => $this->integer()->notNull()->defaultValue(0), + 'default_value' => $this->integer(), + 'alias' => $this->string(50)->notNull()->defaultValue(''), + ], $tableOptions); + $this->addForeignKey('fki_tax_option_tax_group_id', 'tax_option', 'tax_group_id', 'tax_group', 'tax_group_id', 'CASCADE', 'CASCADE'); + + $this->createTable('{{%tax_group_to_group}}', [ + 'tax_group1_id' => $this->integer()->notNull(), + 'tax_group2_id' => $this->integer()->notNull(), + 'alias' => $this->string(50)->notNull(), + 'sort' => $this->integer(), + ], $tableOptions); + $this->addForeignKey('fki_tax_group_to_group1', 'tax_group_to_group', 'tax_group1_id', 'tax_group', 'tax_group_id', 'CASCADE', 'CASCADE'); + $this->addForeignKey('fki_tax_group_to_group2', 'tax_group_to_group', 'tax_group2_id', 'tax_group', 'tax_group_id', 'CASCADE', 'CASCADE'); + $this->addPrimaryKey('pki_tax_group_to_group', 'tax_group_to_group', ['tax_group1_id', 'tax_group2_id', 'alias']); + + $this->createTable('{{%tax_option_to_group}}', [ + 'tax_option_id' => $this->integer()->notNull(), + 'tax_group_id' => $this->integer()->notNull(), + 'alias' => $this->string(50)->notNull(), + 'sort' => $this->integer(), + ], $tableOptions); + $this->addForeignKey('tax_option_to_group_option_id', 'tax_option_to_group', 'tax_option_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + $this->addForeignKey('tax_option_to_group_group_id', 'tax_option_to_group', 'tax_group_id', 'tax_group', 'tax_group_id', 'CASCADE', 'CASCADE'); + $this->addPrimaryKey('pki_tax_option_to_group', 'tax_option_to_group', ['tax_option_id', 'tax_group_id', 'alias']); + + $this->createTable('{{%tax_option_to_option}}', [ + 'tax_option1_id' => $this->integer()->notNull(), + 'tax_option2_id' => $this->integer()->notNull(), + 'alias' => $this->string(50)->notNull(), + 'sort' => $this->integer(), + ], $tableOptions); + $this->addForeignKey('fki_tax_option_to_option1', 'tax_option_to_option', 'tax_option1_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + $this->addForeignKey('fki_tax_option_to_option2', 'tax_option_to_option', 'tax_option2_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + $this->addPrimaryKey('pki_tax_option_to_option', 'tax_option_to_option', ['tax_option1_id', 'tax_option2_id', 'alias']); + + /*$this->createTable('{{%tax_entity_relation}}', [ + 'tax_option_id' => $this->integer()->notNull(), + 'entity_id' => $this->integer()->notNull(), + 'entity_table_name' => $this->string(50)->notNull(), + 'entity_key_name' => $this->string(50)->notNull(), + 'alias' => $this->string(50)->notNull(), + ], $tableOptions); + $this->addForeignKey('fki_tax_entity_relation_option_id', 'tax_entity_relation', 'tax_option_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + $this->addPrimaryKey('pki_tax_entity_relation', 'tax_entity_relation', ['tax_option_id', 'entity_id', 'entity_table_name', 'entity_key_name', 'alias']);*/ + + $this->createTable('{{%tax_value_int}}', [ + 'tax_value_id' => $this->primaryKey(), + 'tax_option_id' => $this->integer()->notNull(), + 'value' => $this->integer(), + ], $tableOptions); + $this->addForeignKey('fki_tax_value_int_option_id', 'tax_value_int', 'tax_option_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + + $this->createTable('{{%tax_value_string}}', [ + 'tax_value_id' => $this->primaryKey(), + 'tax_option_id' => $this->integer()->notNull(), + 'value' => $this->string(), + ], $tableOptions); + $this->addForeignKey('fki_tax_value_string_option_id', 'tax_value_string', 'tax_option_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + + $this->createTable('{{%tax_value_text}}', [ + 'tax_value_id' => $this->primaryKey(), + 'tax_option_id' => $this->integer()->notNull(), + 'value' => $this->text(), + ], $tableOptions); + $this->addForeignKey('fki_tax_value_text_option_id', 'tax_value_text', 'tax_option_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + + $this->createTable('{{%tax_value_float}}', [ + 'tax_value_id' => $this->bigPrimaryKey(), + 'tax_option_id' => $this->integer()->notNull(), + 'value' => $this->float(), + ], $tableOptions); + $this->addForeignKey('fki_tax_value_float_option_id', 'tax_value_float', 'tax_option_id', 'tax_option', 'tax_option_id', 'CASCADE', 'CASCADE'); + } + + public function safeDown() + { + $this->dropTable('{{%tax_value_int}}'); + $this->dropTable('{{%tax_value_string}}'); + $this->dropTable('{{%tax_value_text}}'); + $this->dropTable('{{%tax_value_float}}'); +// $this->dropTable('{{%tax_entity_relation}}'); + $this->dropTable('{{%tax_group_to_group}}'); + $this->dropTable('{{%tax_option_to_group}}'); + $this->dropTable('{{%tax_option_to_option}}'); + $this->dropTable('{{%tax_option}}'); + $this->dropTable('{{%tax_group}}'); + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/console/migrations/m160128_101543_fields.php b/console/migrations/m160128_101543_fields.php new file mode 100644 index 0000000..cb49e9e --- /dev/null +++ b/console/migrations/m160128_101543_fields.php @@ -0,0 +1,29 @@ +createTable('{{%fields}}', [ + 'id' => $this->primaryKey(), + 'table_name' => $this->string(255)->notNull(), + 'table_id' => $this->integer(), + 'value' => $this->string(255), + 'field_name' => $this->string(), + 'field_type' => $this->string(32)->notNull(), + 'language' => $this->string(3), + 'key' => $this->integer(), + 'parent_key' => $this->integer() + ], $tableOptions); + + } + + public function down() + { + $this->dropTable('{{%fields}}'); + } +} diff --git a/console/migrations/m160208_111900_blog.php b/console/migrations/m160208_111900_blog.php new file mode 100644 index 0000000..ca88b1c --- /dev/null +++ b/console/migrations/m160208_111900_blog.php @@ -0,0 +1,30 @@ +createTable('{{%blog}}', [ + 'blog_id' => $this->primaryKey(), + 'user_id' => $this->integer()->notNull(), + 'name' => $this->string(255)->notNull(), + 'link' => $this->string(255), + 'date_add' => $this->timestamp()->notNull(), + 'user_add_id' => $this->integer(), + 'view_count' => $this->integer()->defaultValue(0), + 'description' => $this->text(), + 'cover' => $this->string(255), + ], $tableOptions); + + } + + public function down() + { + $this->dropTable('{{%blog}}'); + } + +} diff --git a/console/migrations/m160304_054017_realtion.php b/console/migrations/m160304_054017_realtion.php new file mode 100644 index 0000000..4f5b11c --- /dev/null +++ b/console/migrations/m160304_054017_realtion.php @@ -0,0 +1,44 @@ +db->driverName === 'mysql') { + // Only for MySQL + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + + // @todo https://habrahabr.ru/post/138947/ + } elseif ($this->db->driverName === 'pgsql') { + // Only for PostgreSQL + // @todo use intarray field for tax_options + } + $this->createTable('{{%relation}}', [ + 'alias' => $this->string(50)->notNull(), + 'entity1_id' => $this->integer()->notNull(), + 'entity2_id' => $this->integer()->notNull(), + ], $tableOptions); + $this->addPrimaryKey('relation_ukey', 'relation', ['alias', 'entity1_id', 'entity2_id']); + } + + public function down() + { + $this->dropTable('{{%relation}}'); + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/console/migrations/m160304_065108_product.php b/console/migrations/m160304_065108_product.php new file mode 100644 index 0000000..7925483 --- /dev/null +++ b/console/migrations/m160304_065108_product.php @@ -0,0 +1,50 @@ +db->driverName === 'mysql') { + // Only for MySQL + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + + // @todo https://habrahabr.ru/post/138947/ + } elseif ($this->db->driverName === 'pgsql') { + // Only for PostgreSQL + // @todo use intarray field for tax_options + } + $this->createTable('{{%product}}', [ + 'product_id' => $this->primaryKey(), + 'name' => $this->string(255)->notNull(), + ], $tableOptions); + + $this->createTable('{{%product_category}}', [ + 'product_id' => $this->integer()->notNull(), + 'category_id' => $this->integer()->notNull(), + ], $tableOptions); + $this->addForeignKey('fki_product_id', 'product_category', 'product_id', 'product', 'product_id', 'NO ACTION', 'NO ACTION'); + $this->addForeignKey('fki_category_id', 'product_category', 'category_id', 'tax_option', 'tax_option_id', 'NO ACTION', 'NO ACTION'); + } + + public function down() + { + $this->dropTable('{{%product}}'); + $this->dropTable('{{%product_category}}'); + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/console/models/.gitkeep b/console/models/.gitkeep new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/console/models/.gitkeep @@ -0,0 +1 @@ +* diff --git a/console/runtime/.gitignore b/console/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/console/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/environments/dev/backend/config/main-local.php b/environments/dev/backend/config/main-local.php new file mode 100644 index 0000000..d9a8ceb --- /dev/null +++ b/environments/dev/backend/config/main-local.php @@ -0,0 +1,25 @@ + [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], +]; + +if (!YII_ENV_TEST) { + // configuration adjustments for 'dev' environment + $config['bootstrap'][] = 'debug'; + $config['modules']['debug'] = [ + 'class' => 'yii\debug\Module', + ]; + + $config['bootstrap'][] = 'gii'; + $config['modules']['gii'] = [ + 'class' => 'yii\gii\Module', + ]; +} + +return $config; diff --git a/environments/dev/backend/config/params-local.php b/environments/dev/backend/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/dev/backend/config/params-local.php @@ -0,0 +1,3 @@ +run(); diff --git a/environments/dev/backend/web/index.php b/environments/dev/backend/web/index.php new file mode 100644 index 0000000..6038167 --- /dev/null +++ b/environments/dev/backend/web/index.php @@ -0,0 +1,18 @@ +run(); diff --git a/environments/dev/common/config/main-local.php b/environments/dev/common/config/main-local.php new file mode 100644 index 0000000..43db30e --- /dev/null +++ b/environments/dev/common/config/main-local.php @@ -0,0 +1,20 @@ + [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ], + 'mailer' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'viewPath' => '@common/mail', + // send all mails to a file by default. You have to set + // 'useFileTransport' to false and configure a transport + // for the mailer to send real emails. + 'useFileTransport' => true, + ], + ], +]; diff --git a/environments/dev/common/config/params-local.php b/environments/dev/common/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/dev/common/config/params-local.php @@ -0,0 +1,3 @@ + ['gii'], + 'modules' => [ + 'gii' => 'yii\gii\Module', + ], +]; diff --git a/environments/dev/console/config/params-local.php b/environments/dev/console/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/dev/console/config/params-local.php @@ -0,0 +1,3 @@ + [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], +]; + +if (!YII_ENV_TEST) { + // configuration adjustments for 'dev' environment + $config['bootstrap'][] = 'debug'; + $config['modules']['debug'] = [ + 'class' => 'yii\debug\Module', + ]; + $config['bootstrap'][] = 'gii'; + $config['modules']['gii'] = [ + 'class' => 'yii\gii\Module', + ]; +} + +return $config; diff --git a/environments/dev/frontend/config/params-local.php b/environments/dev/frontend/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/dev/frontend/config/params-local.php @@ -0,0 +1,3 @@ +run(); diff --git a/environments/dev/frontend/web/index.php b/environments/dev/frontend/web/index.php new file mode 100644 index 0000000..6038167 --- /dev/null +++ b/environments/dev/frontend/web/index.php @@ -0,0 +1,18 @@ +run(); diff --git a/environments/dev/yii b/environments/dev/yii new file mode 100644 index 0000000..6f0c6d2 --- /dev/null +++ b/environments/dev/yii @@ -0,0 +1,28 @@ +#!/usr/bin/env php +run(); +exit($exitCode); diff --git a/environments/index.php b/environments/index.php new file mode 100644 index 0000000..19c989d --- /dev/null +++ b/environments/index.php @@ -0,0 +1,65 @@ + [ + * 'path' => 'directory storing the local files', + * 'skipFiles' => [ + * // list of files that should only copied once and skipped if they already exist + * ], + * 'setWritable' => [ + * // list of directories that should be set writable + * ], + * 'setExecutable' => [ + * // list of files that should be set executable + * ], + * 'setCookieValidationKey' => [ + * // list of config files that need to be inserted with automatically generated cookie validation keys + * ], + * 'createSymlink' => [ + * // list of symlinks to be created. Keys are symlinks, and values are the targets. + * ], + * ], + * ]; + * ``` + */ +return [ + 'Development' => [ + 'path' => 'dev', + 'setWritable' => [ + 'backend/runtime', + 'backend/web/assets', + 'frontend/runtime', + 'frontend/web/assets', + ], + 'setExecutable' => [ + 'yii', + 'tests/codeception/bin/yii', + ], + 'setCookieValidationKey' => [ + 'backend/config/main-local.php', + 'frontend/config/main-local.php', + ], + ], + 'Production' => [ + 'path' => 'prod', + 'setWritable' => [ + 'backend/runtime', + 'backend/web/assets', + 'frontend/runtime', + 'frontend/web/assets', + ], + 'setExecutable' => [ + 'yii', + ], + 'setCookieValidationKey' => [ + 'backend/config/main-local.php', + 'frontend/config/main-local.php', + ], + ], +]; diff --git a/environments/prod/backend/config/main-local.php b/environments/prod/backend/config/main-local.php new file mode 100644 index 0000000..af46ba3 --- /dev/null +++ b/environments/prod/backend/config/main-local.php @@ -0,0 +1,9 @@ + [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], +]; diff --git a/environments/prod/backend/config/params-local.php b/environments/prod/backend/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/prod/backend/config/params-local.php @@ -0,0 +1,3 @@ +run(); diff --git a/environments/prod/common/config/main-local.php b/environments/prod/common/config/main-local.php new file mode 100644 index 0000000..84c4d9f --- /dev/null +++ b/environments/prod/common/config/main-local.php @@ -0,0 +1,16 @@ + [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ], + 'mailer' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'viewPath' => '@common/mail', + ], + ], +]; diff --git a/environments/prod/common/config/params-local.php b/environments/prod/common/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/prod/common/config/params-local.php @@ -0,0 +1,3 @@ + [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], +]; diff --git a/environments/prod/frontend/config/params-local.php b/environments/prod/frontend/config/params-local.php new file mode 100644 index 0000000..d0b9c34 --- /dev/null +++ b/environments/prod/frontend/config/params-local.php @@ -0,0 +1,3 @@ +run(); diff --git a/environments/prod/yii b/environments/prod/yii new file mode 100644 index 0000000..1fe0342 --- /dev/null +++ b/environments/prod/yii @@ -0,0 +1,28 @@ +#!/usr/bin/env php +run(); +exit($exitCode); diff --git a/frontend/config/.gitignore b/frontend/config/.gitignore new file mode 100644 index 0000000..20da318 --- /dev/null +++ b/frontend/config/.gitignore @@ -0,0 +1,2 @@ +main-local.php +params-local.php \ No newline at end of file diff --git a/frontend/config/bootstrap.php b/frontend/config/bootstrap.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/frontend/config/bootstrap.php @@ -0,0 +1 @@ + 'app-frontend', + 'basePath' => dirname(__DIR__), + 'bootstrap' => ['log'], + 'controllerNamespace' => 'frontend\controllers', + 'components' => [ + 'user' => [ + 'identityClass' => 'common\models\User', + 'enableAutoLogin' => true, + 'identityCookie' => [ + 'name' => '_frontendUser', // unique for frontend + ] + ], + 'request'=>[ + 'cookieValidationKey' => 'ndahjhjjidasuidrqeswuiuirqw89', + 'csrfParam' => '_frontendCSRF', + 'class' => 'common\components\Request', + + 'web'=> '/frontend/web' + + ], + + 'log' => [ + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + 'errorHandler' => [ + 'errorAction' => 'site/error', + ], + + 'urlManager' => [ + 'baseUrl' => '/', + 'enablePrettyUrl' => true, + 'showScriptName' => false, + 'rules' => [ + ] + ] + + ], + 'params' => $params, +]; diff --git a/frontend/config/params.php b/frontend/config/params.php new file mode 100644 index 0000000..7f754b9 --- /dev/null +++ b/frontend/config/params.php @@ -0,0 +1,4 @@ + 'admin@example.com', +]; diff --git a/frontend/controllers/SiteController.php b/frontend/controllers/SiteController.php new file mode 100644 index 0000000..cf691ae --- /dev/null +++ b/frontend/controllers/SiteController.php @@ -0,0 +1,213 @@ + [ + 'class' => AccessControl::className(), + 'only' => ['logout', 'signup'], + 'rules' => [ + [ + 'actions' => ['signup'], + 'allow' => true, + 'roles' => ['?'], + ], + [ + 'actions' => ['logout'], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'error' => [ + 'class' => 'yii\web\ErrorAction', + ], + 'captcha' => [ + 'class' => 'yii\captcha\CaptchaAction', + 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, + ], + ]; + } + + /** + * Displays homepage. + * + * @return mixed + */ + public function actionIndex() + { + return $this->render('index'); + } + + /** + * Logs in a user. + * + * @return mixed + */ + public function actionLogin() + { + if (!\Yii::$app->user->isGuest) { + return $this->goHome(); + } + + $model = new LoginForm(); + if ($model->load(Yii::$app->request->post()) && $model->login()) { + return $this->goBack(); + } else { + return $this->render('login', [ + 'model' => $model, + ]); + } + } + + /** + * Logs out the current user. + * + * @return mixed + */ + public function actionLogout() + { + Yii::$app->user->logout(); + + return $this->goHome(); + } + + /** + * Displays contact page. + * + * @return mixed + */ + public function actionContact() + { + $model = new ContactForm(); + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + if ($model->sendEmail(Yii::$app->params['adminEmail'])) { + Yii::$app->session->setFlash('success', 'Thank you for contacting us. We will respond to you as soon as possible.'); + } else { + Yii::$app->session->setFlash('error', 'There was an error sending email.'); + } + + return $this->refresh(); + } else { + return $this->render('contact', [ + 'model' => $model, + ]); + } + } + + /** + * Displays about page. + * + * @return mixed + */ + public function actionAbout() + { + return $this->render('about'); + } + + /** + * Signs user up. + * + * @return mixed + */ + public function actionSignup() + { + $model = new SignupForm(); + if ($model->load(Yii::$app->request->post())) { + if ($user = $model->signup()) { + if (Yii::$app->getUser()->login($user)) { + return $this->goHome(); + } + } + } + + return $this->render('signup', [ + 'model' => $model, + ]); + } + + /** + * Requests password reset. + * + * @return mixed + */ + public function actionRequestPasswordReset() + { + $model = new PasswordResetRequestForm(); + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + if ($model->sendEmail()) { + Yii::$app->session->setFlash('success', 'Check your email for further instructions.'); + + return $this->goHome(); + } else { + Yii::$app->session->setFlash('error', 'Sorry, we are unable to reset password for email provided.'); + } + } + + return $this->render('requestPasswordResetToken', [ + 'model' => $model, + ]); + } + + /** + * Resets password. + * + * @param string $token + * @return mixed + * @throws BadRequestHttpException + */ + public function actionResetPassword($token) + { + try { + $model = new ResetPasswordForm($token); + } catch (InvalidParamException $e) { + throw new BadRequestHttpException($e->getMessage()); + } + + if ($model->load(Yii::$app->request->post()) && $model->validate() && $model->resetPassword()) { + Yii::$app->session->setFlash('success', 'New password was saved.'); + + return $this->goHome(); + } + + return $this->render('resetPassword', [ + 'model' => $model, + ]); + } +} diff --git a/frontend/models/ContactForm.php b/frontend/models/ContactForm.php new file mode 100644 index 0000000..613abb5 --- /dev/null +++ b/frontend/models/ContactForm.php @@ -0,0 +1,59 @@ + 'Verification Code', + ]; + } + + /** + * Sends an email to the specified email address using the information collected by this model. + * + * @param string $email the target email address + * @return boolean whether the email was sent + */ + public function sendEmail($email) + { + return Yii::$app->mailer->compose() + ->setTo($email) + ->setFrom([$this->email => $this->name]) + ->setSubject($this->subject) + ->setTextBody($this->body) + ->send(); + } +} diff --git a/frontend/models/PasswordResetRequestForm.php b/frontend/models/PasswordResetRequestForm.php new file mode 100644 index 0000000..c09f6f7 --- /dev/null +++ b/frontend/models/PasswordResetRequestForm.php @@ -0,0 +1,68 @@ + 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'exist', + 'targetClass' => '\common\models\User', + 'filter' => ['status' => User::STATUS_ACTIVE], + 'message' => 'There is no user with such email.' + ], + ]; + } + + /** + * Sends an email with a link, for resetting the password. + * + * @return boolean whether the email was send + */ + public function sendEmail() + { + /* @var $user User */ + $user = User::findOne([ + 'status' => User::STATUS_ACTIVE, + 'email' => $this->email, + ]); + + if (!$user) { + return false; + } + + if (!User::isPasswordResetTokenValid($user->password_reset_token)) { + $user->generatePasswordResetToken(); + } + + if (!$user->save()) { + return false; + } + + return Yii::$app + ->mailer + ->compose( + ['html' => 'passwordResetToken-html', 'text' => 'passwordResetToken-text'], + ['user' => $user] + ) + ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot']) + ->setTo($this->email) + ->setSubject('Password reset for ' . \Yii::$app->name) + ->send(); + } +} diff --git a/frontend/models/ResetPasswordForm.php b/frontend/models/ResetPasswordForm.php new file mode 100644 index 0000000..dd48f52 --- /dev/null +++ b/frontend/models/ResetPasswordForm.php @@ -0,0 +1,65 @@ +_user = User::findByPasswordResetToken($token); + if (!$this->_user) { + throw new InvalidParamException('Wrong password reset token.'); + } + parent::__construct($config); + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['password', 'required'], + ['password', 'string', 'min' => 6], + ]; + } + + /** + * Resets password. + * + * @return boolean if password was reset. + */ + public function resetPassword() + { + $user = $this->_user; + $user->setPassword($this->password); + $user->removePasswordResetToken(); + + return $user->save(false); + } +} diff --git a/frontend/models/SignupForm.php b/frontend/models/SignupForm.php new file mode 100644 index 0000000..bd6722f --- /dev/null +++ b/frontend/models/SignupForm.php @@ -0,0 +1,58 @@ + 'trim'], + ['username', 'required'], + ['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'], + ['username', 'string', 'min' => 2, 'max' => 255], + + ['email', 'filter', 'filter' => 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'string', 'max' => 255], + ['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken.'], + + ['password', 'required'], + ['password', 'string', 'min' => 6], + ]; + } + + /** + * Signs user up. + * + * @return User|null the saved model or null if saving fails + */ + public function signup() + { + if (!$this->validate()) { + return null; + } + + $user = new User(); + $user->username = $this->username; + $user->email = $this->email; + $user->setPassword($this->password); + $user->generateAuthKey(); + + return $user->save() ? $user : null; + } +} diff --git a/frontend/runtime/.gitignore b/frontend/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/frontend/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/frontend/views/layouts/main.php b/frontend/views/layouts/main.php new file mode 100644 index 0000000..c86aa9b --- /dev/null +++ b/frontend/views/layouts/main.php @@ -0,0 +1,82 @@ + +beginPage() ?> + + + + + + + <?= Html::encode($this->title) ?> + head() ?> + + +beginBody() ?> + +
    + 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => [ + 'class' => 'navbar-inverse navbar-fixed-top', + ], + ]); + $menuItems = [ + ['label' => 'Home', 'url' => ['/site/index']], + ['label' => 'About', 'url' => ['/site/about']], + ['label' => 'Contact', 'url' => ['/site/contact']], + ]; + if (Yii::$app->user->isGuest) { + $menuItems[] = ['label' => 'Signup', 'url' => ['/site/signup']]; + $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; + } else { + $menuItems[] = '
  • ' + . Html::beginForm(['/site/logout'], 'post') + . Html::submitButton( + 'Logout (' . Yii::$app->user->identity->username . ')', + ['class' => 'btn btn-link'] + ) + . Html::endForm() + . '
  • '; + } + echo Nav::widget([ + 'options' => ['class' => 'navbar-nav navbar-right'], + 'items' => $menuItems, + ]); + NavBar::end(); + ?> + +
    + isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], + ]) ?> + + +
    +
    + +
    +
    +

    © My Company

    + +

    +
    +
    + +endBody() ?> + + +endPage() ?> diff --git a/frontend/views/site/about.php b/frontend/views/site/about.php new file mode 100644 index 0000000..8eb0764 --- /dev/null +++ b/frontend/views/site/about.php @@ -0,0 +1,16 @@ +title = 'About'; +$this->params['breadcrumbs'][] = $this->title; +?> +
    +

    title) ?>

    + +

    This is the About page. You may modify the following file to customize its content:

    + + +
    diff --git a/frontend/views/site/contact.php b/frontend/views/site/contact.php new file mode 100644 index 0000000..dcad18f --- /dev/null +++ b/frontend/views/site/contact.php @@ -0,0 +1,45 @@ +title = 'Contact'; +$this->params['breadcrumbs'][] = $this->title; +?> +
    +

    title) ?>

    + +

    + If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. +

    + +
    +
    + 'contact-form']); ?> + + field($model, 'name')->textInput(['autofocus' => true]) ?> + + field($model, 'email') ?> + + field($model, 'subject') ?> + + field($model, 'body')->textArea(['rows' => 6]) ?> + + field($model, 'verifyCode')->widget(Captcha::className(), [ + 'template' => '
    {image}
    {input}
    ', + ]) ?> + +
    + 'btn btn-primary', 'name' => 'contact-button']) ?> +
    + + +
    +
    + +
    diff --git a/frontend/views/site/error.php b/frontend/views/site/error.php new file mode 100644 index 0000000..0ba2574 --- /dev/null +++ b/frontend/views/site/error.php @@ -0,0 +1,27 @@ +title = $name; +?> +
    + +

    title) ?>

    + +
    + +
    + +

    + The above error occurred while the Web server was processing your request. +

    +

    + Please contact us if you think this is a server error. Thank you. +

    + +
    diff --git a/frontend/views/site/index.php b/frontend/views/site/index.php new file mode 100644 index 0000000..f780610 --- /dev/null +++ b/frontend/views/site/index.php @@ -0,0 +1,53 @@ +title = 'My Yii Application'; +?> +
    + +
    +

    Congratulations!

    + +

    You have successfully created your Yii-powered application.

    + +

    Get started with Yii

    +
    + +
    + +
    +
    +

    Heading

    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

    + +

    Yii Documentation »

    +
    +
    +

    Heading

    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

    + +

    Yii Forum »

    +
    +
    +

    Heading

    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

    + +

    Yii Extensions »

    +
    +
    + +
    +
    diff --git a/frontend/views/site/login.php b/frontend/views/site/login.php new file mode 100644 index 0000000..56ea98e --- /dev/null +++ b/frontend/views/site/login.php @@ -0,0 +1,39 @@ +title = 'Login'; +$this->params['breadcrumbs'][] = $this->title; +?> + diff --git a/frontend/views/site/requestPasswordResetToken.php b/frontend/views/site/requestPasswordResetToken.php new file mode 100644 index 0000000..9f6822e --- /dev/null +++ b/frontend/views/site/requestPasswordResetToken.php @@ -0,0 +1,31 @@ +title = 'Request password reset'; +$this->params['breadcrumbs'][] = $this->title; +?> +
    +

    title) ?>

    + +

    Please fill out your email. A link to reset password will be sent there.

    + +
    +
    + 'request-password-reset-form']); ?> + + field($model, 'email')->textInput(['autofocus' => true]) ?> + +
    + 'btn btn-primary']) ?> +
    + + +
    +
    +
    diff --git a/frontend/views/site/resetPassword.php b/frontend/views/site/resetPassword.php new file mode 100644 index 0000000..36ef452 --- /dev/null +++ b/frontend/views/site/resetPassword.php @@ -0,0 +1,31 @@ +title = 'Reset password'; +$this->params['breadcrumbs'][] = $this->title; +?> +
    +

    title) ?>

    + +

    Please choose your new password:

    + +
    +
    + 'reset-password-form']); ?> + + field($model, 'password')->passwordInput(['autofocus' => true]) ?> + +
    + 'btn btn-primary']) ?> +
    + + +
    +
    +
    diff --git a/frontend/views/site/signup.php b/frontend/views/site/signup.php new file mode 100644 index 0000000..de9dad6 --- /dev/null +++ b/frontend/views/site/signup.php @@ -0,0 +1,35 @@ +title = 'Signup'; +$this->params['breadcrumbs'][] = $this->title; +?> + diff --git a/frontend/web/.gitignore b/frontend/web/.gitignore new file mode 100644 index 0000000..25c74e6 --- /dev/null +++ b/frontend/web/.gitignore @@ -0,0 +1,2 @@ +/index.php +/index-test.php diff --git a/frontend/web/assets/.gitignore b/frontend/web/assets/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/frontend/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css new file mode 100644 index 0000000..25df5fb --- /dev/null +++ b/frontend/web/css/site.css @@ -0,0 +1,105 @@ +html, +body { + height: 100%; +} + +.wrap { + min-height: 100%; + height: auto; + margin: 0 auto -60px; + padding: 0 0 60px; +} + +.wrap > .container { + padding: 70px 15px 20px; +} + +.footer { + height: 60px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + padding-top: 20px; +} + +.jumbotron { + text-align: center; + background-color: transparent; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + +.not-set { + color: #c55; + font-style: italic; +} + +/* add sorting icons to gridview sort links */ +a.asc:after, a.desc:after { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + padding-left: 5px; +} + +a.asc:after { + content: "\e151"; +} + +a.desc:after { + content: "\e152"; +} + +.sort-numerical a.asc:after { + content: "\e153"; +} + +.sort-numerical a.desc:after { + content: "\e154"; +} + +.sort-ordinal a.asc:after { + content: "\e155"; +} + +.sort-ordinal a.desc:after { + content: "\e156"; +} + +.grid-view td { + white-space: nowrap; +} + +.grid-view .filters input, +.grid-view .filters select { + min-width: 50px; +} + +.hint-block { + display: block; + margin-top: 5px; + color: #999; +} + +.error-summary { + color: #a94442; + background: #fdf7f7; + border-left: 3px solid #eed3d7; + padding: 10px 20px; + margin: 0 0 15px 0; +} + +/* align the logout "link" (button in form) of the navbar */ +.nav > li > form { + padding: 8px; +} + +.nav > li > form > button:hover { + text-decoration: none; +} diff --git a/frontend/web/favicon.ico b/frontend/web/favicon.ico new file mode 100644 index 0000000..580ed73 Binary files /dev/null and b/frontend/web/favicon.ico differ diff --git a/frontend/web/robots.txt b/frontend/web/robots.txt new file mode 100644 index 0000000..6f27bb6 --- /dev/null +++ b/frontend/web/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: \ No newline at end of file diff --git a/init b/init new file mode 100644 index 0000000..6b8dd76 --- /dev/null +++ b/init @@ -0,0 +1,213 @@ +#!/usr/bin/env php + + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +if (!extension_loaded('openssl')) { + die('The OpenSSL PHP extension is required by Yii2.'); +} + +$params = getParams(); +$root = str_replace('\\', '/', __DIR__); +$envs = require("$root/environments/index.php"); +$envNames = array_keys($envs); + +echo "Yii Application Initialization Tool v1.0\n\n"; + +$envName = null; +if (empty($params['env']) || $params['env'] === '1') { + echo "Which environment do you want the application to be initialized in?\n\n"; + foreach ($envNames as $i => $name) { + echo " [$i] $name\n"; + } + echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] '; + $answer = trim(fgets(STDIN)); + + if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) { + echo "\n Quit initialization.\n"; + exit(0); + } + + if (isset($envNames[$answer])) { + $envName = $envNames[$answer]; + } +} else { + $envName = $params['env']; +} + +if (!in_array($envName, $envNames)) { + $envsList = implode(', ', $envNames); + echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n"; + exit(2); +} + +$env = $envs[$envName]; + +if (empty($params['env'])) { + echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] "; + $answer = trim(fgets(STDIN)); + if (strncasecmp($answer, 'y', 1)) { + echo "\n Quit initialization.\n"; + exit(0); + } +} + +echo "\n Start initialization ...\n\n"; +$files = getFileList("$root/environments/{$env['path']}"); +if (isset($env['skipFiles'])) { + $skipFiles = $env['skipFiles']; + array_walk($skipFiles, function(&$value) use($env, $root) { $value = "$root/$value"; }); + $files = array_diff($files, array_intersect_key($env['skipFiles'], array_filter($skipFiles, 'file_exists'))); +} +$all = false; +foreach ($files as $file) { + if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) { + break; + } +} + +$callbacks = ['setCookieValidationKey', 'setWritable', 'setExecutable', 'createSymlink']; +foreach ($callbacks as $callback) { + if (!empty($env[$callback])) { + $callback($root, $env[$callback]); + } +} + +echo "\n ... initialization completed.\n\n"; + +function getFileList($root, $basePath = '') +{ + $files = []; + $handle = opendir($root); + while (($path = readdir($handle)) !== false) { + if ($path === '.git' || $path === '.svn' || $path === '.' || $path === '..') { + continue; + } + $fullPath = "$root/$path"; + $relativePath = $basePath === '' ? $path : "$basePath/$path"; + if (is_dir($fullPath)) { + $files = array_merge($files, getFileList($fullPath, $relativePath)); + } else { + $files[] = $relativePath; + } + } + closedir($handle); + return $files; +} + +function copyFile($root, $source, $target, &$all, $params) +{ + if (!is_file($root . '/' . $source)) { + echo " skip $target ($source not exist)\n"; + return true; + } + if (is_file($root . '/' . $target)) { + if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) { + echo " unchanged $target\n"; + return true; + } + if ($all) { + echo " overwrite $target\n"; + } else { + echo " exist $target\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + + + $answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN)); + if (!strncasecmp($answer, 'q', 1)) { + return false; + } else { + if (!strncasecmp($answer, 'y', 1)) { + echo " overwrite $target\n"; + } else { + if (!strncasecmp($answer, 'a', 1)) { + echo " overwrite $target\n"; + $all = true; + } else { + echo " skip $target\n"; + return true; + } + } + } + } + file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); + return true; + } + echo " generate $target\n"; + @mkdir(dirname($root . '/' . $target), 0777, true); + file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); + return true; +} + +function getParams() +{ + $rawParams = []; + if (isset($_SERVER['argv'])) { + $rawParams = $_SERVER['argv']; + array_shift($rawParams); + } + + $params = []; + foreach ($rawParams as $param) { + if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { + $name = $matches[1]; + $params[$name] = isset($matches[3]) ? $matches[3] : true; + } else { + $params[] = $param; + } + } + return $params; +} + +function setWritable($root, $paths) +{ + foreach ($paths as $writable) { + if (is_dir("$root/$writable")) { + echo " chmod 0777 $writable\n"; + @chmod("$root/$writable", 0777); + } else { + echo "\n Error. Directory $writable does not exist. \n"; + } + } +} + +function setExecutable($root, $paths) +{ + foreach ($paths as $executable) { + echo " chmod 0755 $executable\n"; + @chmod("$root/$executable", 0755); + } +} + +function setCookieValidationKey($root, $paths) +{ + foreach ($paths as $file) { + echo " generate cookie validation key in $file\n"; + $file = $root . '/' . $file; + $length = 32; + $bytes = openssl_random_pseudo_bytes($length); + $key = strtr(substr(base64_encode($bytes), 0, $length), '+/=', '_-.'); + $content = preg_replace('/(("|\')cookieValidationKey("|\')\s*=>\s*)(""|\'\')/', "\\1'$key'", file_get_contents($file)); + file_put_contents($file, $content); + } +} + +function createSymlink($root, $links) { + foreach ($links as $link => $target) { + echo " symlink " . $root . "/" . $target . " " . $root . "/" . $link . "\n"; + //first removing folders to avoid errors if the folder already exists + @rmdir($root . "/" . $link); + @symlink($root . "/" . $target, $root . "/" . $link); + } +} diff --git a/init.bat b/init.bat new file mode 100644 index 0000000..e50c242 --- /dev/null +++ b/init.bat @@ -0,0 +1,20 @@ +@echo off + +rem ------------------------------------------------------------- +rem Yii command line init script for Windows. +rem +rem @author Qiang Xue +rem @link http://www.yiiframework.com/ +rem @copyright Copyright (c) 2008 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem ------------------------------------------------------------- + +@setlocal + +set YII_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +"%PHP_COMMAND%" "%YII_PATH%init" %* + +@endlocal diff --git a/requirements.php b/requirements.php new file mode 100644 index 0000000..fd84f47 --- /dev/null +++ b/requirements.php @@ -0,0 +1,132 @@ +Error'; + echo '

    The path to yii framework seems to be incorrect.

    '; + echo '

    You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . '.

    '; + echo '

    Please refer to the README on how to install Yii.

    '; +} + +require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); +$requirementsChecker = new YiiRequirementChecker(); + +$gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.'; +$gdOK = $imagickOK = false; + +if (extension_loaded('imagick')) { + $imagick = new Imagick(); + $imagickFormats = $imagick->queryFormats('PNG'); + if (in_array('PNG', $imagickFormats)) { + $imagickOK = true; + } else { + $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.'; + } +} + +if (extension_loaded('gd')) { + $gdInfo = gd_info(); + if (!empty($gdInfo['FreeType Support'])) { + $gdOK = true; + } else { + $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.'; + } +} + +/** + * Adjust requirements according to your application specifics. + */ +$requirements = array( + // Database : + array( + 'name' => 'PDO extension', + 'mandatory' => true, + 'condition' => extension_loaded('pdo'), + 'by' => 'All DB-related classes', + ), + array( + 'name' => 'PDO SQLite extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_sqlite'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for SQLite database.', + ), + array( + 'name' => 'PDO MySQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_mysql'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for MySQL database.', + ), + array( + 'name' => 'PDO PostgreSQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_pgsql'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for PostgreSQL database.', + ), + // Cache : + array( + 'name' => 'Memcache extension', + 'mandatory' => false, + 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), + 'by' => 'MemCache', + 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached to true.' : '' + ), + array( + 'name' => 'APC extension', + 'mandatory' => false, + 'condition' => extension_loaded('apc'), + 'by' => 'ApcCache', + ), + // CAPTCHA: + array( + 'name' => 'GD PHP extension with FreeType support', + 'mandatory' => false, + 'condition' => $gdOK, + 'by' => 'Captcha', + 'memo' => $gdMemo, + ), + array( + 'name' => 'ImageMagick PHP extension with PNG support', + 'mandatory' => false, + 'condition' => $imagickOK, + 'by' => 'Captcha', + 'memo' => $imagickMemo, + ), + // PHP ini : + 'phpExposePhp' => array( + 'name' => 'Expose PHP', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), + 'by' => 'Security reasons', + 'memo' => '"expose_php" should be disabled at php.ini', + ), + 'phpAllowUrlInclude' => array( + 'name' => 'PHP allow url include', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), + 'by' => 'Security reasons', + 'memo' => '"allow_url_include" should be disabled at php.ini', + ), + 'phpSmtp' => array( + 'name' => 'PHP mail SMTP', + 'mandatory' => false, + 'condition' => strlen(ini_get('SMTP')) > 0, + 'by' => 'Email sending', + 'memo' => 'PHP mail SMTP server required', + ), +); +$requirementsChecker->checkYii()->check($requirements)->render(); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..ad7f016 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,58 @@ +This directory contains various tests for the advanced applications. + +Tests in `codeception` directory are developed with [Codeception PHP Testing Framework](http://codeception.com/). + +After creating and setting up the advanced application, follow these steps to prepare for the tests: + +1. Install Codeception if it's not yet installed: + + ``` + composer global require "codeception/codeception=2.0.*" "codeception/specify=*" "codeception/verify=*" + ``` + + If you've never used Composer for global packages run `composer global status`. It should output: + + ``` + Changed current directory to + ``` + + Then add `/vendor/bin` to you `PATH` environment variable. Now you're able to use `codecept` from command + line globally. + +2. Install faker extension by running the following from template root directory where `composer.json` is: + + ``` + composer require --dev yiisoft/yii2-faker:* + ``` + +3. Create `yii2_advanced_tests` database then update it by applying migrations: + + ``` + codeception/bin/yii migrate + ``` + +4. In order to be able to run acceptance tests you need to start a webserver. The simplest way is to use PHP built in + webserver. In the root directory where `common`, `frontend` etc. are execute the following: + + ``` + php -S localhost:8080 + ``` + +5. Now you can run the tests with the following commands, assuming you are in the `tests/codeception` directory: + + ``` + # frontend tests + cd frontend + codecept build + codecept run + + # backend tests + + cd backend + codecept build + codecept run + + # etc. + ``` + + If you already have run `codecept build` for each application, you can skip that step and run all tests by a single `codecept run`. diff --git a/tests/codeception.yml b/tests/codeception.yml new file mode 100644 index 0000000..1a793ed --- /dev/null +++ b/tests/codeception.yml @@ -0,0 +1,11 @@ +include: + - codeception/common + - codeception/console + - codeception/backend + - codeception/frontend + +paths: + log: codeception/_output + +settings: + colors: true diff --git a/tests/codeception/_output/.gitignore b/tests/codeception/_output/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/codeception/_output/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/codeception/backend/.gitignore b/tests/codeception/backend/.gitignore new file mode 100644 index 0000000..985dbb4 --- /dev/null +++ b/tests/codeception/backend/.gitignore @@ -0,0 +1,4 @@ +# these files are auto generated by codeception build +/unit/UnitTester.php +/functional/FunctionalTester.php +/acceptance/AcceptanceTester.php diff --git a/tests/codeception/backend/_bootstrap.php b/tests/codeception/backend/_bootstrap.php new file mode 100644 index 0000000..a28a3d2 --- /dev/null +++ b/tests/codeception/backend/_bootstrap.php @@ -0,0 +1,23 @@ +wantTo('ensure login page works'); + +$loginPage = LoginPage::openBy($I); + +$I->amGoingTo('submit login form with no data'); +$loginPage->login('', ''); +if (method_exists($I, 'wait')) { + $I->wait(3); // only for selenium +} +$I->expectTo('see validations errors'); +$I->see('Username cannot be blank.', '.help-block'); +$I->see('Password cannot be blank.', '.help-block'); + +$I->amGoingTo('try to login with wrong credentials'); +$I->expectTo('see validations errors'); +$loginPage->login('admin', 'wrong'); +if (method_exists($I, 'wait')) { + $I->wait(3); // only for selenium +} +$I->expectTo('see validations errors'); +$I->see('Incorrect username or password.', '.help-block'); + +$I->amGoingTo('try to login with correct credentials'); +$loginPage->login('erau', 'password_0'); +if (method_exists($I, 'wait')) { + $I->wait(3); // only for selenium +} +$I->expectTo('see that user is logged'); +$I->see('Logout (erau)', 'form button[type=submit]'); +$I->dontSeeLink('Login'); +$I->dontSeeLink('Signup'); +/** Uncomment if using WebDriver + * $I->click('Logout (erau)'); + * $I->dontSeeLink('Logout (erau)'); + * $I->seeLink('Login'); + */ diff --git a/tests/codeception/backend/acceptance/_bootstrap.php b/tests/codeception/backend/acceptance/_bootstrap.php new file mode 100644 index 0000000..411855e --- /dev/null +++ b/tests/codeception/backend/acceptance/_bootstrap.php @@ -0,0 +1,2 @@ +wantTo('ensure login page works'); + +$loginPage = LoginPage::openBy($I); + +$I->amGoingTo('submit login form with no data'); +$loginPage->login('', ''); +$I->expectTo('see validations errors'); +$I->see('Username cannot be blank.', '.help-block'); +$I->see('Password cannot be blank.', '.help-block'); + +$I->amGoingTo('try to login with wrong credentials'); +$I->expectTo('see validations errors'); +$loginPage->login('admin', 'wrong'); +$I->expectTo('see validations errors'); +$I->see('Incorrect username or password.', '.help-block'); + +$I->amGoingTo('try to login with correct credentials'); +$loginPage->login('erau', 'password_0'); +$I->expectTo('see that user is logged'); +$I->see('Logout (erau)', 'form button[type=submit]'); +$I->dontSeeLink('Login'); +$I->dontSeeLink('Signup'); diff --git a/tests/codeception/backend/functional/_bootstrap.php b/tests/codeception/backend/functional/_bootstrap.php new file mode 100644 index 0000000..94f3fbd --- /dev/null +++ b/tests/codeception/backend/functional/_bootstrap.php @@ -0,0 +1,2 @@ +run(); +exit($exitCode); diff --git a/tests/codeception/bin/yii.bat b/tests/codeception/bin/yii.bat new file mode 100644 index 0000000..d516b3a --- /dev/null +++ b/tests/codeception/bin/yii.bat @@ -0,0 +1,20 @@ +@echo off + +rem ------------------------------------------------------------- +rem Yii command line bootstrap script for Windows. +rem +rem @author Qiang Xue +rem @link http://www.yiiframework.com/ +rem @copyright Copyright (c) 2008 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem ------------------------------------------------------------- + +@setlocal + +set YII_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +"%PHP_COMMAND%" "%YII_PATH%yii" %* + +@endlocal diff --git a/tests/codeception/common/.gitignore b/tests/codeception/common/.gitignore new file mode 100644 index 0000000..985dbb4 --- /dev/null +++ b/tests/codeception/common/.gitignore @@ -0,0 +1,4 @@ +# these files are auto generated by codeception build +/unit/UnitTester.php +/functional/FunctionalTester.php +/acceptance/AcceptanceTester.php diff --git a/tests/codeception/common/_bootstrap.php b/tests/codeception/common/_bootstrap.php new file mode 100644 index 0000000..cea3ee5 --- /dev/null +++ b/tests/codeception/common/_bootstrap.php @@ -0,0 +1,15 @@ +actor->fillField('input[name="LoginForm[username]"]', $username); + $this->actor->fillField('input[name="LoginForm[password]"]', $password); + $this->actor->click('login-button'); + } +} diff --git a/tests/codeception/common/_support/FixtureHelper.php b/tests/codeception/common/_support/FixtureHelper.php new file mode 100644 index 0000000..63739ef --- /dev/null +++ b/tests/codeception/common/_support/FixtureHelper.php @@ -0,0 +1,73 @@ +loadFixtures(); + } + + /** + * Method is called after all suite tests run + */ + public function _afterSuite() + { + $this->unloadFixtures(); + } + + /** + * @inheritdoc + */ + public function globalFixtures() + { + return [ + InitDbFixture::className(), + ]; + } + + /** + * @inheritdoc + */ + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@tests/codeception/common/fixtures/data/init_login.php', + ], + ]; + } +} diff --git a/tests/codeception/common/codeception.yml b/tests/codeception/common/codeception.yml new file mode 100644 index 0000000..e8a3407 --- /dev/null +++ b/tests/codeception/common/codeception.yml @@ -0,0 +1,13 @@ +namespace: tests\codeception\common +actor: Tester +paths: + tests: . + log: _output + data: _data + helpers: _support +settings: + bootstrap: _bootstrap.php + suite_class: \PHPUnit_Framework_TestSuite + colors: true + memory_limit: 1024M + log: true diff --git a/tests/codeception/common/fixtures/UserFixture.php b/tests/codeception/common/fixtures/UserFixture.php new file mode 100644 index 0000000..7153c8c --- /dev/null +++ b/tests/codeception/common/fixtures/UserFixture.php @@ -0,0 +1,13 @@ + 'erau', + 'auth_key' => 'tUu1qHcde0diwUol3xeI-18MuHkkprQI', + // password_0 + 'password_hash' => '$2y$13$nJ1WDlBaGcbCdbNC5.5l4.sgy.OMEKCqtDQOdQ2OWpgiKRWYyzzne', + 'password_reset_token' => 'RkD_Jw0_8HEedzLk7MM-ZKEFfYR7VbMr_1392559490', + 'created_at' => '1392559490', + 'updated_at' => '1392559490', + 'email' => 'sfriesen@jenkins.info', + ], +]; diff --git a/tests/codeception/common/templates/fixtures/user.php b/tests/codeception/common/templates/fixtures/user.php new file mode 100644 index 0000000..d3f83b5 --- /dev/null +++ b/tests/codeception/common/templates/fixtures/user.php @@ -0,0 +1,17 @@ +getSecurity(); + +return [ + 'username' => $faker->userName, + 'email' => $faker->email, + 'auth_key' => $security->generateRandomString(), + 'password_hash' => $security->generatePasswordHash('password_' . $index), + 'password_reset_token' => $security->generateRandomString() . '_' . time(), + 'created_at' => time(), + 'updated_at' => time(), +]; diff --git a/tests/codeception/common/unit.suite.yml b/tests/codeception/common/unit.suite.yml new file mode 100644 index 0000000..a0582a5 --- /dev/null +++ b/tests/codeception/common/unit.suite.yml @@ -0,0 +1,6 @@ +# Codeception Test Suite Configuration + +# suite for unit (internal) tests. +# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. + +class_name: UnitTester diff --git a/tests/codeception/common/unit/DbTestCase.php b/tests/codeception/common/unit/DbTestCase.php new file mode 100644 index 0000000..2159a69 --- /dev/null +++ b/tests/codeception/common/unit/DbTestCase.php @@ -0,0 +1,11 @@ + 'bayer.hudson', + 'auth_key' => 'HP187Mvq7Mmm3CTU80dLkGmni_FUH_lR', + //password_0 + 'password_hash' => '$2y$13$EjaPFBnZOQsHdGuHI.xvhuDp1fHpo8hKRSk6yshqa9c5EG8s3C3lO', + 'password_reset_token' => 'ExzkCOaYc1L8IOBs4wdTGGbgNiG3Wz1I_1402312317', + 'created_at' => '1402312317', + 'updated_at' => '1402312317', + 'email' => 'nicole.paucek@schultz.info', + ], +]; diff --git a/tests/codeception/common/unit/models/LoginFormTest.php b/tests/codeception/common/unit/models/LoginFormTest.php new file mode 100644 index 0000000..54c8209 --- /dev/null +++ b/tests/codeception/common/unit/models/LoginFormTest.php @@ -0,0 +1,93 @@ + [ + 'user' => [ + 'class' => 'yii\web\User', + 'identityClass' => 'common\models\User', + ], + ], + ]); + } + + protected function tearDown() + { + Yii::$app->user->logout(); + parent::tearDown(); + } + + public function testLoginNoUser() + { + $model = new LoginForm([ + 'username' => 'not_existing_username', + 'password' => 'not_existing_password', + ]); + + $this->specify('user should not be able to login, when there is no identity', function () use ($model) { + expect('model should not login user', $model->login())->false(); + expect('user should not be logged in', Yii::$app->user->isGuest)->true(); + }); + } + + public function testLoginWrongPassword() + { + $model = new LoginForm([ + 'username' => 'bayer.hudson', + 'password' => 'wrong_password', + ]); + + $this->specify('user should not be able to login with wrong password', function () use ($model) { + expect('model should not login user', $model->login())->false(); + expect('error message should be set', $model->errors)->hasKey('password'); + expect('user should not be logged in', Yii::$app->user->isGuest)->true(); + }); + } + + public function testLoginCorrect() + { + + $model = new LoginForm([ + 'username' => 'bayer.hudson', + 'password' => 'password_0', + ]); + + $this->specify('user should be able to login with correct credentials', function () use ($model) { + expect('model should login user', $model->login())->true(); + expect('error message should not be set', $model->errors)->hasntKey('password'); + expect('user should be logged in', Yii::$app->user->isGuest)->false(); + }); + } + + /** + * @inheritdoc + */ + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@tests/codeception/common/unit/fixtures/data/models/user.php' + ], + ]; + } +} diff --git a/tests/codeception/config/acceptance.php b/tests/codeception/config/acceptance.php new file mode 100644 index 0000000..9318da5 --- /dev/null +++ b/tests/codeception/config/acceptance.php @@ -0,0 +1,7 @@ + 'app-common', + 'basePath' => dirname(__DIR__), + ] +); diff --git a/tests/codeception/config/config.php b/tests/codeception/config/config.php new file mode 100644 index 0000000..b478679 --- /dev/null +++ b/tests/codeception/config/config.php @@ -0,0 +1,26 @@ + 'en-US', + 'controllerMap' => [ + 'fixture' => [ + 'class' => 'yii\faker\FixtureController', + 'fixtureDataPath' => '@tests/codeception/common/fixtures/data', + 'templatePath' => '@tests/codeception/common/templates/fixtures', + 'namespace' => 'tests\codeception\common\fixtures', + ], + ], + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_tests', + ], + 'mailer' => [ + 'useFileTransport' => true, + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], +]; diff --git a/tests/codeception/config/console/unit.php b/tests/codeception/config/console/unit.php new file mode 100644 index 0000000..4d3aeb0 --- /dev/null +++ b/tests/codeception/config/console/unit.php @@ -0,0 +1,14 @@ + [ + 'request' => [ + // it's not recommended to run functional tests with CSRF validation enabled + 'enableCsrfValidation' => false, + // but if you absolutely need it set cookie domain to localhost + /* + 'csrfCookie' => [ + 'domain' => 'localhost', + ], + */ + ], + ], +]; \ No newline at end of file diff --git a/tests/codeception/config/unit.php b/tests/codeception/config/unit.php new file mode 100644 index 0000000..6bd08d3 --- /dev/null +++ b/tests/codeception/config/unit.php @@ -0,0 +1,7 @@ + $value) { + $inputType = $field === 'body' ? 'textarea' : 'input'; + $this->actor->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value); + } + $this->actor->click('contact-button'); + } +} diff --git a/tests/codeception/frontend/_pages/SignupPage.php b/tests/codeception/frontend/_pages/SignupPage.php new file mode 100644 index 0000000..0e1cefa --- /dev/null +++ b/tests/codeception/frontend/_pages/SignupPage.php @@ -0,0 +1,27 @@ + $value) { + $inputType = $field === 'body' ? 'textarea' : 'input'; + $this->actor->fillField($inputType . '[name="SignupForm[' . $field . ']"]', $value); + } + $this->actor->click('signup-button'); + } +} diff --git a/tests/codeception/frontend/acceptance.suite.yml b/tests/codeception/frontend/acceptance.suite.yml new file mode 100644 index 0000000..1828a04 --- /dev/null +++ b/tests/codeception/frontend/acceptance.suite.yml @@ -0,0 +1,28 @@ +# Codeception Test Suite Configuration + +# suite for acceptance tests. +# perform tests in browser using the Selenium-like tools. +# powered by Mink (http://mink.behat.org). +# (tip: that's what your customer will see). +# (tip: test your ajax and javascript by one of Mink drivers). + +# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. + +class_name: AcceptanceTester +modules: + enabled: + - PhpBrowser + - tests\codeception\common\_support\FixtureHelper +# you can use WebDriver instead of PhpBrowser to test javascript and ajax. +# This will require you to install selenium. See http://codeception.com/docs/04-AcceptanceTests#Selenium +# "restart" option is used by the WebDriver to start each time per test-file new session and cookies, +# it is useful if you want to login in your app in each test. +# - WebDriver + config: + PhpBrowser: +# PLEASE ADJUST IT TO THE ACTUAL ENTRY POINT WITHOUT PATH INFO + url: http://localhost:8080 +# WebDriver: +# url: http://localhost:8080 +# browser: firefox +# restart: true diff --git a/tests/codeception/frontend/acceptance/AboutCept.php b/tests/codeception/frontend/acceptance/AboutCept.php new file mode 100644 index 0000000..50bf9ba --- /dev/null +++ b/tests/codeception/frontend/acceptance/AboutCept.php @@ -0,0 +1,10 @@ +wantTo('ensure that about works'); +AboutPage::openBy($I); +$I->see('About', 'h1'); diff --git a/tests/codeception/frontend/acceptance/ContactCept.php b/tests/codeception/frontend/acceptance/ContactCept.php new file mode 100644 index 0000000..5e989a5 --- /dev/null +++ b/tests/codeception/frontend/acceptance/ContactCept.php @@ -0,0 +1,56 @@ +wantTo('ensure that contact works'); + +$contactPage = ContactPage::openBy($I); + +$I->see('Contact', 'h1'); + +$I->amGoingTo('submit contact form with no data'); +$contactPage->submit([]); +if (method_exists($I, 'wait')) { + $I->wait(3); // only for selenium +} +$I->expectTo('see validations errors'); +$I->see('Contact', 'h1'); +$I->see('Name cannot be blank', '.help-block'); +$I->see('Email cannot be blank', '.help-block'); +$I->see('Subject cannot be blank', '.help-block'); +$I->see('Body cannot be blank', '.help-block'); +$I->see('The verification code is incorrect', '.help-block'); + +$I->amGoingTo('submit contact form with not correct email'); +$contactPage->submit([ + 'name' => 'tester', + 'email' => 'tester.email', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', +]); +if (method_exists($I, 'wait')) { + $I->wait(3); // only for selenium +} +$I->expectTo('see that email address is wrong'); +$I->dontSee('Name cannot be blank', '.help-block'); +$I->see('Email is not a valid email address.', '.help-block'); +$I->dontSee('Subject cannot be blank', '.help-block'); +$I->dontSee('Body cannot be blank', '.help-block'); +$I->dontSee('The verification code is incorrect', '.help-block'); + +$I->amGoingTo('submit contact form with correct data'); +$contactPage->submit([ + 'name' => 'tester', + 'email' => 'tester@example.com', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', +]); +if (method_exists($I, 'wait')) { + $I->wait(3); // only for selenium +} +$I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/tests/codeception/frontend/acceptance/HomeCept.php b/tests/codeception/frontend/acceptance/HomeCept.php new file mode 100644 index 0000000..9566a2e --- /dev/null +++ b/tests/codeception/frontend/acceptance/HomeCept.php @@ -0,0 +1,12 @@ +wantTo('ensure that home page works'); +$I->amOnPage(Yii::$app->homeUrl); +$I->see('My Company'); +$I->seeLink('About'); +$I->click('About'); +$I->see('This is the About page.'); diff --git a/tests/codeception/frontend/acceptance/LoginCept.php b/tests/codeception/frontend/acceptance/LoginCept.php new file mode 100644 index 0000000..7c433af --- /dev/null +++ b/tests/codeception/frontend/acceptance/LoginCept.php @@ -0,0 +1,34 @@ +wantTo('ensure login page works'); + +$loginPage = LoginPage::openBy($I); + +$I->amGoingTo('submit login form with no data'); +$loginPage->login('', ''); +$I->expectTo('see validations errors'); +$I->see('Username cannot be blank.', '.help-block'); +$I->see('Password cannot be blank.', '.help-block'); + +$I->amGoingTo('try to login with wrong credentials'); +$I->expectTo('see validations errors'); +$loginPage->login('admin', 'wrong'); +$I->expectTo('see validations errors'); +$I->see('Incorrect username or password.', '.help-block'); + +$I->amGoingTo('try to login with correct credentials'); +$loginPage->login('erau', 'password_0'); +$I->expectTo('see that user is logged'); +$I->see('Logout (erau)', 'form button[type=submit]'); +$I->dontSeeLink('Login'); +$I->dontSeeLink('Signup'); +/** Uncomment if using WebDriver + * $I->click('Logout (erau)'); + * $I->dontSeeLink('Logout (erau)'); + * $I->seeLink('Login'); + */ diff --git a/tests/codeception/frontend/acceptance/SignupCest.php b/tests/codeception/frontend/acceptance/SignupCest.php new file mode 100644 index 0000000..e8431e5 --- /dev/null +++ b/tests/codeception/frontend/acceptance/SignupCest.php @@ -0,0 +1,82 @@ + 'tester.email@example.com', + 'username' => 'tester', + ]); + } + + /** + * This method is called when test fails. + * @param \Codeception\Event\FailEvent $event + */ + public function _fail($event) + { + } + + /** + * @param \codeception_frontend\AcceptanceTester $I + * @param \Codeception\Scenario $scenario + */ + public function testUserSignup($I, $scenario) + { + $I->wantTo('ensure that signup works'); + + $signupPage = SignupPage::openBy($I); + $I->see('Signup', 'h1'); + $I->see('Please fill out the following fields to signup:'); + + $I->amGoingTo('submit signup form with no data'); + + $signupPage->submit([]); + + $I->expectTo('see validation errors'); + $I->see('Username cannot be blank.', '.help-block'); + $I->see('Email cannot be blank.', '.help-block'); + $I->see('Password cannot be blank.', '.help-block'); + + $I->amGoingTo('submit signup form with not correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email', + 'password' => 'tester_password', + ]); + + $I->expectTo('see that email address is wrong'); + $I->dontSee('Username cannot be blank.', '.help-block'); + $I->dontSee('Password cannot be blank.', '.help-block'); + $I->see('Email is not a valid email address.', '.help-block'); + + $I->amGoingTo('submit signup form with correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email@example.com', + 'password' => 'tester_password', + ]); + + $I->expectTo('see that user logged in'); + $I->see('Logout (tester)', 'form button[type=submit]'); + } +} diff --git a/tests/codeception/frontend/acceptance/_bootstrap.php b/tests/codeception/frontend/acceptance/_bootstrap.php new file mode 100644 index 0000000..b0a40ef --- /dev/null +++ b/tests/codeception/frontend/acceptance/_bootstrap.php @@ -0,0 +1,2 @@ +wantTo('ensure that about works'); +AboutPage::openBy($I); +$I->see('About', 'h1'); diff --git a/tests/codeception/frontend/functional/ContactCept.php b/tests/codeception/frontend/functional/ContactCept.php new file mode 100644 index 0000000..a93d7e4 --- /dev/null +++ b/tests/codeception/frontend/functional/ContactCept.php @@ -0,0 +1,47 @@ +wantTo('ensure that contact works'); + +$contactPage = ContactPage::openBy($I); + +$I->see('Contact', 'h1'); + +$I->amGoingTo('submit contact form with no data'); +$contactPage->submit([]); +$I->expectTo('see validations errors'); +$I->see('Contact', 'h1'); +$I->see('Name cannot be blank', '.help-block'); +$I->see('Email cannot be blank', '.help-block'); +$I->see('Subject cannot be blank', '.help-block'); +$I->see('Body cannot be blank', '.help-block'); +$I->see('The verification code is incorrect', '.help-block'); + +$I->amGoingTo('submit contact form with not correct email'); +$contactPage->submit([ + 'name' => 'tester', + 'email' => 'tester.email', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', +]); +$I->expectTo('see that email address is wrong'); +$I->dontSee('Name cannot be blank', '.help-block'); +$I->see('Email is not a valid email address.', '.help-block'); +$I->dontSee('Subject cannot be blank', '.help-block'); +$I->dontSee('Body cannot be blank', '.help-block'); +$I->dontSee('The verification code is incorrect', '.help-block'); + +$I->amGoingTo('submit contact form with correct data'); +$contactPage->submit([ + 'name' => 'tester', + 'email' => 'tester@example.com', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', +]); +$I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/tests/codeception/frontend/functional/HomeCept.php b/tests/codeception/frontend/functional/HomeCept.php new file mode 100644 index 0000000..f340061 --- /dev/null +++ b/tests/codeception/frontend/functional/HomeCept.php @@ -0,0 +1,12 @@ +wantTo('ensure that home page works'); +$I->amOnPage(Yii::$app->homeUrl); +$I->see('My Company'); +$I->seeLink('About'); +$I->click('About'); +$I->see('This is the About page.'); diff --git a/tests/codeception/frontend/functional/LoginCept.php b/tests/codeception/frontend/functional/LoginCept.php new file mode 100644 index 0000000..e1d2f9b --- /dev/null +++ b/tests/codeception/frontend/functional/LoginCept.php @@ -0,0 +1,29 @@ +wantTo('ensure login page works'); + +$loginPage = LoginPage::openBy($I); + +$I->amGoingTo('submit login form with no data'); +$loginPage->login('', ''); +$I->expectTo('see validations errors'); +$I->see('Username cannot be blank.', '.help-block'); +$I->see('Password cannot be blank.', '.help-block'); + +$I->amGoingTo('try to login with wrong credentials'); +$I->expectTo('see validations errors'); +$loginPage->login('admin', 'wrong'); +$I->expectTo('see validations errors'); +$I->see('Incorrect username or password.', '.help-block'); + +$I->amGoingTo('try to login with correct credentials'); +$loginPage->login('erau', 'password_0'); +$I->expectTo('see that user is logged'); +$I->see('Logout (erau)', 'form button[type=submit]'); +$I->dontSeeLink('Login'); +$I->dontSeeLink('Signup'); diff --git a/tests/codeception/frontend/functional/SignupCest.php b/tests/codeception/frontend/functional/SignupCest.php new file mode 100644 index 0000000..16410de --- /dev/null +++ b/tests/codeception/frontend/functional/SignupCest.php @@ -0,0 +1,88 @@ +loadFixtures(); + } + + /** + * This method is called when test fails. + * @param \codeception_frontend\FunctionalTester $I + */ + public function _failed($I) + { + + } + + /** + * + * @param \codeception_frontend\FunctionalTester $I + * @param \Codeception\Scenario $scenario + */ + public function testUserSignup($I, $scenario) + { + $I->wantTo('ensure that signup works'); + + $signupPage = SignupPage::openBy($I); + $I->see('Signup', 'h1'); + $I->see('Please fill out the following fields to signup:'); + + $I->amGoingTo('submit signup form with no data'); + + $signupPage->submit([]); + + $I->expectTo('see validation errors'); + $I->see('Username cannot be blank.', '.help-block'); + $I->see('Email cannot be blank.', '.help-block'); + $I->see('Password cannot be blank.', '.help-block'); + + $I->amGoingTo('submit signup form with not correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email', + 'password' => 'tester_password', + ]); + + $I->expectTo('see that email address is wrong'); + $I->dontSee('Username cannot be blank.', '.help-block'); + $I->dontSee('Password cannot be blank.', '.help-block'); + $I->see('Email is not a valid email address.', '.help-block'); + + $I->amGoingTo('submit signup form with correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email@example.com', + 'password' => 'tester_password', + ]); + + $I->expectTo('see that user is created'); + $I->seeRecord('common\models\User', [ + 'username' => 'tester', + 'email' => 'tester.email@example.com', + ]); + + $I->expectTo('see that user logged in'); + $I->see('Logout (tester)', 'form button[type=submit]'); + } +} diff --git a/tests/codeception/frontend/functional/_bootstrap.php b/tests/codeception/frontend/functional/_bootstrap.php new file mode 100644 index 0000000..1abc491 --- /dev/null +++ b/tests/codeception/frontend/functional/_bootstrap.php @@ -0,0 +1,3 @@ + 'okirlin', + 'auth_key' => 'iwTNae9t34OmnK6l4vT4IeaTk-YWI2Rv', + 'password_hash' => '$2y$13$CXT0Rkle1EMJ/c1l5bylL.EylfmQ39O5JlHJVFpNn618OUS1HwaIi', + 'password_reset_token' => 't5GU9NwpuGYSfb7FEZMAxqtuz2PkEvv_' . time(), + 'created_at' => '1391885313', + 'updated_at' => '1391885313', + 'email' => 'brady.renner@rutherford.com', + ], + [ + 'username' => 'troy.becker', + 'auth_key' => 'EdKfXrx88weFMV0vIxuTMWKgfK2tS3Lp', + 'password_hash' => '$2y$13$g5nv41Px7VBqhS3hVsVN2.MKfgT3jFdkXEsMC4rQJLfaMa7VaJqL2', + 'password_reset_token' => '4BSNyiZNAuxjs5Mty990c47sVrgllIi_' . time(), + 'created_at' => '1391885313', + 'updated_at' => '1391885313', + 'email' => 'nicolas.dianna@hotmail.com', + 'status' => '0', + ], +]; diff --git a/tests/codeception/frontend/unit/models/ContactFormTest.php b/tests/codeception/frontend/unit/models/ContactFormTest.php new file mode 100644 index 0000000..9aaf595 --- /dev/null +++ b/tests/codeception/frontend/unit/models/ContactFormTest.php @@ -0,0 +1,59 @@ +mailer->fileTransportCallback = function ($mailer, $message) { + return 'testing_message.eml'; + }; + } + + protected function tearDown() + { + unlink($this->getMessageFile()); + parent::tearDown(); + } + + public function testContact() + { + $model = new ContactForm(); + + $model->attributes = [ + 'name' => 'Tester', + 'email' => 'tester@example.com', + 'subject' => 'very important letter subject', + 'body' => 'body of current message', + ]; + + $model->sendEmail('admin@example.com'); + + $this->specify('email should be send', function () { + expect('email file should exist', file_exists($this->getMessageFile()))->true(); + }); + + $this->specify('message should contain correct data', function () use ($model) { + $emailMessage = file_get_contents($this->getMessageFile()); + + expect('email should contain user name', $emailMessage)->contains($model->name); + expect('email should contain sender email', $emailMessage)->contains($model->email); + expect('email should contain subject', $emailMessage)->contains($model->subject); + expect('email should contain body', $emailMessage)->contains($model->body); + }); + } + + private function getMessageFile() + { + return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml'; + } +} diff --git a/tests/codeception/frontend/unit/models/PasswordResetRequestFormTest.php b/tests/codeception/frontend/unit/models/PasswordResetRequestFormTest.php new file mode 100644 index 0000000..ced8cce --- /dev/null +++ b/tests/codeception/frontend/unit/models/PasswordResetRequestFormTest.php @@ -0,0 +1,87 @@ +mailer->fileTransportCallback = function ($mailer, $message) { + return 'testing_message.eml'; + }; + } + + protected function tearDown() + { + @unlink($this->getMessageFile()); + + parent::tearDown(); + } + + public function testSendEmailWrongUser() + { + $this->specify('no user with such email, message should not be sent', function () { + + $model = new PasswordResetRequestForm(); + $model->email = 'not-existing-email@example.com'; + + expect('email not sent', $model->sendEmail())->false(); + + }); + + $this->specify('user is not active, message should not be sent', function () { + + $model = new PasswordResetRequestForm(); + $model->email = $this->user[1]['email']; + + expect('email not sent', $model->sendEmail())->false(); + + }); + } + + public function testSendEmailCorrectUser() + { + $model = new PasswordResetRequestForm(); + $model->email = $this->user[0]['email']; + $user = User::findOne(['password_reset_token' => $this->user[0]['password_reset_token']]); + + expect('email sent', $model->sendEmail())->true(); + expect('user has valid token', $user->password_reset_token)->notNull(); + + $this->specify('message has correct format', function () use ($model) { + + expect('message file exists', file_exists($this->getMessageFile()))->true(); + + $message = file_get_contents($this->getMessageFile()); + expect('message "from" is correct', $message)->contains(Yii::$app->params['supportEmail']); + expect('message "to" is correct', $message)->contains($model->email); + + }); + } + + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@tests/codeception/frontend/unit/fixtures/data/models/user.php' + ], + ]; + } + + private function getMessageFile() + { + return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml'; + } +} diff --git a/tests/codeception/frontend/unit/models/ResetPasswordFormTest.php b/tests/codeception/frontend/unit/models/ResetPasswordFormTest.php new file mode 100644 index 0000000..a4dd021 --- /dev/null +++ b/tests/codeception/frontend/unit/models/ResetPasswordFormTest.php @@ -0,0 +1,43 @@ +user[0]['password_reset_token']); + expect('password should be resetted', $form->resetPassword())->true(); + } + + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@tests/codeception/frontend/unit/fixtures/data/models/user.php' + ], + ]; + } +} diff --git a/tests/codeception/frontend/unit/models/SignupFormTest.php b/tests/codeception/frontend/unit/models/SignupFormTest.php new file mode 100644 index 0000000..4d08e8c --- /dev/null +++ b/tests/codeception/frontend/unit/models/SignupFormTest.php @@ -0,0 +1,52 @@ + 'some_username', + 'email' => 'some_email@example.com', + 'password' => 'some_password', + ]); + + $user = $model->signup(); + + $this->assertInstanceOf('common\models\User', $user, 'user should be valid'); + + expect('username should be correct', $user->username)->equals('some_username'); + expect('email should be correct', $user->email)->equals('some_email@example.com'); + expect('password should be correct', $user->validatePassword('some_password'))->true(); + } + + public function testNotCorrectSignup() + { + $model = new SignupForm([ + 'username' => 'troy.becker', + 'email' => 'nicolas.dianna@hotmail.com', + 'password' => 'some_password', + ]); + + expect('username and email are in use, user should not be created', $model->signup())->null(); + } + + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@tests/codeception/frontend/unit/fixtures/data/models/user.php', + ], + ]; + } +} diff --git a/yii.bat b/yii.bat new file mode 100644 index 0000000..d516b3a --- /dev/null +++ b/yii.bat @@ -0,0 +1,20 @@ +@echo off + +rem ------------------------------------------------------------- +rem Yii command line bootstrap script for Windows. +rem +rem @author Qiang Xue +rem @link http://www.yiiframework.com/ +rem @copyright Copyright (c) 2008 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem ------------------------------------------------------------- + +@setlocal + +set YII_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +"%PHP_COMMAND%" "%YII_PATH%yii" %* + +@endlocal -- libgit2 0.21.4