Skip to content
Merged

Dev #3271

Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 52 additions & 17 deletions app/Http/Controllers/BulkEventUploadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use Maatwebsite\Excel\Facades\Excel;
Expand All @@ -26,11 +27,23 @@ public function index(Request $request): View
{
$validationMissing = $request->session()->get('validation_missing')
?? $request->session()->get(self::SESSION_VALIDATION_MISSING, []);
$validationPassed = $request->session()->get(self::SESSION_VALIDATION_PASSED, false);
$filePath = $request->session()->get(self::SESSION_FILE_PATH);
$tempDisk = config('filesystems.bulk_upload_temp_disk', 'local');
$importPayload = '';
if ($validationPassed && $filePath) {
$importPayload = Crypt::encryptString(json_encode([
'path' => $filePath,
'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR),
'disk' => $tempDisk,
]));
}

return view('admin.bulk-upload.index', [
'validationPassed' => $request->session()->get(self::SESSION_VALIDATION_PASSED, false),
'validationPassed' => $validationPassed,
'validationMissing' => $validationMissing,
'storedDefaultCreatorEmail' => $request->session()->get(self::SESSION_DEFAULT_CREATOR),
'import_payload' => $importPayload,
]);
}

Expand Down Expand Up @@ -81,25 +94,25 @@ function ($attribute, $value, $fail) {
->withInput();
}

// Remove any previously stored file
$tempDisk = config('filesystems.bulk_upload_temp_disk', 'local');
$oldPath = $request->session()->get(self::SESSION_FILE_PATH);
if ($oldPath && Storage::exists($oldPath)) {
Storage::delete($oldPath);
if ($oldPath && Storage::disk($tempDisk)->exists($oldPath)) {
Storage::disk($tempDisk)->delete($oldPath);
}
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_VALIDATION_PASSED, self::SESSION_VALIDATION_MISSING, self::SESSION_DEFAULT_CREATOR]);

$path = $file->storeAs('temp', 'bulk_events_'.time().'.'.$extension);
$path = $file->storeAs('temp', 'bulk_events_'.time().'.'.$extension, $tempDisk);

$headerCheck = BulkEventUploadValidator::validateRequiredColumns($path, 'local');
$headerCheck = BulkEventUploadValidator::validateRequiredColumns($path, $tempDisk);
if (isset($headerCheck['error'])) {
Storage::delete($path);
Storage::disk($tempDisk)->delete($path);

return redirect()->route('admin.bulk-upload.index')
->withErrors(['file' => 'Could not read file: '.$headerCheck['error']])
->withInput();
}
if (! $headerCheck['valid']) {
Storage::delete($path);
Storage::disk($tempDisk)->delete($path);

return redirect()->route('admin.bulk-upload.index')
->with('validation_missing', $headerCheck['missing'])
Expand All @@ -117,26 +130,48 @@ function ($attribute, $value, $fail) {
}

/**
* Run the import using the file stored in session (after validate).
* Run the import using file path from encrypted form payload or session.
* Payload avoids "No validated file found" when session is not shared (e.g. load balancer).
*/
public function import(Request $request): View|RedirectResponse
{
$path = $request->session()->get(self::SESSION_FILE_PATH);
if (! $path || ! Storage::exists($path)) {
$path = null;
$defaultCreatorEmail = null;
$tempDisk = config('filesystems.bulk_upload_temp_disk', 'local');

$payload = $request->input('import_payload');
if (is_string($payload) && $payload !== '') {
try {
$decoded = json_decode(Crypt::decryptString($payload), true);
if (is_array($decoded) && ! empty($decoded['path'])) {
$path = $decoded['path'];
$defaultCreatorEmail = $decoded['default_creator_email'] ?? null;
if (! empty($decoded['disk'])) {
$tempDisk = $decoded['disk'];
}
}
} catch (\Throwable $e) {
// Fall back to session
}
}
if (! $path) {
$path = $request->session()->get(self::SESSION_FILE_PATH);
$defaultCreatorEmail = $request->session()->get(self::SESSION_DEFAULT_CREATOR);
}

if (! $path || ! Storage::disk($tempDisk)->exists($path)) {
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_DEFAULT_CREATOR, self::SESSION_VALIDATION_PASSED, self::SESSION_VALIDATION_MISSING]);

return redirect()->route('admin.bulk-upload.index')
->withErrors(['import' => 'No validated file found. Please upload and validate a file first.']);
}

$defaultCreatorEmail = $request->session()->get(self::SESSION_DEFAULT_CREATOR);

try {
$result = new BulkEventImportResult;
$import = new GenericEventsImport($defaultCreatorEmail, $result);
Excel::import($import, $path, 'local');
Excel::import($import, $path, $tempDisk);

Storage::delete($path);
Storage::disk($tempDisk)->delete($path);
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_DEFAULT_CREATOR, self::SESSION_VALIDATION_PASSED, self::SESSION_VALIDATION_MISSING]);

$this->clearMapCache();
Expand All @@ -146,8 +181,8 @@ public function import(Request $request): View|RedirectResponse

return redirect()->route('admin.bulk-upload.report');
} catch (\Throwable $e) {
if (Storage::exists($path)) {
Storage::delete($path);
if (Storage::disk($tempDisk)->exists($path)) {
Storage::disk($tempDisk)->delete($path);
}
$request->session()->forget([self::SESSION_FILE_PATH, self::SESSION_DEFAULT_CREATOR, self::SESSION_VALIDATION_PASSED, self::SESSION_VALIDATION_MISSING]);

Expand Down
3 changes: 3 additions & 0 deletions config/filesystems.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

'cloud' => env('FILESYSTEM_CLOUD', 's3'),

'resources_import_temp_disk' => env('RESOURCES_IMPORT_TEMP_DISK', 'local'),
'bulk_upload_temp_disk' => env('BULK_UPLOAD_TEMP_DISK', 'local'),

'disks' => [
'latex' => [
'driver' => 'local',
Expand Down
56 changes: 48 additions & 8 deletions resources/views/admin/bulk-upload/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
{{-- Step 1: Upload & validate --}}
<div class="mb-8">
<h2 class="text-lg font-semibold mb-2">Step 1: Upload &amp; validate</h2>
<form method="POST" action="{{ route('admin.bulk-upload.validate') }}" enctype="multipart/form-data" class="codeweek-form">
<form id="bulk-upload-validate-form" method="POST" action="{{ route('admin.bulk-upload.validate') }}" enctype="multipart/form-data" class="codeweek-form">
@csrf

<div class="codeweek-form-inner-container">
Expand All @@ -49,19 +49,52 @@ class="w-full max-w-md px-3 py-2 border rounded"
</div>

<div class="mb-4">
<label for="file" class="block font-medium mb-1">Excel / CSV file <span class="text-red-600">*</span></label>
<input type="file" name="file" id="file" accept=".csv,.xlsx,.xls" required
class="block w-full max-w-md">
<p class="text-sm text-gray-600 mt-1">Select a file above (required), then click the button below. Max 10 MB. Required columns: activity_title, name_of_organisation, type_of_organisation, activity_type, description, address, country, start_date, end_date, longitude, latitude, contact_email, organiser_website, participants_count, males_count, females_count, other_count.</p>
<div class="flex flex-wrap items-center gap-2 mb-1">
<label for="bulk-upload-file" class="font-medium">Excel / CSV file <span class="text-red-600">*</span></label>
<input type="file" name="file" id="bulk-upload-file" accept=".csv,.xlsx,.xls" required
class="text-sm file:mr-2 file:py-2 file:px-4 file:rounded-full file:border-0 file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300 file:cursor-pointer cursor-pointer" aria-required="true">
<span id="bulk-upload-file-name" class="text-sm text-gray-600 italic">No file chosen</span>
<span id="bulk-upload-file-attached" class="hidden text-sm font-medium text-green-700 bg-green-100 px-2 py-0.5 rounded">Attached</span>
</div>
<p class="text-sm text-gray-600 mt-1">Max 10 MB. Required columns: activity_title, name_of_organisation, type_of_organisation, activity_type, description, address, country, start_date, end_date, longitude, latitude, contact_email, organiser_website, participants_count, males_count, females_count, other_count.</p>
</div>

<div class="codeweek-form-button-container">
<div class="codeweek-button">
<input type="submit" value="Upload &amp; validate" class="bg-primary cursor-pointer px-6 py-3 rounded-full font-semibold text-[#20262C] hover:bg-hover-orange duration-300">
<button type="submit" id="bulk-upload-validate-btn" class="bg-primary cursor-pointer px-6 py-3 rounded-full font-semibold text-[#20262C] hover:bg-hover-orange duration-300">
Upload &amp; validate
</button>
</div>
</div>
</div>
</form>
<script>
(function () {
var fileInput = document.getElementById('bulk-upload-file');
var fileNameEl = document.getElementById('bulk-upload-file-name');
var attachedBadge = document.getElementById('bulk-upload-file-attached');
var form = document.getElementById('bulk-upload-validate-form');
var btn = document.getElementById('bulk-upload-validate-btn');
fileInput.addEventListener('change', function () {
if (this.files && this.files.length > 0) {
fileNameEl.textContent = this.files[0].name;
fileNameEl.classList.remove('italic', 'text-gray-600');
fileNameEl.classList.add('text-gray-800', 'font-medium');
if (attachedBadge) { attachedBadge.classList.remove('hidden'); }
} else {
fileNameEl.textContent = 'No file chosen';
fileNameEl.classList.add('italic', 'text-gray-600');
fileNameEl.classList.remove('text-gray-800', 'font-medium');
if (attachedBadge) { attachedBadge.classList.add('hidden'); }
}
});
form.addEventListener('submit', function () {
if (!fileInput.files || fileInput.files.length === 0) return;
btn.disabled = true;
btn.textContent = 'Uploading…';
});
})();
</script>
</div>

{{-- Validation result: missing columns --}}
Expand All @@ -83,10 +116,17 @@ class="block w-full max-w-md">
<div class="p-4 rounded bg-green-50 border border-green-200">
<h2 class="text-lg font-semibold mb-2 text-green-800">Step 2: Import</h2>
<p class="mb-3 text-green-700">All required columns are present. Click the button below to run the import.</p>
<form method="POST" action="{{ route('admin.bulk-upload.import') }}" class="inline">
<form method="POST" action="{{ route('admin.bulk-upload.import') }}" class="inline" id="bulk-upload-import-form">
@csrf
<input type="submit" value="Import" class="bg-primary cursor-pointer px-6 py-3 rounded-full font-semibold text-[#20262C] hover:bg-hover-orange duration-300">
<input type="hidden" name="import_payload" value="{{ $import_payload ?? '' }}">
<button type="submit" id="bulk-upload-import-btn" class="bg-primary cursor-pointer px-6 py-3 rounded-full font-semibold text-[#20262C] hover:bg-hover-orange duration-300">Import</button>
</form>
<script>
document.getElementById('bulk-upload-import-form').addEventListener('submit', function () {
var btn = document.getElementById('bulk-upload-import-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Importing…'; }
});
</script>
</div>
@endif
</section>
Expand Down
Loading