From 31bf8e0959b1e56c2934a4e9e385ccd52ef4879e Mon Sep 17 00:00:00 2001 From: Chris Mellor Date: Mon, 21 Aug 2023 22:34:26 +0100 Subject: [PATCH 1/4] Add feature to freeze a streak --- config/level-up.php | 10 ++++ ..._feature_columns_to_streaks_table.php.stub | 23 ++++++++ src/Concerns/HasStreaks.php | 31 ++++++++--- src/LevelUpServiceProvider.php | 1 + src/Models/Streak.php | 1 + tests/Concerns/HasStreaksTest.php | 52 +++++++++++++++++++ tests/TestCase.php | 40 ++++++-------- 7 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub diff --git a/config/level-up.php b/config/level-up.php index c87d296..fb251cd 100644 --- a/config/level-up.php +++ b/config/level-up.php @@ -86,4 +86,14 @@ 'archive_streak_history' => [ 'enabled' => env(key: 'ARCHIVE_STREAK_HISTORY_ENABLED', default: true), ], + + /* + | ------------------------------------------------------------------------- + | Default Streak Freeze Time + | ------------------------------------------------------------------------- + | + | Set the default time in days that a streak will be frozen for. + | + */ + 'freeze_duration' => env(key: 'STREAK_FREEZE_DURATION', default: 1), ]; diff --git a/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub b/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub new file mode 100644 index 0000000..d4aea0d --- /dev/null +++ b/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub @@ -0,0 +1,23 @@ +after('activity_at', function (Blueprint $table) { + $table->timestamp('frozen_until')->nullable(); + }); + }); + } + + public function down(): void + { + Schema::table('streaks', function (Blueprint $table) { + $table->dropColumn('frozen_until'); + }); + } +}; diff --git a/src/Concerns/HasStreaks.php b/src/Concerns/HasStreaks.php index 721b63a..dd60e68 100644 --- a/src/Concerns/HasStreaks.php +++ b/src/Concerns/HasStreaks.php @@ -18,6 +18,8 @@ public function recordStreak(Activity $activity): void // If the user doesn't have a streak for this activity, start a new one if (! $this->hasStreakForActivity(activity: $activity)) { $this->startNewStreak($activity); + + return; } $diffInDays = $this->getStreakLastActivity($activity) @@ -25,6 +27,11 @@ public function recordStreak(Activity $activity): void ->startOfDay() ->diffInDays(now()->startOfDay()); + // Checking to see if the streak is frozen + if ($this->getStreakLastActivity($activity)->frozen_until && now()->lessThan($this->getStreakLastActivity($activity)->frozen_until)) { + return; + } + if ($diffInDays === 0) { return; } @@ -125,10 +132,22 @@ public function hasStreakToday(Activity $activity): bool ->isToday(); } - // public function getLongestStreak(Activity $activity): int - // { - // return $this->streaks() - // ->whereBelongsTo(related: $activity) - // ->max(column: 'count'); - // } + public function freezeStreak(Activity $activity, int $days = null): bool + { + $days = $days ?? config(key: 'level-up.freeze_duration'); + + return $this->getStreakLastActivity($activity) + ->update(['frozen_until' => now()->addDays(value: $days)->startOfDay()]); + } + + public function unFreezeStreak(Activity $activity): bool + { + return $this->getStreakLastActivity($activity) + ->update(['frozen_until' => null]); + } + + public function isStreakFrozen(Activity $activity): bool + { + return ! is_null($this->getStreakLastActivity($activity)->frozen_until); + } } diff --git a/src/LevelUpServiceProvider.php b/src/LevelUpServiceProvider.php index 54c5561..0daa79d 100644 --- a/src/LevelUpServiceProvider.php +++ b/src/LevelUpServiceProvider.php @@ -27,6 +27,7 @@ public function configurePackage(Package $package): void 'create_streak_activities_table', 'create_streaks_table', 'create_streak_histories_table', + 'add_streak_freeze_feature_columns_to_streaks_table', ]); } diff --git a/src/Models/Streak.php b/src/Models/Streak.php index 1f6bb45..17ce0bc 100644 --- a/src/Models/Streak.php +++ b/src/Models/Streak.php @@ -14,6 +14,7 @@ class Streak extends Model protected $casts = [ 'activity_at' => 'datetime', + 'frozen_until' => 'datetime', ]; public function user(): BelongsTo diff --git a/tests/Concerns/HasStreaksTest.php b/tests/Concerns/HasStreaksTest.php index 429e9a4..89cb098 100644 --- a/tests/Concerns/HasStreaksTest.php +++ b/tests/Concerns/HasStreaksTest.php @@ -176,3 +176,55 @@ 'ended_at' => now()->subDays(value: 2), ]); }); + +test(description: 'a streak can be frozen', closure: function () { + $this->user->recordStreak($this->activity); + + $this->user->freezeStreak($this->activity); + + expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay()); +}); + +test(description: 'a streak can be unfrozen', closure: function () { + $this->user->recordStreak($this->activity); + + $this->user->freezeStreak($this->activity); + + expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay()); + + $this->user->unFreezeStreak($this->activity); + + expect($this->user->isStreakFrozen($this->activity))->toBeFalse(); +}); + +test(description: 'when a streak is frozen, it does not break', closure: function () { + $this->user->recordStreak($this->activity); + + testTime()->addDays(); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 2); + + $this->user->freezeStreak($this->activity); + + testTime()->addDays(); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 3); +}); + +test('when a streak is frozen and freeze duration has passed, streak count will reset', function () { + $this->user->recordStreak($this->activity); + + testTime()->addDays(); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 2); + + $this->user->freezeStreak($this->activity); + + testTime()->addDays(2); + $this->user->recordStreak($this->activity); + + expect($this->user->getCurrentStreakCount($this->activity))->toBe(expected: 1); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index aff49c4..d97e6fa 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -38,31 +38,23 @@ protected function getEnvironmentSetUp($app): void $table->timestamps(); }); - $migration = include __DIR__.'/../database/migrations/create_levels_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_experiences_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/add_level_relationship_to_users_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_experience_audits_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_achievements_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_achievement_user_pivot_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_streak_activities_table.php.stub'; - $migration->up(); + $migrationFiles = [ + 'create_levels_table', + 'create_experiences_table', + 'add_level_relationship_to_users_table', + 'create_experience_audits_table', + 'create_achievements_table', + 'create_achievement_user_pivot_table', + 'create_streak_activities_table', + 'create_streaks_table', + 'create_streak_histories_table', + 'add_streak_freeze_feature_columns_to_streaks_table', + ]; - $migration = include __DIR__.'/../database/migrations/create_streaks_table.php.stub'; - $migration->up(); + foreach ($migrationFiles as $migrationFile) { + $migration = include __DIR__."/../database/migrations/$migrationFile.php.stub"; - $migration = include __DIR__.'/../database/migrations/create_streak_histories_table.php.stub'; - $migration->up(); + $migration->up(); + } } } From 746a94ab7afe2a2efc73a0a0e75e5269f8a66830 Mon Sep 17 00:00:00 2001 From: Chris Mellor Date: Mon, 21 Aug 2023 22:34:55 +0100 Subject: [PATCH 2/4] Add linter action --- .github/workflows/run-linter.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/run-linter.yml diff --git a/.github/workflows/run-linter.yml b/.github/workflows/run-linter.yml new file mode 100644 index 0000000..8285dfd --- /dev/null +++ b/.github/workflows/run-linter.yml @@ -0,0 +1,21 @@ +name: "Run Linter" + +on: pull_request + +jobs: + lint: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Lint with Pint + uses: aglipanci/laravel-pint-action@2.3.0 + + - name: Commit linted files + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "fix: Files linted with Pint" + From c937b804cc66d796e1c6c0aa2e5830afd8699545 Mon Sep 17 00:00:00 2001 From: Chris Mellor Date: Mon, 21 Aug 2023 22:50:29 +0100 Subject: [PATCH 3/4] Add events --- src/Concerns/HasStreaks.php | 10 ++++++++++ src/Events/StreakFrozen.php | 14 ++++++++++++++ src/Events/StreakUnfroze.php | 10 ++++++++++ tests/Concerns/HasStreaksTest.php | 14 ++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 src/Events/StreakFrozen.php create mode 100644 src/Events/StreakUnfroze.php diff --git a/src/Concerns/HasStreaks.php b/src/Concerns/HasStreaks.php index dd60e68..62b6ec6 100644 --- a/src/Concerns/HasStreaks.php +++ b/src/Concerns/HasStreaks.php @@ -4,9 +4,12 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Event; use LevelUp\Experience\Events\StreakBroken; +use LevelUp\Experience\Events\StreakFrozen; use LevelUp\Experience\Events\StreakIncreased; use LevelUp\Experience\Events\StreakStarted; +use LevelUp\Experience\Events\StreakUnfroze; use LevelUp\Experience\Models\Activity; use LevelUp\Experience\Models\Streak; use LevelUp\Experience\Models\StreakHistory; @@ -136,12 +139,19 @@ public function freezeStreak(Activity $activity, int $days = null): bool { $days = $days ?? config(key: 'level-up.freeze_duration'); + Event::dispatch(new StreakFrozen( + frozenStreakLength: $days, + frozenUntil: now()->addDays(value: $days)->startOfDay() + )); + return $this->getStreakLastActivity($activity) ->update(['frozen_until' => now()->addDays(value: $days)->startOfDay()]); } public function unFreezeStreak(Activity $activity): bool { + Event::dispatch(new StreakUnfroze()); + return $this->getStreakLastActivity($activity) ->update(['frozen_until' => null]); } diff --git a/src/Events/StreakFrozen.php b/src/Events/StreakFrozen.php new file mode 100644 index 0000000..aa22cc5 --- /dev/null +++ b/src/Events/StreakFrozen.php @@ -0,0 +1,14 @@ +user->recordStreak($this->activity); $this->user->freezeStreak($this->activity); expect($this->user->streaks->first()->frozen_until)->toBeCarbon(now()->addDays()->startOfDay()); + + Event::assertDispatched( + event: StreakFrozen::class, + callback: fn (StreakFrozen $event): bool => $event->frozenStreakLength === config(key: 'level-up.freeze_duration') + && $event->frozenUntil->isTomorrow(), + ); }); test(description: 'a streak can be unfrozen', closure: function () { + Event::fake(); + $this->user->recordStreak($this->activity); $this->user->freezeStreak($this->activity); @@ -195,6 +207,8 @@ $this->user->unFreezeStreak($this->activity); expect($this->user->isStreakFrozen($this->activity))->toBeFalse(); + + Event::assertDispatched(event: StreakUnfroze::class); }); test(description: 'when a streak is frozen, it does not break', closure: function () { From 09ea68057891952f978037cbe3337bef96efbb75 Mon Sep 17 00:00:00 2001 From: Chris Mellor Date: Mon, 21 Aug 2023 23:04:15 +0100 Subject: [PATCH 4/4] Add docs --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index ec0c393..d2a5234 100644 --- a/README.md +++ b/README.md @@ -591,6 +591,59 @@ public Activity $activity, public Streak $streak, ``` +## 🥶 Streak Freezing + +Streaks can be frozen, which means they will not be broken if a day is skipped. This is useful for when you want to allow users to take a break from an activity without losing their streak. + +The freeze duration is a configurable option in the config file. + +```php +'freeze_duration' => env(key: 'STREAK_FREEZE_DURATION', default: 1), +``` + +### Freeze a Streak + +Fetch the activity you want to freeze and pass it to the `freezeStreak` method. A second parameter can be passed to set the duration of the freeze. The default is `1` day (as set in the config) + +A `StreakFrozen` Event is ran when a streak is frozen. + +```php +$user->freezeStreak(activity: $activity); + +$user->freezeStreak(activity: $activity, days: 5); // freeze for 5 days +``` + +### Unfreeze a Streak + +The opposite of freezing a streak is unfreezing it. This will allow the streak to be broken again. + +A `StreakUnfrozen` Event is run when a streak is unfrozen. + +```php +$user->unfreezeStreak($activity); +``` + +### Check if a Streak is Frozen + +```php +$user->isStreakFrozen($activity); +``` + +### Events + +**StreakFrozen** - When a streak is frozen. + +```php +public int $frozenStreakLength, +public Carbon $frozenUntil, +``` + +**StreakUnfrozen** - When a streak is unfrozen. + +``` +No data is sent with this event +``` + # Testing ```