Skip to content

Commit fb17a47

Browse files
authored
Create Model for Term Of Use acceptance (#977)
* Create Model for Term Of Use acceptance * minor fix * Add missing Migration for tou_acceptances table * add lastest() method to get lastest version of ToU * Add test case * Add test for ToU acceptance when user was created * Fix test * fix linting errors * fix linting errors * fix linting errors * Fix typo in classes' names * Add Job to generate 'tou_version' and 'tou_accepted_at' for existing users * fix linting errors * Change UserTermsOfUseAcceptances' relation with 'user_id' * Minor fix * fix UserTouAcceptanceJobTest * fix linting error * Refactor: Change TermsOfUseVersion from enum to Model * Fix: Remove hard dependency on pre-seeded ToU version * fix linting errors * fix linting error * minor changes * Fix: removed unnecessary fields * fix testing wrong version * fix linting error * rename the method that retrieve latest active version of ToU
1 parent 6638305 commit fb17a47

11 files changed

+299
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\TermsOfUseVersion;
6+
use Illuminate\Bus\Batchable;
7+
use Illuminate\Foundation\Bus\Dispatchable;
8+
use Illuminate\Support\Facades\Log;
9+
use Throwable;
10+
11+
class CreateFirstTermsOfUseVersionJob extends Job {
12+
use Batchable;
13+
use Dispatchable;
14+
15+
public function handle(): void {
16+
try {
17+
TermsOfUseVersion::create([
18+
'version' => '2022-01-01',
19+
'active' => true,
20+
]);
21+
} catch (Throwable $exception) {
22+
Log::error("Failure creating initial Terms of Use version: {$exception->getMessage()}");
23+
$this->fail($exception);
24+
}
25+
}
26+
}

app/Jobs/UserCreateJob.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace App\Jobs;
44

5+
use App\TermsOfUseVersion;
56
use App\User;
7+
use App\UserTermsOfUseAcceptance;
68
use Illuminate\Support\Facades\Hash;
9+
use Illuminate\Support\Facades\Log;
710

811
class UserCreateJob extends Job {
912
private $email;
@@ -30,6 +33,17 @@ public function handle() {
3033
'verified' => $this->verified,
3134
]);
3235

36+
$latest = TermsOfUseVersion::latestActiveVersion();
37+
if ($latest) {
38+
UserTermsOfUseAcceptance::create([
39+
'user_id' => $user->id,
40+
'tou_version' => $latest->version,
41+
'tou_accepted_at' => now(),
42+
]);
43+
} else {
44+
Log::warning("No active Terms of Use version found when creating user {$user->email} (ID {$user->id}).");
45+
}
46+
3347
return $user;
3448
}
3549
}

app/Jobs/UserTouAcceptanceJob.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\TermsOfUseVersion;
6+
use App\User;
7+
use App\UserTermsOfUseAcceptance;
8+
use Illuminate\Bus\Batchable;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Support\Facades\Log;
11+
use Throwable;
12+
13+
/**
14+
* Bug: T401165 https://phabricator.wikimedia.org/T401165
15+
* Job to record Terms of Use acceptance for all preexisting users.
16+
* This job should only be run ONCE to seed the data for terms of use users have agreed to before we started tracking it explicitly.
17+
* This job iterates through all users and creates a UserTermsOfUseAcceptance record
18+
* for each, using the latest (only) Terms of Use version and the user's creation date as ToU acceptance date.
19+
* Errors during processing are logged and the job is marked as failed if accepting the terms of use for any user fails.
20+
*/
21+
class UserTouAcceptanceJob extends Job {
22+
use Batchable;
23+
use Dispatchable;
24+
25+
public function handle(): void {
26+
$users = User::all();
27+
foreach ($users as $user) {
28+
try {
29+
UserTermsOfUseAcceptance::create([
30+
'user_id' => $user->id,
31+
'tou_version' => TermsOfUseVersion::latestActiveVersion()->version,
32+
'tou_accepted_at' => $user->created_at,
33+
]);
34+
} catch (Throwable $exception) {
35+
Log::error("Failure processing user {$user->email} for UserTouAcceptanceJob: {$exception->getMessage()}");
36+
$this->fail($exception);
37+
}
38+
}
39+
}
40+
}

app/TermsOfUseVersion.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
/**
9+
* Bug: T401165 https://phabricator.wikimedia.org/T401165
10+
* Be mindful that multiple ToU versions may exist over time,
11+
* but only one should be active at a time.
12+
*/
13+
class TermsOfUseVersion extends Model {
14+
use HasFactory;
15+
16+
protected $table = 'tou_versions';
17+
18+
const FIELDS = [
19+
'version',
20+
'active',
21+
];
22+
23+
protected $fillable = self::FIELDS;
24+
25+
protected $visible = self::FIELDS;
26+
27+
protected $casts = [
28+
'version' => 'string',
29+
'active' => 'boolean',
30+
];
31+
32+
public static function latestActiveVersion(): ?self {
33+
return self::query()->where('active', true)->latest()->first();
34+
}
35+
}

app/User.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ public function managesWikis(): \Illuminate\Database\Eloquent\Relations\BelongsT
9898
return $this->belongsToMany(Wiki::class, 'wiki_managers');
9999
}
100100

101+
public function touAcceptances(): \Illuminate\Database\Eloquent\Relations\HasMany {
102+
return $this->hasMany(UserTermsOfUseAcceptance::class, 'user_id');
103+
}
104+
101105
public function hasVerifiedEmail() {
102106
return (bool) $this->verified;
103107
}

app/UserTermsOfUseAcceptance.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace App;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
9+
class UserTermsOfUseAcceptance extends Model {
10+
use HasFactory;
11+
12+
public const FIELDS = [
13+
'user_id',
14+
'tou_version',
15+
'tou_accepted_at',
16+
];
17+
18+
protected $fillable = self::FIELDS;
19+
20+
protected $visible = self::FIELDS;
21+
22+
protected $casts = [
23+
'tou_version' => 'string',
24+
'tou_accepted_at' => 'datetime',
25+
];
26+
27+
protected $table = 'tou_acceptances';
28+
29+
public function user(): BelongsTo {
30+
return $this->belongsTo(User::class);
31+
}
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
/**
9+
* Run the migrations.
10+
*/
11+
public function up(): void {
12+
Schema::create('tou_acceptances', function (Blueprint $table) {
13+
$table->id();
14+
$table->unsignedInteger('user_id');
15+
$table->string('tou_version', 10);
16+
$table->timestamp('tou_accepted_at');
17+
$table->timestamps();
18+
$table->unique(['user_id', 'tou_version']);
19+
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
20+
});
21+
}
22+
23+
/**
24+
* Reverse the migrations.
25+
*/
26+
public function down(): void {
27+
Schema::dropIfExists('tou_acceptances');
28+
}
29+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
/**
9+
* Run the migrations.
10+
*/
11+
public function up(): void {
12+
Schema::create('tou_versions', function (Blueprint $table) {
13+
$table->id();
14+
$table->string('version')->unique();
15+
$table->boolean('active')->default(false);
16+
$table->timestamps();
17+
});
18+
}
19+
20+
/**
21+
* Reverse the migrations.
22+
*/
23+
public function down(): void {
24+
Schema::dropIfExists('tou_versions');
25+
}
26+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Tests\Jobs;
4+
5+
use App\Jobs\CreateFirstTermsOfUseVersionJob;
6+
use Illuminate\Foundation\Testing\RefreshDatabase;
7+
use Tests\TestCase;
8+
9+
class CreateFirstTermsOfUseVersionJobTest extends TestCase {
10+
use RefreshDatabase;
11+
12+
public function testCreateFirstTermsOfUseVersionJob(): void {
13+
$this->assertDatabaseCount('tou_versions', 0);
14+
15+
(new CreateFirstTermsOfUseVersionJob)->handle();
16+
17+
$this->assertDatabaseHas('tou_versions', [
18+
'version' => '2022-01-01',
19+
'active' => true,
20+
]);
21+
}
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Tests\Jobs;
4+
5+
use App\Jobs\CreateFirstTermsOfUseVersionJob;
6+
use App\Jobs\UserCreateJob;
7+
use App\TermsOfUseVersion;
8+
use App\UserTermsOfUseAcceptance;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use Tests\TestCase;
11+
12+
class UserTermsOfUseAcceptanceTest extends TestCase {
13+
use RefreshDatabase;
14+
15+
public function testUserCreationCreatesTouAcceptance(): void {
16+
(new CreateFirstTermsOfUseVersionJob)->handle();
17+
$email = 'test+' . uniqid('', true) . '@example.com';
18+
$user = (new UserCreateJob($email, 'thisisapassword123', true))->handle();
19+
20+
$this->assertDatabaseHas('tou_acceptances', [
21+
'user_id' => $user->id,
22+
'tou_version' => TermsOfUseVersion::latestActiveVersion()->version,
23+
]);
24+
25+
$rows = UserTermsOfUseAcceptance::where('user_id', $user->id)->get();
26+
$this->assertCount(1, $rows);
27+
$acceptance = $rows->first();
28+
29+
$this->assertSame(TermsOfUseVersion::latestActiveVersion()->version, $acceptance->tou_version);
30+
$this->assertNotNull($acceptance->tou_accepted_at);
31+
}
32+
}

0 commit comments

Comments
 (0)