WIP: Road to 1.0.0 #1

Draft
miguel456 wants to merge 123 commits from develop into master
44 changed files with 1308 additions and 691 deletions
Showing only changes of commit 8942623bde - Show all commits

View File

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/database/factories" isTestSource="false" packagePrefix="Database\Factories\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tests\" />
<sourceFolder url="file://$MODULE_DIR$/database/seeders" isTestSource="false" packagePrefix="Database\Seeders\" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" packagePrefix="App\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/almasaeed2010/adminlte" />
<excludeFolder url="file://$MODULE_DIR$/vendor/asm89/stack-cors" />
<excludeFolder url="file://$MODULE_DIR$/vendor/awssat/discord-notification-channel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-debugbar" />
<excludeFolder url="file://$MODULE_DIR$/vendor/berkayk/onesignal-laravel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/brick/math" />
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/stream-filter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/dnoegel/php-xdg-base-dir" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/dragonmantank/cron-expression" />
<excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/facade/flare-client-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/facade/ignition" />
<excludeFolder url="file://$MODULE_DIR$/vendor/facade/ignition-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fideloper/proxy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/filp/whoops" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fruitcake/laravel-cors" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fzaninotto/faker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/graham-campbell/result-type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/hamcrest/hamcrest-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/http-interop/http-factory-guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/jean85/pretty-package-versions" />
<excludeFolder url="file://$MODULE_DIR$/vendor/jeroennoten/laravel-adminlte" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/framework" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/tinker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/ui" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/commonmark" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/flysystem" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/mime-type-detection" />
<excludeFolder url="file://$MODULE_DIR$/vendor/maximebf/debugbar" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mcamara/laravel-localization" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mockery/mockery" />
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mpociot/teamwork" />
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nunomaduro/collision" />
<excludeFolder url="file://$MODULE_DIR$/vendor/opis/closure" />
<excludeFolder url="file://$MODULE_DIR$/vendor/paragonie/random_compat" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/client-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/discovery" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/guzzle6-adapter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/httplug" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/message-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/promise" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoption/phpoption" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpspec/prophecy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-token-stream" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psy/psysh" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/collection" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/scrivo/highlight.php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/resource-operations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sentry/sentry" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sentry/sentry-laravel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/spatie/laravel-permission" />
<excludeFolder url="file://$MODULE_DIR$/vendor/swiftmailer/swiftmailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/debug" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-iconv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php72" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php73" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/psr-http-message-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
<excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/voku/portable-ascii" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/sanctum" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hrm-mcserver.iml" filepath="$PROJECT_DIR$/.idea/hrm-mcserver.iml" /> <module fileurl="file://$PROJECT_DIR$/../rbrecruiter/.idea/rbrecruiter.iml" filepath="$PROJECT_DIR$/../rbrecruiter/.idea/rbrecruiter.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@ -0,0 +1,11 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class ApplicationNotFoundException extends ModelNotFoundException
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class EmptyFormException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class EmptyOptionsException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class FailedCaptchaException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class FileUploadException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class FormHasConstraintsException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class IncompleteApplicationException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidAppointmentException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidAppointmentStatusException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidGamePreferenceException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class InvalidInviteException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class OptionCategoryNotFoundException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class OptionNotFoundException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ProfileNotFoundException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class PublicTeamInviteException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class UnavailableApplicationException extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class UserAlreadyInvitedException extends Exception
{
//
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class VacancyNotFoundException extends ModelNotFoundException
{
//
}

View File

@ -21,6 +21,8 @@
namespace App\Helpers; namespace App\Helpers;
use App\Exceptions\EmptyOptionsException;
use App\Exceptions\OptionNotFoundException;
use App\Options as Option; use App\Options as Option;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@ -34,7 +36,7 @@ class Options
/** /**
* Returns an assortment of settings found in the mentioned category * Returns an assortment of settings found in the mentioned category
* *
* @param $category The category * @param $category The category
* @return Collection The settings in this category * @return Collection The settings in this category
*/ */
@ -43,7 +45,7 @@ class Options
$options = Option::where('option_category', $category)->get(); $options = Option::where('option_category', $category)->get();
if ($options->isEmpty()) if ($options->isEmpty())
{ {
throw new \Exception('There are no options in category ' . $category); throw new EmptyOptionsException('There are no options in category ' . $category);
} }
return $options; return $options;
} }
@ -52,13 +54,13 @@ class Options
public function getOption(string $option): string public function getOption(string $option): string
{ {
$value = Cache::get($option); $value = Cache::get($option);
if (is_null($value)) { if (is_null($value)) {
Log::debug('Option '.$option.'not found in cache, refreshing from database'); Log::debug('Option '.$option.'not found in cache, refreshing from database');
$value = Option::where('option_name', $option)->first(); $value = Option::where('option_name', $option)->first();
if (is_null($value)) { if (is_null($value)) {
throw new \Exception('This option does not exist.'); throw new OptionNotFoundException('This option does not exist.');
} }
Cache::put($option, $value->option_value); Cache::put($option, $value->option_value);
Cache::put($option.'_desc', 'Undefined description'); Cache::put($option.'_desc', 'Undefined description');
@ -118,7 +120,7 @@ class Options
Cache::put('option_name', $newValue, now()->addDay()); Cache::put('option_name', $newValue, now()->addDay());
} else { } else {
throw new \Exception('This option does not exist.'); throw new OptionNotFoundException('This option does not exist.');
} }
} }

View File

@ -22,35 +22,24 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Application; use App\Application;
use App\Events\ApplicationDeniedEvent; use App\Exceptions\IncompleteApplicationException;
use App\Facades\JSON; use App\Exceptions\UnavailableApplicationException;
use App\Notifications\ApplicationMoved; use App\Exceptions\VacancyNotFoundException;
use App\Notifications\NewApplicant; use App\Services\ApplicationService;
use App\Response;
use App\User;
use App\Vacancy;
use ContextAwareValidator;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class ApplicationController extends Controller class ApplicationController extends Controller
{ {
private $applicationService;
private function canVote($votes): bool public function __construct(ApplicationService $applicationService) {
{
$allvotes = collect([]);
foreach ($votes as $vote) { $this->applicationService = $applicationService;
if ($vote->userID == Auth::user()->id) {
$allvotes->push($vote);
}
}
return !(($allvotes->count() == 1));
} }
public function showUserApps() public function showUserApps()
{ {
return view('dashboard.user.applications') return view('dashboard.user.applications')
@ -70,7 +59,7 @@ class ApplicationController extends Controller
'structuredResponses' => json_decode($application->response->responseData, true), 'structuredResponses' => json_decode($application->response->responseData, true),
'formStructure' => $application->response->form, 'formStructure' => $application->response->form,
'vacancy' => $application->response->vacancy, 'vacancy' => $application->response->vacancy,
'canVote' => $this->canVote($application->votes), 'canVote' => $this->applicationService->canVote($application->votes),
] ]
); );
} else { } else {
@ -90,91 +79,27 @@ class ApplicationController extends Controller
} }
public function renderApplicationForm(Request $request, $vacancySlug) public function renderApplicationForm($vacancySlug)
{ {
$vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); return $this->applicationService->renderForm($vacancySlug);
$firstVacancy = $vacancyWithForm->first();
if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN') {
return view('dashboard.application-rendering.apply')
->with([
'vacancy' => $vacancyWithForm->first(),
'preprocessedForm' => json_decode($vacancyWithForm->first()->forms->formStructure, true),
]);
} else {
abort(404, __('The application you\'re looking for could not be found or it is currently unavailable.'));
}
} }
public function saveApplicationAnswers(Request $request, $vacancySlug) public function saveApplicationAnswers(Request $request, $vacancySlug)
{ {
$vacancy = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); try {
if ($vacancy->isEmpty()) { $this->applicationService->fillForm(Auth::user(), $request->all(), $vacancySlug);
} catch (VacancyNotFoundException | IncompleteApplicationException | UnavailableApplicationException $e) {
return redirect() return redirect()
->back() ->back()
->with('error', __('This vacancy doesn\'t exist; Please use the proper buttons to apply to one.')); ->with('error', $e->getMessage());
} }
if ($vacancy->first()->vacancyCount == 0 || $vacancy->first()->vacancyStatus !== 'OPEN') {
return redirect()
->back()
->with('error', __('This application is unavailable'));
}
Log::info('Processing new application!');
$formStructure = json_decode($vacancy->first()->forms->formStructure, true);
$responseValidation = ContextAwareValidator::getResponseValidator($request->all(), $formStructure);
$applicant = Auth::user();
// API users may specify ID 1 for an anonymous application, but they'll have to submit contact details for it to become active.
// User ID 1 is exempt from application rate limits
Log::info('Built response & validator structure!');
if (!$responseValidation->get('validator')->fails()) {
$response = Response::create([
'responseFormID' => $vacancy->first()->forms->id,
'associatedVacancyID' => $vacancy->first()->id, // Since a form can be used by multiple vacancies, we can only know which specific vacancy this response ties to by using a vacancy ID
'responseData' => $responseValidation->get('responseStructure'),
]);
Log::info('Registered form response!', [
'applicant' => $applicant->name,
'vacancy' => $vacancy->first()->vacancyName
]);
$application = Application::create([
'applicantUserID' => $applicant->id,
'applicantFormResponseID' => $response->id,
'applicationStatus' => 'STAGE_SUBMITTED',
]);
Log::info('Submitted an application!', [
'responseID' => $response->id,
'applicant' => $applicant->name
]);
foreach (User::all() as $user) {
if ($user->hasRole('admin')) {
$user->notify((new NewApplicant($application, $vacancy->first()))->delay(now()->addSeconds(10)));
}
}
$request->session()->flash('success', __('Thank you for your application! It will be reviewed as soon as possible.'));
return redirect(route('showUserApps'));
}
Log::warning('Application form for ' . $applicant->name . ' contained errors, resetting!');
return redirect() return redirect()
->back() ->back()
->with('error', __('There are one or more errors in your application. Please make sure none of your fields are empty, since they are all required.')); ->with('success', __('Thank you! Your application has been processed and our team will get to it shortly.'));
} }
public function updateApplicationStatus(Request $request, Application $application, $newStatus) public function updateApplicationStatus(Request $request, Application $application, $newStatus)
@ -182,36 +107,28 @@ class ApplicationController extends Controller
$messageIsError = false; $messageIsError = false;
$this->authorize('update', Application::class); $this->authorize('update', Application::class);
try {
switch ($newStatus) { $status = $this->applicationService->updateStatus($application, $newStatus);
case 'deny': } catch (\LogicException $ex)
{
event(new ApplicationDeniedEvent($application)); return redirect()
$message = __("Application denied successfully."); ->back()
->with('error', $ex->getMessage());
break;
case 'interview':
Log::info(' Moved application ID ' . $application->id . 'to interview stage!');
$message = __('Application moved to interview stage!');
$application->setStatus('STAGE_INTERVIEW');
$application->user->notify(new ApplicationMoved());
break;
default:
$message = __("There are no suitable statuses to update to.");
$messageIsError = true;
} }
return redirect()->back(); return redirect()
->back()
->with('success', $status);
} }
/**
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Exception
*/
public function delete(Request $request, Application $application) public function delete(Request $request, Application $application)
{ {
$this->authorize('delete', $application); $this->authorize('delete', $application);
$application->delete(); // observers will run, cleaning it up $this->applicationService->delete($application);
return redirect() return redirect()
->back() ->back()

View File

@ -23,85 +23,79 @@ namespace App\Http\Controllers;
use App\Application; use App\Application;
use App\Appointment; use App\Appointment;
use App\Exceptions\InvalidAppointmentException;
use App\Exceptions\InvalidAppointmentStatusException;
use App\Http\Requests\SaveNotesRequest; use App\Http\Requests\SaveNotesRequest;
use App\Notifications\ApplicationMoved; use App\Services\AppointmentService;
use App\Notifications\AppointmentScheduled; use App\Services\MeetingNoteService;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class AppointmentController extends Controller class AppointmentController extends Controller
{ {
private $allowedPlatforms = [
'ZOOM', private $appointmentService;
'DISCORD', private $meetingNoteService;
'SKYPE',
'MEET',
'TEAMSPEAK',
];
public function saveAppointment(Request $request, Application $application) public function __construct(AppointmentService $appointmentService, MeetingNoteService $meetingNoteService) {
{
$this->authorize('create', Appointment::class);
$appointmentDate = Carbon::parse($request->appointmentDateTime);
$appointment = Appointment::create([ $this->appointmentService = $appointmentService;
'appointmentDescription' => $request->appointmentDescription, $this->meetingNoteService = $meetingNoteService;
'appointmentDate' => $appointmentDate->toDateTimeString(),
'applicationID' => $application->id,
'appointmentLocation' => (in_array($request->appointmentLocation, $this->allowedPlatforms)) ? $request->appointmentLocation : 'DISCORD',
]);
$application->setStatus('STAGE_INTERVIEW_SCHEDULED');
Log::info('User '.Auth::user()->name.' has scheduled an appointment with '.$application->user->name.' for application ID'.$application->id, [
'datetime' => $appointmentDate->toDateTimeString(),
'scheduled' => now(),
]);
$application->user->notify(new AppointmentScheduled($appointment));
$request->session()->flash('success', __('Appointment successfully scheduled @ :appointmentTime', ['appointmentTime', $appointmentDate->toDateTimeString()]));
return redirect()->back();
} }
public function updateAppointment(Request $request, Application $application, $status) public function saveAppointment(Request $request, Application $application): RedirectResponse
{
$this->authorize('create', Appointment::class);
$appointmentDate = Carbon::parse($request->appointmentDateTime);
$this->appointmentService->createAppointment($application, $appointmentDate, $request->appointmentDescription, $request->appointmentLocation);
return redirect()
->back()
->with('success',__('Appointment successfully scheduled @ :appointmentTime', ['appointmentTime', $appointmentDate->toDateTimeString()]));
}
/**
* @throws AuthorizationException
*/
public function updateAppointment(Application $application, $status): RedirectResponse
{ {
$this->authorize('update', $application->appointment); $this->authorize('update', $application->appointment);
$validStatuses = [ try {
'SCHEDULED', $this->appointmentService->updateAppointment($application, $status);
'CONCLUDED',
];
// NOTE: This is a little confusing, refactor return redirect()
$application->appointment->appointmentStatus = (in_array($status, $validStatuses)) ? strtoupper($status) : 'SCHEDULED'; ->back()
$application->appointment->save(); ->with('success', __("Interview finished! Staff members can now vote on it."));
$application->setStatus('STAGE_PEERAPPROVAL'); }
$application->user->notify(new ApplicationMoved()); catch (InvalidAppointmentStatusException $ex) {
return redirect()
$request->session()->flash('success', __('Interview finished! Staff members can now vote on it.')); ->back()
->with('error', $ex->getMessage());
return redirect()->back();
}
// also updates
public function saveNotes(SaveNotesRequest $request, Application $application)
{
if (! is_null($application)) {
$application->load('appointment');
$application->appointment->meetingNotes = $request->noteText;
$application->appointment->save();
$request->session()->flash('success', __('Meeting notes have been saved.'));
} else {
$request->session()->flash('error', __('There\'s no appointment to save notes to!'));
} }
return redirect()->back();
}
public function saveNotes(SaveNotesRequest $request, Application $application)
{
try {
$this->meetingNoteService->addToApplication($application, $request->noteText);
return redirect()
->back()
->with('success', 'Saved notes.');
} catch (InvalidAppointmentException $ex) {
return redirect()
->back()
->with('error', $ex->getMessage());
}
} }
} }

View File

@ -24,21 +24,22 @@ namespace App\Http\Controllers;
use App\Application; use App\Application;
use App\Comment; use App\Comment;
use App\Http\Requests\NewCommentRequest; use App\Http\Requests\NewCommentRequest;
use App\Services\CommentService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class CommentController extends Controller class CommentController extends Controller
{ {
private $commentService;
public function __construct(CommentService $commentService) {
$this->commentService = $commentService;
}
public function insert(NewCommentRequest $request, Application $application) public function insert(NewCommentRequest $request, Application $application)
{ {
$this->authorize('create', Comment::class); $this->authorize('create', Comment::class);
$comment = $this->commentService->addComment($application, $request->comment);
$comment = Comment::create([
'authorID' => Auth::user()->id,
'applicationID' => $application->id,
'text' => $request->comment,
]);
if ($comment) { if ($comment) {
$request->session()->flash('success', __('Comment posted!')); $request->session()->flash('success', __('Comment posted!'));
@ -52,10 +53,10 @@ class CommentController extends Controller
public function delete(Request $request, Comment $comment) public function delete(Request $request, Comment $comment)
{ {
$this->authorize('delete', $comment); $this->authorize('delete', $comment);
$this->commentService->deleteComment($comment);
$comment->delete(); return redirect()
$request->session()->flash('success', __('Comment deleted!')); ->back()
->with('success', __('Comment deleted!'));
return redirect()->back();
} }
} }

View File

@ -21,7 +21,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exceptions\FailedCaptchaException;
use App\Notifications\NewContact; use App\Notifications\NewContact;
use App\Services\ContactService;
use App\User; use App\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -30,47 +32,32 @@ class ContactController extends Controller
{ {
protected $users; protected $users;
public function __construct(User $users) private $contactService;
public function __construct(User $users, ContactService $contactService)
{ {
$this->contactService = $contactService;
$this->users = $users; $this->users = $users;
} }
public function create(Request $request) public function create(Request $request)
{ {
$name = $request->name; try {
$email = $request->email;
$subject = $request->subject;
$msg = $request->msg;
$challenge = $request->input('captcha'); $email = $request->email;
$msg = $request->msg;
$challenge = $request->input('captcha');
// TODO: now: add middleware for this verification, move to invisible captcha $this->contactService->sendMessage($request->ip(), $msg, $email, $challenge);
$verifyrequest = Http::asForm()->post(config('recaptcha.verify.apiurl'), [
'secret' => config('recaptcha.keys.secret'),
'response' => $challenge,
'remoteip' => $request->ip(),
]);
$response = json_decode($verifyrequest->getBody(), true); return redirect()
->back()
->with('success',__('Message sent successfully! We usually respond within 48 hours.'));
if (! $response['success']) { } catch (FailedCaptchaException $ex) {
$request->session()->flash('error', __('Beep beep boop... Robot? Submission failed.')); return redirect()
->back()
return redirect()->back(); ->with('error', $ex->getMessage());
} }
foreach (User::all() as $user) {
if ($user->hasRole('admin')) {
$user->notify(new NewContact(collect([
'message' => $msg,
'ip' => $request->ip(),
'email' => $email,
])));
}
}
$request->session()->flash('success', __('Message sent successfully! We usually respond within 48 hours.'));
return redirect()->back();
} }
} }

View File

@ -27,6 +27,8 @@ use App\Vacancy;
class DashboardController extends Controller class DashboardController extends Controller
{ {
// Note: The dashboard doesn't need a service because it doesn't contain any significant business logic
public function index() public function index()
{ {
$totalPeerReview = Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get()->count(); $totalPeerReview = Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get()->count();

View File

@ -21,12 +21,20 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exceptions\FormHasConstraintsException;
use App\Form; use App\Form;
use App\Services\FormManagementService;
use ContextAwareValidator; use ContextAwareValidator;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class FormController extends Controller class FormController extends Controller
{ {
private $formService;
public function __construct(FormManagementService $formService) {
$this->formService = $formService;
}
public function index() public function index()
{ {
$forms = Form::all(); $forms = Form::all();
@ -45,60 +53,38 @@ class FormController extends Controller
public function saveForm(Request $request) public function saveForm(Request $request)
{ {
$this->authorize('create', Form::class); $form = $this->formService->addForm($request->all());
$fields = $request->all();
if (count($fields) == 2) { // Form is boolean or array
// form is probably empty, since forms with fields will alawys have more than 2 items if ($form)
{
$request->session()->flash('error', __('Sorry, but you may not create empty forms.')); return redirect()
->back()
return redirect()->to(route('showForms')); ->with('success', __('Form created!'));
} }
$contextValidation = ContextAwareValidator::getValidator($fields, true, true); return redirect()
->back()
if (! $contextValidation->get('validator')->fails()) { ->with('errors', $form);
$storableFormStructure = $contextValidation->get('structure');
Form::create(
[
'formName' => $fields['formName'],
'formStructure' => $storableFormStructure,
'formStatus' => 'ACTIVE',
]
);
$request->session()->flash('success', __('Form created! You can now link this form to a vacancy.'));
return redirect()->to(route('showForms'));
}
$request->session()->flash('errors', $contextValidation->get('validator')->errors()->getMessages());
return redirect()->back();
} }
public function destroy(Request $request, Form $form) public function destroy(Request $request, Form $form)
{ {
$this->authorize('delete', $form); $this->authorize('delete', $form);
try {
$deletable = true; $this->formService->deleteForm($form);
return redirect()
->back()
->with('success', __('Form deleted successfuly'));
} catch (FormHasConstraintsException $ex) {
return redirect()
->back()
->with('error', $ex->getMessage());
if (! is_null($form) && ! is_null($form->vacancies) && $form->vacancies->count() !== 0 || ! is_null($form->responses)) {
$deletable = false;
} }
if ($deletable) {
$form->delete();
$request->session()->flash('success', __('Form deleted successfully.'));
} else {
$request->session()->flash('error', __('You cannot delete this form because it\'s tied to one or more applications and ranks, or because it doesn\'t exist.'));
}
return redirect()->back();
} }
public function preview(Request $request, Form $form) public function preview(Request $request, Form $form)
@ -124,22 +110,15 @@ class FormController extends Controller
public function update(Request $request, Form $form) public function update(Request $request, Form $form)
{ {
$this->authorize('update', $form); $this->authorize('update', $form);
$updatedForm = $this->formService->updateForm($form, $request->all());
$contextValidation = ContextAwareValidator::getValidator($request->all(), true); if ($updatedForm instanceof Form) {
$this->authorize('update', $form); return redirect()->to(route('previewForm', ['form' => $updatedForm->id]));
if (! $contextValidation->get('validator')->fails()) {
// Add the new structure into the form. New, subsquent fields will be identified by the "new" prefix
// This prefix doesn't actually change the app's behavior when it receives applications.
// Additionally, old applications won't of course display new and updated fields, because we can't travel into the past and get data for them
$form->formStructure = $contextValidation->get('structure');
$form->save();
$request->session()->flash('success', __('Hooray! Your form was updated. New applications for it\'s vacancy will use it.'));
} else {
$request->session()->flash('errors', $contextValidation->get('validator')->errors()->getMessages());
} }
return redirect()->to(route('previewForm', ['form' => $form->id])); // array of errors
return redirect()
->back()
->with('errors', $updatedForm);
} }
} }

View File

@ -25,6 +25,8 @@ use App\Vacancy;
class HomeController extends Controller class HomeController extends Controller
{ {
// doesn't need a service, because it doesn't contain major logic.
/** /**
* Show the application dashboard. * Show the application dashboard.
* *

View File

@ -21,14 +21,25 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exceptions\InvalidGamePreferenceException;
use App\Exceptions\OptionNotFoundException;
use App\Facades\Options; use App\Facades\Options;
use App\Options as Option; use App\Options as Option;
use App\Services\ConfigurationService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class OptionsController extends Controller class OptionsController extends Controller
{ {
private $configurationService;
public function __construct(ConfigurationService $configurationService) {
$this->configurationService = $configurationService;
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
@ -36,7 +47,7 @@ class OptionsController extends Controller
*/ */
public function index() public function index()
{ {
// TODO: Replace with settings package
return view('dashboard.administration.settings') return view('dashboard.administration.settings')
->with([ ->with([
'options' => Options::getCategory('notifications'), 'options' => Options::getCategory('notifications'),
@ -51,65 +62,44 @@ class OptionsController extends Controller
]); ]);
} }
public function saveSettings(Request $request) public function saveSettings(Request $request): \Illuminate\Http\RedirectResponse
{ {
if (Auth::user()->can('admin.settings.edit')) { try {
Log::debug('Updating application options', [
'ip' => $request->ip(),
'ua' => $request->userAgent(),
'username' => Auth::user()->username,
]);
foreach ($request->all() as $optionName => $option) {
try {
Log::debug('Going through option '.$optionName);
if (Options::optionExists($optionName)) {
Log::debug('Option exists, updating to new values', [
'opt' => $optionName,
'new_value' => $option,
]);
Options::changeOption($optionName, $option);
}
} catch (\Exception $ex) {
Log::error('Unable to update options!', [
'msg' => $ex->getMessage(),
'trace' => $ex->getTraceAsString(),
]);
report($ex);
$errorCond = true; if (Auth::user()->can('admin.settings.edit')) {
$request->session()->flash('error', __('An error occurred while trying to save settings: :message ', ['message' => $ex->getMessage()])); $this->configurationService->saveConfiguration($request->all());
}
return redirect()
->back()
->with('success', __('Options updated successfully!'));
} }
if (! isset($errorCond)) { } catch (OptionNotFoundException | \Exception $ex) {
$request->session()->flash('success', __('Settings saved successfully!'));
} return redirect()
} else { ->back()
$request->session()->flash('error', __('You do not have permission to update this resource.')); ->with('error', $ex->getMessage());
} }
return redirect()->back(); return redirect()
->back()
->with('error', __('You do not have permission to update this resource.'));
} }
public function saveGameIntegration(Request $request) public function saveGameIntegration(Request $request)
{ {
$supportedGames = [ try {
'RUST',
'MINECRAFT',
'SE',
'GMOD'
];
if (!is_null($request->gamePref) && in_array($request->gamePref, $supportedGames)) $this->configurationService->saveGameIntegration($request->gamePref);
{ return redirect()
Options::changeOption('currentGame', $request->gamePref); ->back()
$request->session()->flash('success', __('Updated current game.')); ->with('success', __('Game preference updated.'));
return redirect()->back(); } catch (InvalidGamePreferenceException $ex) {
return redirect()
->back()
->with('error', $ex->getMessage());
} }
$request->session()->flash('error', __('Unsupported game :game.', ['game' => $request->gamePref ]));
return redirect()->back();
} }
} }

View File

@ -23,6 +23,7 @@ namespace App\Http\Controllers;
use App\Facades\IP; use App\Facades\IP;
use App\Http\Requests\ProfileSave; use App\Http\Requests\ProfileSave;
use App\Services\ProfileService;
use App\User; use App\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -31,6 +32,12 @@ use Spatie\Permission\Models\Role;
class ProfileController extends Controller class ProfileController extends Controller
{ {
private $profileService;
public function __construct(ProfileService $profileService) {
$this->profileService = $profileService;
}
public function index() public function index()
{ {
return view('dashboard.user.directory') return view('dashboard.user.directory')
@ -39,6 +46,7 @@ class ProfileController extends Controller
public function showProfile() public function showProfile()
{ {
// TODO: Come up with cleaner social media solution, e.g. social media object
$socialLinks = Auth::user()->profile->socialLinks ?? '[]'; $socialLinks = Auth::user()->profile->socialLinks ?? '[]';
$socialMediaProfiles = json_decode($socialLinks, true); $socialMediaProfiles = json_decode($socialLinks, true);
@ -52,8 +60,7 @@ class ProfileController extends Controller
]); ]);
} }
// Route model binding public function showSingleProfile(User $user)
public function showSingleProfile(Request $request, User $user)
{ {
$socialMediaProfiles = json_decode($user->profile->socialLinks, true); $socialMediaProfiles = json_decode($user->profile->socialLinks, true);
$createdDate = Carbon::parse($user->created_at); $createdDate = Carbon::parse($user->created_at);
@ -102,36 +109,9 @@ class ProfileController extends Controller
public function saveProfile(ProfileSave $request) public function saveProfile(ProfileSave $request)
{ {
$profile = User::find(Auth::user()->id)->profile; $this->profileService->updateProfile(Auth::user()->id, $request);
$social = []; return redirect()
->back()
if (! is_null($profile)) { ->with('success', __('Profile updated.'));
switch ($request->avatarPref) {
case 'MOJANG':
$avatarPref = 'crafatar';
break;
case 'GRAVATAR':
$avatarPref = strtolower($request->avatarPref);
break;
}
$social['links']['github'] = $request->socialGithub;
$social['links']['twitter'] = $request->socialTwitter;
$social['links']['insta'] = $request->socialInsta;
$social['links']['discord'] = $request->socialDiscord;
$profile->profileShortBio = $request->shortBio;
$profile->profileAboutMe = $request->aboutMe;
$profile->avatarPreference = $avatarPref;
$profile->socialLinks = json_encode($social);
$newProfile = $profile->save();
$request->session()->flash('success', __('Profile settings saved successfully.'));
}
return redirect()->back();
} }
} }

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Facades\Options; use App\Facades\Options;
use App\Http\Requests\SaveSecuritySettings; use App\Http\Requests\SaveSecuritySettings;
use App\Services\SecuritySettingsService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -11,38 +12,24 @@ use function PHPSTORM_META\map;
class SecuritySettingsController extends Controller class SecuritySettingsController extends Controller
{ {
private $securityService;
public function __construct(SecuritySettingsService $securityService) {
$this->securityService = $securityService;
}
public function save(SaveSecuritySettings $request) public function save(SaveSecuritySettings $request)
{ {
$validPolicies = [ $this->securityService->save($request->secPolicy, [
'off', 'graceperiod' => $request->graceperiod,
'low', 'pwExpiry' => $request->pwExpiry,
'medium', 'enforce2fa' => $request->enforce2fa,
'high' 'requirePMC' => $request->requirePMC
]; ]);
if (in_array($request->secPolicy, $validPolicies)) return redirect()
{ ->back()
Options::changeOption('pw_security_policy', $request->secPolicy); ->with('success', __('Settings saved.'));
Log::debug('[Options] Changing option pw_security_policy', [
'new_value' => $request->secPolicy
]);
}
else
{
Log::debug('[WARN] Ignoring bogus policy', [
'avaliable' => $validPolicies,
'given' >= $request->secPolicy
]);
}
Options::changeOption('graceperiod', $request->graceperiod);
Options::changeOption('password_expiry', $request->pwExpiry);
Options::changeOption('force2fa', $request->enforce2fa);
Options::changeOption('requireGameLicense', $request->requirePMC);
$request->session()->flash('success', __('Settings saved successfully.'));
return redirect()->back();
} }
} }

View File

@ -21,13 +21,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exceptions\InvalidInviteException;
use App\Exceptions\PublicTeamInviteException;
use App\Exceptions\UserAlreadyInvitedException;
use App\Http\Requests\EditTeamRequest; use App\Http\Requests\EditTeamRequest;
use App\Http\Requests\NewTeamRequest; use App\Http\Requests\NewTeamRequest;
use App\Http\Requests\SendInviteRequest; use App\Http\Requests\SendInviteRequest;
use App\Mail\InviteToTeam; use App\Mail\InviteToTeam;
use App\Services\TeamService;
use App\Team; use App\Team;
use App\User; use App\User;
use App\Vacancy; use App\Vacancy;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
@ -37,10 +42,15 @@ use Mpociot\Teamwork\TeamInvite;
class TeamController extends Controller class TeamController extends Controller
{ {
private $teamService;
public function __construct(TeamService $teamService) {
$this->teamService = $teamService;
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\Response
*/ */
public function index() public function index()
{ {
@ -56,23 +66,17 @@ class TeamController extends Controller
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param NewTeamRequest $request * @param NewTeamRequest $request
* @return \Illuminate\Http\RedirectResponse * @return RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*/ */
public function store(NewTeamRequest $request) public function store(NewTeamRequest $request)
{ {
$this->authorize('create', Team::class); $this->authorize('create', Team::class);
$this->teamService->createTeam($request->teamName, Auth::user()->id);
$team = Team::create([ return redirect()
'name' => $request->teamName, ->back()
'owner_id' => Auth::user()->id, ->with('success', __('Team successfully created.'));
]);
Auth::user()->teams()->attach($team->id);
$request->session()->flash('success', __('Team successfully created.'));
return redirect()->back();
} }
/** /**
@ -98,21 +102,24 @@ class TeamController extends Controller
* *
* @param EditTeamRequest $request * @param EditTeamRequest $request
* @param Team $team * @param Team $team
* @return \Illuminate\Http\Response * @return RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*/ */
public function update(EditTeamRequest $request, Team $team): \Illuminate\Http\Response public function update(EditTeamRequest $request, Team $team): RedirectResponse
{ {
$this->authorize('update', $team); $this->authorize('update', $team);
$team = $this->teamService->updateTeam($team, $request->teamDescription, $team->joinType);
$team->description = $request->teamDescription; if ($team) {
$team->openJoin = $request->joinType; return redirect()
->to(route('teams.index'))
->with('success', __('Team updated.'));
}
$team->save(); return redirect()
$request->session()->flash('success', __('Team edited successfully.')); ->back()
->with('error', __('An error ocurred while trying to update this team.'));
return redirect()->to(route('teams.index'));
} }
/** /**
@ -126,68 +133,45 @@ class TeamController extends Controller
// wip // wip
} }
public function invite(SendInviteRequest $request, Team $team): \Illuminate\Http\RedirectResponse public function invite(SendInviteRequest $request, Team $team): RedirectResponse
{ {
$this->authorize('invite', $team); $this->authorize('invite', $team);
$user = User::findOrFail($request->user); try {
if (! $team->openJoin) { $this->teamService->inviteUser($team, $request->user);
if (! Teamwork::hasPendingInvite($user->email, $team)) {
Teamwork::inviteToTeam($user, $team, function (TeamInvite $invite) use ($user) {
Mail::to($user)->send(new InviteToTeam($invite));
});
$request->session()->flash('success', __('Invite sent! They can now accept or deny it.')); return redirect()
} else { ->back()
$request->session()->flash('error', __('This user has already been invited.')); ->with('success', __('User invited successfully!'));
}
} else { } catch (UserAlreadyInvitedException | PublicTeamInviteException $ex) {
$request->session()->flash('error', __('You can\'t invite users to public teams.')); return redirect()
->back()
->with('error', $ex->getMessage());
} }
return redirect()->back();
} }
public function processInviteAction(Request $request, $action, $token): \Illuminate\Http\RedirectResponse public function processInviteAction(Request $request, $action, $token): RedirectResponse
{ {
switch ($action) { try {
case 'accept':
$invite = Teamwork::getInviteFromAcceptToken($token); $this->teamService->processInvite(Auth::user(), $action, $token);
if ($invite && $invite->user->is(Auth::user())) { return redirect()
Teamwork::acceptInvite($invite); ->to(route('teams.index'))
$request->session()->flash('success', __('Invite accepted! You have now joined :teamName.', ['teamName' => $invite->team->name])); ->with('success', __('Invite processed successfully!'));
} else {
$request->session()->flash('error', __('Invalid or expired invite URL.'));
}
break; } catch (InvalidInviteException $e) {
case 'deny': return redirect()
->back()
$invite = Teamwork::getInviteFromDenyToken($token); ->with('error', $e->getMessage());
if ($invite && $invite->user->is(Auth::user())) {
Teamwork::denyInvite($invite);
$request->session()->flash('success', __('Invite denied! Ask for another invite if this isn\'t what you meant.'));
} else {
$request->session()->flash('error', __('Invalid or expired invite URL.'));
}
break;
default:
$request->session()->flash('error', 'Sorry, but the invite URL you followed was malformed. Try asking for another invite, or submit a bug report.');
} }
// This page will show the user's current teams
return redirect()->to(route('teams.index'));
} }
public function switchTeam(Request $request, Team $team): \Illuminate\Http\RedirectResponse public function switchTeam(Request $request, Team $team): RedirectResponse
{ {
$this->authorize('switchTeam', $team); $this->authorize('switchTeam', $team);
@ -203,44 +187,13 @@ class TeamController extends Controller
} }
// Since it's a separate form, we shouldn't use the same update method // Since it's a separate form, we shouldn't use the same update method
public function assignVacancies(Request $request, Team $team): \Illuminate\Http\RedirectResponse public function assignVacancies(Request $request, Team $team): RedirectResponse
{ {
$this->authorize('update', $team); $this->authorize('update', $team);
$message = $this->teamService->updateVacancies($team, $request->assocVacancies);
// P.S. To future developers return redirect()
// This method gave me a lot of trouble lol. It's hard to write code when you're half asleep. ->back()
// There may be an n+1 query in the view and I don't think there's a way to avoid that without writing a lot of extra code. ->with('success', $message);
$requestVacancies = $request->assocVacancies;
$currentVacancies = $team->vacancies->pluck('id')->all();
if (is_null($requestVacancies)) {
foreach ($team->vacancies as $vacancy) {
$team->vacancies()->detach($vacancy->id);
}
$request->session()->flash('success', __('Removed all vacancy associations.'));
return redirect()->back();
}
$vacancyDiff = array_diff($requestVacancies, $currentVacancies);
$deselectedDiff = array_diff($currentVacancies, $requestVacancies);
if (! empty($vacancyDiff) || ! empty($deselectedDiff)) {
foreach ($vacancyDiff as $selectedVacancy) {
$team->vacancies()->attach($selectedVacancy);
}
foreach ($deselectedDiff as $deselectedVacancy) {
$team->vacancies()->detach($deselectedVacancy);
}
} else {
$team->vacancies()->attach($requestVacancies);
}
$request->session()->flash('success', __('Assignments changed successfully.'));
return redirect()->back();
} }
} }

View File

@ -5,6 +5,8 @@ namespace App\Http\Controllers;
// Most of these namespaces have no effect on the code, however, they're used by IDEs so they can resolve return types and for PHPDocumentor as well // Most of these namespaces have no effect on the code, however, they're used by IDEs so they can resolve return types and for PHPDocumentor as well
use App\Exceptions\FileUploadException;
use App\Services\TeamFileService;
use App\TeamFile; use App\TeamFile;
use App\Http\Requests\UploadFileRequest; use App\Http\Requests\UploadFileRequest;
@ -24,11 +26,16 @@ use Illuminate\Http\Response;
class TeamFileController extends Controller class TeamFileController extends Controller
{ {
private $fileService;
public function __construct(TeamFileService $fileService) {
$this->fileService = $fileService;
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param Request $request * @param Request $request
* @return Application|Factory|View|Response
*/ */
public function index(Request $request) public function index(Request $request)
{ {
@ -55,33 +62,24 @@ class TeamFileController extends Controller
{ {
$this->authorize('store', TeamFile::class); $this->authorize('store', TeamFile::class);
$upload = $request->file('file'); try {
$caption = $request->caption;
$description = $request->description;
$file = $upload->store('uploads'); $this->fileService->addFile($request->file('file'), Auth::user()->id, Auth::user()->currentTeam->id, $caption, $description);
$originalFileName = $upload->getClientOriginalName();
$originalFileExtension = $upload->extension();
$originalFileSize = $upload->getSize();
$fileEntry = TeamFile::create([ return redirect()
'uploaded_by' => Auth::user()->id, ->back()
'team_id' => Auth::user()->currentTeam->id, ->with('success', __('File uploaded successfully.'));
'name' => $originalFileName,
'caption' => $request->caption, } catch (FileUploadException $uploadException) {
'description' => $request->description,
'fs_location' => $file, return redirect()
'extension' => $originalFileExtension, ->back()
'size' => $originalFileSize ->with('error', $uploadException->getMessage());
]);
if ($fileEntry && !is_bool($file))
{
$request->session()->flash('success', 'File uploaded successfully!');
return redirect()->back();
} }
$request->session()->flash('error', 'There was an unknown error whilst trying to upload your file.');
return redirect()->back();
} }
@ -101,29 +99,6 @@ class TeamFileController extends Controller
} }
} }
/**
* Show the form for editing the specified resource.
*
* @param \App\TeamFile $teamFile
* @return Response
*/
public function edit(TeamFile $teamFile)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\TeamFile $teamFile
* @return Response
*/
public function update(Request $request, TeamFile $teamFile)
{
//
}
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *

View File

@ -0,0 +1,158 @@
<?php
namespace App\Services;
use ContextAwareValidator;
use App\Application;
use App\Events\ApplicationDeniedEvent;
use App\Exceptions\ApplicationNotFoundException;
use App\Exceptions\IncompleteApplicationException;
use App\Exceptions\UnavailableApplicationException;
use App\Exceptions\VacancyNotFoundException;
use App\Notifications\ApplicationMoved;
use App\Notifications\NewApplicant;
use App\Response;
use App\User;
use App\Vacancy;
use Illuminate\Auth\Authenticatable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class ApplicationService
{
public function renderForm($vacancySlug)
{
$vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get();
$firstVacancy = $vacancyWithForm->first();
if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN') {
return view('dashboard.application-rendering.apply')
->with([
'vacancy' => $vacancyWithForm->first(),
'preprocessedForm' => json_decode($vacancyWithForm->first()->forms->formStructure, true),
]);
} else {
throw new ApplicationNotFoundException('The application you\'re looking for could not be found or it is currently unavailable.', 404);
}
}
/**
* Fills a vacancy's form with submitted data.
*
* @throws UnavailableApplicationException Thrown when the application has no vacancies or is closed
* @throws VacancyNotFoundException Thrown when the associated vacancy is not found
* @throws IncompleteApplicationException Thrown when there are missing fields
*/
public function fillForm(Authenticatable $applicant, array $formData, $vacancySlug): bool
{
$vacancy = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get();
if ($vacancy->isEmpty()) {
throw new VacancyNotFoundException('This vacancy doesn\'t exist; Please use the proper buttons to apply to one.', 404);
}
if ($vacancy->first()->vacancyCount == 0 || $vacancy->first()->vacancyStatus !== 'OPEN') {
throw new UnavailableApplicationException("This application is unavailable.");
}
Log::info('Processing new application!');
$formStructure = json_decode($vacancy->first()->forms->formStructure, true);
$responseValidation = ContextAwareValidator::getResponseValidator($formData, $formStructure);
Log::info('Built response & validator structure!');
if (!$responseValidation->get('validator')->fails()) {
$response = Response::create([
'responseFormID' => $vacancy->first()->forms->id,
'associatedVacancyID' => $vacancy->first()->id, // Since a form can be used by multiple vacancies, we can only know which specific vacancy this response ties to by using a vacancy ID
'responseData' => $responseValidation->get('responseStructure'),
]);
Log::info('Registered form response!', [
'applicant' => $applicant->name,
'vacancy' => $vacancy->first()->vacancyName
]);
$application = Application::create([
'applicantUserID' => $applicant->id,
'applicantFormResponseID' => $response->id,
'applicationStatus' => 'STAGE_SUBMITTED',
]);
Log::info('Submitted an application!', [
'responseID' => $response->id,
'applicant' => $applicant->name
]);
foreach (User::all() as $user) {
if ($user->hasRole('admin')) {
$user->notify((new NewApplicant($application, $vacancy->first()))->delay(now()->addSeconds(10)));
}
}
return true;
}
Log::warning('Application form for ' . $applicant->name . ' contained errors, resetting!');
throw new IncompleteApplicationException('There are one or more errors in your application. Please make sure none of your fields are empty, since they are all required.');
}
public function updateStatus(Application $application, $newStatus)
{
switch ($newStatus) {
case 'deny':
event(new ApplicationDeniedEvent($application));
$message = __("Application denied successfully.");
break;
case 'interview':
Log::info(' Moved application ID ' . $application->id . 'to interview stage!');
$message = __('Application moved to interview stage!');
$application->setStatus('STAGE_INTERVIEW');
$application->user->notify(new ApplicationMoved());
break;
default:
throw new \LogicException("Wrong status parameter. Please notify a developer.");
}
return $message;
}
/**
* @throws \Exception
*/
public function delete(Application $application): ?bool
{
return $application->delete();
}
public function canVote($votes): bool
{
$allvotes = collect([]);
foreach ($votes as $vote) {
if ($vote->userID == Auth::user()->id) {
$allvotes->push($vote);
}
}
return !(($allvotes->count() == 1));
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Services;
use App\Application;
use App\Appointment;
use App\Exceptions\InvalidAppointmentStatusException;
use App\Notifications\ApplicationMoved;
use App\Notifications\AppointmentScheduled;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class AppointmentService
{
private $allowedPlatforms = [
'ZOOM',
'DISCORD',
'SKYPE',
'MEET',
'TEAMSPEAK',
];
public function createAppointment(Application $application, Carbon $appointmentDate, $appointmentDescription, $appointmentLocation)
{
$appointment = Appointment::create([
'appointmentDescription' => $appointmentDescription,
'appointmentDate' => $appointmentDate->toDateTimeString(),
'applicationID' => $application->id,
'appointmentLocation' => (in_array($appointmentLocation, $this->allowedPlatforms)) ? $appointmentLocation : 'DISCORD',
]);
$application->setStatus('STAGE_INTERVIEW_SCHEDULED');
Log::info('User '.Auth::user()->name.' has scheduled an appointment with '.$application->user->name.' for application ID'.$application->id, [
'datetime' => $appointmentDate->toDateTimeString(),
'scheduled' => now(),
]);
$application->user->notify(new AppointmentScheduled($appointment));
return true;
}
/**
* Updates the appointment with the new $status.
* It also sets the application's status to peer approval.
*
* Set $updateApplication to false to only update its status
*
* @throws InvalidAppointmentStatusException
*/
public function updateAppointment(Application $application, $status, $updateApplication = true)
{
$validStatuses = [
'SCHEDULED',
'CONCLUDED',
];
if ($status == 'SCHEDULED' || $status == 'CONCLUDED')
{
$application->appointment->appointmentStatus = strtoupper($status);
$application->appointment->save();
if ($updateApplication)
{
$application->setStatus('STAGE_PEERAPPROVAL');
$application->user->notify(new ApplicationMoved());
}
}
else
{
throw new InvalidAppointmentStatusException("Invalid appointment status!");
}
}
/**
* @return string[]
*/
public function getAllowedPlatforms(): array
{
return $this->allowedPlatforms;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Services;
use App\Application;
use App\Comment;
use Illuminate\Support\Facades\Auth;
class CommentService
{
public function addComment(Application $application, $comment): Comment {
return Comment::create([
'authorID' => Auth::user()->id,
'applicationID' => $application->id,
'text' => $comment,
]);
}
public function deleteComment(Comment $comment): ?bool
{
return $comment->delete();
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Services;
use App\Exceptions\InvalidGamePreferenceException;
use App\Exceptions\OptionNotFoundException;
use App\Facades\Options;
use Illuminate\Auth\Authenticatable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class ConfigurationService
{
/**
* @throws OptionNotFoundException|\Exception
*
*/
public function saveConfiguration($configuration) {
foreach ($configuration as $optionName => $option) {
try {
Log::debug('Going through option '.$optionName);
if (Options::optionExists($optionName)) {
Log::debug('Option exists, updating to new values', [
'opt' => $optionName,
'new_value' => $option,
]);
Options::changeOption($optionName, $option);
}
} catch (\Exception $ex) {
Log::error('Unable to update options!', [
'msg' => $ex->getMessage(),
'trace' => $ex->getTraceAsString(),
]);
// Let service caller handle this without failing here
throw $ex;
}
}
}
/**
* Saves the chosen game integration
*
* @throws InvalidGamePreferenceException
* @returns bool
*/
public function saveGameIntegration($gamePreference): bool
{
// TODO: Find solution to dynamically support games
$supportedGames = [
'RUST',
'MINECRAFT',
'SE',
'GMOD'
];
if (!is_null($gamePreference) && in_array($gamePreference, $supportedGames))
{
Options::changeOption('currentGame', $gamePreference);
return true;
}
throw new InvalidGamePreferenceException("Unsupported game " . $gamePreference);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Services;
use App\Exceptions\FailedCaptchaException;
use App\Notifications\NewContact;
use App\User;
use Illuminate\Support\Facades\Http;
class ContactService
{
/**
* Sends a message to all admins.
*
* @throws FailedCaptchaException
*/
public function sendMessage($ipAddress, $message, $email, $challenge)
{
// TODO: now: add middleware for this verification, move to invisible captcha
$verifyrequest = Http::asForm()->post(config('recaptcha.verify.apiurl'), [
'secret' => config('recaptcha.keys.secret'),
'response' => $challenge,
'remoteip' => $ipAddress,
]);
$response = json_decode($verifyrequest->getBody(), true);
if (! $response['success']) {
throw new FailedCaptchaException('Beep beep boop... Robot? Submission failed.');
}
foreach (User::all() as $user) {
if ($user->hasRole('admin')) {
$user->notify(new NewContact(collect([
'message' => $message,
'ip' => $ipAddress,
'email' => $email,
])));
}
}
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Services;
use App\Exceptions\EmptyFormException;
use App\Exceptions\FormHasConstraintsException;
use App\Form;
use ContextAwareValidator;
class FormManagementService
{
public function addForm($fields) {
if (count($fields) == 2) {
// form is probably empty, since forms with fields will always have more than 2 items
throw new EmptyFormException('Sorry, but you may not create empty forms.');
}
$contextValidation = ContextAwareValidator::getValidator($fields, true, true);
if (! $contextValidation->get('validator')->fails()) {
$storableFormStructure = $contextValidation->get('structure');
Form::create(
[
'formName' => $fields['formName'],
'formStructure' => $storableFormStructure,
'formStatus' => 'ACTIVE',
]
);
return true;
}
return $contextValidation->get('validator')->errors()->getMessages();
}
public function deleteForm(Form $form) {
$deletable = true;
if (! is_null($form) && ! is_null($form->vacancies) && $form->vacancies->count() !== 0 || ! is_null($form->responses)) {
$deletable = false;
}
if ($deletable) {
$form->delete();
return true;
} else {
throw new FormHasConstraintsException(__('You cannot delete this form because it\'s tied to one or more applications and ranks, or because it doesn\'t exist.'));
}
}
public function updateForm(Form $form, $fields) {
$contextValidation = ContextAwareValidator::getValidator($fields, true);
if (! $contextValidation->get('validator')->fails()) {
// Add the new structure into the form. New, subsquent fields will be identified by the "new" prefix
// This prefix doesn't actually change the app's behavior when it receives applications.
// Additionally, old applications won't of course display new and updated fields, because we can't travel into the past and get data for them
$form->formStructure = $contextValidation->get('structure');
$form->save();
return $form;
} else {
return $contextValidation->get('validator')->errors()->getMessages();
}
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Services;
use App\Application;
use App\Exceptions\InvalidAppointmentException;
class MeetingNoteService
{
/**
* Adds meeting notes to an application.
*
* @param Application $application
* @param $noteText
* @return bool
* @throws InvalidAppointmentException Thrown when an application doesn't have an appointment to save notes to
*/
public function addToApplication(Application $application, $noteText): bool {
if (! is_null($application)) {
$application->load('appointment');
$application->appointment->meetingNotes = $noteText;
$application->appointment->save();
return true;
} else {
throw new InvalidAppointmentException('There\'s no appointment to save notes to!');
}
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Services;
use App\Exceptions\ProfileNotFoundException;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ProfileService
{
/**
* @throws ProfileNotFoundException
*/
public function updateProfile($userID, Request $request) {
$profile = User::find($userID)->profile;
$social = [];
if (! is_null($profile)) {
switch ($request->avatarPref) {
case 'MOJANG':
$avatarPref = 'crafatar';
break;
case 'GRAVATAR':
$avatarPref = strtolower($request->avatarPref);
break;
}
$social['links']['github'] = $request->socialGithub;
$social['links']['twitter'] = $request->socialTwitter;
$social['links']['insta'] = $request->socialInsta;
$social['links']['discord'] = $request->socialDiscord;
$profile->profileShortBio = $request->shortBio;
$profile->profileAboutMe = $request->aboutMe;
$profile->avatarPreference = $avatarPref;
$profile->socialLinks = json_encode($social);
return $profile->save();
}
throw new ProfileNotFoundException("This profile does not exist.");
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Services;
use App\Facades\Options;
use Illuminate\Support\Facades\Log;
class SecuritySettingsService
{
/**
* Saves the app security settings.
*
* @param $policy
* @param array $options
* @return bool
*/
public function save($policy, $options = []) {
$validPolicies = [
'off',
'low',
'medium',
'high'
];
if (in_array($policy, $validPolicies))
{
Options::changeOption('pw_security_policy', $policy);
Log::debug('[Options] Changing option pw_security_policy', [
'new_value' => $policy
]);
}
else
{
Log::debug('[WARN] Ignoring bogus policy', [
'avaliable' => $validPolicies,
'given' => $policy
]);
}
Options::changeOption('graceperiod', $options['graceperiod']);
Options::changeOption('password_expiry', $options['pwexpiry']);
Options::changeOption('force2fa', $options['enforce2fa']);
Options::changeOption('requireGameLicense', $options['requirePMC']);
return true;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Services;
use App\Exceptions\FileUploadException;
use App\TeamFile;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
class TeamFileService
{
public function addFile(UploadedFile $upload, $uploader, $team, $caption, $description) {
$file = $upload->store('uploads');
$originalFileName = $upload->getClientOriginalName();
$originalFileExtension = $upload->extension();
$originalFileSize = $upload->getSize();
$fileEntry = TeamFile::create([
'uploaded_by' => $uploader,
'team_id' => $team,
'name' => $originalFileName,
'caption' => $caption,
'description' => $description,
'fs_location' => $file,
'extension' => $originalFileExtension,
'size' => $originalFileSize
]);
if ($fileEntry && !is_bool($file))
{
return $fileEntry;
}
throw new FileUploadException("There was an unknown error whilst trying to upload your file.");
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace App\Services;
use App\Exceptions\InvalidInviteException;
use App\Exceptions\PublicTeamInviteException;
use App\Exceptions\UserAlreadyInvitedException;
use App\Mail\InviteToTeam;
use App\Team;
use App\User;
use Illuminate\Auth\Authenticatable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Mpociot\Teamwork\Facades\Teamwork;
use Mpociot\Teamwork\TeamInvite;
class TeamService
{
/**
* Create a team
*
* @param $teamName
* @param $ownerID
* @return Team
*/
public function createTeam($teamName, $ownerID): Team {
$team = Team::create([
'name' => $teamName,
'owner_id' => $ownerID,
]);
Auth::user()->teams()->attach($team->id);
return $team;
}
public function updateTeam(Team $team, $teamDescription, $joinType): bool
{
$team->description = $teamDescription;
$team->openJoin = $joinType;
return $team->save();
}
/**
* Invites a user to a $team.
*
* @throws PublicTeamInviteException Thrown when trying to invite a user to a public team
* @throws UserAlreadyInvitedException Thrown when a user is already invited
*/
public function inviteUser(Team $team, $userID): bool
{
$user = User::findOrFail($userID);
if (! $team->openJoin) {
if (! Teamwork::hasPendingInvite($user->email, $team)) {
Teamwork::inviteToTeam($user, $team, function (TeamInvite $invite) use ($user) {
Mail::to($user)->send(new InviteToTeam($invite));
});
return true;
} else {
throw new UserAlreadyInvitedException('This user has already been invited.');
}
} else {
throw new PublicTeamInviteException('You can\'t invite users to public teams.');
}
}
/**
* Accepts or denies a user invite
*
* @param Authenticatable $user
* @param $action
* @param $token
* @return bool True on success or exception on failure
* @throws InvalidInviteException Thrown when the invite code / url is invalid
*/
public function processInvite(Authenticatable $user, $action, $token): bool {
switch ($action) {
case 'accept':
$invite = Teamwork::getInviteFromAcceptToken($token);
if ($invite && $invite->user->is($user)) {
Teamwork::acceptInvite($invite);
} else {
throw new InvalidInviteException('Invalid or expired invite URL.');
}
break;
case 'deny':
$invite = Teamwork::getInviteFromDenyToken($token);
if ($invite && $invite->user->is($user)) {
Teamwork::denyInvite($invite);
} else {
throw new InvalidInviteException('Invalid or expired invite URL.');
}
break;
default:
throw new InvalidInviteException('Sorry, but the invite URL you followed was malformed.');
}
return true;
}
/**
* @param Team $team
* @param $associatedVacancies
* @return string The success message, exception/bool if error
*/
public function updateVacancies(Team $team, $associatedVacancies): string
{
// P.S. To future developers
// This method gave me a lot of trouble lol. It's hard to write code when you're half asleep.
// There may be an n+1 query in the view and I don't think there's a way to avoid that without writing a lot of extra code.
$requestVacancies = $associatedVacancies;
$currentVacancies = $team->vacancies->pluck('id')->all();
if (is_null($requestVacancies)) {
foreach ($team->vacancies as $vacancy) {
$team->vacancies()->detach($vacancy->id);
}
return 'Removed all vacancy associations.';
}
$vacancyDiff = array_diff($requestVacancies, $currentVacancies);
$deselectedDiff = array_diff($currentVacancies, $requestVacancies);
if (! empty($vacancyDiff) || ! empty($deselectedDiff)) {
foreach ($vacancyDiff as $selectedVacancy) {
$team->vacancies()->attach($selectedVacancy);
}
foreach ($deselectedDiff as $deselectedVacancy) {
$team->vacancies()->detach($deselectedVacancy);
}
} else {
$team->vacancies()->attach($requestVacancies);
}
return 'Assignments changed successfully.';
}
}