diff --git a/README.md b/README.md index 9e364e6..b116917 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -

-

ZxCMS - Yii2 site management system

-
-

+# ZxCMS - Yii2 site management system Yii 2 Advanced Project Template is a skeleton [Yii 2](http://www.yiiframework.com/) application best for developing complex Web applications with multiple tiers. diff --git a/backend/controllers/UserController.php b/backend/controllers/UserController.php index ea4c4d4..b47b564 100644 --- a/backend/controllers/UserController.php +++ b/backend/controllers/UserController.php @@ -2,7 +2,9 @@ namespace backend\controllers; +use core\forms\user\ProfileEditForm; use core\forms\user\UserForm; +use core\services\user\ProfileService; use core\services\user\UserManageService; use Yii; use core\entities\user\User; @@ -11,6 +13,7 @@ use yii\web\Controller; use yii\web\NotFoundHttpException; use yii\filters\VerbFilter; use yii\filters\AccessControl; +use yii\web\UploadedFile; /** * UserController implements the CRUD actions for User model. @@ -18,11 +21,13 @@ use yii\filters\AccessControl; class UserController extends Controller { private $service; + private $profile_service; - public function __construct($id, $module, UserManageService $service, $config = []) + public function __construct($id, $module, UserManageService $service, ProfileService $profile_service, $config = []) { parent::__construct($id, $module, $config); $this->service = $service; + $this->profile_service = $profile_service; } /** @@ -39,6 +44,11 @@ class UserController extends Controller 'allow' => true, 'roles' => ['UserManagement'], ], + [ + 'actions' => ['profile'], + 'allow' => true, + 'roles' => ['@'], + ], [ // all the action are accessible to admin 'allow' => true, 'roles' => ['admin'], @@ -142,6 +152,28 @@ class UserController extends Controller return $this->redirect(['index']); } + public function actionProfile() + { + $user = $this->findModel(Yii::$app->user->id); + $form = new ProfileEditForm($user); + + if ($form->load(Yii::$app->request->post()) && $form->validate()) { + try { + $form->user_pic = UploadedFile::getInstance($form, 'user_pic'); + $this->profile_service->edit(Yii::$app->user->id, $form); + Yii::$app->session->setFlash('success', Yii::t('user', 'Profile is saved.')); + } catch (\DomainException $e) { + Yii::$app->errorHandler->logException($e); + Yii::$app->session->setFlash('error', $e->getMessage()); + } + } + + return $this->render('profile', [ + 'model' => $form, + 'user' => $user, + ]); + } + /** * Finds the User model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. diff --git a/backend/messages/ru/main.php b/backend/messages/ru/main.php index cfe0f46..27ec374 100644 --- a/backend/messages/ru/main.php +++ b/backend/messages/ru/main.php @@ -26,4 +26,5 @@ return [ 'Change at your own risk' => 'Редактируйте на свой страх и риск', 'Online' => 'В сети', 'Search results' => 'Поиск', + 'Search...' => 'Поиск...', ]; \ No newline at end of file diff --git a/backend/messages/ru/user.php b/backend/messages/ru/user.php index 68241ed..08d9a29 100644 --- a/backend/messages/ru/user.php +++ b/backend/messages/ru/user.php @@ -32,4 +32,9 @@ return [ 'Update Permission: {permission}' => 'Редактирование разрешения: {permission}', 'Administrator has full access rules' => 'Администратор обладает всеми правами доступа', 'Children roles and permissions for "{role}" is updated.' => 'Дочерние роли и разрешения для роли "{role}" обновлены.', + 'Profile' => 'Профиль', + 'Profile is saved.' => 'Профиль обновлен.', + 'Profile: {username}' => 'Профиль: {username}', + 'User Picture' => 'Аватар', + 'Registered: {date}' => 'Дата регистрации: {date}', ]; \ No newline at end of file diff --git a/backend/views/layouts/header.php b/backend/views/layouts/header.php index 50bc064..e0cf4e0 100644 --- a/backend/views/layouts/header.php +++ b/backend/views/layouts/header.php @@ -232,36 +232,28 @@ use core\components\avatar_generator\AvatarGenerator; - - -
  • - -
  • diff --git a/backend/views/layouts/left.php b/backend/views/layouts/left.php index 2a11b15..a257c95 100644 --- a/backend/views/layouts/left.php +++ b/backend/views/layouts/left.php @@ -46,7 +46,7 @@ $model = new SearchForm(); ] ], 'template' => "{input}", - ])->textInput(['placeholder' => 'Search...'])->label(false)->hint(false); ?> + ])->textInput(['placeholder' => Yii::t('main', 'Search...')])->label(false)->hint(false); ?> diff --git a/backend/views/user/profile.php b/backend/views/user/profile.php new file mode 100644 index 0000000..e74deaf --- /dev/null +++ b/backend/views/user/profile.php @@ -0,0 +1,59 @@ +title = Yii::t('user', 'Profile: {username}', ['username' => $user->username]); +$this->params['breadcrumbs'][] = ['label' => $user->username, 'url' => ['view', 'id' => $user->id]]; +$this->params['breadcrumbs'][] = Yii::t('user', 'Profile'); +?> +
    + + + +
    +
    +
    +
    +
    + <?= Yii::$app->user->identity->user->username ?> +
    + + field($model, 'user_pic')->widget(\kartik\widgets\FileInput::class, [ + 'options' => [ + 'accept' => 'image/*' + ], + 'pluginOptions' => [ + 'showPreview' => false, + 'showCaption' => true, + 'showRemove' => true, + 'showUpload' => false + ], + ]); ?> +
    +
    +
    +
    +
    +
    + field($model, 'username')->textInput(['maxLength' => true]) ?> + field($model, 'email')->textInput(['maxLength' => true]) ?> + field($model, 'password')->passwordInput(['maxLength' => true]) ?> +
    +
    +
    +
    + + + +
    + 'btn btn-primary']) ?> +
    + + + +
    diff --git a/common/config/main.php b/common/config/main.php index 2fee663..70a843d 100644 --- a/common/config/main.php +++ b/common/config/main.php @@ -32,6 +32,8 @@ return [ 'font_size' => 200, // default: 200 'salt' => 'my_cms_salt', // salt for image file names 'texture' => ['sun', 'rain'], // texture name + 'texture_over_image' => false, + 'text_over_image' => false, ], ], ]; diff --git a/common/modules/blog/entities/queries/BlogPostQuery.php b/common/modules/blog/entities/queries/BlogPostQuery.php index 6b414c6..2876180 100644 --- a/common/modules/blog/entities/queries/BlogPostQuery.php +++ b/common/modules/blog/entities/queries/BlogPostQuery.php @@ -18,6 +18,11 @@ class BlogPostQuery extends ActiveQuery ]); } + public function pubDate() + { + return $this->andWhere(['<', 'published_at', time()]); + } + public function byType($type) { return $this->andWhere(['type_id' => $type]); diff --git a/common/modules/blog/forms/BlogPostForm.php b/common/modules/blog/forms/BlogPostForm.php index cf148c3..eb03dbe 100644 --- a/common/modules/blog/forms/BlogPostForm.php +++ b/common/modules/blog/forms/BlogPostForm.php @@ -89,7 +89,13 @@ class BlogPostForm extends CompositeForm ]; } - public function categoriesList(): array + public function attributeHints() { + return [ + 'published_at' => Yii::t('blog', 'The article will be published after the specified date if its status is not a draft'), + ]; + } + + public function categoriesList(): array { return ArrayHelper::map(BlogCategory::find()->orderBy('sort')->asArray()->all(), 'id', 'name'); } diff --git a/common/modules/blog/messages/ru/blog.php b/common/modules/blog/messages/ru/blog.php index efd455b..f40d671 100644 --- a/common/modules/blog/messages/ru/blog.php +++ b/common/modules/blog/messages/ru/blog.php @@ -61,4 +61,6 @@ return [ 'Set tags...' => 'Укажите теги...', 'Publish' => 'Публикация', 'Preview on site' => 'Просмотр на сайте', + 'Publish Date' => 'Дата публикации', + 'The article will be published after the specified date if its status is not a draft' => 'Статья будет опубликована после наступления указанной даты, если ее статус не черновик.', ]; \ No newline at end of file diff --git a/common/modules/blog/repositories/read/BlogPostReadRepository.php b/common/modules/blog/repositories/read/BlogPostReadRepository.php index a39d3bc..7a8bc40 100644 --- a/common/modules/blog/repositories/read/BlogPostReadRepository.php +++ b/common/modules/blog/repositories/read/BlogPostReadRepository.php @@ -13,34 +13,34 @@ class BlogPostReadRepository { public function count(): int { - return BlogPost::find()->active()->count(); + return BlogPost::find()->active()->pubDate()->count(); } public function getAllByRange($offset, $limit): array { - return BlogPost::find()->active()->orderBy(['id' => SORT_ASC])->limit($limit)->offset($offset)->all(); + return BlogPost::find()->active()->pubDate()->orderBy(['id' => SORT_ASC])->limit($limit)->offset($offset)->all(); } public function getAll(): DataProviderInterface { - $query = BlogPost::find()->active()->with('category'); + $query = BlogPost::find()->active()->pubDate()->with('category'); return $this->getProvider($query); } public function getAllByCategory(BlogCategory $category): DataProviderInterface { - $query = BlogPost::find()->active()->andWhere(['category_id' => $category->id])->with('category'); + $query = BlogPost::find()->active()->pubDate()->andWhere(['category_id' => $category->id])->with('category'); return $this->getProvider($query); } public function findNext(int $id): ?BlogPost { - return BlogPost::find()->active()->andWhere(['>', 'id', $id])->one(); + return BlogPost::find()->active()->pubDate()->andWhere(['>', 'id', $id])->one(); } public function findPrev(int $id): ?BlogPost { - return BlogPost::find()->active()->andWhere(['<', 'id', $id])->orderBy(['id' => SORT_DESC])->one(); + return BlogPost::find()->active()->pubDate()->andWhere(['<', 'id', $id])->orderBy(['id' => SORT_DESC])->one(); } diff --git a/common/modules/blog/views/manage/post/_form.php b/common/modules/blog/views/manage/post/_form.php index 27274a8..d7b3a61 100644 --- a/common/modules/blog/views/manage/post/_form.php +++ b/common/modules/blog/views/manage/post/_form.php @@ -3,7 +3,7 @@ use kartik\file\FileInput; use mihaildev\ckeditor\CKEditor; use yii\helpers\Html; -use yii\bootstrap\ActiveForm; +use kartik\form\ActiveForm; use yii\web\JsExpression; use yii\helpers\Url; use yii\helpers\Json; @@ -30,6 +30,22 @@ if (isset($model->_post)) { JS; $this->registerJs( $js, $this::POS_READY ); } + +$js2 = ' +$(".hint-block").each(function () { + var $hint = $(this); + var label = $hint.parent().find("label"); + label.html(label.html() + \' \'); + label.addClass("help").popover({ + html: true, + trigger: "hover", + placement: "bottom", + content: $hint.html() + }); + $(this).hide(); +}); +'; +$this->registerJs($js2); ?>
    @@ -46,24 +62,15 @@ JS;
    -
    field($model, 'category_id')->dropDownList($model->categoriesList(), ['prompt' => '']) ?> - field($model, 'published_at')->widget(DateTimePicker::classname(), [ - 'options' => [], - 'pluginOptions' => [ - 'autoclose' => true, - 'format' => 'dd.mm.yyyy hh:ii:ss', - ] - ]); ?>
    -
    - field($model->tags, 'new_tags')->widget(Select2::classname(), [ + field($model->tags, 'new_tags')->widget(Select2::class, [ 'options' => [ 'placeholder' => Yii::t('blog','Set tags...'), 'multiple' => true, @@ -81,7 +88,7 @@ JS; 'templateResult' => new JsExpression('function(tag) { return tag.text; }'), 'templateSelection' => new JsExpression('function (tag) { return tag.text; }'), ], - ])->label(false); ?> + ])->label(Yii::t('blog', 'Tags')); ?>
    @@ -92,7 +99,7 @@ JS; field($model, 'title')->textInput(['maxlength' => true]) ?> field($model, 'slug')->textInput(['maxlength' => true]) ?> field($model, 'description')->textarea(['rows' => 5]) ?> - field($model, 'content')->widget(CKEditor::className()) ?> + field($model, 'content')->widget(CKEditor::class) ?>
    @@ -154,6 +161,15 @@ JS;
    field($model, 'status')->radioList(BlogPostHelper::statusList()) ?> +
    + field($model, 'published_at')->widget(DateTimePicker::class, [ + 'options' => [], + 'removeButton' => false, + 'pluginOptions' => [ + 'autoclose' => true, + 'format' => 'dd.mm.yyyy hh:ii:ss', + ] + ])->label(Yii::t('blog', 'Publish Date')); ?> diff --git a/core/components/avatar_generator/AvatarGenerator.php b/core/components/avatar_generator/AvatarGenerator.php deleted file mode 100644 index 3e59f39..0000000 --- a/core/components/avatar_generator/AvatarGenerator.php +++ /dev/null @@ -1,171 +0,0 @@ -originPath = Yii::getAlias('@staticRoot') . '/origin'; - $this->cachePath = Yii::getAlias('@staticRoot') . '/cache'; - } - - /** - * @param string $type - * @param string $name - * @param array|null $options - * - * @return string - * @throws NotFoundHttpException - * @throws \yii\base\Exception - */ - public function image(string $type, string $name, array $options = null): string - { - $originPath = $this->originPath . '/' . $type; - $origin = $originPath . '/' . $name; - - if (!file_exists($origin)) - { - return ''; - } - - $cachePath = $this->cachePath . '/' . $type; - $cache = $cachePath . '/' . (isset($options['newName']) ? $options['newName'] : $name); - - $cacheUrl = Yii::getAlias('@static') . '/cache/' . $type . '/' . $name; - - if (isset($options['size'])) - { - $size = $options['size']; - } - - if (!isset($options['regenerate'])) - { - $options['regenerate'] = false; - } - - if (!file_exists($origin)) - { - throw new NotFoundHttpException('Image "' . $name . '" does not exists.'); - } - - if (!file_exists($cache) || $options['regenerate'] == true) - { - FileHelper::createDirectory($cachePath, 0755, true); - if (empty($size)) { - //$size = isset(Yii::$app->params['imageSizes'][$type]) ? Yii::$app->params['imageSizes'][$type] : Yii::$app->params['imageSizes']['default']; - $size = [300, 300]; - } - Image::thumbnail($origin, $size[0], $size[1])->save($cache); - } - return $cacheUrl; - } - - public function avatar(int $id): string - { - if (!$user = User::findOne($id)) - { - throw new NotFoundHttpException('User does not exists.'); - } - if (empty($user->user_pic) || !file_exists($this->originPath . '/avatar/' . $user->user_pic)) - { - $user->user_pic = $this->generateAvatarByName($user->username); - $user->save(); - } - return $this->image('avatar', $user->user_pic); - } - - public function generateAvatarFromFile($file) - { - $fileName = md5('avatar-' . md5($file) . time()) . '.png'; - $originPath = $this->originPath . '/avatar'; - $origin = $originPath . '/' . $fileName; - FileHelper::createDirectory($originPath, 0755, true); - copy($file, $origin); - return $fileName; - } - - public function generateAvatarByName(string $name): string - { - $width = 300; - $height = 300; - $fontSize = 100; - $font = Yii::getAlias('@core') . '/components/avatar_generator/Play-Bold.ttf'; - - $parts = explode(' ', $name); - $text = is_array($parts) && count($parts)>1 ? mb_substr($parts[0],0,1,"UTF-8") . mb_substr($parts[1],0,1,"UTF-8") : mb_substr($name,0,1,"UTF-8"); - - $fileName = md5('avatar-' . $name . time()) . '.png'; - - $originPath = $this->originPath . '/avatar'; - $origin = $originPath . '/' . $fileName; - - FileHelper::createDirectory($originPath, 0755, true); - - $img = imagecreatetruecolor($width, $height); - - $bgcolor = substr(md5($name), 0, 6); - - $rgb = []; - list($rgb['r'], $rgb['g'], $rgb['b']) = sscanf($bgcolor, "%02x%02x%02x"); - $rgb['rgb'] = $rgb['b'] + ($rgb['g'] << 0x8) + ($rgb['r'] << 0x10); - - $contrast = $this->RgbContrast($rgb['r'], $rgb['g'], $rgb['b']); - - $fillColor = imagecolorallocate($img, $rgb['r'], $rgb['g'], $rgb['b']); - imagefill($img, 0,0, $fillColor); - - $cor = imagecolorallocate($img, $contrast['r'], $contrast['g'], $contrast['b']); - - $box = imageftbbox( $fontSize, 0, $font, $text ); - $x = ($width - ($box[2] - $box[0])) / 2; - $y = ($height - ($box[1] - $box[7])) / 2; - $y -= $box[7]; - - imagettftext($img, $fontSize, 0, $x, $y, $cor, $font, $text); - imagepng($img, $origin); - imagedestroy($img); - return $fileName; - } - - public function getPath(string $type): string - { - if (!file_exists($this->originPath . '/' . $type)) - { - mkdir($this->originPath . '/' . $type, 0777, true); - } - return $this->originPath . '/' . $type; - } - - public function remove(string $type, string $fileName = null): void - { - if ($fileName && file_exists($this->originPath . '/' . $type . '/' . $fileName)) { - unlink($this->originPath . '/' . $type . '/' . $fileName); - } - - if ($fileName && file_exists($this->cachePath . '/' . $type . '/' . $fileName)) { - unlink($this->cachePath . '/' . $type . '/' . $fileName); - } - } - - public function RgbContrast($r, $g, $b) { - return array( - 'r' => ($r < 128) ? 255 : 0, - 'g' => ($g < 128) ? 255 : 0, - 'b' => ($b < 128) ? 255 : 0 - ); - } -} \ No newline at end of file diff --git a/core/components/avatar_generator/Play-Bold.ttf b/core/components/avatar_generator/Play-Bold.ttf deleted file mode 100644 index f60bdd1..0000000 Binary files a/core/components/avatar_generator/Play-Bold.ttf and /dev/null differ diff --git a/core/entities/user/User.php b/core/entities/user/User.php index 1d5a79e..b1a5698 100644 --- a/core/entities/user/User.php +++ b/core/entities/user/User.php @@ -1,7 +1,6 @@ setPassword( $password ); } $this->updated_at = time(); + + /* @var $user_pic \yii\web\UploadedFile */ if ($user_pic) { $fileName = md5('avatar-' . $user_pic->baseName . time()) . '.' . $user_pic->extension; - if ($user_pic->saveAs((new AvatarGenerator())->getPath('avatar') . '/' . $fileName)) - { - (new AvatarGenerator())->remove('avatar', $this->user_pic); - $this->user_pic = $fileName; + $path = Yii::getAlias( '@runtime/' . $fileName); + $user_pic->saveAs($path); + $this->user_pic = basename(Yii::$app->avatar->update($username, null, $path)); + if (file_exists($path)) { + unlink($path); } } } diff --git a/core/forms/user/ProfileEditForm.php b/core/forms/user/ProfileEditForm.php index 9778d02..2b1fb52 100644 --- a/core/forms/user/ProfileEditForm.php +++ b/core/forms/user/ProfileEditForm.php @@ -16,6 +16,7 @@ class ProfileEditForm extends Model public $email; public $username; public $password; + public $user_pic; public $_user; @@ -24,6 +25,7 @@ class ProfileEditForm extends Model $this->email = $user->email; $this->username = $user->username; $this->_user = $user; + $this->user_pic = $user->user_pic; parent::__construct($config); } @@ -36,6 +38,11 @@ class ProfileEditForm extends Model [['email', 'password'], 'string', 'max' => 255], [['username', 'email'], 'unique', 'targetClass' => User::class, 'filter' => ['<>', 'id', $this->_user->id]], ['username', 'string', 'min' => 2, 'max' => 255], + ['user_pic', 'image', 'extensions' => 'png, jpg, jpeg, gif', + 'skipOnEmpty' => true, + 'minWidth' => 100, + 'minHeight' => 100, + ], ]; } @@ -45,6 +52,7 @@ class ProfileEditForm extends Model 'email' => Yii::t('user', 'E-mail'), 'username' => Yii::t('user', 'Username'), 'password' => Yii::t('user', 'Password'), + 'user_pic' => Yii::t('user', 'User Picture'), ]; } } \ No newline at end of file diff --git a/core/services/user/ProfileService.php b/core/services/user/ProfileService.php index 29d84fc..396a368 100644 --- a/core/services/user/ProfileService.php +++ b/core/services/user/ProfileService.php @@ -21,7 +21,7 @@ class ProfileService public function edit($id, ProfileEditForm $form): void { $user = $this->users->get($id); - $user->editProfile($form->email, $form->username, $form->password); + $user->editProfile($form->email, $form->username, $form->password, $form->user_pic); $this->users->save($user); } } \ No newline at end of file diff --git a/docs/develope/Modules.md b/docs/develope/Modules.md new file mode 100644 index 0000000..77d1122 --- /dev/null +++ b/docs/develope/Modules.md @@ -0,0 +1,81 @@ +### Модули + +Модули располагаются в папке `common/modules` + +Папка модуля должна содержать основной файл, например, `BlogModule.php` следующего содержания +```php +params['search_rules'][] = "SELECT title, content, CONCAT('/blog/manage/post/view/', id) AS url FROM {{blog_posts}}"; + + // Добавление правила роутинга + $app->getUrlManager()->addRules([ + 'blog' => 'blog/post/index', + ]); + + // Добавление правил роутинга, в случае использования классов + $app->getUrlManager()->addRules([ + ['class' => 'common\modules\blog\urls\BlogPostUrlRule'], + ['class' => 'common\modules\blog\urls\BlogCategoryUrlRule'], + ]); + + // Добавление локализации + $app->getI18n()->translations = ArrayHelper::merge($app->getI18n()->translations, [ + 'blog' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'basePath' => '@common/modules/blog/messages', + ], + 'blog_public' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'basePath' => '@common/modules/blog/messages', + ], + ]); + + // Добавление пунктов в меню администратора + if (basename($app->getBasePath()) === 'backend') { + $app->params['adminMenu'][] = [ + 'label' => \Yii::t( 'blog', 'Blog' ), + 'icon' => 'book', + 'items' => [ + [ + 'label' => \Yii::t( 'blog', 'Categories' ), + 'icon' => 'caret-right', + 'url' => [ '/blog/manage/category/index' ] + ], + [ + 'label' => \Yii::t( 'blog', 'Posts' ), + 'icon' => 'caret-right', + 'url' => [ '/blog/manage/post/index' ] + ], + [ + 'label' => \Yii::t( 'blog', 'Comments' ), + 'icon' => 'caret-right', + 'url' => [ '/blog/manage/comment/index' ] + ], + ], + 'visible' => \Yii::$app->user->can( 'admin' ) || \Yii::$app->user->can( 'BlogManagement' ) + ]; + } + } +} +``` \ No newline at end of file diff --git a/docs/develope/README.md b/docs/develope/README.md new file mode 100644 index 0000000..5d41cbc --- /dev/null +++ b/docs/develope/README.md @@ -0,0 +1,18 @@ +# Zertex CMS +Система управления сайтом на Yii2 + +**Для разработчика** + +* [Модули](Modules.md) + +#### Дополнительные параметры Yii::$app + +1. `$app->params['search_rules']` - Массив правил для поиска. + +Пример: +```php +$app->params['search_rules'][] = "SELECT title, content, CONCAT('/blog/manage/post/view/', id) AS url FROM {{blog_posts}}"; +``` +Поиск осуществляется по полям TITLE и CONTENT. В качестве URL указывается ссылка на конкрутную запись. + +Если название полей отличаются от TITLE и CONTENT, но, сгласно правилам SQL, указывайте ваше поле, затем `as TITLE` или `as CONTENT` \ No newline at end of file