waterloobae / crowdmarkapilaravel
A dashboard for crowdmark
Package info
git.uwaterloo.ca/cemc-development-team-public/crowdmark-api-laravel.git
pkg:composer/waterloobae/crowdmarkapilaravel
Requires
- php: ^8.2
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- setasign/fpdf: ^1.8
- setasign/fpdi: ^2.3
This package is not auto-updated.
Last update: 2026-04-23 16:31:42 UTC
README
Install
composer require waterloobae/crowdmarkapilaravel
Set environment variables:
CROWDMARK_API_KEY=your-secret-key
CROWDMARK_BASE_URL=https://app.crowdmark.com/
No package service provider or publish step is required for Crowdmark config.
You can use .env only (plus optional host services.php fallback).
Optional config/services.php fallback:
// config/services.php
return [
// ... existing services
'crowdmark' => [
'base_url' => env('CROWDMARK_BASE_URL', 'https://app.crowdmark.com/'),
'api_key' => env('CROWDMARK_API_KEY'),
],
];
Queue Jobs Included In Package
These queue jobs are provided by the package and can be dispatched directly:
Waterloobae\CrowdmarkApiLaravel\Jobs\GenerateCrowdmarkBookletPagesJsonJobWaterloobae\CrowdmarkApiLaravel\Jobs\GenerateCrowdmarkPagesPdfJobWaterloobae\CrowdmarkApiLaravel\Jobs\GenerateCrowdmarkOddPagesPdfJob
Run a worker for async operations:
php artisan queue:work --timeout=0 --tries=1
Host App Integration Example (Routes Source)
routes/web.php is not part of this package. Add routes like the following in your app.
Shared imports:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Waterloobae\CrowdmarkApiLaravel\Jobs\GenerateCrowdmarkBookletPagesJsonJob;
use Waterloobae\CrowdmarkApiLaravel\Jobs\GenerateCrowdmarkPagesPdfJob;
use Waterloobae\CrowdmarkApiLaravel\Jobs\GenerateCrowdmarkOddPagesPdfJob;
Route 1. Crowdmark page entry point.
Route::get('/crowdmark', function () {
return view('crowdmark');
})->name('crowdmark');
Route 2. Queue booklet/page JSON cache build and return a polling token.
Route::post('/crowdmark/save-booklet-pages-json', function (Request $request) {
$assessmentIds = array_values(array_filter(
array_map('trim', explode(',', $request->input('assessment_ids', '')))
));
if (empty($assessmentIds)) {
return response()->json(['error' => 'No assessment IDs provided.'], 422);
}
$forceRefresh = filter_var($request->input('force_refresh', false), FILTER_VALIDATE_BOOL);
$jsonPath = trim((string) $request->input('json_path', '')) ?: null;
$token = \Illuminate\Support\Str::uuid()->toString();
Cache::put("crowdmark_json_{$token}", ['status' => 'pending'], now()->addHours(24));
GenerateCrowdmarkBookletPagesJsonJob::dispatch($token, $assessmentIds, $forceRefresh, $jsonPath);
return response()->json(['token' => $token]);
})->name('crowdmark.save-booklet-pages-json');
Route 3. Check JSON cache job status and return download URL when done.
Route::get('/crowdmark/json-status/{token}', function (string $token) {
$raw = Cache::get("crowdmark_json_{$token}");
if ($raw === null) {
return response()->json(['status' => 'not_found'], 404);
}
if (($raw['status'] ?? '') === 'done') {
$cacheKey = (string) ($raw['cache_key'] ?? '');
$savedPath = (string) ($raw['path'] ?? '');
return response()->json([
'status' => 'done',
'cache_key' => $cacheKey,
'count' => (int) ($raw['count'] ?? 0),
'created_at' => (string) ($raw['created_at'] ?? ''),
'download_url' => $savedPath !== ''
? route('crowdmark.download-booklet-pages-json-by-token', ['token' => $token])
: route('crowdmark.download-booklet-pages-json', ['cacheKey' => $cacheKey]),
]);
}
if (($raw['status'] ?? '') === 'failed') {
return response()->json([
'status' => 'failed',
'error' => (string) ($raw['error'] ?? 'Unknown job failure.'),
]);
}
return response()->json(['status' => 'pending']);
})->name('crowdmark.json-status');
Route 4. Download JSON cache by hash key (default path).
Route::get('/crowdmark/booklet-pages-json/{cacheKey}', function (string $cacheKey) {
$path = storage_path('app/crowdmark-cache/' . $cacheKey . '.json');
if (!is_file($path)) {
abort(404, 'JSON cache file not found.');
}
return response()->download($path, 'crowdmark_booklet_pages_' . $cacheKey . '.json', [
'Content-Type' => 'application/json',
]);
})->name('crowdmark.download-booklet-pages-json');
Route 5. Download JSON cache by token (custom json_path support).
Route::get('/crowdmark/booklet-pages-json-token/{token}', function (string $token) {
$raw = Cache::get("crowdmark_json_{$token}");
$path = (string) ($raw['path'] ?? '');
if (!is_array($raw) || ($raw['status'] ?? '') !== 'done' || $path === '' || !is_file($path)) {
abort(404, 'JSON cache file not ready.');
}
return response()->download($path, basename($path) ?: 'crowdmark_booklet_pages.json', [
'Content-Type' => 'application/json',
]);
})->name('crowdmark.download-booklet-pages-json-by-token');
Route 6. Queue single-page PDF generation for all selected assessments.
Route::post('/crowdmark/download-pages', function (Request $request) {
$assessmentIds = array_values(array_filter(
array_map('trim', explode(',', $request->input('assessment_ids', '')))
));
if (empty($assessmentIds)) {
return response()->json(['error' => 'No assessment IDs provided.'], 422);
}
$token = \Illuminate\Support\Str::uuid()->toString();
Cache::put("crowdmark_pdf_{$token}", 'pending', now()->addHours(2));
GenerateCrowdmarkPagesPdfJob::dispatch($token, $assessmentIds, (string) $request->input('page', '1'));
return response()->json(['token' => $token]);
})->name('crowdmark.download-pages');
Route 7. Poll single-page PDF job status.
Route::get('/crowdmark/pdf-status/{token}', function (string $token) {
$raw = Cache::get("crowdmark_pdf_{$token}");
if ($raw === null) {
return response()->json(['status' => 'not_found'], 404);
}
if (str_starts_with((string) $raw, 'failed:')) {
return response()->json(['status' => 'failed', 'error' => substr((string) $raw, 7)]);
}
return response()->json(['status' => $raw]);
})->name('crowdmark.pdf-status');
Route 8. Download generated single-page PDF.
Route::get('/crowdmark/pdf-download/{token}', function (string $token) {
$status = Cache::get("crowdmark_pdf_{$token}");
if ($status !== 'done') {
abort(404, 'PDF not ready.');
}
$path = "crowdmark-pdfs/{$token}.pdf";
if (!Storage::exists($path)) {
abort(404, 'PDF file missing.');
}
return response()->download(Storage::path($path), "pages_{$token}.pdf", [
'Content-Type' => 'application/pdf',
]);
})->name('crowdmark.pdf-download');
Route 9. Queue odd-pages ZIP generation (supports baseline json_path + zip_save_path).
Route::post('/crowdmark/download-odd-pages', function (Request $request) {
$assessmentIds = array_values(array_filter(
array_map('trim', explode(',', $request->input('assessment_ids', '')))
));
if (empty($assessmentIds)) {
return response()->json(['error' => 'No assessment IDs provided.'], 422);
}
$token = \Illuminate\Support\Str::uuid()->toString();
Cache::put("crowdmark_pdf_{$token}", 'pending', now()->addHours(24));
GenerateCrowdmarkOddPagesPdfJob::dispatch(
$token,
$assessmentIds,
(int) $request->input('max_page', 39),
trim((string) $request->input('json_path', '')) ?: null,
trim((string) $request->input('zip_save_path', '')) ?: null,
);
return response()->json(['token' => $token]);
})->name('crowdmark.download-odd-pages');
Route 10. Download generated odd-pages ZIP.
Route::get('/crowdmark/zip-download/{token}', function (string $token) {
$status = Cache::get("crowdmark_pdf_{$token}");
if ($status !== 'done') {
abort(404, 'ZIP not ready.');
}
$path = (string) Cache::get("crowdmark_pdf_path_{$token}", "crowdmark-pdfs/{$token}.zip");
if (!Storage::exists($path)) {
abort(404, 'ZIP file missing.');
}
return response()->download(Storage::path($path), basename($path) ?: "odd_pages_{$token}.zip", [
'Content-Type' => 'application/zip',
]);
})->name('crowdmark.zip-download');
Host App Integration Example (Blade Source)
resources/views/crowdmark.blade.php is not part of this package. Example form snippets:
<h2>Save Booklet/Page JSON Cache</h2>
<form id="json-cache-form">
@csrf
<textarea name="assessment_ids" rows="3" cols="60"></textarea>
<label>
<input name="force_refresh" type="checkbox" value="1">
Force refresh from API
</label>
<input name="json_path" type="text" placeholder="crowdmark-cache/custom/booklet-pages.json">
<button type="submit">Save JSON Cache</button>
</form>
<h2>All odd pages - ZIP of booklet-based PDFs</h2>
<form id="zip-form">
@csrf
<textarea name="assessment_ids" rows="3" cols="60"></textarea>
<input name="max_page" type="number" min="1" value="39">
<input name="json_path" type="text" placeholder="crowdmark-cache/custom/booklet-pages.json">
<input name="zip_save_path" type="text" placeholder="crowdmark-pdfs/custom/odd-pages.zip">
<button type="submit">Generate ZIP</button>
</form>
Async submit/poll pattern (example):
<script>
async function postForm(url, form) {
const formData = new FormData(form);
const res = await fetch(url, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': formData.get('_token'),
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData),
});
return res.json();
}
async function pollStatus(statusUrl, onDone) {
const timer = setInterval(async () => {
const res = await fetch(statusUrl, { headers: { 'Accept': 'application/json' } });
const data = await res.json();
if (data.status === 'done') {
clearInterval(timer);
onDone(data);
}
if (data.status === 'failed') {
clearInterval(timer);
console.error(data.error ?? 'Job failed');
}
}, 5000);
}
</script>
Odd-pages Incremental Behavior
Odd-pages ZIP uses JSON as baseline and only downloads changed/new odd pages by checking:
assessment_idbooklet_idpage_numberupdated_at
After a successful ZIP run with downloaded pages, JSON cache is refreshed at json_path (or default path).
Index timestamps are derived from API page timestamps, not system datetime.