diff --git a/.env.ci b/.env.ci index debcc64c6..713546d6a 100644 --- a/.env.ci +++ b/.env.ci @@ -7,3 +7,4 @@ DB_PORT=3306 DB_DATABASE=test DB_USERNAME=root DB_PASSWORD=password +PRINTER_NAME=printer diff --git a/.env.example b/.env.example index e643de935..cdc4e4060 100644 --- a/.env.example +++ b/.env.example @@ -71,9 +71,11 @@ PREVENT_ACCESSING_MISSING_ATTRIBUTES=false PRINT_COST_ONESIDED=8 PRINT_COST_TWOSIDED=12 + +#Data used during migration in 2023_12_25_182310_create_printers_table.php to create a new printer PRINTER_NAME=NemUjBela -PRINTER_ADDITIONAL_ARGS= -PRINTER_STAT_ADDITIONAL_ARGS= +PRINTER_IP= +PRINTER_PORT= NETREG=1000 KKT=2000 @@ -88,6 +90,7 @@ CANCEL_COMMAND=cancel PDFINFO_COMMAND=pdfinfo PING_COMMAND=ping PDFLATEX_COMMAND=/usr/bin/pdflatex +RUN_COMMANDS_IN_DEBUG_MODE=false WORKSHOP_BALANCE_EXTERN=0.45 WORKSHOP_BALANCE_RESIDENT=0.6 diff --git a/app/Console/Commands.php b/app/Console/Commands.php index 89ca6500c..a91249ba8 100644 --- a/app/Console/Commands.php +++ b/app/Console/Commands.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Utils\Process; use Illuminate\Support\Facades\Log; /** @@ -10,84 +11,23 @@ */ class Commands { - private static function isDebugMode() - { - return config('app.debug'); - } - - public static function getCompletedPrintingJobs() - { - $command = config('commands.lpstat') . " " . config('print.stat_additional_args') . " -W completed -o " . config('print.printer_name') . " | awk '{print $1}'"; - if (self::isDebugMode()) { - $result = [0]; - } else { - $result = []; - exec($command, $result); - } - Log::info([$command, $result]); - return $result; - } - - public static function print($command) - { - if (self::isDebugMode()) { - $job_id = 0; - $result = "request id is " . config('print.printer_name') . "-" . $job_id . " (1 file(s))"; - } else { - $result = exec($command); - } - Log::info([$command, $result]); - return $result; - } - - public static function cancelPrintJob(string $jobID) - { - $command = config('commands.cancel') . " " . $jobID; - if (self::isDebugMode()) { - // cancel(1) exits with status code 0 if it succeeds - $result = ['output' => '', 'exit_code' => 0]; - } else { - $output = exec($command, $result, $exit_code); - $result = ['output' => $output, 'exit_code' => $exit_code]; - } - Log::info([$command, $result]); - return $result; - } - - public static function getPages($path) - { - $command = config('commands.pdfinfo') . " " . $path . " | grep '^Pages' | awk '{print $2}' 2>&1"; - if (self::isDebugMode()) { - $result = rand(1, 10); - } else { - $result = exec($command); - } - Log::info([$command, $result]); - return $result; - } - public static function pingRouter($router) { - if (self::isDebugMode()) { - $result = rand(1, 10) > 9 ? "error" : ''; - } else { - // This happens too often to log. - $command = config('commands.ping') . " " . $router->ip . " -c 1 | grep 'error\|unreachable'"; - $result = exec($command); + if (!filter_var($router->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + throw new \InvalidArgumentException("Invalid IP address: " . $router->ip); } + + $process = Process::fromShellCommandline(config('commands.ping') . " $router->ip -c 1 | grep 'error\|unreachable'"); + $process->run($log = false); + $result = $process->getOutput(debugOutput: rand(1, 10) > 9 ? "error" : ''); return $result; } public static function latexToPdf($path, $outputDir) { - if (self::isDebugMode()) { - $result = "ok"; - } else { - $command = config('commands.pdflatex') . " " . "-interaction=nonstopmode -output-dir " . $outputDir . " " . $path . " 2>&1"; - Log::info($command); - $result = exec($command); - Log::info($result); - } + $process = new Process([config('commands.pdflatex'), '-interaction=nonstopmode', '-output-dir', $outputDir, $path]); + $process->run(); + $result = $process->getOutput(debugOutput: 'ok'); return $result; } } diff --git a/app/Enums/PrintJobStatus.php b/app/Enums/PrintJobStatus.php new file mode 100644 index 000000000..d6a2664f8 --- /dev/null +++ b/app/Enums/PrintJobStatus.php @@ -0,0 +1,11 @@ + Hash::make($data['password']), ]); - $user->roles()->attach(Role::get(Role::PRINTER)->id); - if ($data['user_type'] == Role::TENANT) { $user->roles()->attach(Role::get(Role::TENANT)); $user->personalInformation()->create([ diff --git a/app/Http/Controllers/Dormitory/PrintController.php b/app/Http/Controllers/Dormitory/PrintController.php deleted file mode 100644 index 69d764172..000000000 --- a/app/Http/Controllers/Dormitory/PrintController.php +++ /dev/null @@ -1,322 +0,0 @@ -middleware('can:use,App\Models\PrintAccount'); - } - - public function index() - { - return view('dormitory.print.app', [ - "users" => User::printers(), - "free_pages" => user()->sumOfActiveFreePages() - ]); - } - - public function noPaper() - { - $reporterName = user()->name; - $admins = User::withRole(Role::SYS_ADMIN)->get(); - foreach ($admins as $admin) { - Mail::to($admin)->send(new NoPaper($admin->name, $reporterName)); - } - Cache::put('print.no-paper', now(), 3600); - return redirect()->back()->with('message', __('mail.email_sent')); - } - - public function addedPaper() - { - $this->authorize('handleAny', PrintAccount::class); - - Cache::forget('print.no-paper'); - return redirect()->back()->with('message', __('general.successful_modification')); - } - - public function admin() - { - $this->authorize('handleAny', PrintAccount::class); - - return view('dormitory.print.manage.app', ["users" => User::printers()]); - } - - public function print(Request $request) - { - $validator = Validator::make($request->all(), [ - 'file_to_upload' => 'required|file|mimes:pdf|max:' . config('print.pdf_size_limit'), - 'number_of_copies' => 'required|integer|min:1' - ]); - $validator->validate(); - - $is_two_sided = $request->has('two_sided'); - $number_of_copies = $request->number_of_copies; - $use_free_pages = $request->use_free_pages; - $file = $request->file_to_upload; - $filename = $file->getClientOriginalName(); - $path = $this->storeFile($file); - - $printer = new Printer($filename, $path, $use_free_pages, $is_two_sided, $number_of_copies); - - return $printer->print(); - } - - public function transferBalance(Request $request) - { - $validator = Validator::make($request->all(), [ - 'balance' => 'required|integer|min:1', - 'user_to_send' => 'required|integer|exists:users,id' - ]); - $validator->validate(); - - $balance = $request->balance; - $user = User::find($request->user_to_send); - $from_account = user()->printAccount; - $to_account = $user->printAccount; - - if (!$from_account->hasEnoughMoney($balance)) { - return $this->handleNoBalance(); - } - $to_account->update(['last_modified_by' => user()->id]); - $from_account->update(['last_modified_by' => user()->id]); - - $from_account->decrement('balance', $balance); - $to_account->increment('balance', $balance); - - // Send notification mail - Mail::to($user)->queue(new ChangedPrintBalance($user, $balance, user()->name)); - - return redirect()->back()->with('message', __('general.successful_transaction')); - } - - public function modifyBalance(Request $request) - { - $validator = Validator::make($request->all(), [ - 'user_id_modify' => 'required|integer|exists:users,id', - 'balance' => 'required|integer', - ]); - $validator->validate(); - - $balance = $request->balance; - $user = User::find($request->user_id_modify); - $print_account = $user->printAccount; - - $this->authorize('modify', $print_account); - - if ($balance < 0 && !$print_account->hasEnoughMoney($balance)) { - return $this->handleNoBalance(); - } - $print_account->update(['last_modified_by' => user()->id]); - $print_account->increment('balance', $balance); - - $admin_checkout = Checkout::admin(); - Transaction::create([ - 'checkout_id' => $admin_checkout->id, - 'receiver_id' => user()->id, - 'payer_id' => $user->id, - 'semester_id' => Semester::current()->id, - 'amount' => $request->balance, - 'payment_type_id' => PaymentType::print()->id, - 'comment' => null, - 'moved_to_checkout' => null, - ]); - - // Send notification mail - Mail::to($user)->queue(new ChangedPrintBalance($user, $balance, user()->name)); - - return redirect()->back()->with('message', __('general.successful_modification')); - } - - public function addFreePages(Request $request) - { - $validator = Validator::make($request->all(), [ - 'user_id_free' => 'required|integer|exists:users,id', - 'free_pages' => 'required|integer|min:1', - 'deadline' => 'required|date|after:now', - ]); - $validator->validate(); - - $this->authorize('create', FreePages::class); - - FreePages::create([ - 'user_id' => $request->user_id_free, - 'amount' => $request->free_pages, - 'deadline' => $request->deadline, - 'last_modified_by' => user()->id, - 'comment' => $request->comment, - ]); - - return redirect()->back()->with('message', __('general.successfully_added')); - } - - public function listAllPrintJobs() - { - $this->authorize('viewAny', PrintJob::class); - - $this->updateCompletedPrintingJobs(); - - $columns = ['created_at', 'filename', 'cost', 'state', 'user.name']; - // @phpstan-ignore-next-line - $printJobs = PrintJob::join('users as user', 'user.id', '=', 'user_id') - ->select('print_jobs.*') - ->with('user') - ->orderby('print_jobs.created_at', 'desc'); - - return $this->printJobsPaginator($printJobs, $columns); - } - - public function listPrintJobs() - { - $this->authorize('viewSelf', PrintJob::class); - - $this->updateCompletedPrintingJobs(); - - $columns = ['created_at', 'filename', 'cost', 'state']; - $printJobs = user()->printJobs()->orderby('created_at', 'desc'); - - return $this->printJobsPaginator($printJobs, $columns); - } - - public function listAllFreePages() - { - $this->authorize('viewAny', FreePages::class); - - $columns = ['amount', 'deadline', 'modifier', 'comment', 'user.name', 'created_at']; - - $freePages = FreePages::join('users as user', 'user.id', '=', 'user_id'); - - return $this->freePagesPaginator($freePages, $columns); - } - - public function listFreePages() - { - $this->authorize('viewSelf', FreePages::class); - - $columns = ['amount', 'deadline', 'modifier', 'comment']; - $freePages = user()->freePages(); - - return $this->freePagesPaginator($freePages, $columns); - } - - public function listPrintAccountHistory() - { - $this->authorize('viewAny', PrintJob::class); - - $columns = ['user_name', 'balance_change', 'free_page_change', 'deadline_change', 'modifier_name', 'modified_at']; - $paginator = TabulatorPaginator::from( - PrintAccountHistory::join('users as user', 'user.id', '=', 'user_id') - ->join('users as modifier', 'modifier.id', '=', 'modified_by') - ->select(['user.name as user_name', 'balance_change', 'free_page_change', 'deadline_change', 'modifier.name as modifier_name', 'modified_at']) - )->sortable($columns) - ->filterable($columns) - ->paginate(); - return $paginator; - } - - public function cancelPrintJob($id) - { - $printJob = PrintJob::findOrFail($id); - - $this->authorize('update', $printJob); - - if ($printJob->state === PrintJob::QUEUED) { - $result = Commands::cancelPrintJob($printJob->job_id); - - if ($result['exit_code'] == 0) { - // Command was successful, job cancelled. - $printJob->state = PrintJob::CANCELLED; - // Reverting balance change - // TODO: test what happens when cancelled right before the end - $printAccount = $printJob->user->printAccount; - $printAccount->update(['last_modified_by' => user()->id]); - $printAccount->increment('balance', $printJob->cost); - } else { - if (strpos($result['output'], "already canceled") !== false) { - return redirect()->back()->with('error', __('print.already_cancelled')); - } elseif (strpos($result['output'], "already completed") !== false) { - $printJob->state = PrintJob::SUCCESS; - return redirect()->back()->with('message', __('general.successful_modification')); - } else { - Log::warning("cannot cancel print job " . $printJob->job_id .".", [$result]); - return redirect()->back()->with('error', __('general.unknown_error')); - } - } - $printJob->save(); - } - } - - /** Private helper functions */ - - private function printJobsPaginator($printJobs, $columns) - { - $paginator = TabulatorPaginator::from($printJobs)->sortable($columns)->filterable($columns)->paginate(); - - $paginator->getCollection()->transform(PrintJob::translateStates()); - $paginator->getCollection()->transform(PrintJob::addCurrencyTag()); - - return $paginator; - } - - private function freePagesPaginator($freePages, $columns) - { - $paginator = TabulatorPaginator::from( - $freePages->join('users as creator', 'creator.id', '=', 'last_modified_by') - ->select('creator.name as modifier', 'printing_free_pages.*') - ->with('user') - )->sortable($columns)->filterable($columns)->paginate(); - return $paginator; - } - - private function updateCompletedPrintingJobs() - { - try { - $result = Commands::getCompletedPrintingJobs(); - PrintJob::whereIn('job_id', $result)->update(['state' => PrintJob::SUCCESS]); - } catch (\Exception $e) { - Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). " . $e->getMessage()); - } - } - - private function storeFile($file) - { - $path = $file->storePubliclyAs( - '', - md5(rand(0, 100000) . date('c')) . '.pdf', - 'printing' - ); - $path = Storage::disk('printing')->path($path); - - return $path; - } - - private function handleNoBalance() - { - return back()->withInput()->with('error', __('print.no_balance')); - } -} diff --git a/app/Http/Controllers/Dormitory/Printing/FreePagesController.php b/app/Http/Controllers/Dormitory/Printing/FreePagesController.php new file mode 100644 index 000000000..02c1a4541 --- /dev/null +++ b/app/Http/Controllers/Dormitory/Printing/FreePagesController.php @@ -0,0 +1,89 @@ +authorize('viewSelf', FreePages::class); + + return $this->freePagesPaginator( + freePages: user()->freePages(), + columns: [ + 'amount', + 'deadline', + 'modifier', + 'comment', + ] + ); + } + + /** + * Returns a paginated list of all `FreePages`. + * @return LengthAwarePaginator + */ + public function adminIndex() + { + $this->authorize('viewAny', FreePages::class); + + return $this->freePagesPaginator( + freePages: FreePages::with('user'), + columns: [ + 'amount', + 'deadline', + 'modifier', + 'comment', + 'user.name', + 'created_at', + ] + ); + } + + + /** + * Private helper function to create a paginator for `FreePages`. + */ + private function freePagesPaginator(Builder $freePages, array $columns) + { + $paginator = TabulatorPaginator::from( + $freePages->with('modifier') + )->sortable($columns)->filterable($columns)->paginate(); + return $paginator; + } + + /** + * Adds new free pages to a user's account. + * @param Request $request + * @return RedirectResponse + */ + public function store(Request $request) + { + $data = $request->validate([ + "user_id" => "required|exists:users,id", + "amount" => "required|integer|min:1", + "deadline" => "required|date|after:date:now", + "comment" => "string", + ]); + + $this->authorize('create', FreePages::class); + + FreePages::create($data + [ + "last_modified_by" => user()->id, + ]); + + return redirect()->back()->with('message', __('general.successfully_added')); + } +} diff --git a/app/Http/Controllers/Dormitory/Printing/PrintAccountController.php b/app/Http/Controllers/Dormitory/Printing/PrintAccountController.php new file mode 100644 index 000000000..81ec92de9 --- /dev/null +++ b/app/Http/Controllers/Dormitory/Printing/PrintAccountController.php @@ -0,0 +1,159 @@ +validate([ + 'amount' => 'required|integer', + 'user' => 'required|exists:users,id', // Normally this would be a path parameter for the PrintAccount, but we can't do that because of the limitations of blade templates + 'other_user' => 'nullable|exists:users,id', + ]); + + $printAccount = User::find($request->get('user'))->printAccount; + + // If user can not even transfer balance, we can stop here + $this->authorize('transferBalance', $printAccount); + + $otherAccount = $request->other_user ? User::find($request->get('other_user'))->printAccount : null; + + // This is a transfer between accounts + if ($otherAccount !== null) { + return $this->transferBalance($printAccount, $otherAccount, $request->get('amount')); + } + // This is a modification of the current account + else { + return $this->modifyBalance($printAccount, $request->get('amount')); + } + } + + /** + * Private helper function to transfer balance between two `PrintAccount`s. + * @param PrintAccount $printAccount The account from which the money is transfered. + * @param PrintAccount $otherAccount The account to which the money is transfered. + * @param int $amount The amount of money to be transfered. + * @return RedirectResponse + */ + private function transferBalance(PrintAccount $printAccount, PrintAccount $otherAccount, int $amount) + { + DB::beginTransaction(); + // Cannot transfer to yourself + if ($otherAccount->user_id === $printAccount->user_id) { + abort(400); + } + + // Cannot transfer from other user's account (even if you are admin) + if ($printAccount->user_id !== user()->id) { + abort(403); + } + + // This would be effectively stealing printing money from the other account + if ($amount < 0) { + abort(400); + } + + // Cannot transfer if there is not enough balance to be transfered + if ($printAccount->balance < $amount) { + return $this->returnNoBalance(); + } + + $printAccount->update([ + 'balance' => $printAccount->balance - $amount, + 'last_modified_by' => user()->id, + ]); + + $otherAccount->update([ + 'balance' => $otherAccount->balance + $amount, + 'last_modified_by' => user()->id, + ]); + + DB::commit(); + + Mail::to($printAccount->user)->queue(new ChangedPrintBalance($printAccount->user, $printAccount->user, -$amount, user()->name)); + Mail::to($otherAccount->user)->queue(new ChangedPrintBalance($otherAccount->user, $printAccount->user, -$amount, user()->name)); + Mail::to($printAccount->user)->queue(new ChangedPrintBalance($printAccount->user, $otherAccount->user, $amount, user()->name)); + Mail::to($otherAccount->user)->queue(new ChangedPrintBalance($otherAccount->user, $otherAccount->user, $amount, user()->name)); + + return redirect()->back()->with('message', __('general.successful_transaction')); + } + + /** + * Private helper function to modify the balance of a `PrintAccount`. + * @param PrintAccount $printAccount The account to be modified. + * @param int $amount The amount of money to be added or subtracted. + * @return RedirectResponse + */ + private function modifyBalance(PrintAccount $printAccount, int $amount) + { + DB::beginTransaction(); + // Only admins can modify accounts + $this->authorize('modify', $printAccount); + + if ($amount < 0 && $printAccount->balance < abs($amount)) { + return $this->returnNoBalance(); + } + + $printAccount->update([ + 'balance' => $printAccount->balance + $amount, + 'last_modified_by' => user()->id, + ]); + + $adminCheckout = Checkout::admin(); + Transaction::create([ + 'checkout_id' => $adminCheckout->id, + 'receiver_id' => user()->id, + 'payer_id' => $printAccount->user->id, + 'semester_id' => Semester::current()->id, + 'amount' => $amount, + 'payment_type_id' => PaymentType::print()->id, + 'comment' => null, + 'moved_to_checkout' => null, + ]); + + DB::commit(); + + Mail::to(user())->queue(new ChangedPrintBalance(user(), $printAccount->user, $amount, user()->name)); + + //Do not send duplicate emails + if($printAccount->user->id !== user()->id) { + Mail::to($printAccount->user)->queue(new ChangedPrintBalance($printAccount->user, $printAccount->user, $amount, user()->name)); + } + + return redirect()->back()->with('message', __('general.successful_modification')); + } + + /** + * Private helper function to return a redirect with an error message if there is not enough balance. + * @return RedirectResponse + * @throws BindingResolutionException + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + */ + private function returnNoBalance() + { + return back()->withInput()->with('error', __('print.no_balance')); + } +} diff --git a/app/Http/Controllers/Dormitory/Printing/PrintAccountHistoryController.php b/app/Http/Controllers/Dormitory/Printing/PrintAccountHistoryController.php new file mode 100644 index 000000000..66a9ba602 --- /dev/null +++ b/app/Http/Controllers/Dormitory/Printing/PrintAccountHistoryController.php @@ -0,0 +1,34 @@ +authorize('viewAny', PrintJob::class); + + $columns = ['user.name', 'balance_change', 'free_page_change', 'deadline_change', 'modifier.name', 'modified_at']; + return TabulatorPaginator::from( + PrintAccountHistory::with(['user', 'modifier'])->select('print_account_history.*') + )->sortable($columns) + ->filterable($columns) + ->paginate(); + } +} diff --git a/app/Http/Controllers/Dormitory/Printing/PrintJobController.php b/app/Http/Controllers/Dormitory/Printing/PrintJobController.php new file mode 100644 index 000000000..464482561 --- /dev/null +++ b/app/Http/Controllers/Dormitory/Printing/PrintJobController.php @@ -0,0 +1,185 @@ +authorize('viewSelf', PrintJob::class); + + PrinterHelper::updateCompletedPrintJobs(); + + return $this->paginatorFrom( + printJobs: user() + ->printJobs() + ->orderBy('created_at', 'desc'), + columns: [ + 'created_at', + 'filename', + 'cost', + 'state', + ] + ); + } + + /** + * Returns a paginated list of all `PrintJob`s. + * @return LengthAwarePaginator + */ + public function adminIndex() + { + $this->authorize('viewAny', PrintJob::class); + + PrinterHelper::updateCompletedPrintJobs(); + + return $this->paginatorFrom( + printJobs: PrintJob::with('user') + ->orderBy('print_jobs.created_at', 'desc'), + columns: [ + 'created_at', + 'filename', + 'cost', + 'state', + 'user.name', + ] + ); + } + + /** + * Prints a document, then stores the corresponding `PrintJob`. + * @param Request $request + * @return RedirectResponse + */ + public function store(Request $request) + { + DB::beginTransaction(); + + $request->validate([ + 'file' => 'required|file', + 'copies' => 'required|integer|min:1', + 'two_sided' => 'in:on,off', + 'use_free_pages' => 'in:on,off', + ]); + + $useFreePages = $request->boolean('use_free_pages'); + $copyNumber = $request->input('copies'); + $twoSided = $request->boolean('two_sided'); + $file = $request->file('file'); + + /** @var Printer */ + $printer = Printer::firstWhere('name', config('print.printer_name')); + + $path = $file->store('', 'printing'); + $originalName = $file->getClientOriginalName(); + $pageNumber = PrinterHelper::getDocumentPageNumber(Storage::disk('printing')->path($path)); + + /** @var PrintAccount */ + $printAccount = user()->printAccount; + + if (!$printAccount->hasEnoughBalanceOrFreePages($useFreePages, $pageNumber, $copyNumber, $twoSided)) { + DB::rollBack(); + return back()->with('error', __('print.no_balance')); + } + + $cost = $useFreePages ? + PrinterHelper::getFreePagesNeeded($pageNumber, $copyNumber, $twoSided) : + PrinterHelper::getBalanceNeeded($pageNumber, $copyNumber, $twoSided); + + $printAccount->updateHistory($useFreePages, $cost); + + try { + $printJob = $printer->createPrintJob($useFreePages, $cost, Storage::disk('printing')->path($path), $originalName, $twoSided, $copyNumber); + Log::info("User $printAccount->user_id started print job a document for $cost. Job ID: $printJob->job_id. Used free pages: $useFreePages. File: $originalName"); + } catch (\Exception $e) { + DB::rollBack(); + Log::error("Error while creating print job: " . $e->getMessage()); + return back()->with('error', __('print.error_printing')); + } finally { + Storage::disk('printing')->delete($path); + } + + DB::commit(); + + return back()->with('message', __('print.success')); + } + + /** + * Cancels a `PrintJob` + * @param PrintJob $job + * @return RedirectResponse + */ + public function update(PrintJob $job, Request $request) + { + $this->authorize('update', $job); + + $data = $request->validate([ + 'state' => ['required', Rule::enum(PrintJobStatus::class)->only(PrintJobStatus::CANCELLED)], + ]); + + /** @var PrintJobStatus */ + $newState = $data['state']; + + switch ($newState->value) { + case PrintJobStatus::CANCELLED: + if ($job->state === PrintJobStatus::QUEUED) { + /** @var PrinterCancelResult */ + $result = $job->cancel(); + + if ($result === PrinterCancelResult::Success) { + return back()->with('message', __('general.successful_modification')); + } + return back()->with('error', __("print.$result->value")); + } + return back()->with('error', __('print.cannot_cancel')); + default: + abort(422); + } + } + + /** + * Returns a paginated list of `PrintJob`s. + * @param Builder $printJobs + * @param array $columns + * @return LengthAwarePaginator + * @throws BindingResolutionException + * @throws InvalidArgumentException + */ + private function paginatorFrom(Builder $printJobs, array $columns) + { + $paginator = TabulatorPaginator::from($printJobs)->sortable($columns)->filterable($columns)->paginate(); + + // Process the data before showing it in a table. + $paginator->getCollection()->append([ + 'translated_cost', + 'translated_state', + ]); + + return $paginator; + } +} diff --git a/app/Http/Controllers/Dormitory/Printing/PrinterController.php b/app/Http/Controllers/Dormitory/Printing/PrinterController.php new file mode 100644 index 000000000..b67328581 --- /dev/null +++ b/app/Http/Controllers/Dormitory/Printing/PrinterController.php @@ -0,0 +1,64 @@ + User::all(), + "user" => user(), + "printer" => Printer::firstWhere('name', config('print.printer_name')), + ]); + } + + /** + * Returns the admin print page. + * @return View + */ + public function adminIndex() + { + $this->authorize('handleAny', PrintAccount::class); + + return view('dormitory.print.manage.app', ["users" => User::all()]); + } + + /** + * Sets the given printer's out of paper sign. + */ + public function update(Request $request, Printer $printer) + { + $request->validate([ + "no_paper" => "boolean", + ]); + + if ($request->boolean("no_paper")) { + if ($printer->paper_out_at === null || now()->diffInMinutes($printer->paper_out_at) > 30) { + Mail::to(User::withRole(Role::SYS_ADMIN)->get())->queue(new NoPaper(user()->name)); + } + $printer->update(['paper_out_at' => now()]); + return redirect()->back()->with('message', __('mail.email_sent')); + } else { + $this->authorize('handleAny', PrintAccount::class); + $printer->update([ + "paper_out_at" => null, + ]); + return redirect()->back()->with('message', __('general.successful_modification')); + } + } +} diff --git a/app/Http/Controllers/Secretariat/DocumentController.php b/app/Http/Controllers/Secretariat/DocumentController.php index 1932d4e27..d19730080 100644 --- a/app/Http/Controllers/Secretariat/DocumentController.php +++ b/app/Http/Controllers/Secretariat/DocumentController.php @@ -158,7 +158,7 @@ private function generatePDF($path, $data) // TODO: figure out result Commands::latexToPdf($pathTex, $outputDir); - if (config('app.debug')) { + if (config('app.debug') && !config('commands.run_in_debug')) { return $pathTex; } else { return $pathPdf; diff --git a/app/Mail/ChangedPrintBalance.php b/app/Mail/ChangedPrintBalance.php index 492ad5cf0..2a4f5b89b 100644 --- a/app/Mail/ChangedPrintBalance.php +++ b/app/Mail/ChangedPrintBalance.php @@ -12,6 +12,7 @@ class ChangedPrintBalance extends Mailable use SerializesModels; public $recipient; //User model + public $print_account_holder; //User model public $amount; //how much the balance has changed public $modifier; //modifier's name @@ -20,9 +21,10 @@ class ChangedPrintBalance extends Mailable * * @return void */ - public function __construct($recipient, $amount, $modifier) + public function __construct($recipient, $print_account_holder, $amount, $modifier) { $this->recipient = $recipient; + $this->print_account_holder = $print_account_holder; $this->amount = $amount; $this->modifier = $modifier; } diff --git a/app/Mail/NoPaper.php b/app/Mail/NoPaper.php index ec3fcaf24..7ba4f4965 100644 --- a/app/Mail/NoPaper.php +++ b/app/Mail/NoPaper.php @@ -11,15 +11,13 @@ class NoPaper extends Mailable use Queueable; use SerializesModels; - public string $recipient; public string $reporter; /** * Create a new message instance. */ - public function __construct(string $recipient, string $reporter) + public function __construct(string $reporter) { - $this->recipient = $recipient; $this->reporter = $reporter; } diff --git a/app/Models/FreePages.php b/app/Models/FreePages.php index 936e0fdc5..e06de7551 100644 --- a/app/Models/FreePages.php +++ b/app/Models/FreePages.php @@ -2,8 +2,10 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * Model to keep track of the users' free pages. @@ -51,23 +53,73 @@ class FreePages extends Model 'comment', ]; - public function user() + protected $casts = [ + 'deadline' => 'date', + ]; + + /** + * The user this free pages entry belongs to. + * @return BelongsTo + */ + public function user(): BelongsTo { - return $this->belongsTo('App\Models\User'); + return $this->belongsTo(User::class); } + /** + * The print account this free pages entry belongs to. + * @return BelongsTo + */ public function printAccount() { - return $this->belongsTo('App\Models\PrintAccount', 'user_id', 'user_id'); + return $this->belongsTo(PrintAccount::class, 'user_id', 'user_id'); + } + + /** + * Wether the free pages are still available. + * @return bool + */ + protected function available(): Attribute + { + return Attribute::make( + get: fn () => now()->isBefore($this->deadline) + ); } - public function available() + /** + * The user who last modified this free pages entry. + * @return BelongsTo + */ + public function modifier(): BelongsTo + { + return $this->belongsTo(User::class, 'last_modified_by'); + } + + + /** + * Returns the amount of pages that can be subtracted from the free pages. + * If the amount is greater than the available pages, only the available pages are subtracted. + * @param int $amount + * @return int + */ + public function calculateSubtractablePages(int $amount) { - return $this->deadline > date('Y-m-d'); + return min($amount, $this->amount); } - public function lastModifiedBy() + /** + * Subtracts the given amount of pages from the free pages. + * If the amount is greater than the available pages, only the available pages are subtracted. + * @param int $amount + */ + public function subtractPages(int $amount) { - return User::find($this->last_modified_by); + if ($amount <= 0 || $amount > $this->amount) { + throw new \InvalidArgumentException("Amount must be greater than 0 and less than or equal to the available pages."); + } + $this->update([ + 'last_modified_by' => user()->id, + 'amount' => $this->amount - $amount, + ]); } } diff --git a/app/Models/PrintAccount.php b/app/Models/PrintAccount.php index 8089f590d..2ccb29cf3 100644 --- a/app/Models/PrintAccount.php +++ b/app/Models/PrintAccount.php @@ -2,8 +2,14 @@ namespace App\Models; +use App\Utils\PrinterHelper; +use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Container\ContainerExceptionInterface; /** * Model to keep track of the users' print balance. @@ -31,7 +37,6 @@ class PrintAccount extends Model { use HasFactory; - protected $table = 'print_accounts'; protected $primaryKey = 'user_id'; public $incrementing = false; public $timestamps = false; @@ -54,31 +59,99 @@ class PrintAccount extends Model public function user() { - return $this->belongsTo('App\Models\User'); + return $this->belongsTo(User::class); } public function freePages() { - return $this->hasMany('App\Models\FreePages', 'user_id', 'user_id'); + return $this->hasMany(FreePages::class, 'user_id', 'user_id'); } - public function hasEnoughMoney($balance) + /** + * The free pages which are currently available. Sorts the free pages by their deadline. + * @return Collection + */ + public function availableFreePages() { - return $this->balance >= abs($balance); + return $this->freePages()->where('deadline', '>', now())->orderBy('deadline')->get(); } - public static function getCost($pages, $is_two_sided, $number_of_copies) + /** + * Returns wether the user has enough free pages to print a document. + * A free page is enough to print either a one sided or a two sided page. + * @param int $pages + * @param int $copies + * @param bool $twoSided + * @return bool + */ + public function hasEnoughFreePages(int $pages, int $copies, bool $twoSided) { - if (!$is_two_sided) { - return $pages * self::$COST['one_sided'] * $number_of_copies; - } + return $this->availableFreePages()->sum('amount') >= + PrinterHelper::getFreePagesNeeded($pages, $copies, $twoSided); + } - $orphan_ending = $pages % 2; - $one_copy_cost = floor($pages / 2) * self::$COST['two_sided'] - + $orphan_ending * self::$COST['one_sided']; + /** + * Returns wether the user has enough balance to print a document. + * @param int $pages + * @param int $copies + * @param bool $twoSided + * @return bool + */ + public function hasEnoughBalance(int $pages, int $copies, bool $twoSided) + { + return $this->balance >= PrinterHelper::getBalanceNeeded($pages, $copies, $twoSided); + } - return $one_copy_cost * $number_of_copies; + /** + * Returns wether the user has enough balance or free pages to print a document. + * @param bool $useFreePages + * @param int $pages + * @param int $copies + * @param bool $twoSided + * @return bool + */ + public function hasEnoughBalanceOrFreePages(bool $useFreePages, int $pages, int $copies, bool $twoSided) + { + return $useFreePages ? $this->hasEnoughFreePages($pages, $copies, $twoSided) : $this->hasEnoughBalance($pages, $copies, $twoSided); } -} -PrintAccount::$COST = config('print.cost'); + /** + * Updates the print account history and the print account balance. + * Important note: This function should only be called within a transaction. Otherwise, the history may not be consistent. + * @param bool $useFreePages + * @param int $cost + */ + public function updateHistory(bool $useFreePages, int $cost) + { + // Update the print account history + $this->last_modified_by = user()->id; + + if ($useFreePages) { + $freePagesToSubtract = $cost; + $availableFreePages = $this->availableFreePages()->where('amount', '>', 0); + + // Subtract the pages from the free pages pool, as many free pages as necessary + /** @var FreePages */ + foreach ($availableFreePages as $freePages) { + $subtractablePages = $freePages->calculateSubtractablePages($freePagesToSubtract); + $freePages->subtractPages($subtractablePages); + $freePagesToSubtract -= $subtractablePages; + + if ($freePagesToSubtract <= 0) { // < should not be necessary, but better safe than sorry + break; + } + } + // Set value in the session so that free page checkbox stays checked + session()->put('use_free_pages', true); + } else { + $this->balance -= $cost; + + // Remove value regarding the free page checkbox from the session + session()->remove('use_free_pages'); + } + + $this->save(); + } + + +} diff --git a/app/Models/PrintAccountHistory.php b/app/Models/PrintAccountHistory.php index 75c0d79bb..3471eb89c 100644 --- a/app/Models/PrintAccountHistory.php +++ b/app/Models/PrintAccountHistory.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Model; // Note: the elements of this class should no be changed manually. -// Triggers are set up in the database (see migration). +// Observers for updating entries are set up. /** * App\Models\PrintAccountHistory * @@ -47,11 +47,11 @@ class PrintAccountHistory extends Model public function user() { - return $this->belongsTo('App\Models\User', 'user_id'); + return $this->belongsTo(User::class); } public function modifier() { - return $this->belongsTo('App\Models\User', 'modified_by'); + return $this->belongsTo(User::class, 'modified_by'); } } diff --git a/app/Models/PrintJob.php b/app/Models/PrintJob.php index 25f73b0e5..195e82ba2 100644 --- a/app/Models/PrintJob.php +++ b/app/Models/PrintJob.php @@ -2,8 +2,16 @@ namespace App\Models; +use App\Enums\PrinterCancelResult; +use App\Enums\PrintJobStatus; +use App\Utils\Process; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOneThrough; +use Illuminate\Support\Facades\DB; +use Log; /** * App\Models\PrintJob @@ -37,46 +45,121 @@ class PrintJob extends Model { use HasFactory; - protected $table = 'print_jobs'; - protected $primaryKey = 'id'; - public $incrementing = true; public $timestamps = true; - public const QUEUED = 'QUEUED'; - public const ERROR = 'ERROR'; - public const CANCELLED = 'CANCELLED'; - public const SUCCESS = 'SUCCESS'; - public const STATES = [ - self::QUEUED, - self::ERROR, - self::CANCELLED, - self::SUCCESS, + protected $fillable = [ + 'user_id', + 'state', + 'job_id', + 'cost', + 'printer_id', + 'used_free_pages', + 'filename', ]; - protected $fillable = [ - 'filename', 'filepath', 'user_id', 'state', 'job_id', 'cost', + protected $casts = [ + 'state' => PrintJobStatus::class, + 'used_free_pages' => 'boolean', ]; public function user() { - return $this->belongsTo('App\Models\User'); + return $this->belongsTo(User::class); + } + + /** + * `Printer` which this `PrintJob` was sent to. + * @return BelongsTo + */ + public function printer() + { + return $this->belongsTo(Printer::class); + } + + /** + * `PrintAccount` which is related to this `PrintJob` through the `User`. + * The `PrintJob` and the `PrintAccount` both belong to the `User`, in this sense this relationship is articifial. + * Trying to fix the decision made for the database a few years ago. + * @return HasOneThrough + */ + public function printAccount() + { + return $this->hasOneThrough( + PrintAccount::class, + User::class, + 'id', // Foreign key on users + 'user_id', // Foreign key on print_accounts + 'user_id', // Local key on print_jobs + 'id', // Local key on users + ); } - public static function translateStates(): \Closure + /** + * Attribute for the translated cost. + */ + public function translatedCost(): Attribute { - return function ($data) { - $data->state = __('print.'.strtoupper($data->state)); + return Attribute::make( + get: fn () => $this->used_free_pages ? "$this->cost ingyenes oldal" : "$this->cost HUF" + ); + } - return $data; - }; + /** + * Attribute for the translated state. + */ + public function translatedState(): Attribute + { + return Attribute::make( + get: fn () => __("print." . strtoupper($this->state->value)) + ); } - public static function addCurrencyTag(): \Closure + /** + * Attemts to cancel the given `PrintJob`. Returns wether it was successful. + * @param PrintJob $this + * @return PrinterCancelResult + */ + public function cancel() { - return function ($data) { - $data->cost = "{$data->cost} HUF"; + return DB::transaction(function () { + $printer = $this->printer; + $process = new Process([config('commands.cancel'), $this->job_id, '-h', "$printer->ip:$printer->port"]); + $process->run(); + $result = ['output' => $process->getOutput(), 'exit_code' => $process->getExitCode()]; + + if ($result['exit_code'] == 0) { + $this->update([ + 'state' => PrintJobStatus::CANCELLED, + ]); + $printAccount = $this->printAccount; + $printAccount->last_modified_by = user()->id; + + if ($this->used_free_pages) { + $pages = $printAccount->availableFreePages()->first(); + $pages->update([ + 'last_modified_by' => user()->id, + 'amount' => $pages->amount + $this->cost, + ]); + } else { + $printAccount->balance += $this->cost; + } - return $data; - }; + $this->save(); + return PrinterCancelResult::Success; + } + if (strpos($result['output'], "already canceled") !== false) { + $this->update([ + 'state' => PrintJobStatus::CANCELLED, + ]); + return PrinterCancelResult::AlreadyCancelled; + } + if (strpos($result['output'], "already completed") !== false) { + $this->update([ + 'state' => PrintJobStatus::SUCCESS, + ]); + return PrinterCancelResult::AlreadyCompleted; + } + return PrinterCancelResult::CannotCancel; + }); } } diff --git a/app/Models/Printer.php b/app/Models/Printer.php new file mode 100644 index 000000000..34a403394 --- /dev/null +++ b/app/Models/Printer.php @@ -0,0 +1,150 @@ + 'datetime', + ]; + + /** + * Returns the `PrintJob`s that were executed by this printer. + * @return HasMany + */ + public function printJobs() + { + return $this->hasMany(PrintJob::class); + } + + + /** + * Starts a new print job for the current user and saves it. + * @param bool $useFreePages + * @param int $cost + * @param string $filePath + * @param string $originalName + * @param bool $twoSided + * @param int $copyNumber + * @return Model + * @throws AuthenticationException + * @throws PrinterException + * @throws MassAssignmentException + */ + public function createPrintJob(bool $useFreePages, int $cost, string $filePath, string $originalName, bool $twoSided, int $copyNumber) + { + $jobId = $this->print($twoSided, $copyNumber, $filePath); + + return user()->printJobs()->create([ + 'printer_id' => $this->id, + 'state' => PrintJobStatus::QUEUED, + 'job_id' => $jobId, + 'cost' => $cost, + 'used_free_pages' => $useFreePages, + 'filename' => $originalName, + ]); + } + + /** + * Asks the printer to print a document with the given configuration. + * @param bool $twoSided + * @param int $copies + * @param string $path + * @return int The `jobId` belonging to the printjob + * @throws PrinterException If the printing fails + */ + public function print(bool $twoSided, int $copies, string $path) + { + if (config('app.debug')) { + return -1; + } + $jobId = null; + try { + $process = new Process([ + 'lp', + '-h', "$this->ip:$this->port", + '-d', $this->name, + ($twoSided ? '-o sides=two-sided-long-edge' : ''), + '-n', $copies, + $path + ]); + $process->run(); + if (!$process->isSuccessful()) { + Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). " . $process->getErrorOutput()); + throw new PrinterException($process->getErrorOutput()); + } + $result = $process->getOutput(); + if (!preg_match("/^request id is ([^\s]*) \\([0-9]* file\\(s\\)\\)$/", $result, $matches)) { + Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). result:" + . print_r($result, true)); + throw new PrinterException($result); + } + $jobId = intval($matches[1]); + } catch (\Exception $e) { + Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). " . $e->getMessage()); + throw new PrinterException($e->getMessage(), $e->getCode(), $e->getPrevious()); + } + + return $jobId; + } + + /** + * Returns the completed printjobs for this printer. + * @return array + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + */ + public function getCompletedPrintJobs() + { + try { + $process = Process::fromShellCommandline(config('commands.lpstat') . " -h $this->ip:$this->port -W completed -o $this->name | awk '{print $1}'"); + $process->run(); + $result = explode("\n", $process->getOutput()); + return $result; + } catch (\Exception $e) { + Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). " . $e->getMessage()); + throw new PrinterException($e->getMessage(), $e->getCode(), $e->getPrevious()); + } + } + + /** + * Updates the state of the completed printjobs to `PrintJobStatus::SUCCESS`. + */ + public function updateCompletedPrintJobs() + { + return DB::transaction(function () { + PrintJob::where('state', PrintJobStatus::QUEUED)->whereIn( + 'job_id', + $this->getCompletedPrintJobs() + )->update(['state' => PrintJobStatus::SUCCESS]); + }); + } +} + +class PrinterException extends \Exception +{ + // +} diff --git a/app/Models/Role.php b/app/Models/Role.php index 7cf8e62cb..26a46f483 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -103,9 +103,6 @@ class Role extends Model self::ETHICS_COMMISSIONER, ]; - // Module-related roles - public const PRINTER = 'printer'; - //collegist related roles public const RESIDENT = 'resident'; public const EXTERN = 'extern'; @@ -122,7 +119,6 @@ class Role extends Model self::SECRETARY, self::DIRECTOR, self::STAFF, - self::PRINTER, self::LOCALE_ADMIN, self::STUDENT_COUNCIL, self::STUDENT_COUNCIL_SECRETARY, @@ -306,7 +302,6 @@ public function color(): string self::SECRETARY => 'indigo', self::DIRECTOR => 'blue', self::STAFF => 'cyan', - self::PRINTER => 'teal', self::LOCALE_ADMIN => 'amber', self::STUDENT_COUNCIL => 'green darken-4', self::APPLICATION_COMMITTEE_MEMBER => 'light-blue darken-4', diff --git a/app/Models/User.php b/app/Models/User.php index 62b81c0c5..23f3981f9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1092,15 +1092,6 @@ public static function staff(): ?User { return self::withRole(Role::STAFF)->first(); } - - /** - * @return array|Collection|User[] the users with printer role - */ - public static function printers(): Collection|array - { - return self::withRole(Role::PRINTER)->get(); - } - /** * @return array|Collection|User[] the users with printer role */ diff --git a/app/Policies/FreePagesPolicy.php b/app/Policies/FreePagesPolicy.php index d1800a264..bf6f0d388 100644 --- a/app/Policies/FreePagesPolicy.php +++ b/app/Policies/FreePagesPolicy.php @@ -16,10 +16,6 @@ public function before(User $user) if ($user->isAdmin()) { return true; } - - if (!$user->hasRole(Role::PRINTER)) { - return false; - } } public function create(User $user) diff --git a/app/Policies/PrintAccountPolicy.php b/app/Policies/PrintAccountPolicy.php index 4269655dd..f3105fed4 100644 --- a/app/Policies/PrintAccountPolicy.php +++ b/app/Policies/PrintAccountPolicy.php @@ -16,9 +16,6 @@ public function before(User $user, $ability) if ($user->isAdmin()) { return true; } - if (!$user->hasRole(Role::PRINTER)) { - return false; - } } /** @@ -43,4 +40,14 @@ public function modify(User $user): bool { return false; } + /** + * Determine whether the user can transfer balance from the print account. + * @param User $user + * @param PrintAccount $printAccount + * @return bool + */ + public function transferBalance(User $user, PrintAccount $printAccount): bool + { + return $user->id == $printAccount->user_id; + } } diff --git a/app/Policies/PrintJobPolicy.php b/app/Policies/PrintJobPolicy.php index 78620027b..ef6184d38 100644 --- a/app/Policies/PrintJobPolicy.php +++ b/app/Policies/PrintJobPolicy.php @@ -16,9 +16,6 @@ public function before(User $user) if ($user->isAdmin()) { return true; } - if (!$user->hasRole(Role::PRINTER)) { - return false; - } } /** diff --git a/app/Utils/Printer.php b/app/Utils/Printer.php deleted file mode 100644 index 2cd5ab6ad..000000000 --- a/app/Utils/Printer.php +++ /dev/null @@ -1,163 +0,0 @@ -filename = $filename; - $this->path = $path; - $this->is_two_sided = $is_two_sided; - $this->number_of_copies = $number_of_copies; - $this->use_free_pages = $use_free_pages; - $this->print_account = user()->printAccount; - } - - public function print() - { - // Getting the number of pages from the document - $errors = $this->setPages(); - if ($errors != null) { - return $errors; - } - - // If using free pages, check the amount that can be used - if ($this->use_free_pages) { - - if (!$this->planFreePageUsage()) { - return back()->withInput()->with('error', __('print.no_balance')); - } - - // Print document - return $this->printDocument(); - } else { - - // Calculate cost - $this->cost = PrintAccount::getCost($this->pages, $this->is_two_sided, $this->number_of_copies); - - // Check balance - if (!$this->print_account->hasEnoughMoney($this->cost)) { - return back()->withInput()->with('error', __('print.no_balance')); - } - - // Print document - return $this->printDocument(); - } - } - - /** - * Only calculating the values here to see how many pages can be covered free of charge. - */ - private function planFreePageUsage() - { - $requested_pages = $this->number_of_copies * $this->pages; - $this->free_page_update = []; - $deducted_pages = 0; - $all_pages = user()->freePages - ->where('deadline', '>', Carbon::now()) - ->sortBy('deadline'); - - foreach ($all_pages as $key => $free_page) { - $deduce_from_current = min($requested_pages - $deducted_pages, $free_page->amount); - $this->free_page_update[] = [ - 'page' => $free_page, - 'new_amount' => $free_page->amount - $deduce_from_current - ]; - $deducted_pages += $deduce_from_current; - if($deducted_pages == $requested_pages) { - break; - } - } - return $deducted_pages == $requested_pages; - } - - private function printDocument() - { - // Print file and return on error - if (!$this->printFile()) { - return back()->with('error', __('print.error_printing')); - } - - // Update print account history - $this->print_account->update(['last_modified_by' => user()->id]); - foreach ($this->free_page_update as $fp) { - $fp['page']->update([ - 'amount' => $fp['new_amount'], - 'last_modified_by' => user()->id - ]); - } - - // Update print account - $this->print_account->decrement('balance', $this->cost); - - return back()->with('message', __('print.success')); - } - - private function printFile() - { - $printer_name = config('print.printer_name'); - $state = PrintJob::QUEUED; - try { - $command = "lp " . config('print.additional_args') . " -d " . $printer_name - . ($this->is_two_sided ? " -o sides=two-sided-long-edge " : " ") - . "-n " . $this->number_of_copies . " " - . $this->path . " 2>&1"; - $result = Commands::print($command); - if (!preg_match("/^request id is ([^\s]*) \\([0-9]* file\\(s\\)\\)$/", $result, $job)) { - Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). result:" - . print_r($result, true) . ". Command: " . $command); - $state = PrintJob::ERROR; - } - $job_id = $job[1]; - } catch (\Exception $e) { - Log::error("Printing error at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). " . $e->getMessage()); - $state = PrintJob::ERROR; - $job_id = ""; - $this->path = ""; - } - - PrintJob::create([ - 'filename' => $this->filename, - 'filepath' => $this->path, - 'user_id' => user()->id, - 'state' => $state, - 'job_id' => $job_id, - 'cost' => $this->cost, - ]); - return $state == PrintJob::QUEUED; - } - - private function setPages() - { - try { - $this->pages = Commands::getPages($this->path); - } catch (\Exception $e) { - Log::error("File retrieval exception at line: " . __FILE__ . ":" . __LINE__ . " (in function " . __FUNCTION__ . "). " . $e->getMessage()); - $this->pages = ""; - } - - if ($this->pages == "" || !is_numeric($this->pages) || $this->pages <= 0) { - Log::error("Cannot get number of pages for uploaded file!" . print_r($this->pages, true)); - return back()->withInput()->with('error', __('print.invalid_pdf')); - } - return null; - } -} diff --git a/app/Utils/PrinterHelper.php b/app/Utils/PrinterHelper.php new file mode 100644 index 000000000..dc7741454 --- /dev/null +++ b/app/Utils/PrinterHelper.php @@ -0,0 +1,87 @@ +run(); + $result = intval($process->getOutput(strval(rand(1, 10)))); + return $result; + } + + /** + * Returns an array with the number of one-sided and two-sided pages needed to print the given number of pages. + * @param int $pages + * @param bool $twoSided + * @return array + */ + public static function getPageTypesNeeded(int $pages, bool $twoSided) + { + $oneSidedPages = 0; + $twoSidedPages = 0; + if (!$twoSided) { + $oneSidedPages = $pages; + } else { + $oneSidedPages = $pages % 2; + $twoSidedPages = floor($pages / 2); + } + + return [ + 'one_sided' => $oneSidedPages, + 'two_sided' => $twoSidedPages, + ]; + } + + /** + * Returns the number of free pages needed to print with given configuration. + * @param int $pages + * @param mixed $copies + * @param mixed $twoSided + * @return int|float + */ + public static function getFreePagesNeeded(int $pages, int $copies, bool $twoSided) + { + return $pages * $copies; //We are charging for each of the printed sides of paper + } + + /** + * Returns the amount of money needed to print with given configuration. + * @param int $pages + * @param int $copies + * @param bool $twoSided + * @return mixed + * @throws BindingResolutionException + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + */ + public static function getBalanceNeeded(int $pages, int $copies, bool $twoSided) + { + $pageTypesNeeded = self::getPageTypesNeeded($pages, $twoSided); + + return $pageTypesNeeded['one_sided'] * config('print.one_sided_cost') * $copies + + $pageTypesNeeded['two_sided'] * config('print.two_sided_cost') * $copies; + } + + /** + * Gets the printjob-status with every printer, updates the status of the completed printjobs. + */ + public static function updateCompletedPrintJobs() + { + foreach(PrintJob::where('state', PrintJobStatus::QUEUED)->whereNotNull('printer_id')->pluck('printer_id')->unique() as $printer_id) { + Printer::find($printer_id)->updateCompletedPrintJobs(); + } + } +} diff --git a/app/Utils/Process.php b/app/Utils/Process.php new file mode 100644 index 000000000..7552134b8 --- /dev/null +++ b/app/Utils/Process.php @@ -0,0 +1,63 @@ +getCommandLine() . " executed successfully. With output: " . $this->getOutput() . " and error output: " . $this->getErrorOutput()); + } else { + Log::error("Command: " . $this->getCommandLine() . " failed with error code: " . $return_value . "\nWith output: " . $this->getOutput() . " and error output: " . $this->getErrorOutput()); + } + } + return $return_value; + } + Log::info("Process not executed in debug mode."); + return 0; + } + + /** + * Get the output of the process. + * + * @param string $debugOutput + * @return string + */ + public function getOutput(string $debugOutput = ''): string + { + if (config('app.debug') === false || config('commands.run_in_debug') === true) { + return parent::getOutput(); + } + Log::info("Process output not available in debug mode."); + return $debugOutput; + } + + /** + * Get the exit code of the process. + * + * @param string $debugOutput + * @return int|null + */ + public function getExitCode(): ?int + { + if (config('app.debug') === false || config('commands.run_in_debug') === true) { + return parent::getExitCode(); + } + Log::info("Process exit code not available in debug mode."); + return 0; + } +} diff --git a/config/commands.php b/config/commands.php index c3ea4f12c..528076759 100644 --- a/config/commands.php +++ b/config/commands.php @@ -6,4 +6,5 @@ 'pdfinfo' => env('PDFINFO_COMMAND', 'pdfinfo'), 'ping' => env('PING_COMMAND', 'ping'), 'pdflatex' => env('PDFLATEX_COMMAND', '/usr/bin/pdflatex'), + 'run_in_debug' => env('RUN_COMMANDS_IN_DEBUG_MODE', false), ]; diff --git a/config/print.php b/config/print.php index 2e3e78244..cac1c2444 100644 --- a/config/print.php +++ b/config/print.php @@ -2,10 +2,8 @@ return [ - 'cost' => [ - 'one_sided' => env('PRINT_COST_ONESIDED'), - 'two_sided' => env('PRINT_COST_TWOSIDED'), - ], + 'one_sided_cost' => env('PRINT_COST_ONESIDED'), + 'two_sided_cost' => env('PRINT_COST_TWOSIDED'), // maximum accepted PDF size in kilobytes 'pdf_size_limit' => env('PRINT_MAX_FILE_SIZE', 10000), diff --git a/database/factories/PrintJobFactory.php b/database/factories/PrintJobFactory.php index 801b7ee4c..d29dbbc96 100644 --- a/database/factories/PrintJobFactory.php +++ b/database/factories/PrintJobFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Enums\PrintJobStatus; use App\Models\PrintJob; use Illuminate\Database\Eloquent\Factories\Factory; @@ -12,9 +13,9 @@ class PrintJobFactory extends Factory public function definition() { return [ + 'printer_id' => 1, 'filename' => $this->faker->text, - 'filepath' => $this->faker->text, - 'state' => $this->faker->randomElement(PrintJob::STATES), + 'state' => $this->faker->randomElement(PrintJobStatus::cases()), 'job_id' => $this->faker->randomNumber, 'cost' => $this->faker->numberBetween(8, 1000), ]; diff --git a/database/migrations/2019_10_06_224327_create_print_jobs_table.php b/database/migrations/2019_10_06_224327_create_print_jobs_table.php index 0f3817600..f20bacf52 100644 --- a/database/migrations/2019_10_06_224327_create_print_jobs_table.php +++ b/database/migrations/2019_10_06_224327_create_print_jobs_table.php @@ -1,5 +1,6 @@ text('filename'); $table->text('filepath'); $table->unsignedBigInteger('user_id')->nullable(); - $table->set('state', \App\Models\PrintJob::STATES); + $table->set('state', array_map(fn ($state) => $state->value, PrintJobStatus::cases())); $table->unsignedBigInteger('job_id'); $table->unsignedInteger('cost'); $table->timestamps(); diff --git a/database/migrations/2019_10_13_130718_create_roles_table.php b/database/migrations/2019_10_13_130718_create_roles_table.php index db80fd83f..e62fd1f04 100644 --- a/database/migrations/2019_10_13_130718_create_roles_table.php +++ b/database/migrations/2019_10_13_130718_create_roles_table.php @@ -1,6 +1,5 @@ $role) { - DB::table('roles')->insert(['name' => $role]); + foreach (['sys-admin', 'collegist', 'tenant', 'workshop-administrator', 'workshop-leader', 'application-committee', 'aggregated-application-committee', 'secretary', 'director', 'staff', 'printer', 'locale-admin', 'student-council', 'student-council-secretary', 'board-of-trustees-member', 'ethics-commissioner', 'alumni', 'receptionist'] as $role_name) { + DB::table('roles')->insert(['name' => $role_name]); } } diff --git a/database/migrations/2023_12_25_182310_create_printers_table.php b/database/migrations/2023_12_25_182310_create_printers_table.php new file mode 100644 index 000000000..3de21d905 --- /dev/null +++ b/database/migrations/2023_12_25_182310_create_printers_table.php @@ -0,0 +1,42 @@ +id(); + $table->string('name')->unique(); + $table->string('ip')->nullable(); + $table->string('port')->nullable(); + $table->timestamp('paper_out_at')->nullable(); + }); + + + // Create the default printer + DB::table('printers')->insert([ + 'name' => env('PRINTER_NAME'), + 'ip' => env('PRINTER_IP'), + 'port' => env('PRINTER_PORT'), + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('printers'); + } +}; diff --git a/database/migrations/2023_12_25_184733_update_print_jobs_table.php b/database/migrations/2023_12_25_184733_update_print_jobs_table.php new file mode 100644 index 000000000..b77c95f5d --- /dev/null +++ b/database/migrations/2023_12_25_184733_update_print_jobs_table.php @@ -0,0 +1,44 @@ +foreignIdFor(Printer::class)->nullable()->after('user_id')->constrained(); + $table->boolean('used_free_pages')->default(false)->after('cost'); + $table->dropColumn('filepath'); + }); + DB::table('print_jobs')->update([ + 'printer_id' => DB::table('printers')->first()->id + ]); + Schema::table('print_jobs', function (Blueprint $table) { + $table->foreignIdFor(Printer::class)->nullable(false)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('print_jobs', function (Blueprint $table) { + $table->dropForeign(['printer_id']); + $table->dropColumn('printer_id'); + $table->dropColumn('used_free_pages'); + $table->string('filepath')->after('user_id'); + }); + } +}; diff --git a/database/migrations/2024_12_25_112824_remove_printer_role.php b/database/migrations/2024_12_25_112824_remove_printer_role.php new file mode 100644 index 000000000..df08c3287 --- /dev/null +++ b/database/migrations/2024_12_25_112824_remove_printer_role.php @@ -0,0 +1,36 @@ +where('name', 'printer')->first()->id; + + DB::table('role_users')->where('role_id', $printer_role_id)->delete(); + + DB::table('roles')->where('name', 'printer')->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $printer_role_id = DB::table('roles')->insertGetId([ + 'name' => 'printer' + ]); + + foreach(DB::table('users')->where('verified', 1)->get() as $user) { + DB::table('role_users')->insert([ + 'role_id' => $printer_role_id, + 'user_id' => $user->id + ]); + } + } +}; diff --git a/database/seeders/UsersTableSeeder.php b/database/seeders/UsersTableSeeder.php index b86fc46b7..62acc766c 100644 --- a/database/seeders/UsersTableSeeder.php +++ b/database/seeders/UsersTableSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\Semester; use App\Models\Checkout; use App\Models\Faculty; use App\Models\FreePages; @@ -27,6 +28,8 @@ class UsersTableSeeder extends Seeder */ public function run() { + Semester::current(); // generate current semester if still not exists + $this->createSuperUser(); $this->createStaff(); @@ -118,7 +121,6 @@ private function createCollegist($user) $this->attachEducationalInformation($user); $this->attachFaculties($user); - $user->roles()->attach(Role::get(Role::PRINTER)->id); $wifi_username = $user->internetAccess->setWifiCredentials(); WifiConnection::factory($user->id % 5)->create(['wifi_username' => $wifi_username]); $this->attachStudyLines($user); diff --git a/resources/lang/en/print.php b/resources/lang/en/print.php index fdc65f6e7..6a1b95ad7 100644 --- a/resources/lang/en/print.php +++ b/resources/lang/en/print.php @@ -7,6 +7,7 @@ 'SUCCESS' => 'Completed', 'add' => 'Add', 'already_cancelled' => 'The print job was already cancelled.', + 'already_completed' => 'The print job was already completed.', 'amount' => 'Amount', 'available_free_pages' => 'Number of free pages left: :number_of_free_pages', 'available_money' => 'Available money', @@ -14,8 +15,8 @@ 'balance_change' => 'Balance change', 'cancel' => 'Cancel', 'cancel_job' => 'Abort', + 'cannot_cancel' => 'Unable to cancel the print job', 'changed_balance' => 'Modified print balance', - 'changed_balance_descr' => ':modifier have been added :amount HUF to your print balance. Your new balance is :balance HUF.', 'confirm_cancel' => 'Are you sure you wish to abort printing this document?', 'cost' => 'Cost', 'costs' => 'One-sided printing costs :one_sided, and two-sided printing costs :two_sided HUF.', @@ -43,6 +44,7 @@ 'number_of_copies' => 'Number of copies', 'number_of_printed_documents' => 'Number of printed documents', 'options' => 'Printing options', + 'others_balance_changed_descr' => ':modifier has changed the print balance of :holder-name by HUF :amount.', 'payment_methods_cannot_be_mixed' => 'A print job can be either free or paid as they cannot be mixed.', 'pdf_description' => 'Only .pdf files can be printed.', 'pdf_maxsize' => 'The maximum file size is :maxsize MB.', @@ -61,4 +63,5 @@ 'upload_money' => 'You can upload money to your account at the system admins.', 'use_free_pages' => 'Use free pages', 'user' => 'User', + 'your_balance_changed_descr' => ':modifier has changed your print balance by HUF :amount. The new balance is HUF :balance.', ]; diff --git a/resources/lang/hu/print.php b/resources/lang/hu/print.php index 72ee27cdf..fa44e4668 100644 --- a/resources/lang/hu/print.php +++ b/resources/lang/hu/print.php @@ -6,7 +6,8 @@ 'QUEUED' => 'Sorban áll', 'SUCCESS' => 'Kész', 'add' => 'Hozzáadás', - 'already_cancelled' => 'A nyomtatás már vissza lett vonva.', + 'already_cancelled' => 'A nyomtatás már visszavonásra került.', + 'already_completed' => 'A nyomtatás már befejeződött.', 'amount' => 'Összeg', 'available_free_pages' => 'Ezen felül ingyenesen nyomtatható :number_of_free_pages oldal', 'available_money' => 'Rendelkezésre álló összeg', @@ -14,8 +15,8 @@ 'balance_change' => 'Egyenlegváltozás', 'cancel' => 'Mégse', 'cancel_job' => 'Megszakítás', + 'cannot_cancel' => 'Nem lehetett megszakítani a nyomtatást', 'changed_balance' => 'Megváltozott nyomtatási egyenleg', - 'changed_balance_descr' => ':modifier módosította a nyomtatási egyenleged ennyivel: :amount. Az új egyenleged :balance Ft.', 'confirm_cancel' => 'Biztos, hogy meg szeretnéd szakítani a dokumentum nyomtatását?', 'cost' => 'Költség', 'costs' => 'Az egyoldalas nyomtatás díja :one_sided, a kétoldalasé :two_sided Ft.', @@ -43,6 +44,7 @@ 'number_of_copies' => 'Példányszám', 'number_of_printed_documents' => 'Nyomtatott dokumentumok száma', 'options' => 'Beállítások', + 'others_balance_changed_descr' => ':modifier módosította :holder-name egyenlegét :amount Ft-tal.', 'payment_methods_cannot_be_mixed' => 'Egy nyomtatás vagy ingyenes vagy fizetős lehet, nem keverhető.', 'pdf_description' => 'Csak .pdf fájl nyomtatható.', 'pdf_maxsize' => 'A fájl mérete legfeljebb :maxsize MB lehet.', @@ -61,4 +63,5 @@ 'upload_money' => 'Pénzt feltölteni a rendszergazdáknál tudsz.', 'use_free_pages' => 'Ingyenes oldalak használata', 'user' => 'Felhasználó', + 'your_balance_changed_descr' => ':modifier módosította a nyomtatási egyenleged :amount Ft-tal. Az új egyenleged :balance Ft.', ]; diff --git a/resources/views/dormitory/print/app.blade.php b/resources/views/dormitory/print/app.blade.php index f3c818d0b..ad0f5f05d 100644 --- a/resources/views/dormitory/print/app.blade.php +++ b/resources/views/dormitory/print/app.blade.php @@ -9,10 +9,10 @@ @include("dormitory.print.print")