git init
This commit is contained in:
46
app/Events/PerceptronTrainingEnded.php
Normal file
46
app/Events/PerceptronTrainingEnded.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PerceptronTrainingEnded implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $reason,
|
||||
public string $sessionId,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new Channel($this->sessionId . '-perceptron-training'),
|
||||
];
|
||||
}
|
||||
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'reason' => $this->reason,
|
||||
];
|
||||
}
|
||||
}
|
||||
52
app/Events/PerceptronTrainingIteration.php
Normal file
52
app/Events/PerceptronTrainingIteration.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PerceptronTrainingIteration implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $iteration,
|
||||
public int $exampleIndex,
|
||||
public float $error,
|
||||
public array $synaptic_weights,
|
||||
public string $sessionId,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
// Log::debug("Broadcasting on channel: " . $this->sessionId . '-perceptron-training');
|
||||
return [
|
||||
new Channel($this->sessionId . '-perceptron-training'),
|
||||
];
|
||||
}
|
||||
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'iteration' => $this->iteration,
|
||||
'exampleIndex' => $this->exampleIndex,
|
||||
'error' => $this->error,
|
||||
'synaptic_weights' => $this->synaptic_weights,
|
||||
];
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
126
app/Http/Controllers/PerceptronController.php
Normal file
126
app/Http/Controllers/PerceptronController.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\SimplePerceptron;
|
||||
use App\Services\DataSetReader;
|
||||
use App\Services\ISynapticWeights;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PerceptronController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perceptronType = $request->query('type', 'simple');
|
||||
$dataSet = $request->input('data_set', 'logic_and');
|
||||
$dataSetReader = $this->getDataSetReader($dataSet);
|
||||
|
||||
return inertia('PerceptronViewer', [
|
||||
'type' => $perceptronType,
|
||||
'sessionId' => session()->getId(),
|
||||
'dataset' => $dataSetReader->lines,
|
||||
'csrf_token' => csrf_token(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getDataSetReader(string $dataSet): DataSetReader
|
||||
{
|
||||
$dataSetFileName = "data_sets/{$dataSet}.csv";
|
||||
return new DataSetReader($dataSetFileName);
|
||||
}
|
||||
|
||||
public function run(Request $request, ISynapticWeights $synapticWeightsProvider)
|
||||
{
|
||||
$perceptronType = $request->query('type', 'simple');
|
||||
$minError = $request->query('min_error', 0.01);
|
||||
$dataSet = $request->input('data_set', 'logic_and');
|
||||
$learningRate = $request->input('learning_rate', 0.1);
|
||||
$sessionId = $request->input('session_id', session()->getId());
|
||||
|
||||
$dataSetReader = $this->getDataSetReader($dataSet);
|
||||
|
||||
$MAX_ITERATIONS = 100;
|
||||
$stopCondition;
|
||||
$trainFunction;
|
||||
$trainFunctionState = [];
|
||||
|
||||
switch ($perceptronType) {
|
||||
case 'simple':
|
||||
$stopCondition = function($iteration, $iterationErrorCounter) use ($sessionId) {
|
||||
$condition = $iterationErrorCounter == 0;
|
||||
if ($condition === true) {
|
||||
Log::info("Perceptron training ended after {$iteration} iterations with no errors.");
|
||||
event(new \App\Events\PerceptronTrainingEnded('Le perceptron ne commet plus d\'erreurs sur aucune des données', $sessionId));
|
||||
}
|
||||
return $iterationErrorCounter == 0;
|
||||
};
|
||||
$iterationFunction = function(&$state) use ($synapticWeightsProvider, $learningRate, $minError) {
|
||||
if (!isset($state['perceptron'])) {
|
||||
$state['perceptron'] = new SimplePerceptron($synapticWeightsProvider->generate(2));
|
||||
}
|
||||
|
||||
$perceptron = $state['perceptron'];
|
||||
$inputs = $state['inputs'];
|
||||
$correctOutput = $state['correctOutput'];
|
||||
$iterationErrorCounter = $state['iterationErrorCounter'] ?? 0;
|
||||
|
||||
$output = $perceptron->test($inputs);
|
||||
|
||||
$error = $correctOutput - $output;
|
||||
if (abs($error) > $minError) {
|
||||
$iterationErrorCounter++;
|
||||
}
|
||||
|
||||
if ($error !== 0) { // Update synaptic weights if needed
|
||||
$synaptic_weights = $perceptron->getSynapticWeights();
|
||||
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
||||
$new_weights = array_map(fn($weight, $input) => $weight + $learningRate * $error * $input, $synaptic_weights, $inputs_with_bias);
|
||||
$perceptron->setSynapticWeights($new_weights);
|
||||
}
|
||||
|
||||
return [$error, $perceptron->getSynapticWeights(), $iterationErrorCounter, $state];
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return response()->json(['error' => 'Invalid perceptron type'], 400);
|
||||
}
|
||||
|
||||
$iteration = 0;
|
||||
$error = 1.0; // Initial error
|
||||
do {
|
||||
$iterationErrorCounter = 0;
|
||||
$iteration++;
|
||||
|
||||
while ($nextRow = $dataSetReader->getRandomLine()) {
|
||||
$inputs = array_slice($nextRow, 0, -1);
|
||||
$correctOutput = end($nextRow);
|
||||
$trainFunctionState['inputs'] = $inputs;
|
||||
$trainFunctionState['correctOutput'] = $correctOutput;
|
||||
$trainFunctionState['iterationErrorCounter'] = $iterationErrorCounter;
|
||||
|
||||
[$error, $synaptic_weights, $iterationErrorCounter, $trainFunctionState] = $iterationFunction($trainFunctionState);
|
||||
|
||||
$error = abs($error); // Use absolute error
|
||||
|
||||
// Broadcast the training iteration event
|
||||
event(new \App\Events\PerceptronTrainingIteration($iteration, $dataSetReader->getLastReadLineIndex(), $error, $synaptic_weights, $sessionId));
|
||||
}
|
||||
$dataSetReader->reset(); // Reset the dataset for the next iteration
|
||||
} while ($iteration < $MAX_ITERATIONS && !$stopCondition($iteration, $iterationErrorCounter));
|
||||
|
||||
if ($iteration >= $MAX_ITERATIONS) {
|
||||
event(new \App\Events\PerceptronTrainingEnded('Le nombre maximal d\'itérations a été atteint', $sessionId));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Training completed',
|
||||
'iterations' => $iteration,
|
||||
'final_error' => $error,
|
||||
'final_synaptic_weights' => isset($trainFunctionState['perceptron']) ? $trainFunctionState['perceptron']->getSynapticWeights() : [0],
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
47
app/Http/Middleware/HandleInertiaRequests.php
Normal file
47
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Models/Perceptron.php
Normal file
38
app/Models/Perceptron.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class Perceptron extends Model
|
||||
{
|
||||
public function __construct(
|
||||
private array $synaptic_weights,
|
||||
) {
|
||||
$this->synaptic_weights = $synaptic_weights; // Add bias weight
|
||||
}
|
||||
|
||||
public function test(array $inputs): int
|
||||
{
|
||||
$inputs = array_merge([1], $inputs); // Add bias input
|
||||
|
||||
if (count($inputs) !== count($this->synaptic_weights)) { // Check
|
||||
throw new \InvalidArgumentException("Number of inputs must match number of synaptic weights.");
|
||||
}
|
||||
|
||||
$weighted_sum = array_sum(array_map(fn($input, $weight) => $input * $weight, $inputs, $this->synaptic_weights));
|
||||
return $this->activationFunction($weighted_sum);
|
||||
}
|
||||
|
||||
abstract protected function activationFunction(float $weighted_sum): int;
|
||||
|
||||
public function getSynapticWeights(): array
|
||||
{
|
||||
return $this->synaptic_weights;
|
||||
}
|
||||
|
||||
public function setSynapticWeights(array $synaptic_weights): void
|
||||
{
|
||||
$this->synaptic_weights = $synaptic_weights;
|
||||
}
|
||||
}
|
||||
18
app/Models/SimplePerceptron.php
Normal file
18
app/Models/SimplePerceptron.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class SimplePerceptron extends Perceptron {
|
||||
|
||||
public function __construct(
|
||||
array $synaptic_weights,
|
||||
) {
|
||||
parent::__construct($synaptic_weights);
|
||||
}
|
||||
|
||||
protected function activationFunction(float $weighted_sum): int
|
||||
{
|
||||
return $weighted_sum >= 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
}
|
||||
40
app/Providers/AppServiceProvider.php
Normal file
40
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure default behaviors for production-ready applications.
|
||||
*/
|
||||
protected function configureDefaults(): void
|
||||
{
|
||||
Date::use(CarbonImmutable::class);
|
||||
|
||||
DB::prohibitDestructiveCommands(
|
||||
app()->isProduction(),
|
||||
);
|
||||
}
|
||||
}
|
||||
91
app/Providers/FortifyServiceProvider.php
Normal file
91
app/Providers/FortifyServiceProvider.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureActions();
|
||||
$this->configureViews();
|
||||
$this->configureRateLimiting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify actions.
|
||||
*/
|
||||
private function configureActions(): void
|
||||
{
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify views.
|
||||
*/
|
||||
private function configureViews(): void
|
||||
{
|
||||
Fortify::loginView(fn (Request $request) => Inertia::render('auth/Login', [
|
||||
'canResetPassword' => Features::enabled(Features::resetPasswords()),
|
||||
'canRegister' => Features::enabled(Features::registration()),
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/ResetPassword', [
|
||||
'email' => $request->email,
|
||||
'token' => $request->route('token'),
|
||||
]));
|
||||
|
||||
Fortify::requestPasswordResetLinkView(fn (Request $request) => Inertia::render('auth/ForgotPassword', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::verifyEmailView(fn (Request $request) => Inertia::render('auth/VerifyEmail', [
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::registerView(fn () => Inertia::render('auth/Register'));
|
||||
|
||||
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/TwoFactorChallenge'));
|
||||
|
||||
Fortify::confirmPasswordView(fn () => Inertia::render('auth/ConfirmPassword'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure rate limiting.
|
||||
*/
|
||||
private function configureRateLimiting(): void
|
||||
{
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||
});
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
28
app/Providers/InitialSynapticWeightsProvider.php
Normal file
28
app/Providers/InitialSynapticWeightsProvider.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\ISynapticWeights;
|
||||
use App\Services\RandomSynapticWeights;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class InitialSynapticWeightsProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(ISynapticWeights::class, function ($app) {
|
||||
return new RandomSynapticWeights();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
30
app/Services/CsvReader.php
Normal file
30
app/Services/CsvReader.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class CsvReader {
|
||||
private $file;
|
||||
private array $headers;
|
||||
|
||||
public array $lines = [];
|
||||
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
)
|
||||
{
|
||||
$this->file = fopen(public_path($filename), "r");
|
||||
if (!$this->file) {
|
||||
throw new \RuntimeException("Failed to open file: " . $filename);
|
||||
}
|
||||
|
||||
$this->headers = $this->readNextLine();
|
||||
}
|
||||
|
||||
public function readNextLine(): ?array
|
||||
{
|
||||
if (($data = fgetcsv($this->file, 1000, ",")) !== FALSE) {
|
||||
return $data;
|
||||
}
|
||||
return null; // End of file or error
|
||||
}
|
||||
}
|
||||
49
app/Services/DataSetReader.php
Normal file
49
app/Services/DataSetReader.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class DataSetReader {
|
||||
public array $lines = [];
|
||||
private array $currentLines = [];
|
||||
|
||||
private int $lastReadLineIndex = -1;
|
||||
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
) {
|
||||
// For now, we only support CSV files, so we can delegate to CsvReader
|
||||
$csvReader = new CsvReader($filename);
|
||||
$this->readEntireFile($csvReader);
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
private function readEntireFile(CsvReader $reader): void
|
||||
{
|
||||
while ($line = $reader->readNextLine()) {
|
||||
$this->lines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
public function getRandomLine(): array | null
|
||||
{
|
||||
if (empty($this->currentLines)) {
|
||||
return null; // No more lines to read
|
||||
}
|
||||
$randomNumber = array_rand($this->currentLines);
|
||||
$randomLine = $this->currentLines[$randomNumber];
|
||||
// Remove the line from the current lines to avoid repetition
|
||||
unset($this->currentLines[$randomNumber]);
|
||||
$this->lastReadLineIndex = array_search($randomLine, $this->lines, true);
|
||||
return $randomLine;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->currentLines = $this->lines;
|
||||
}
|
||||
|
||||
public function getLastReadLineIndex(): int
|
||||
{
|
||||
return $this->lastReadLineIndex;
|
||||
}
|
||||
}
|
||||
7
app/Services/ISynapticWeights.php
Normal file
7
app/Services/ISynapticWeights.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
interface ISynapticWeights {
|
||||
public function generate(int $input_size): array;
|
||||
}
|
||||
14
app/Services/RandomSynapticWeights.php
Normal file
14
app/Services/RandomSynapticWeights.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class RandomSynapticWeights implements ISynapticWeights {
|
||||
public function generate(int $input_size): array
|
||||
{
|
||||
$weights = [];
|
||||
for ($i = 0; $i < $input_size + 1; $i++) { // +1 for bias weight
|
||||
$weights[] = rand(-100, 100) / 100; // Random weights between -1 and 1
|
||||
}
|
||||
return $weights;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user