diff --git a/backend/controllers/GroupController.php b/backend/controllers/GroupController.php index 48e1d487..1755327b 100644 --- a/backend/controllers/GroupController.php +++ b/backend/controllers/GroupController.php @@ -63,7 +63,7 @@ public function actionIndex(): string } $searchModel = new GroupQuery(self::POSITIONS_PER_PAGE); - $dataProvider = $searchModel->search(Yii::$app->request->queryParams); // todo switch to searchForOperator() once Group has updated_at field; see issue #379 + $dataProvider = $searchModel->searchForOperator(Yii::$app->request->queryParams); return $this->render('index', [ 'searchModel' => $searchModel, diff --git a/backend/views/game/_form.php b/backend/views/game/_form.php index 61a7a06f..654acd79 100644 --- a/backend/views/game/_form.php +++ b/backend/views/game/_form.php @@ -2,8 +2,8 @@ use backend\assets\GameAsset; use common\models\EpicQuery; -use common\models\RecapQuery; use common\models\Game; +use common\models\RecapQuery; use kartik\date\DatePicker; use yii\helpers\Html; use yii\widgets\ActiveForm; @@ -19,17 +19,29 @@ -
+
field($model, 'epic_id')->dropDownList(EpicQuery::getListOfEpicsForSelector()); ?>
- field($model, 'status') - ->dropDownList($model->isNewRecord ? Game::statusNames() : $model->getAllowedChangeNames()) + field($model, 'status') + ->dropDownList( + $model->isNewRecord + ? Game::statusNames() + : $model->getAllowedChangeNames() + ) ?>
-
+
+ field($model, 'recap_id')->dropDownList( + RecapQuery::allFromCurrentEpicForSelector(), + ['prompt' => ' --- ' . Yii::t('app', 'RECAP_PROMPT') . ' --- '] + ) ?> +
+ +
field($model, 'planned_date')->widget( DatePicker::class, [ @@ -43,11 +55,8 @@ ) ?>
-
- field($model, 'recap_id')->dropDownList( - RecapQuery::allFromCurrentEpicForSelector(), - ['prompt' => ' --- ' . Yii::t('app', 'RECAP_PROMPT') . ' --- '] - ) ?> +
+ field($model, 'planned_location') ?>
diff --git a/backend/views/game/view.php b/backend/views/game/view.php index 7d9da88e..472725fa 100644 --- a/backend/views/game/view.php +++ b/backend/views/game/view.php @@ -41,6 +41,8 @@ 'format' => 'raw', 'value' => Html::a($model->epic->name, ['epic/view', 'key' => $model->epic->key], []), ], + 'planned_date', + 'planned_location', [ 'attribute' => 'recap_id', 'format' => 'raw', diff --git a/backend/views/group/_view_basic.php b/backend/views/group/_view_basic.php index 2c2505d9..9ca78943 100644 --- a/backend/views/group/_view_basic.php +++ b/backend/views/group/_view_basic.php @@ -37,11 +37,19 @@ 'label' => Yii::t('app', 'GROUP_IMPORTANCE'), 'value' => $model->getImportanceCategory(), ], + [ + 'label' => Yii::t('app', 'DESCRIPTION_COUNT_UNIQUE'), + 'value' => $model->descriptionPack->getUniqueDescriptionTypesCount(), + ], [ 'label' => Yii::t('app', 'DESCRIPTION_COUNT_EXPECTED'), 'format' => 'raw', 'value' => $model->getImportanceCategoryObject()->minimum() . ' — ' . $model->getImportanceCategoryObject()->maximum(), ], + [ + 'attribute' => 'updated_at', + 'format' => 'datetime', + ], [ 'attribute' => 'master_group_id', 'format' => 'raw', diff --git a/backend/web/js/game.js b/backend/web/js/game.js index a61dfb31..8c6e57db 100644 --- a/backend/web/js/game.js +++ b/backend/web/js/game.js @@ -1,6 +1,19 @@ -$('#game-planned_date').on('change', function () { - $('#game-basics-constructed').val($(this).val()); -}); +$('#game-basics-constructed').val($('#game-basics').val()); + +function setBasicsConstructed() { + var datetime = $('#game-planned_date').val(); + var location = $('#game-planned_location').val(); + var separator = ''; + + if (datetime && location) { + separator = ', '; + } + + $('#game-basics-constructed').val(location + separator + datetime); +} + +$('#game-planned_date').on('change', setBasicsConstructed); +$('#game-planned_location').on('keyup', setBasicsConstructed); $('#game-basics-transfer').on('click', function () { $('#game-basics').val($('#game-basics-constructed').val()); diff --git a/common/messages/en/app.php b/common/messages/en/app.php index 05ab2771..d7d74ca6 100644 --- a/common/messages/en/app.php +++ b/common/messages/en/app.php @@ -318,6 +318,7 @@ 'GAME_ID' => 'ID', 'GAME_NOTES' => 'Notes', 'GAME_PLANNED_DATE' => 'Planned date', + 'GAME_PLANNED_LOCATION' => 'Planned location', 'GAME_POSITION' => 'Position', 'GAME_SESSION_NOT_AVAILABLE' => 'Session not available', 'GAME_STATUS' => 'Status', @@ -359,6 +360,7 @@ 'GROUP_NOT_AVAILABLE' => 'Group not available', 'GROUP_STATISTICS' => 'Technical data', 'GROUP_SUBGROUPS' => 'Subgroups', + 'GROUP_UPDATED_AT' => 'Changed', 'GROUP_VISIBILITY' => 'Visibility', 'GROUP_WITHOUT_MASTER' => 'no master group', 'GROUP_WITHOUT_SUBGROUPS' => 'no subgroups', @@ -600,6 +602,19 @@ 'SCENARIO_TECHNICAL_DETAILS' => 'Technical details', 'SCENARIO_TITLE_CREATE' => 'Create scenario', 'SCENARIO_TITLE_UPDATE' => 'Modify scenario', + 'SCRIBBLES_BUTTON_NO' => 'Click to mark as favorite', + 'SCRIBBLES_BUTTON_WORKING' => 'Status change in progress...', + 'SCRIBBLES_BUTTON_YES' => 'Marked as favorite; click to unmark', + 'SCRIBBLES_FAVORITE_ERROR_GENERIC' => 'Status change error', + 'SCRIBBLES_TITLE_NO' => 'Click for player\'s scribbles', + 'SCRIBBLES_TITLE_YES' => 'Click for player\'s scribbles', + 'SCRIBBLE_DENIED_ACCESS' => 'You have no rights to access this', + 'SCRIBBLE_ID' => 'ID', + 'SCRIBBLE_IS_FAVORITE' => 'Favorited?', + 'SCRIBBLE_PACK' => 'Pack of scribbles', + 'SCRIBBLE_PACK_CLASS' => 'Class', + 'SCRIBBLE_PACK_ID' => 'ID', + 'SCRIBBLE_TITLE' => 'Player\'s scribbles', 'SEEN_ALERT' => 'Alert threshold', 'SEEN_BEFORE_UPDATE' => 'Modified for:', 'SEEN_ID' => 'ID', diff --git a/common/messages/pl/app.php b/common/messages/pl/app.php index 9b08b983..83ffaaf9 100644 --- a/common/messages/pl/app.php +++ b/common/messages/pl/app.php @@ -318,6 +318,7 @@ 'GAME_ID' => 'ID', 'GAME_NOTES' => 'Notatki', 'GAME_PLANNED_DATE' => 'Planowana data', + 'GAME_PLANNED_LOCATION' => 'Planowane miejsce', 'GAME_POSITION' => 'Pozycja', 'GAME_SESSION_NOT_AVAILABLE' => 'Sesja niedostępna', 'GAME_STATUS' => 'Status', @@ -359,6 +360,7 @@ 'GROUP_NOT_AVAILABLE' => 'Grupa niedostępna', 'GROUP_STATISTICS' => 'Dane techniczne', 'GROUP_SUBGROUPS' => 'Grupy podrzędne', + 'GROUP_UPDATED_AT' => 'Zmieniano', 'GROUP_VISIBILITY' => 'Widoczność', 'GROUP_WITHOUT_MASTER' => 'Brak grupy nadrzędnej', 'GROUP_WITHOUT_SUBGROUPS' => 'Brak grup podrzędnych', @@ -600,6 +602,19 @@ 'SCENARIO_TECHNICAL_DETAILS' => 'Szczegóły techniczne', 'SCENARIO_TITLE_CREATE' => 'Dodaj scenariusz', 'SCENARIO_TITLE_UPDATE' => 'Zmień scenariusz', + 'SCRIBBLES_BUTTON_NO' => 'Klinij, by dodać do ulubionych', + 'SCRIBBLES_BUTTON_WORKING' => 'Zmiana statusu w toku...', + 'SCRIBBLES_BUTTON_YES' => 'W ulubionych; kliknij, by usunąć z ulubionych', + 'SCRIBBLES_FAVORITE_ERROR_GENERIC' => 'Błąd zapisu statusu', + 'SCRIBBLES_TITLE_NO' => 'Zapiski', + 'SCRIBBLES_TITLE_YES' => 'Zapiski', + 'SCRIBBLE_DENIED_ACCESS' => 'Brak dostępu', + 'SCRIBBLE_ID' => 'ID', + 'SCRIBBLE_IS_FAVORITE' => 'Ulubione?', + 'SCRIBBLE_PACK' => 'Paczka zapisków', + 'SCRIBBLE_PACK_CLASS' => 'Klasa', + 'SCRIBBLE_PACK_ID' => 'ID', + 'SCRIBBLE_TITLE' => 'Zapiski', 'SEEN_ALERT' => 'Próg sygnalizacji', 'SEEN_BEFORE_UPDATE' => 'Zmienione dla:', 'SEEN_ID' => 'ID', diff --git a/common/models/Character.php b/common/models/Character.php index 34d0b6c8..82e8d06e 100644 --- a/common/models/Character.php +++ b/common/models/Character.php @@ -7,6 +7,7 @@ use common\models\core\HasEpicControl; use common\models\core\HasImportance; use common\models\core\HasImportanceCategory; +use common\models\core\HasScribbles; use common\models\core\HasSightings; use common\models\core\HasVisibility; use common\models\core\ImportanceCategory; @@ -36,6 +37,7 @@ * @property string $external_data_pack_id * @property string $seen_pack_id * @property string $importance_pack_id + * @property int|null $scribble_pack_id * @property string $utility_bag_id * * @property Epic $epic @@ -44,12 +46,13 @@ * @property ExternalDataPack $externalDataPack * @property ImportancePack $importancePack * @property SeenPack $seenPack + * @property ScribblePack $scribblePack * @property UtilityBag $utilityBag * @property CharacterSheet[] $characterSheets * @property GroupMembership[] $groupMemberships * @property GroupMembership[] $groupMembershipsVisibleToUser */ -class Character extends ActiveRecord implements Displayable, HasDescriptions, HasEpicControl, HasImportance, HasImportanceCategory, HasReputations, HasVisibility, HasSightings +class Character extends ActiveRecord implements Displayable, HasDescriptions, HasEpicControl, HasImportance, HasImportanceCategory, HasReputations, HasVisibility, HasScribbles, HasSightings { use ToolsForEntity; use ToolsForHasDescriptions; @@ -63,7 +66,7 @@ public function rules() { return [ [['epic_id', 'name', 'tagline', 'visibility', 'importance_category'], 'required'], - [['epic_id', 'character_sheet_id', 'description_pack_id'], 'integer'], + [['epic_id', 'character_sheet_id', 'description_pack_id', 'scribble_pack_id'], 'integer'], [['data', 'visibility', 'importance_category'], 'string'], [['key'], 'string', 'max' => 80], [['name', 'tagline'], 'string', 'max' => 120], @@ -95,6 +98,13 @@ public function rules() 'targetClass' => ExternalDataPack::class, 'targetAttribute' => ['external_data_pack_id' => 'external_data_pack_id'] ], + [ + ['scribble_pack_id'], + 'exist', + 'skipOnError' => true, + 'targetClass' => ScribblePack::class, + 'targetAttribute' => ['scribble_pack_id' => 'scribble_pack_id'] + ], [ ['visibility'], 'in', @@ -122,6 +132,7 @@ public function attributeLabels() 'external_data_pack_id' => Yii::t('app', 'EXTERNAL_DATA_PACK'), 'seen_pack_id' => Yii::t('app', 'SEEN_PACK_ID'), 'importance_pack_id' => Yii::t('app', 'IMPORTANCE_PACK'), + 'scribble_pack_id' => Yii::t('app', 'SCRIBBLE_PACK'), 'utility_bag_id' => Yii::t('app', 'UTILITY_BAG'), ]; } @@ -167,6 +178,11 @@ public function beforeSave($insert) $this->importance_pack_id = $pack->importance_pack_id; } + if (empty($this->scribble_pack_id)) { + $pack = ScribblePack::create('Character'); + $this->scribble_pack_id = $pack->scribble_pack_id; + } + return parent::beforeSave($insert); } @@ -204,7 +220,7 @@ static public function allowedVisibilities(): array /** * @return ActiveQuery */ - public function getEpic(): ActiveQuery + public function getEpic(): ActiveQuery { return $this->hasOne(Epic::class, ['epic_id' => 'epic_id']); } @@ -238,6 +254,16 @@ public function getImportancePack() return $this->hasOne(ImportancePack::class, ['importance_pack_id' => 'importance_pack_id']); } + /** + * Gets query for [[ScribblePack]]. + * + * @return \yii\db\ActiveQuery|ScribblePackQuery + */ + public function getScribblePack(): ActiveQuery|ScribblePackQuery + { + return $this->hasOne(ScribblePack::class, ['scribble_pack_id' => 'scribble_pack_id']); + } + /** * @return ActiveQuery */ diff --git a/common/models/DescriptionPack.php b/common/models/DescriptionPack.php index f1ad800b..9c759275 100644 --- a/common/models/DescriptionPack.php +++ b/common/models/DescriptionPack.php @@ -3,7 +3,7 @@ namespace common\models; use common\models\core\HasEpicControl; -use common\models\core\IsPack; +use common\models\core\IsEditablePack; use common\models\core\Language; use Yii; use yii\behaviors\TimestampBehavior; @@ -21,7 +21,7 @@ * @property Character[] $people * @property Epic $epic */ -final class DescriptionPack extends ActiveRecord implements Displayable, IsPack +final class DescriptionPack extends ActiveRecord implements Displayable, IsEditablePack { public static function tableName() { diff --git a/common/models/ExternalDataPack.php b/common/models/ExternalDataPack.php index 12c1667e..f381ee51 100644 --- a/common/models/ExternalDataPack.php +++ b/common/models/ExternalDataPack.php @@ -3,7 +3,7 @@ namespace common\models; use common\models\core\HasEpicControl; -use common\models\core\IsPack; +use common\models\core\IsEditablePack; use common\models\core\Visibility; use Yii; use yii\behaviors\TimestampBehavior; @@ -20,7 +20,7 @@ * * @property Epic $epic */ -class ExternalDataPack extends ActiveRecord implements IsPack +class ExternalDataPack extends ActiveRecord implements IsEditablePack { public static function tableName() { diff --git a/common/models/Game.php b/common/models/Game.php index 3d81c992..e0ac7298 100644 --- a/common/models/Game.php +++ b/common/models/Game.php @@ -20,6 +20,7 @@ * @property string $epic_id * @property string $basics * @property string $planned_date + * @property string $planned_location * @property string $status * @property int $position * @property string $notes @@ -45,20 +46,21 @@ class Game extends ActiveRecord implements HasEpicControl, HasStatus const STATUS_COMPLETED = 'completed'; // game was completed; next: CLOSED const STATUS_CLOSED = 'closed'; // game was described; next: none - public static function tableName() + public static function tableName(): string { return 'game'; } - public function rules() + public function rules(): array { return [ [['epic_id'], 'required'], [['epic_id', 'position', 'recap_id', 'utility_bag_id'], 'integer'], [['planned_date'], 'safe'], - [['planned_date'], 'default', 'value' => null], + [['planned_date', 'planned_location'], 'default', 'value' => null], [['notes'], 'string'], [['basics'], 'string', 'max' => 255], + [['planned_location'], 'string', 'max' => 80], [['status'], 'string', 'max' => 20], [ ['status'], @@ -95,13 +97,17 @@ public function rules() ]; } - public function attributeLabels() + /** + * @return string[] + */ + public function attributeLabels(): array { return [ 'game_id' => Yii::t('app', 'GAME_ID'), 'epic_id' => Yii::t('app', 'LABEL_EPIC'), 'basics' => Yii::t('app', 'GAME_BASICS'), 'planned_date' => Yii::t('app', 'GAME_PLANNED_DATE'), + 'planned_location' => Yii::t('app', 'GAME_PLANNED_LOCATION'), 'status' => Yii::t('app', 'GAME_STATUS'), 'position' => Yii::t('app', 'GAME_POSITION'), 'notes' => Yii::t('app', 'GAME_NOTES'), @@ -110,7 +116,7 @@ public function attributeLabels() ]; } - public function beforeSave($insert) + public function beforeSave($insert): bool { if (empty($this->utility_bag_id)) { $pack = UtilityBag::create('Game'); @@ -120,13 +126,13 @@ public function beforeSave($insert) return parent::beforeSave($insert); } - public function afterSave($insert, $changedAttributes) + public function afterSave($insert, $changedAttributes): void { $this->utilityBag->flagAsChanged(); parent::afterSave($insert, $changedAttributes); } - public function behaviors() + public function behaviors(): array { return [ 'positionBehavior' => [ @@ -142,10 +148,7 @@ public function behaviors() ]; } - /** - * @return ActiveQuery - */ - public function getEpic() + public function getEpic(): ActiveQuery { return $this->hasOne(Epic::class, ['epic_id' => 'epic_id']); } @@ -198,13 +201,13 @@ public function statusAllowedChanges(): array public function getStatus(): string { $names = self::statusNames(); - return isset($names[$this->status]) ? $names[$this->status] : '?'; + return $names[$this->status] ?? '?'; } public function getStatusClass(): string { $names = self::statusClasses(); - return isset($names[$this->status]) ? $names[$this->status] : ''; + return $names[$this->status] ?? ''; } public function getAllowedChange(): array @@ -224,10 +227,7 @@ public function getRecap(): ActiveQuery return $this->hasOne(Recap::class, ['recap_id' => 'recap_id']); } - /** - * @return ActiveQuery - */ - public function getUtilityBag() + public function getUtilityBag(): ActiveQuery { return $this->hasOne(UtilityBag::class, ['utility_bag_id' => 'utility_bag_id']); } @@ -272,10 +272,7 @@ static function throwExceptionAboutView() self::thrownExceptionAbout(Yii::t('app', 'NO_RIGHT_TO_VIEW_SESSION')); } - /** - * @return string|null - */ - public function getNotesFormatted() + public function getNotesFormatted(): ?string { return Markdown::process(Html::encode($this->notes), 'gfm'); } diff --git a/common/models/Group.php b/common/models/Group.php index b9d88eb5..2074b27d 100644 --- a/common/models/Group.php +++ b/common/models/Group.php @@ -7,6 +7,7 @@ use common\models\core\HasEpicControl; use common\models\core\HasImportance; use common\models\core\HasImportanceCategory; +use common\models\core\HasScribbles; use common\models\core\HasSightings; use common\models\core\HasVisibility; use common\models\core\ImportanceCategory; @@ -14,7 +15,10 @@ use common\models\external\HasReputations; use common\models\tools\ToolsForEntity; use common\models\tools\ToolsForHasDescriptions; +use DateTimeImmutable; +use ReflectionClass; use Yii; +use yii\behaviors\TimestampBehavior; use yii\db\ActiveQuery; use yii\db\ActiveRecord; use yii\helpers\Html; @@ -30,11 +34,13 @@ * @property string $seen_pack_id * @property string $visibility * @property string $importance_category - * @property string $description_pack_id - * @property string $external_data_pack_id - * @property string $importance_pack_id - * @property string $master_group_id - * @property string $utility_bag_id + * @property string $updated_at + * @property int|null $description_pack_id + * @property int|null $external_data_pack_id + * @property int|null $importance_pack_id + * @property int|null $master_group_id + * @property int|null $scribble_pack_id + * @property int|null $utility_bag_id * * @property DescriptionPack $descriptionPack * @property ExternalDataPack $externalDataPack @@ -50,21 +56,34 @@ * @property GroupMembership[] $groupCharacterMembershipsPassive * @property GroupMembership[] $groupCharacterMembershipsPast */ -class Group extends ActiveRecord implements Displayable, HasDescriptions, HasEpicControl, HasImportance, HasImportanceCategory, HasReputations, HasSightings, HasVisibility +class Group extends ActiveRecord implements Displayable, HasDescriptions, HasEpicControl, HasImportance, HasImportanceCategory, HasReputations, HasScribbles, HasSightings, HasVisibility { use ToolsForEntity; use ToolsForHasDescriptions; - public static function tableName() + public static function tableName(): string { return 'group'; } - public function rules() + public function rules(): array { return [ [['epic_id', 'name'], 'required'], - [['epic_id', 'master_group_id'], 'integer'], + [ + [ + 'epic_id', + 'seen_pack_id', + 'updated_at', + 'description_pack_id', + 'external_data_pack_id', + 'importance_pack_id', + 'scribble_pack_id', + 'utility_bag_id', + 'master_group_id' + ], + 'integer' + ], [['name'], 'string', 'max' => 120], [['visibility', 'importance_category'], 'string', 'max' => 20], [ @@ -81,6 +100,41 @@ public function rules() return $this->allowedVisibilities(); } ], + [ + ['importance_pack_id'], + 'exist', + 'skipOnError' => true, + 'targetClass' => ImportancePack::class, + 'targetAttribute' => ['importance_pack_id' => 'importance_pack_id'] + ], + [ + ['master_group_id'], + 'exist', + 'skipOnError' => true, + 'targetClass' => Group::class, + 'targetAttribute' => ['master_group_id' => 'group_id'] + ], + [ + ['scribble_pack_id'], + 'exist', + 'skipOnError' => true, + 'targetClass' => ScribblePack::class, + 'targetAttribute' => ['scribble_pack_id' => 'scribble_pack_id'] + ], + [ + ['seen_pack_id'], + 'exist', + 'skipOnError' => true, + 'targetClass' => SeenPack::class, + 'targetAttribute' => ['seen_pack_id' => 'seen_pack_id'] + ], + [ + ['utility_bag_id'], + 'exist', + 'skipOnError' => true, + 'targetClass' => UtilityBag::class, + 'targetAttribute' => ['utility_bag_id' => 'utility_bag_id'] + ], ]; } @@ -92,7 +146,7 @@ public function afterFind() parent::afterFind(); } - public function attributeLabels() + public function attributeLabels(): array { return [ 'group_id' => Yii::t('app', 'GROUP_ID'), @@ -102,10 +156,12 @@ public function attributeLabels() 'data' => Yii::t('app', 'GROUP_DATA'), 'visibility' => Yii::t('app', 'GROUP_VISIBILITY'), 'importance_category' => Yii::t('app', 'GROUP_IMPORTANCE'), + 'updated_at' => Yii::t('app', 'GROUP_UPDATED_AT'), 'description_pack_id' => Yii::t('app', 'DESCRIPTION_PACK'), 'external_data_pack_id' => Yii::t('app', 'EXTERNAL_DATA_PACK'), 'importance_pack_id' => Yii::t('app', 'IMPORTANCE_PACK'), 'master_group_id' => Yii::t('app', 'GROUP_MASTER_GROUP'), + 'scribble_pack_id' => Yii::t('app', 'SCRIBBLE_PACK'), 'utility_bag_id' => Yii::t('app', 'UTILITY_BAG'), ]; } @@ -118,10 +174,10 @@ public function afterSave($insert, $changedAttributes) parent::afterSave($insert, $changedAttributes); } - public function beforeSave($insert) + public function beforeSave($insert): bool { if ($insert) { - $this->key = $this->generateKey(strtolower((new \ReflectionClass($this))->getShortName())); + $this->key = $this->generateKey(strtolower((new ReflectionClass($this))->getShortName())); $this->data = json_encode([]); } @@ -150,6 +206,11 @@ public function beforeSave($insert) $this->importance_pack_id = $pack->importance_pack_id; } + if (empty($this->scribble_pack_id)) { + $pack = ScribblePack::create('Group'); + $this->scribble_pack_id = $pack->scribble_pack_id; + } + return parent::beforeSave($insert); } @@ -175,14 +236,18 @@ static public function allowedDescriptionTypes(): array ]; } - public function behaviors() + public function behaviors(): array { return [ 'performedActionBehavior' => [ 'class' => PerformedActionBehavior::class, 'idName' => 'group_id', 'className' => 'Group', - ] + ], + 'timestampBehavior' => [ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => null, + ], ]; } @@ -191,82 +256,60 @@ public function getDescriptionPack(): ActiveQuery return $this->hasOne(DescriptionPack::class, ['description_pack_id' => 'description_pack_id']); } - /** - * @return ActiveQuery - */ - public function getExternalDataPack() + public function getExternalDataPack(): ActiveQuery { return $this->hasOne(ExternalDataPack::class, ['external_data_pack_id' => 'external_data_pack_id']); } - /** - * @return ActiveQuery - */ - public function getEpic() + public function getEpic(): ActiveQuery { return $this->hasOne(Epic::class, ['epic_id' => 'epic_id']); } - /** - * @return ActiveQuery - */ - public function getImportancePack() + public function getImportancePack(): ActiveQuery { return $this->hasOne(ImportancePack::class, ['importance_pack_id' => 'importance_pack_id']); } /** - * @return \yii\db\ActiveQuery + * Gets query for [[ScribblePack]] */ - public function getMasterGroup() + public function getScribblePack(): ActiveQuery|ScribblePackQuery + { + return $this->hasOne(ScribblePack::class, ['scribble_pack_id' => 'scribble_pack_id']); + } + + public function getMasterGroup(): ActiveQuery { return $this->hasOne(Group::class, ['group_id' => 'master_group_id']); } - /** - * @return \yii\db\ActiveQuery - */ - public function getSubGroups() + public function getSubGroups(): ActiveQuery { return $this->hasMany(Group::class, ['master_group_id' => 'group_id']); } - /** - * @return ActiveQuery - */ - public function getSeenPack() + public function getSeenPack(): ActiveQuery { return $this->hasOne(SeenPack::class, ['seen_pack_id' => 'seen_pack_id']); } - /** - * @return ActiveQuery - */ - public function getUtilityBag() + public function getUtilityBag(): ActiveQuery { return $this->hasOne(UtilityBag::class, ['utility_bag_id' => 'utility_bag_id']); } - /** - * @return ActiveQuery - */ - public function getGroupCharacterMemberships() + public function getGroupCharacterMemberships(): ActiveQuery { return $this->hasMany(GroupMembership::class, ['group_id' => 'group_id']); } - /** - * @return ActiveQuery - */ - public function getGroupCharacterMembershipsOrderedByPosition() + public function getGroupCharacterMembershipsOrderedByPosition(): ActiveQuery { return $this->hasMany(GroupMembership::class, ['group_id' => 'group_id'])->orderBy('position ASC'); } - /** - * @return ActiveQuery - */ - public function getGroupCharacterMembershipsActive() + public function getGroupCharacterMembershipsActive(): ActiveQuery { return $this->hasMany(GroupMembership::class, ['group_id' => 'group_id'])->where([ 'status' => GroupMembership::STATUS_ACTIVE, @@ -274,10 +317,7 @@ public function getGroupCharacterMembershipsActive() ])->orderBy('position ASC'); } - /** - * @return ActiveQuery - */ - public function getGroupCharacterMembershipsPast() + public function getGroupCharacterMembershipsPast(): ActiveQuery { return $this->hasMany(GroupMembership::class, ['group_id' => 'group_id'])->where([ 'status' => GroupMembership::STATUS_PAST, @@ -285,10 +325,7 @@ public function getGroupCharacterMembershipsPast() ])->orderBy('position ASC'); } - /** - * @return ActiveQuery - */ - public function getGroupCharacterMembershipsPassive() + public function getGroupCharacterMembershipsPassive(): ActiveQuery { return $this->hasMany(GroupMembership::class, ['group_id' => 'group_id'])->where([ 'status' => GroupMembership::STATUS_PASSIVE, @@ -296,7 +333,7 @@ public function getGroupCharacterMembershipsPassive() ])->orderBy('position ASC'); } - public function getSimpleDataForApi() + public function getSimpleDataForApi(): array { return [ 'name' => $this->name, @@ -314,7 +351,7 @@ public function getCompleteDataForApi() return $decodedData; } - public function isVisibleInApi() + public function isVisibleInApi(): bool { return true; } @@ -420,10 +457,9 @@ public function getVisibilityLowercase(): string return $visibility->getNameLowercase(); } - public function getLastModified(): \DateTimeImmutable + public function getLastModified(): DateTimeImmutable { - /* @todo Implement update date on object */ - return new \DateTimeImmutable('now'); + return new DateTimeImmutable(date("Y-m-d H:i:s", $this->updated_at)); } public function getSeenStatusForUser(int $userId): string diff --git a/common/models/Importance.php b/common/models/Importance.php index 1f4f6846..e298379a 100644 --- a/common/models/Importance.php +++ b/common/models/Importance.php @@ -73,6 +73,16 @@ public function getUser(): ActiveQuery return $this->hasOne(User::class, ['id' => 'user_id']); } + public static function createEmptyForPack(int $userId, ImportancePack $pack): self + { + $object = new Importance(); + $object->user_id = $userId; + $object->importance_pack_id = $pack->importance_pack_id; + $object->importance = 0; + + return $object; + } + /** * Calculates the importance value * diff --git a/common/models/ImportancePack.php b/common/models/ImportancePack.php index 17dcaa16..3e368880 100644 --- a/common/models/ImportancePack.php +++ b/common/models/ImportancePack.php @@ -3,6 +3,8 @@ namespace common\models; use common\models\core\HasImportance; +use common\models\core\IsSelfFillingPack; +use common\models\tools\ToolsForSelfFillingPacks; use Yii; use yii\db\ActiveQuery; use yii\db\ActiveRecord; @@ -17,8 +19,10 @@ * @property Group[] $groups * @property Importance[] $importances */ -class ImportancePack extends ActiveRecord +class ImportancePack extends ActiveRecord implements IsSelfFillingPack { + use ToolsForSelfFillingPacks; + /** * @var HasImportance */ @@ -105,13 +109,22 @@ public function getEpic(): Epic return $this->getControllingObject()->getEpic()->one(); } + public function createEmptyContent(int $userId): Importance + { + return Importance::createEmptyForPack($userId, $this); + } + /** * Recalculates pack importance objects * @return bool */ public function recalculatePack(): bool { - $result = $this->createAbsentImportanceObjects(); + $result = $this->createAbsentRecords( + $this->getEpic(), + $this, + Importance::findAll(['importance_pack_id' => $this->importance_pack_id]) + ); foreach ($this->importances as $importance) { $result = $result && $importance->calculateAndSave(); @@ -119,34 +132,4 @@ public function recalculatePack(): bool return $result; } - - /** - * Creates new Importance objects for users that do not have them - * @return bool - */ - private function createAbsentImportanceObjects(): bool - { - $users = $this->getEpic()->participants; - $importanceObjectsRaw = Importance::findAll(['importance_pack_id' => $this->importance_pack_id]); - $importanceObjectsOrdered = []; - - foreach ($importanceObjectsRaw as $importanceObject) { - $importanceObjectsOrdered[$importanceObject->user_id] = $importanceObject; - } - - $result = true; - - foreach ($users as $user) { - if (!isset($importanceObjectsOrdered[$user->user_id])) { - $importanceObject = new Importance(); - $importanceObject->user_id = $user->user_id; - $importanceObject->importance_pack_id = $this->importance_pack_id; - $importanceObject->importance = 0; - - $saveResult = $importanceObject->save(); - $result = $result && $saveResult; - } - } - return $result; - } } diff --git a/common/models/ParameterPack.php b/common/models/ParameterPack.php index 4bd46c80..866fee29 100644 --- a/common/models/ParameterPack.php +++ b/common/models/ParameterPack.php @@ -3,7 +3,7 @@ namespace common\models; use common\models\core\HasEpicControl; -use common\models\core\IsPack; +use common\models\core\IsEditablePack; use Yii; use yii\behaviors\TimestampBehavior; use yii\db\ActiveQuery; @@ -21,7 +21,7 @@ * @property Story[] $stories * @property Epic $epic */ -class ParameterPack extends ActiveRecord implements IsPack +class ParameterPack extends ActiveRecord implements IsEditablePack { /** * @inheritdoc diff --git a/common/models/Scribble.php b/common/models/Scribble.php new file mode 100644 index 00000000..4db5746f --- /dev/null +++ b/common/models/Scribble.php @@ -0,0 +1,84 @@ + true, 'targetClass' => ScribblePack::class, 'targetAttribute' => ['scribble_pack_id' => 'scribble_pack_id']], + [['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::class, 'targetAttribute' => ['user_id' => 'id']], + ]; + } + + public function attributeLabels() + { + return [ + 'scribble_id' => Yii::t('app', 'SCRIBBLE_ID'), + 'scribble_pack_id' => Yii::t('app', 'SCRIBBLE_PACK'), + 'user_id' => Yii::t('app', 'USER_LABEL'), + 'favorite' => Yii::t('app', 'SCRIBBLE_IS_FAVORITE'), + ]; + } + + /** + * Gets query for [[ScribblePack]]. + * + * @return ActiveQuery|ScribblePackQuery + */ + public function getScribblePack(): ActiveQuery|ScribblePackQuery + { + return $this->hasOne(ScribblePack::class, ['scribble_pack_id' => 'scribble_pack_id']); + } + + /** + * Gets query for [[User]]. + * + * @return ActiveQuery + */ + public function getUser(): ActiveQuery + { + return $this->hasOne(User::class, ['id' => 'user_id']); + } + + /** + * @return ScribbleQuery the active query used by this AR class. + */ + public static function find(): ScribbleQuery + { + return new ScribbleQuery(get_called_class()); + } + + public static function createEmptyForPack(int $userId, ScribblePack $pack): Scribble + { + $object = new Scribble(); + $object->user_id = $userId; + $object->scribble_pack_id = $pack->scribble_pack_id; + $object->favorite = false; + + return $object; + } +} diff --git a/common/models/ScribblePack.php b/common/models/ScribblePack.php new file mode 100644 index 00000000..5675e597 --- /dev/null +++ b/common/models/ScribblePack.php @@ -0,0 +1,123 @@ + 20], + ]; + } + + public function attributeLabels(): array + { + return [ + 'scribble_pack_id' => Yii::t('app', 'SCRIBBLE_PACK_ID'), + 'class' => Yii::t('app', 'SCRIBBLE_PACK_CLASS'), + ]; + } + + /** + * Gets query for [[Characters]]. + * + * @return ActiveQuery + */ + public function getCharacters(): ActiveQuery + { + return $this->hasMany(Character::class, ['scribble_pack_id' => 'scribble_pack_id']); + } + + /** + * Gets query for [[Groups]]. + * + * @return ActiveQuery + */ + public function getGroups(): ActiveQuery + { + return $this->hasMany(Group::class, ['scribble_pack_id' => 'scribble_pack_id']); + } + + /** + * Gets query for [[Scribbles]]. + * + * @return ActiveQuery|ScribbleQuery + */ + public function getScribbles() + { + return $this->hasMany(Scribble::class, ['scribble_pack_id' => 'scribble_pack_id']); + } + + public static function create(string $class): ScribblePack + { + $pack = new ScribblePack(['class' => $class]); + + $pack->save(); + $pack->refresh(); + + return $pack; + } + + public static function find(): ScribblePackQuery + { + return new ScribblePackQuery(get_called_class()); + } + + public function canUserReadYou(): bool + { + $className = 'common\models\\' . $this->class; + /** @var HasEpicControl $object */ + $object = ($className)::findOne(['scribble_pack_id' => $this->scribble_pack_id]); + return $object->canUserViewYou(); + } + + public function canUserControlYou(): bool + { + $className = 'common\models\\' . $this->class; + /** @var HasEpicControl $object */ + $object = ($className)::findOne(['scribble_pack_id' => $this->scribble_pack_id]); + return $object->canUserControlYou(); + } + + public function getScribbleByUserId(int $userId): Scribble + { + $scribble = Scribble::findOne([ + 'scribble_pack_id' => $this->scribble_pack_id, + 'user_id' => $userId, + ]); + + if (empty($scribble)) { + $scribble = Scribble::createEmptyForPack($userId, $this); + $scribble->save(); + $scribble->refresh(); + } + + return $scribble; + } +} diff --git a/common/models/ScribblePackQuery.php b/common/models/ScribblePackQuery.php new file mode 100644 index 00000000..7560d8e7 --- /dev/null +++ b/common/models/ScribblePackQuery.php @@ -0,0 +1,35 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * @return ScribblePack[]|array + */ + public function all($db = null): array + { + return parent::all($db); + } + + /** + * @param null $db + * @return array|ActiveRecord|null + */ + public function one($db = null): array|ActiveRecord|null + { + return parent::one($db); + } +} diff --git a/common/models/ScribbleQuery.php b/common/models/ScribbleQuery.php new file mode 100644 index 00000000..66c61960 --- /dev/null +++ b/common/models/ScribbleQuery.php @@ -0,0 +1,32 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * @return Scribble[]|array + */ + public function all($db = null): array + { + return parent::all($db); + } + + /** + * @return Scribble|array|null + */ + public function one($db = null): Scribble|array|null + { + return parent::one($db); + } +} diff --git a/common/models/core/HasEpic.php b/common/models/core/HasEpic.php new file mode 100644 index 00000000..b9f8e1be --- /dev/null +++ b/common/models/core/HasEpic.php @@ -0,0 +1,10 @@ +participants; + $objectsOrdered = []; + + foreach ($objectsRaw as $object) { + $objectsOrdered[$object->user_id] = $object; + } + + $result = true; + + foreach ($users as $user) { + if (!isset($objectsOrdered[$user->user_id])) { + $scribbleObject = $pack->createEmptyContent($user->user_id); + $saveResult = $scribbleObject->save(); + $result = $result && $saveResult; + } + } + return $result; + } +} diff --git a/console/migrations/m230910_123838_v1_1_0.php b/console/migrations/m230910_123838_v1_1_0.php new file mode 100644 index 00000000..4f7fc1e1 --- /dev/null +++ b/console/migrations/m230910_123838_v1_1_0.php @@ -0,0 +1,117 @@ +addColumn( + '{{%group}}', + 'updated_at', + $this->integer()->notNull()->after('importance_category') + ); + $this->update( + '{{%group}}', + ['updated_at' => time()] + ); + + /* Session location added */ + $this->addColumn('{{%game}}', 'planned_location', $this->string(80)->after('planned_date')); + + /* Scribbles created */ + $this->createTable( + '{{%scribble_pack}}', + [ + 'scribble_pack_id' => $this->primaryKey()->unsigned(), + 'class' => $this->string(20)->notNull()->comment("Name of class this pack belongs to; necessary for proper type assignment"), + ], + $tableOptions + ); + + $this->createTable( + '{{%scribble}}', + [ + 'scribble_id' => $this->primaryKey()->unsigned(), + 'scribble_pack_id' => $this->integer(11)->unsigned(), + 'user_id' => $this->integer(10)->unsigned(), + 'favorite' => $this->boolean(), + ], + $tableOptions + ); + + $this->addForeignKey( + 'scribble_pack', + '{{%scribble}}', + 'scribble_pack_id', + '{{%scribble_pack}}', + 'scribble_pack_id', + 'RESTRICT', + 'CASCADE' + ); + $this->addForeignKey( + 'scribble_user', + '{{%scribble}}', + 'user_id', + '{{%user}}', + 'id', + 'RESTRICT', + 'CASCADE' + ); + + $this->addColumn( + '{{%character}}', + 'scribble_pack_id', + $this->integer(11)->unsigned()->after('importance_pack_id') + ); + $this->addColumn( + '{{%group}}', + 'scribble_pack_id', + $this->integer(11)->unsigned()->after('importance_pack_id') + ); + + $this->addForeignKey( + 'character_scribble_pack', + '{{%character}}', + 'scribble_pack_id', + '{{%scribble_pack}}', + 'scribble_pack_id', + 'RESTRICT', + 'CASCADE' + ); + $this->addForeignKey( + 'group_scribble_pack', + '{{%group}}', + 'scribble_pack_id', + '{{%scribble_pack}}', + 'scribble_pack_id', + 'RESTRICT', + 'CASCADE' + ); + } + + public function safeDown() + { + /* Scribbles removed */ + $this->dropForeignKey('group_scribble_pack', '{{%group}}'); + $this->dropForeignKey('character_scribble_pack', '{{%character}}'); + + $this->dropColumn('{{%group}}', 'scribble_pack_id'); + $this->dropColumn('{{%character}}', 'scribble_pack_id'); + + $this->dropTable('{{%scribble}}'); + $this->dropTable('{{%scribble_pack}}'); + + /* Group updated_at removed */ + $this->dropColumn('{{%group}}', 'updated_at'); + + /* Game location removed */ + $this->dropColumn('{{%game}}', 'planned_location'); + } +} diff --git a/frontend/controllers/CharacterController.php b/frontend/controllers/CharacterController.php index 64aa95b3..eb03797e 100644 --- a/frontend/controllers/CharacterController.php +++ b/frontend/controllers/CharacterController.php @@ -33,7 +33,13 @@ public function behaviors() 'class' => AccessControl::class, 'rules' => [ [ - 'actions' => ['index', 'view', 'external-reputation', 'external-reputation-event'], + 'actions' => [ + 'index', + 'view', + 'external-reputation', + 'external-reputation-event', + 'open-scribble-modal' + ], 'allow' => true, 'roles' => ['@'], ], @@ -190,6 +196,23 @@ public function actionExternalReputationEvent(string $key): string throw new HttpException(204, Yii::t('external', 'NO_DATA')); } + public function actionOpenScribbleModal(string $key): string + { + $model = $this->findModelByKey($key); + + if (!$model->canUserViewYou()) { + Character::throwExceptionAboutView(); + } + + $scribbleModel = $model->scribblePack->getScribbleByUserId(Yii::$app->user->getId()); + + if (Yii::$app->request->isAjax) { + return $this->renderAjax('../scribble/_modal_box', ['model' => $scribbleModel]); + } else { + return $this->render('../scribble/_modal_box', ['model' => $scribbleModel]); + } + } + /** * Finds the Character model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. diff --git a/frontend/controllers/ScribbleController.php b/frontend/controllers/ScribbleController.php new file mode 100644 index 00000000..a36cf22d --- /dev/null +++ b/frontend/controllers/ScribbleController.php @@ -0,0 +1,114 @@ + [ + 'class' => AccessControl::class, + 'rules' => [ + [ + 'actions' => [ + 'reverse-favorite' + ], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::class, + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ] + ); + } + + public function actionReverseFavorite(int $id) + { + $scribble = $this->getModelWithValidation($id); + $scribble->favorite = !$scribble->favorite; + if (!$scribble->save()) { + throw new ServerErrorHttpException(); + } + } + + public function actionSetAsFavorite(int $id) + { + $scribble = $this->getModelWithValidation($id); + $scribble->favorite = true; + if (!$scribble->save()) { + throw new ServerErrorHttpException(); + } + } + + public function actionUnsetAsFavorite(int $id) + { + $scribble = $this->getModelWithValidation($id); + $scribble->favorite = false; + if (!$scribble->save()) { + throw new ServerErrorHttpException(); + } + } + + /** + * @param int $scribbleId + * + * @return Scribble + * + * @throws ForbiddenHttpException + * @throws MethodNotAllowedHttpException + * @throws NotFoundHttpException + */ + private function getModelWithValidation(int $scribbleId): Scribble + { + if (!Yii::$app->request->isAjax) { + throw new MethodNotAllowedHttpException(Yii::t('app', 'ERROR_AJAX_REQUESTS_ONLY')); + } + + $scribble = $this->findModel($scribbleId); + + if (!$scribble->scribblePack->canUserReadYou()) { + throw new ForbiddenHttpException(Yii::t('app', 'SCRIBBLE_DENIED_ACCESS')); + } + + return $scribble; + } + + /** + * Finds the Scribble model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param int $scribble_id Scribble ID + * @return Scribble the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($scribble_id) + { + if (($model = Scribble::findOne(['scribble_id' => $scribble_id])) !== null) { + return $model; + } + + throw new NotFoundHttpException('The requested page does not exist.'); + } +} diff --git a/frontend/views/character/_index_box.php b/frontend/views/character/_index_box.php index 3637a813..2c427f1f 100644 --- a/frontend/views/character/_index_box.php +++ b/frontend/views/character/_index_box.php @@ -29,20 +29,29 @@ break; } +$favorite = false; // to be replaced by an actual value based on the database record + $classesForBox = 'index-box' . ($additionalBoxClasses ? ' ' . $additionalBoxClasses : ''); +$favoriteClass = $favorite ? 'glyphicon-tags' : 'glyphicon-tag'; +$favoriteTitle = $favorite ? Yii::t('app', 'SCRIBBLES_TITLE_YES') : Yii::t('app', 'SCRIBBLES_TITLE_NO'); $titleText = $model->tagline . ($additionalTitleText ? ' ' . $additionalTitleText : ''); ?>
-

+

name, 16, ' (...)', false)), ['view', 'key' => $model->key] ); ?>

+ +

tagline, 16, ' (...)', false) ?>

diff --git a/frontend/views/character/index.php b/frontend/views/character/index.php index a543c272..6023768e 100644 --- a/frontend/views/character/index.php +++ b/frontend/views/character/index.php @@ -1,6 +1,7 @@
+ + 'scribble-modal', + 'header' => '', + 'clientOptions' => ['backdrop' => 'static'], + 'size' => Modal::SIZE_LARGE, +]); ?> + diff --git a/frontend/views/scribble/_modal_box.php b/frontend/views/scribble/_modal_box.php new file mode 100644 index 00000000..d7a0ecde --- /dev/null +++ b/frontend/views/scribble/_modal_box.php @@ -0,0 +1,53 @@ + Yii::t('app', 'SCRIBBLES_BUTTON_NO'), + true => Yii::t('app', 'SCRIBBLES_BUTTON_YES'), +]; + +?> +
+

+ favorite], [ + 'id' => 'favorite-button', + 'class' => 'btn btn-primary btn-block', + 'data-scribble-id' => $model->scribble_id, + ]) ?> +
+ + diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css index 215209b0..711cb063 100644 --- a/frontend/web/css/site.css +++ b/frontend/web/css/site.css @@ -346,6 +346,20 @@ a:focus { border: 3px solid yellowgreen; } +.index-box-header-icon { + color: green; + cursor: pointer; + font-size: 2.5rem; + padding: 5px; + position: absolute; + right: 2%; + top: 2%; +} + +.index-box-header-narrow { + width: 88%; +} + .tab-pane { margin-top: 20px; } diff --git a/frontend/web/js/character.js b/frontend/web/js/character.js index c239423a..c65d2359 100644 --- a/frontend/web/js/character.js +++ b/frontend/web/js/character.js @@ -22,3 +22,14 @@ $.get( } } ); + +$(".scribble-button").on('click', function () { + $.get( + '../character/open-scribble-modal', + {key: $(this).data('character-key')}, + function (data) { + $('.modal-body').html(data); + $('#scribble-modal').modal(); + } + ); +});