-
Notifications
You must be signed in to change notification settings - Fork 3
Create schemas and models for implementing generic policies #1177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b5f898d
695a643
3c34946
3077811
68e1d54
19db0ce
f57f71c
4f36d78
b96b645
3695142
1a90b45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| <?php | ||
|
|
||
| namespace App; | ||
|
|
||
| use Carbon\CarbonImmutable; | ||
| use Eloquent; | ||
| use Illuminate\Database\Eloquent\Builder; | ||
| use Illuminate\Database\Eloquent\Model; | ||
|
|
||
| /** | ||
| * @property int $id | ||
| * @property string $policy_type | ||
| * @property CarbonImmutable|null $active_from | ||
| * @property string $content_vue_file | ||
| * @property CarbonImmutable|null $created_at | ||
| * @property CarbonImmutable|null $updated_at | ||
| * | ||
| * @method static Builder<static>|Policy newModelQuery() | ||
| * @method static Builder<static>|Policy newQuery() | ||
| * @method static Builder<static>|Policy query() | ||
| * @method static Builder<static>|Policy whereActiveFrom($value) | ||
| * @method static Builder<static>|Policy whereContentVueFile($value) | ||
| * @method static Builder<static>|Policy whereCreatedAt($value) | ||
| * @method static Builder<static>|Policy whereId($value) | ||
| * @method static Builder<static>|Policy wherePolicyType($value) | ||
| * @method static Builder<static>|Policy whereUpdatedAt($value) | ||
| * | ||
| * @mixin Eloquent | ||
| */ | ||
| class Policy extends Model { | ||
| // define which attributes are mass assignable | ||
| protected $fillable = [ | ||
| 'policy_type', | ||
| 'active_from', | ||
| 'content_vue_file', | ||
| ]; | ||
|
|
||
| // define the default value of model attributes when a new instance is created | ||
| protected $attributes = [ | ||
| 'active_from' => null, | ||
| ]; | ||
|
|
||
| protected function casts(): array { | ||
| return [ | ||
| // cast `active_from` to a CarbonImmutable instance rather than a string | ||
| 'active_from' => 'immutable_date', | ||
|
|
||
| 'created_at' => 'immutable_datetime', | ||
| 'updated_at' => 'immutable_datetime', | ||
| ]; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| <?php | ||
|
|
||
| namespace App; | ||
|
|
||
| use Carbon\CarbonImmutable; | ||
| use Eloquent; | ||
| use Illuminate\Database\Eloquent\Builder; | ||
| use Illuminate\Database\Eloquent\Model; | ||
|
|
||
| /** | ||
| * This model uses a separate `accepted_at` property rather than renaming the default `created_at` property because: | ||
| * - it remains consistent with other models that use the default timestamps | ||
| * - `accepted_at` will be before `created_at` when backfilling the terms-of-use acceptances | ||
| * | ||
| * @property int $id | ||
| * @property int $user_id | ||
| * @property int $policy_id | ||
| * @property CarbonImmutable|null $created_at | ||
| * @property CarbonImmutable|null $updated_at | ||
| * @property CarbonImmutable $accepted_at | ||
| * | ||
| * @method static Builder<static>|PolicyAcceptance newModelQuery() | ||
| * @method static Builder<static>|PolicyAcceptance newQuery() | ||
| * @method static Builder<static>|PolicyAcceptance query() | ||
| * @method static Builder<static>|PolicyAcceptance whereAcceptedAt($value) | ||
| * @method static Builder<static>|PolicyAcceptance whereCreatedAt($value) | ||
| * @method static Builder<static>|PolicyAcceptance whereId($value) | ||
| * @method static Builder<static>|PolicyAcceptance wherePolicyId($value) | ||
| * @method static Builder<static>|PolicyAcceptance whereUpdatedAt($value) | ||
| * @method static Builder<static>|PolicyAcceptance whereUserId($value) | ||
| * | ||
| * @mixin Eloquent | ||
| */ | ||
| class PolicyAcceptance extends Model { | ||
| protected $fillable = [ | ||
| 'user_id', | ||
| 'policy_id', | ||
| ]; | ||
|
|
||
| protected $guarded = [ | ||
|
tarrow marked this conversation as resolved.
|
||
| // Don't allow `accepted_at` to be mass assigned. | ||
| // Most of the time this will be set to the current timestamp by the database. | ||
| 'accepted_at', | ||
| ]; | ||
|
|
||
| protected function casts(): array { | ||
| return [ | ||
| // cast `accepted_at` to a `CarbonImmutable` instance rather than a string | ||
| 'accepted_at' => 'immutable_datetime', | ||
|
|
||
| 'created_at' => 'immutable_datetime', | ||
| 'updated_at' => 'immutable_datetime', | ||
| ]; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php | ||
|
|
||
| use Illuminate\Database\Migrations\Migration; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
|
|
||
| return new class() extends Migration { | ||
| /** | ||
| * Run the migrations. | ||
| */ | ||
| public function up(): void { | ||
| Schema::create('policies', function (Blueprint $table) { | ||
| $table->id(); | ||
| $table->enum('policy_type', ['terms-of-use', 'hosting-policy']); | ||
| $table->date('active_from')->nullable()->default(null); | ||
| $table->string('content_vue_file', 255); | ||
|
|
||
| // Use Eloquent built in to create nullable `created_at` and `updated_at` | ||
| // timestamp fields | ||
| $table->timestamps(); | ||
|
|
||
| // This prevents two upcoming policies of the same type with `active_from` set to `null`, | ||
| $table->unique(['policy_type', 'active_from']); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Reverse the migrations. | ||
| */ | ||
| public function down(): void { | ||
| Schema::dropIfExists('policies'); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <?php | ||
|
|
||
| use Illuminate\Database\Migrations\Migration; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
|
|
||
| return new class() extends Migration { | ||
| /** | ||
| * Run the migrations. | ||
| */ | ||
| public function up(): void { | ||
| Schema::create('policy_acceptances', function (Blueprint $table) { | ||
| $table->id(); | ||
|
|
||
| // Can't use the `foreignId()` method because the `users.id` column isn't an unsigned big integer | ||
| $table->unsignedInteger('user_id'); | ||
| $table->foreign('user_id')->references('id')->on('users')->restrictOnUpdate()->restrictOnDelete(); | ||
|
|
||
| $table->foreignId('policy_id')->constrained()->restrictOnUpdate()->restrictOnDelete(); | ||
|
|
||
| // Use Eloquent built in to create nullable `created_at` and `updated_at` | ||
| // timestamp fields | ||
| $table->timestamps(); | ||
|
|
||
| // Using a separate `accepted_at` column rather than renaming the default `created_at` column because: | ||
| // * it reduces confusion by remaining consistent with other tables that use the default columns | ||
| // * `accepted_at` will be before `created_at` when backfilling the terms-of-use acceptances | ||
| $table->timestamp('accepted_at')->useCurrent(); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Reverse the migrations. | ||
| */ | ||
| public function down(): void { | ||
| Schema::dropIfExists('policy_acceptances'); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| <?php | ||
|
|
||
| namespace Tests; | ||
|
|
||
| use App\Policy; | ||
| use App\PolicyAcceptance; | ||
| use App\User; | ||
| use Carbon\CarbonImmutable; | ||
| use Illuminate\Foundation\Testing\RefreshDatabase; | ||
|
|
||
| class PolicyAcceptanceTest extends TestCase { | ||
| use RefreshDatabase; | ||
|
|
||
| protected int $userId; | ||
|
|
||
| protected int $policyId; | ||
|
|
||
| protected function setUp(): void { | ||
| parent::setUp(); | ||
| $user = User::factory()->create(); | ||
| $this->userId = $user->id; | ||
| $policy = Policy::create( | ||
| [ | ||
| 'policy_type' => 'terms-of-use', | ||
| 'active_from' => CarbonImmutable::yesterday(), | ||
| 'content_vue_file' => 'terms-of-use/example.vue', | ||
| ]); | ||
| $this->policyId = $policy->id; | ||
| } | ||
|
|
||
| public function testCreatesAndSavesSuccessfully(): void { | ||
| $policyAcceptance = new PolicyAcceptance( | ||
| [ | ||
| 'user_id' => $this->userId, | ||
| 'policy_id' => $this->policyId, | ||
| ] | ||
| ); | ||
| $policyAcceptance->save(); | ||
| $policyAcceptance->refresh(); | ||
|
|
||
| $this->assertDatabaseHas('policy_acceptances', [ | ||
| 'user_id' => $this->userId, | ||
| 'policy_id' => $this->policyId, | ||
| ]); | ||
|
|
||
| $this->assertNotEmpty($policyAcceptance->accepted_at); | ||
|
tarrow marked this conversation as resolved.
|
||
| $this->assertInstanceOf(CarbonImmutable::class, $policyAcceptance->accepted_at); | ||
| } | ||
|
|
||
| public function testAcceptedAtIgnoresMassAssignment(): void { | ||
| $policyAcceptance = PolicyAcceptance::create( | ||
| [ | ||
| 'user_id' => $this->userId, | ||
| 'policy_id' => $this->policyId, | ||
| 'accepted_at' => CarbonImmutable::createFromDate(2026, 1, 1), | ||
| ] | ||
| ); | ||
| $this->assertNull($policyAcceptance->accepted_at); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. put the instance assertion here too is not a bad idea
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's not possible: |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <?php | ||
|
|
||
| namespace Tests; | ||
|
|
||
| use App\Policy; | ||
| use Carbon\Carbon; | ||
| use Illuminate\Foundation\Testing\RefreshDatabase; | ||
|
|
||
| class PolicyTest extends TestCase { | ||
| use RefreshDatabase; | ||
|
|
||
| public function testCreatesSuccessfully(): void { | ||
| Policy::create( | ||
| [ | ||
| 'policy_type' => 'terms-of-use', | ||
| 'active_from' => Carbon::createFromDate(2025, 4, 1), | ||
| 'content_vue_file' => 'terms-of-use/example.vue', | ||
| ] | ||
| ); | ||
|
|
||
| $this->assertDatabaseHas('policies', [ | ||
| 'policy_type' => 'terms-of-use', | ||
| 'active_from' => '2025-04-01', | ||
| 'content_vue_file' => 'terms-of-use/example.vue', | ||
| ]); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.