Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d768691
Create Model for Term Of Use acceptance
dati18 Sep 25, 2025
c458ee1
minor fix
dati18 Sep 29, 2025
7aceaa9
Add missing Migration for tou_acceptances table
dati18 Sep 29, 2025
3c8924c
add lastest() method to get lastest version of ToU
dati18 Sep 29, 2025
dad1cd7
Add test case
dati18 Sep 30, 2025
47e7875
Add test for ToU acceptance when user was created
dati18 Sep 30, 2025
456bf9d
Fix test
dati18 Sep 30, 2025
89eeda0
fix linting errors
dati18 Sep 30, 2025
bf56e3e
fix linting errors
dati18 Sep 30, 2025
95a05e0
fix linting errors
dati18 Sep 30, 2025
eb6640e
Fix typo in classes' names
dati18 Oct 8, 2025
33bb2f0
Add Job to generate 'tou_version' and 'tou_accepted_at' for existing …
dati18 Oct 8, 2025
90954d3
fix linting errors
dati18 Oct 8, 2025
39440e3
Change UserTermsOfUseAcceptances' relation with 'user_id'
dati18 Oct 9, 2025
5e21e2f
Minor fix
dati18 Oct 9, 2025
0eb8579
fix UserTouAcceptanceJobTest
dati18 Oct 9, 2025
e816ab6
fix linting error
dati18 Oct 9, 2025
e6e779b
Refactor: Change TermsOfUseVersion from enum to Model
dati18 Oct 14, 2025
734a52a
Fix: Remove hard dependency on pre-seeded ToU version
dati18 Oct 14, 2025
2652871
fix linting errors
dati18 Oct 14, 2025
0b54f8d
fix linting error
dati18 Oct 14, 2025
b5979d7
minor changes
dati18 Oct 15, 2025
7f30a37
Fix: removed unnecessary fields
dati18 Oct 15, 2025
43d6608
fix testing wrong version
dati18 Oct 15, 2025
9ab90cc
fix linting error
dati18 Oct 15, 2025
f095740
rename the method that retrieve latest active version of ToU
dati18 Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/Jobs/CreateFirstTermsOfUseVersionJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Jobs;

use App\TermsOfUseVersion;
use Illuminate\Bus\Batchable;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
use Throwable;

class CreateFirstTermsOfUseVersionJob extends Job {
use Batchable;
use Dispatchable;

public function handle(): void {
try {
TermsOfUseVersion::create([
'version' => '2022-01-01',
'active' => true,
]);
} catch (Throwable $exception) {
Log::error("Failure creating initial Terms of Use version: {$exception->getMessage()}");
$this->fail($exception);
}
}
}
14 changes: 14 additions & 0 deletions app/Jobs/UserCreateJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace App\Jobs;

use App\TermsOfUseVersion;
use App\User;
use App\UserTermsOfUseAcceptance;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;

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

$latest = TermsOfUseVersion::latestActiveVersion();
if ($latest) {
UserTermsOfUseAcceptance::create([
'user_id' => $user->id,
'tou_version' => $latest->version,
'tou_accepted_at' => now(),
]);
} else {
Log::warning("No active Terms of Use version found when creating user {$user->email} (ID {$user->id}).");
}

return $user;
}
}
40 changes: 40 additions & 0 deletions app/Jobs/UserTouAcceptanceJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace App\Jobs;

use App\TermsOfUseVersion;
use App\User;
use App\UserTermsOfUseAcceptance;
use Illuminate\Bus\Batchable;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
* Bug: T401165 https://phabricator.wikimedia.org/T401165
* Job to record Terms of Use acceptance for all preexisting users.
* 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.
* This job iterates through all users and creates a UserTermsOfUseAcceptance record
* for each, using the latest (only) Terms of Use version and the user's creation date as ToU acceptance date.
* Errors during processing are logged and the job is marked as failed if accepting the terms of use for any user fails.
*/
class UserTouAcceptanceJob extends Job {
use Batchable;
use Dispatchable;

public function handle(): void {
$users = User::all();
foreach ($users as $user) {
try {
UserTermsOfUseAcceptance::create([
'user_id' => $user->id,
'tou_version' => TermsOfUseVersion::latestActiveVersion()->version,
'tou_accepted_at' => $user->created_at,
]);
} catch (Throwable $exception) {
Log::error("Failure processing user {$user->email} for UserTouAcceptanceJob: {$exception->getMessage()}");
$this->fail($exception);
}
}
}
}
35 changes: 35 additions & 0 deletions app/TermsOfUseVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
* Bug: T401165 https://phabricator.wikimedia.org/T401165
* Be mindful that multiple ToU versions may exist over time,
* but only one should be active at a time.
*/
class TermsOfUseVersion extends Model {
use HasFactory;

protected $table = 'tou_versions';

const FIELDS = [
'version',
'active',
];

protected $fillable = self::FIELDS;

protected $visible = self::FIELDS;

protected $casts = [
'version' => 'string',
'active' => 'boolean',
];

public static function latestActiveVersion(): ?self {
return self::query()->where('active', true)->latest()->first();
}
}
4 changes: 4 additions & 0 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ public function managesWikis(): \Illuminate\Database\Eloquent\Relations\BelongsT
return $this->belongsToMany(Wiki::class, 'wiki_managers');
}

public function touAcceptances(): \Illuminate\Database\Eloquent\Relations\HasMany {
return $this->hasMany(UserTermsOfUseAcceptance::class, 'user_id');
}

public function hasVerifiedEmail() {
return (bool) $this->verified;
}
Expand Down
32 changes: 32 additions & 0 deletions app/UserTermsOfUseAcceptance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class UserTermsOfUseAcceptance extends Model {
use HasFactory;

public const FIELDS = [
'user_id',
'tou_version',
'tou_accepted_at',
];

protected $fillable = self::FIELDS;

protected $visible = self::FIELDS;

protected $casts = [
'tou_version' => 'string',
'tou_accepted_at' => 'datetime',
];

protected $table = 'tou_acceptances';

public function user(): BelongsTo {
return $this->belongsTo(User::class);
}
}
29 changes: 29 additions & 0 deletions database/migrations/2025_09_29_194758_tou_acceptances.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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('tou_acceptances', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id');
$table->string('tou_version', 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be linked to the tou_versions table.

$table->timestamp('tou_accepted_at');
$table->timestamps();
$table->unique(['user_id', 'tou_version']);
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
});
}

/**
* Reverse the migrations.
*/
public function down(): void {
Schema::dropIfExists('tou_acceptances');
}
};
26 changes: 26 additions & 0 deletions database/migrations/2025_10_14_091126_tou_versions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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('tou_versions', function (Blueprint $table) {
$table->id();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ID ever used? It's not used in the tou_acceptances table as a foreign key. Although, I'm not sure if an ID is needed here. The version is already a unique identifier, and what is used in the tou_acceptances table, why not make that the primary key?

$table->string('version')->unique();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come the tou_acceptances table uses string('version', 10) and this doesn't?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, as the tou_version string will always be 10 chars (yyyy-mm-dd), I wonder if we should be using $table->char('tou_version', 10) which is a fixed width CHAR(10) rather than $table->string('tou_version', 10) which is a variable width VARCHAR(10).

$table->boolean('active')->default(false);
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void {
Schema::dropIfExists('tou_versions');
}
};
22 changes: 22 additions & 0 deletions tests/Jobs/CreateFirstTermsOfUseVersionJobTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Tests\Jobs;

use App\Jobs\CreateFirstTermsOfUseVersionJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CreateFirstTermsOfUseVersionJobTest extends TestCase {
use RefreshDatabase;

public function testCreateFirstTermsOfUseVersionJob(): void {
$this->assertDatabaseCount('tou_versions', 0);

(new CreateFirstTermsOfUseVersionJob)->handle();

$this->assertDatabaseHas('tou_versions', [
'version' => '2022-01-01',
'active' => true,
]);
}
}
32 changes: 32 additions & 0 deletions tests/Jobs/UserTermsOfUseAcceptanceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Tests\Jobs;

use App\Jobs\CreateFirstTermsOfUseVersionJob;
use App\Jobs\UserCreateJob;
use App\TermsOfUseVersion;
use App\UserTermsOfUseAcceptance;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserTermsOfUseAcceptanceTest extends TestCase {
use RefreshDatabase;

public function testUserCreationCreatesTouAcceptance(): void {
(new CreateFirstTermsOfUseVersionJob)->handle();
$email = 'test+' . uniqid('', true) . '@example.com';
$user = (new UserCreateJob($email, 'thisisapassword123', true))->handle();

$this->assertDatabaseHas('tou_acceptances', [
'user_id' => $user->id,
'tou_version' => TermsOfUseVersion::latestActiveVersion()->version,
]);

$rows = UserTermsOfUseAcceptance::where('user_id', $user->id)->get();
$this->assertCount(1, $rows);
$acceptance = $rows->first();

$this->assertSame(TermsOfUseVersion::latestActiveVersion()->version, $acceptance->tou_version);
$this->assertNotNull($acceptance->tou_accepted_at);
}
}
39 changes: 39 additions & 0 deletions tests/Jobs/UserTouAcceptanceJobTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Tests\Jobs;

use App\Jobs\CreateFirstTermsOfUseVersionJob;
use App\Jobs\UserTouAcceptanceJob;
use App\TermsOfUseVersion;
use App\User;
use App\UserTermsOfUseAcceptance;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;

class UserTouAcceptanceJobTest extends TestCase {
use RefreshDatabase;

public function testTouAcceptanceJob(): void {
$t1 = Carbon::parse('2025-01-01 10:00:00');
$t2 = Carbon::parse('2025-01-02 11:00:00');
$t3 = Carbon::parse('2025-01-03 12:00:00');

$u1 = User::factory()->create(['created_at' => $t1]);
$u2 = User::factory()->create(['created_at' => $t2]);
$u3 = User::factory()->create(['created_at' => $t3]);

(new CreateFirstTermsOfUseVersionJob)->handle();
(new UserTouAcceptanceJob)->handle();

$latest = TermsOfUseVersion::latestActiveVersion()->version;

$this->assertDatabaseHas('tou_acceptances', ['user_id' => $u1->id, 'tou_version' => $latest]);
$this->assertDatabaseHas('tou_acceptances', ['user_id' => $u2->id, 'tou_version' => $latest]);
$this->assertDatabaseHas('tou_acceptances', ['user_id' => $u3->id, 'tou_version' => $latest]);

$this->assertTrue($t1->equalTo(UserTermsOfUseAcceptance::where('user_id', $u1->id)->firstOrFail()->tou_accepted_at));
$this->assertTrue($t2->equalTo(UserTermsOfUseAcceptance::where('user_id', $u2->id)->firstOrFail()->tou_accepted_at));
$this->assertTrue($t3->equalTo(UserTermsOfUseAcceptance::where('user_id', $u3->id)->firstOrFail()->tou_accepted_at));
}
}
Loading