Merge branch 'dev'
final 1.2.2
This commit is contained in:
@@ -59,7 +59,8 @@ class EmailController extends Controller
|
||||
|
||||
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
|
||||
|
||||
$messages = $query->paginate(20)->withQueryString();
|
||||
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||
$messages = $query->paginate($perPage)->withQueryString();
|
||||
$folders = EmailFolder::orderBy('sort_order')->get();
|
||||
$mailingLists = MailingList::where('attiva', true)->get();
|
||||
$gruppi = Gruppo::orderBy('nome')->get();
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Individuo;
|
||||
use App\Models\TipologiaEvento;
|
||||
use App\Services\IcsExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class EventoController extends Controller
|
||||
@@ -43,7 +44,8 @@ class EventoController extends Controller
|
||||
$query->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
$eventi = $query->paginate(20);
|
||||
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||
$eventi = $query->paginate($perPage);
|
||||
$gruppi = Gruppo::orderBy('nome')->get();
|
||||
|
||||
return view('eventi.index', compact('eventi', 'gruppi'));
|
||||
@@ -477,4 +479,101 @@ class EventoController extends Controller
|
||||
|
||||
return $colors['primary'];
|
||||
}
|
||||
|
||||
public function importIcs()
|
||||
{
|
||||
$this->authorizeWrite('eventi');
|
||||
return view('eventi.import');
|
||||
}
|
||||
|
||||
public function importIcsStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('eventi');
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:ics,text/calendar',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$content = file_get_contents($file->path());
|
||||
|
||||
try {
|
||||
$vcalendar = \Sabre\VObject\Reader::read($content);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Errore lettura file ICS: ' . $e->getMessage());
|
||||
return back()->with('error', 'Il file ICS non è valido: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($vcalendar->VEVENT as $vevent) {
|
||||
try {
|
||||
$uid = (string) ($vevent->UID ?? '');
|
||||
if (!empty($uid)) {
|
||||
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||
if ($existing) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$nomeEvento = (string) ($vevent->SUMMARY ?? '');
|
||||
if (empty($nomeEvento)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$dtStart = $vevent->DTSTART ?? null;
|
||||
$dtEnd = $vevent->DTEND ?? null;
|
||||
|
||||
$dataSpecifica = null;
|
||||
$oraInizio = null;
|
||||
$durataMinuti = null;
|
||||
|
||||
if ($dtStart) {
|
||||
$dtStartDateTime = new \DateTime((string) $dtStart);
|
||||
$dataSpecifica = $dtStartDateTime->format('Y-m-d');
|
||||
$oraInizio = $dtStartDateTime->format('H:i');
|
||||
|
||||
if ($dtEnd) {
|
||||
$dtEndDateTime = new \DateTime((string) $dtEnd);
|
||||
$durataMinuti = (int) ($dtEndDateTime->getTimestamp() - $dtStartDateTime->getTimestamp()) / 60;
|
||||
if ($durataMinuti < 1) {
|
||||
$durataMinuti = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$descrizione = (string) ($vevent->DESCRIPTION ?? '');
|
||||
$luogo = (string) ($vevent->LOCATION ?? '');
|
||||
|
||||
Evento::create([
|
||||
'nome_evento' => $nomeEvento,
|
||||
'descrizione' => !empty($descrizione) ? $descrizione : null,
|
||||
'tipo_recorrenza' => 'singolo',
|
||||
'data_specifica' => $dataSpecifica,
|
||||
'ora_inizio' => $oraInizio,
|
||||
'durata_minuti' => $durataMinuti,
|
||||
'luogo_indirizzo' => !empty($luogo) ? $luogo : null,
|
||||
'uid_esterno' => !empty($uid) ? $uid : null,
|
||||
]);
|
||||
|
||||
$imported++;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Errore import evento ICS: ' . $e->getMessage());
|
||||
$errors[] = 'Errore: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$message = 'Importati ' . $imported . ' eventi.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltati ' . $skipped . ' eventi (già presenti o senza nome).';
|
||||
}
|
||||
if (count($errors) > 0) {
|
||||
return back()->with('error', $message . ' Errori: ' . implode('; ', $errors));
|
||||
}
|
||||
|
||||
return redirect('/eventi')->with('success', $message);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use App\Models\Gruppo;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Diocesi;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GruppoController extends Controller
|
||||
{
|
||||
@@ -300,4 +301,101 @@ class GruppoController extends Controller
|
||||
|
||||
return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.');
|
||||
}
|
||||
|
||||
public function import()
|
||||
{
|
||||
$this->authorizeWrite('gruppi');
|
||||
return view('gruppi.import');
|
||||
}
|
||||
|
||||
public function importStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('gruppi');
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
return back()->with('error', 'Il file CSV non è valido.');
|
||||
}
|
||||
|
||||
$header = array_map('trim', $header);
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_combine($header, $row);
|
||||
|
||||
if ($data === false) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_map('trim', $data);
|
||||
$nome = $data['nome'] ?? '';
|
||||
|
||||
if (empty($nome)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Gruppo::create([
|
||||
'nome' => $nome,
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null,
|
||||
'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null,
|
||||
'indirizzo_incontro' => $data['indirizzo_incontro'] ?? null,
|
||||
'cap_incontro' => $data['cap_incontro'] ?? null,
|
||||
'città_incontro' => $data['città_incontro'] ?? null,
|
||||
'sigla_provincia_incontro' => $data['sigla_provincia_incontro'] ?? null,
|
||||
]);
|
||||
|
||||
$imported++;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Errore import gruppo riga ' . $rowNumber . ': ' . $e->getMessage());
|
||||
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$message = 'Importati ' . $imported . ' gruppi.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
}
|
||||
if (count($errors) > 0) {
|
||||
return back()->with('error', $message . ' Errori: ' . implode('; ', $errors));
|
||||
}
|
||||
|
||||
return redirect()->route('gruppi.index')->with('success', $message);
|
||||
}
|
||||
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$this->authorizeRead('gruppi');
|
||||
$headers = ['nome', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
|
||||
$csv = implode(',', $headers) . "\n" . implode(',', $example);
|
||||
|
||||
return response($csv, 200, [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="template_gruppi.csv"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ class IndividuoController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorizeRead('individui');
|
||||
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate(20);
|
||||
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate($perPage);
|
||||
|
||||
$vista = null;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class Evento extends Model
|
||||
'is_incontro_gruppo',
|
||||
'luogo_indirizzo',
|
||||
'luogo_url_maps',
|
||||
'uid_esterno',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -131,8 +131,29 @@ $currentFolder = $folders->firstWhere('type', $folder);
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $messages->withQueryString()->links('vendor.pagination.simple-bootstrap-4') }}
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $messages->links('vendor.pagination.simple-bootstrap-4') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
@extends('layouts.adminlte')
|
||||
@section('title', 'Importa Eventi')
|
||||
@section('page_title', 'Importa Eventi da ICS')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-upload mr-2"></i>Carica file ICS</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('eventi.import.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="file">Seleziona file ICS *</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".ics" required>
|
||||
<small class="form-text text-muted">
|
||||
Il file deve essere in formato ICS (iCalendar).
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i> Importa
|
||||
</button>
|
||||
<a href="/eventi" class="btn btn-secondary ml-2">
|
||||
Annulla
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Informazioni</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Campi importati:</h6>
|
||||
<ul>
|
||||
<li><code>SUMMARY</code> → Nome evento (obbligatorio)</li>
|
||||
<li><code>DESCRIPTION</code> → Descrizione</li>
|
||||
<li><code>DTSTART</code> → Data/Ora inizio</li>
|
||||
<li><code>DTEND</code> → Durata</li>
|
||||
<li><code>LOCATION</code> → Luogo</li>
|
||||
<li><code>UID</code> → ID esterno (per evitare duplicati)</li>
|
||||
</ul>
|
||||
<p class="text-muted small mt-2">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Gli eventi con UID già presente nel sistema verranno saltati automaticamente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -34,6 +34,9 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
</div>
|
||||
@endif
|
||||
@if($canWriteEventi)
|
||||
<a href="/eventi/import" class="btn btn-sm btn-warning mr-2">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa ICS
|
||||
</a>
|
||||
<a href="/eventi/create" class="btn btn-xs btn-success">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||
</a>
|
||||
@@ -247,8 +250,29 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $eventi->withQueryString()->links() }}
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $eventi->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
@extends('layouts.adminlte')
|
||||
@section('title', 'Importa Gruppi')
|
||||
@section('page_title', 'Importa Gruppi da CSV')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-upload mr-2"></i>Carica file CSV</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('gruppi.import.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="file">Seleziona file CSV *</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".csv,.txt" required>
|
||||
<small class="form-text text-muted">
|
||||
Il file deve essere in formato CSV con separatore virgola.
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i> Importa
|
||||
</button>
|
||||
<a href="{{ route('gruppi.index') }}" class="btn btn-secondary ml-2">
|
||||
Annulla
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info">
|
||||
<h3 class="card-title text-white"><i class="fas fa-download mr-2"></i>Template</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Scarica il template CSV per l'importazione:</p>
|
||||
<a href="{{ route('gruppi.template') }}" class="btn btn-success btn-block">
|
||||
<i class="fas fa-file-csv mr-1"></i> Scarica Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Formato CSV</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Campi:</h6>
|
||||
<ul>
|
||||
<li><code>nome</code> (obbligatorio)</li>
|
||||
<li><code>descrizione</code></li>
|
||||
<li><code>parent_id</code> (ID del gruppo padre)</li>
|
||||
<li><code>diocesi_id</code> (ID della diocesi)</li>
|
||||
<li><code>indirizzo_incontro</code></li>
|
||||
<li><code>cap_incontro</code></li>
|
||||
<li><code>città_incontro</code></li>
|
||||
<li><code>sigla_provincia_incontro</code> (2 lettere)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -55,6 +55,9 @@ $visibleColumnsJson = json_encode($visibleColumns);
|
||||
<i class="fas fa-save mr-1"></i> Salva Vista
|
||||
</button>
|
||||
@if($canWriteGruppi)
|
||||
<a href="{{ route('gruppi.import') }}" class="btn btn-warning btn-sm mr-1">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa
|
||||
</a>
|
||||
<a href="{{ route('gruppi.create') }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-plus"></i> Nuovo Gruppo
|
||||
</a>
|
||||
|
||||
@@ -220,8 +220,29 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
|
||||
@endif
|
||||
</div>
|
||||
@if($individui->count() > 0)
|
||||
<div class="card-footer">
|
||||
{{ $individui->links() }}
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $individui->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -87,6 +87,9 @@ Route::patch('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::clas
|
||||
Route::delete('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::class, 'destroy'])->middleware('auth');
|
||||
|
||||
// Gruppi specific routes (MUST be before resource route)
|
||||
Route::get('gruppi/import', [GruppoController::class, 'import'])->middleware('auth')->name('gruppi.import');
|
||||
Route::post('gruppi/import', [GruppoController::class, 'importStore'])->middleware('auth')->name('gruppi.import.store');
|
||||
Route::get('gruppi/template', [GruppoController::class, 'downloadTemplate'])->middleware('auth')->name('gruppi.template');
|
||||
Route::get('gruppi/all', [GruppoController::class, 'allGruppi'])->middleware('auth');
|
||||
Route::get('gruppi/{gruppo}/individui-email', [GruppoController::class, 'individuiEmail'])->middleware('auth');
|
||||
|
||||
@@ -97,6 +100,8 @@ Route::delete('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarContr
|
||||
Route::resource('gruppi', GruppoController::class)->middleware('auth');
|
||||
Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->middleware('auth');
|
||||
Route::get('eventi/calendar/events', [EventoController::class, 'calendarEvents'])->name('eventi.calendar.events')->middleware('auth');
|
||||
Route::get('eventi/import', [EventoController::class, 'importIcs'])->middleware('auth')->name('eventi.import');
|
||||
Route::post('eventi/import', [EventoController::class, 'importIcsStore'])->middleware('auth')->name('eventi.import.store');
|
||||
Route::get('eventi/{evento}/export-ics', [EventoController::class, 'exportIcs'])->name('eventi.export-ics')->middleware('auth');
|
||||
Route::post('eventi/export-ics', [EventoController::class, 'exportSelectedIcs'])->name('eventi.export-selected-ics')->middleware('auth');
|
||||
Route::post('eventi/mass-elimina', [EventoController::class, 'massElimina'])->middleware('auth');
|
||||
|
||||
Reference in New Issue
Block a user