Major changes - Vote system now finished

This commit is contained in:
2020-05-30 00:20:39 +01:00
parent cc8c293cc6
commit d15c0cb12f
32 changed files with 1791 additions and 17 deletions

View File

@@ -14,6 +14,7 @@ class Application extends Model
];
public function user()
{
return $this->belongsTo('App\User', 'applicantUserID', 'id');
@@ -29,10 +30,16 @@ class Application extends Model
return $this->hasOne('App\Appointment', 'applicationID', 'id');
}
public function votes()
{
return $this->belongsToMany('App\Vote', 'votes_has_application');
}
public function setStatus($status)
{
return $this->update([
'applicationStatus' => $status
]);
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Console\Commands;
use App\Application;
use App\Events\ApplicationApprovedEvent;
use App\Events\ApplicationDeniedEvent;
use Illuminate\Console\Command;
class CountVotes extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'votes:evaluate {--d|dryrun : Controls whether passing applicants should be promoted (e.g. only show results)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Iterates through eligible applications and determines if they should be approved based on the number of votes';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$eligibleApps = Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get();
$pbar = $this->output->createProgressBar($eligibleApps->count());
if($eligibleApps->isEmpty())
{
$this->error('𐄂 There are no applications that need to be processed.');
return false;
}
foreach ($eligibleApps as $application)
{
$votes = $application->votes;
$voteCount = $application->votes->count();
$positiveVotes = 0;
$negativeVotes = 0;
if ($voteCount > 5)
{
$this->info('Counting votes for application ID ' . $application->id);
foreach ($votes as $vote)
{
switch ($vote->allowedVoteType)
{
case 'VOTE_APPROVE':
$positiveVotes++;
break;
case 'VOTE_DENY':
$negativeVotes++;
break;
}
}
$this->info('Total votes for application ID ' . $application->id . ': ' . $voteCount);
$this->info('Calculating criteria...');
$negativeVotePercent = floor(($negativeVotes / $voteCount) * 100);
$positiveVotePercent = floor(($positiveVotes / $voteCount) * 100);
$pollResult = $positiveVotePercent > $negativeVotePercent;
$this->table([
'% of approval votes',
'% of denial votes'
], [ // array of arrays, e.g. rows
[
$positiveVotePercent . "%",
$negativeVotePercent . "%"
]
]);
if ($pollResult)
{
$this->info('✓ Dispatched promotion event for applicant ' . $application->user->name);
if (!$this->option('dryrun'))
{
event(new ApplicationApprovedEvent(Application::find($application->id)));
}
else
{
$this->warn('Dry run: Event won\'t be dispatched');
}
$pbar->advance();
}
else {
if (!$this->option('dryrun'))
{
event(new ApplicationDeniedEvent(Application::find($application->id)));
}
else {
$this->warn('Dry run: Event won\'t be dispatched');
}
$pbar->advance();
$this->error('𐄂 Applicant ' . $application->user->name . ' does not meet vote criteria (Majority)');
}
}
else
{
$this->warn("Application ID" . $application->id . " did not have enough votes for processing (min 5)");
}
}
$pbar->finish();
return true;
}
}

View File

@@ -25,6 +25,10 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
$schedule->command('vote:evaluate')
->everyFiveMinutes();
// Production value: Every day
}
/**

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Events;
use App\Application;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ApplicationApprovedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $application;
/**
* Create a new event instance.
*
* @param Application $application
*/
public function __construct(Application $application)
{
$this->application = $application;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Events;
use App\Application;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ApplicationDeniedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $application;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Application $application)
{
$this->application = $application;
}
}

View File

@@ -36,6 +36,10 @@ class Handler extends ExceptionHandler
*/
public function report(Throwable $exception)
{
if (app()->bound('sentry') && $this->shouldReport($exception)) {
app('sentry')->captureException($exception);
}
parent::report($exception);
}

View File

@@ -13,6 +13,22 @@ use Illuminate\Support\Facades\Validator;
class ApplicationController extends Controller
{
private function canVote($votes)
{
$allvotes = collect([]);
foreach ($votes as $vote)
{
if ($vote->userID == Auth::user()->id)
{
Log::debug('Match');
$allvotes->push($vote);
}
}
return $allvotes->count() == 1;
}
public function showUserApps()
{
@@ -33,7 +49,8 @@ class ApplicationController extends Controller
'application' => $application,
'structuredResponses' => json_decode($application->response->responseData, true),
'formStructure' => $application->response->form,
'vacancy' => $application->response->vacancy
'vacancy' => $application->response->vacancy,
'canVote' => $this->canVote($application->votes)
]
);
}
@@ -96,6 +113,7 @@ class ApplicationController extends Controller
{
return view('dashboard.appmanagement.peerreview')
->with('applications', Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get());
}
public function renderApplicationForm(Request $request, $vacancySlug)

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Application;
use App\Http\Requests\SaveNotesRequest;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Appointment;
@@ -84,5 +85,24 @@ class AppointmentController extends Controller
return redirect()->back();
}
// also updates
public function saveNotes(SaveNotesRequest $request, $applicationID)
{
$application = Application::find($applicationID);
if (!is_null($application))
{
$application->appointment->meetingNotes = $request->noteText;
$application->appointment->save();
$request->session()->flash('success', 'Meeting notes have been saved.');
}
else
{
$request->session()->flash('error', 'Sanity check failed: There\'s no appointment to save notes to!');
}
return redirect()->back();
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers;
use App\Application;
use App\Events\ApplicationApprovedEvent;
use Illuminate\Http\Request;
class DevToolsController extends Controller
{
public function index()
{
return view('dashboard.administration.devtools')
->with('applications', Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get());
}
public function forceVoteCount(Request $request)
{
$application = Application::find($request->application);
if (!is_null($application))
{
event(new ApplicationApprovedEvent($application));
$request->session()->flash('success', 'Event dispatched! Please check the debug logs for more info');
}
else
{
$request->session()->flash('error', 'Application doesn\'t exist!');
}
return redirect()->back();
}
}

View File

@@ -2,9 +2,43 @@
namespace App\Http\Controllers;
use App\Application;
use App\Http\Requests\VoteRequest;
use App\Jobs\ProcessVoteList;
use App\Vote;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class VoteController extends Controller
{
//
public function vote(VoteRequest $voteRequest, $applicationID)
{
$application = Application::find($applicationID);
if (!is_null($application))
{
$vote = Vote::create([
'userID' => Auth::user()->id,
'allowedVoteType' => $voteRequest->voteType,
]);
$vote->application()->attach($applicationID);
Log::info('User ' . Auth::user()->name . ' has voted in applicant ' . $application->user->name . '\'s application', [
'voteType' => $voteRequest->voteType
]);
$voteRequest->session()->flash('success', 'Your vote has been registered! You will now be notified about the outcome of this application.');
}
else
{
$voteRequest->session()->flash('error', 'Can\t vote a non existant application!');
}
// Cron job will run command that processes votes
return redirect()->back();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SaveNotesRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'noteText' => 'required|string'
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class VoteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'voteType' => 'required|string|in:VOTE_DENY,VOTE_APPROVE'
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Listeners;
use App\Events\ApplicationDeniedEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class DenyUser
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ApplicationDeniedEvent $event
* @return void
*/
public function handle(ApplicationDeniedEvent $event)
{
$event->application->setStatus('DENIED');
Log::info('User ' . $event->application->user->name . ' just had their application denied.');
// Also dispatch other notifications
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Listeners;
use App\Events\ApplicationApprovedEvent;
use App\StaffProfile;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class PromoteUser
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ApplicationApprovedEvent $event
* @return void
*/
public function handle(ApplicationApprovedEvent $event)
{
$event->application->setStatus('APPROVED');
$staffProfile = StaffProfile::create([
'userID' => $event->application->user->id,
'approvalDate' => now()->toDateTimeString(),
'memberNotes' => 'Approved by staff members. Welcome them to the team!'
]);
Log::info('User ' . $event->application->user->name . ' has just been promoted!', [
'newRank' => $event->application->response->vacancy->permissionGroupName,
'staffProfileID' => $staffProfile->id
]);
// TODO: Dispatch alert email and notifications for the user and staff members
// TODO: Also assign new app role based on the permission group name
}
}

View File

@@ -20,6 +20,12 @@ class EventServiceProvider extends ServiceProvider
SendEmailVerificationNotification::class,
OnUserRegistration::class
],
'App\Events\ApplicationApprovedEvent' => [
'App\Listeners\PromoteUser'
],
'App\Events\ApplicationDeniedEvent' => [
'App\Listeners\DenyUser'
]
];
/**
@@ -29,6 +35,7 @@ class EventServiceProvider extends ServiceProvider
*/
public function boot()
{
parent::boot();
//

View File

@@ -2,6 +2,7 @@
namespace App\Providers;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -32,8 +33,17 @@ class MojangStatusProvider extends ServiceProvider
{
Log::info("Mojang Status Provider: Mojang Status not found in the cache; Sending new request.");
$mcstatus = Http::get(config('general.urls.mojang.statuscheck'));
Cache::put('mojang_status', base64_encode($mcstatus->body()), now()->addMinutes(60));
try
{
$mcstatus = Http::get(config('general.urls.mojang.statuscheck'));
Cache::put('mojang_status', base64_encode($mcstatus->body()), now()->addDays(3));
}
catch(ConnectException $connectException)
{
Log::critical('Could not connect to Mojang servers: Cannot check/refresh status', [
'message' => $connectException->getMessage()
]);
}
}
View::share('mcstatus', json_decode(base64_decode(Cache::get('mojang_status')), true));

View File

@@ -6,5 +6,13 @@ use Illuminate\Database\Eloquent\Model;
class StaffProfile extends Model
{
//
public $fillable = [
'userID',
'approvalDate',
'terminationDate',
'resignationDate',
'memberNotes'
];
}

View File

@@ -42,8 +42,14 @@ class User extends Authenticatable
return $this->hasMany('App\Application', 'applicantUserID', 'id');
}
public function votes()
{
return $this->hasMany('App\Vote', 'userID', 'id');
}
public function profile()
{
return $this->hasOne('App\Profile', 'userID', 'id');
}
}

View File

@@ -6,5 +6,24 @@ use Illuminate\Database\Eloquent\Model;
class Vote extends Model
{
//
public $fillable = [
'userID',
'allowedVoteType',
];
public $touches = [
'application'
];
public function user()
{
return $this->belongsTo('App\User', 'id', 'userID');
}
public function application()
{
return $this->belongsToMany('App\Application', 'votes_has_application');
}
}