2018-10-09 02:38:16 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
use Carbon\Carbon;
|
2018-10-16 03:23:19 +02:00
|
|
|
use Validator;
|
2018-10-17 15:12:20 +02:00
|
|
|
use DB;
|
2018-10-18 05:37:08 +02:00
|
|
|
use Cache;
|
|
|
|
|
2018-10-09 02:38:16 +02:00
|
|
|
use App\Poll;
|
2018-10-17 15:12:20 +02:00
|
|
|
use App\PollVote;
|
|
|
|
use App\PollVotingCode;
|
2018-10-09 02:38:16 +02:00
|
|
|
|
|
|
|
class PollController extends Controller
|
|
|
|
{
|
|
|
|
public function __invoke(Request $request)
|
|
|
|
{
|
|
|
|
return view('create_poll');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function create(Request $request)
|
|
|
|
{
|
2018-10-16 03:23:19 +02:00
|
|
|
if($request->has('options')) {
|
|
|
|
$request['options'] = array_filter($request->input('options'), function($i) { return $i !== null; });
|
|
|
|
}
|
|
|
|
|
|
|
|
$request['allow_multiple_answers'] = $request->has('allow_multiple_answers');
|
|
|
|
$request['hide_results_until_closed'] = $request->has('hide_results_until_closed');
|
|
|
|
$request['automatically_close_poll'] = $request->has('automatically_close_poll');
|
|
|
|
$request['set_admin_password'] = $request->has('set_admin_password');
|
|
|
|
|
2018-10-12 20:11:44 +02:00
|
|
|
$validatedInput = $request->validate([
|
|
|
|
'question' => 'required|string',
|
2018-10-16 03:23:19 +02:00
|
|
|
'options' => 'required|min:2|distinct',
|
|
|
|
'allow_multiple_answers' => 'required|boolean',
|
|
|
|
'hide_results_until_closed' => 'required|boolean',
|
|
|
|
'automatically_close_poll' => 'required|boolean',
|
|
|
|
'automatically_close_poll_datetime' => 'required_if:automatically_close_poll,true|date|after:now',
|
|
|
|
'set_admin_password' => 'required|boolean',
|
|
|
|
'admin_password' => 'required_if:set_admin_password,true|nullable|string',
|
|
|
|
'duplicate_vote_checking' => 'required|in:none,cookies,codes',
|
|
|
|
'number_of_codes' => 'required_if:duplicate_vote_checking,codes|integer|min:2'
|
2018-10-12 20:11:44 +02:00
|
|
|
]);
|
|
|
|
|
2018-10-09 02:38:16 +02:00
|
|
|
$poll = new Poll;
|
2018-10-16 03:23:19 +02:00
|
|
|
$poll->question = $validatedInput['question'];
|
|
|
|
$poll->duplicate_vote_checking = $validatedInput['duplicate_vote_checking'];
|
|
|
|
$poll->allow_multiple_answers = $validatedInput['allow_multiple_answers'];
|
|
|
|
$poll->hide_results_until_closed = $validatedInput['hide_results_until_closed'];
|
2018-10-09 02:38:16 +02:00
|
|
|
$poll->created_at = Carbon::now();
|
2018-10-16 03:23:19 +02:00
|
|
|
if($validatedInput['automatically_close_poll']) {
|
|
|
|
$poll->closes_at = Carbon::parse($validatedInput['automatically_close_poll_datetime']);
|
|
|
|
}
|
|
|
|
if($validatedInput['set_admin_password']) {
|
|
|
|
$poll->admin_password = $validatedInput['admin_password'];
|
|
|
|
}
|
|
|
|
$poll->save();
|
2018-10-12 20:11:44 +02:00
|
|
|
|
2018-10-16 03:23:19 +02:00
|
|
|
$poll->options()->createMany(array_map(function($i) { return ['text' => $i]; }, $validatedInput['options']));
|
|
|
|
$poll->save();
|
|
|
|
|
|
|
|
if($poll->duplicate_vote_checking == 'codes') {
|
|
|
|
$codes = $poll->createVotingCodes($validatedInput['number_of_codes']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return redirect()->action('PollController@view', ['poll' => $poll])->with('new', true);
|
2018-10-09 02:38:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function view(Request $request, Poll $poll)
|
|
|
|
{
|
2018-10-19 20:28:33 +02:00
|
|
|
if($poll->closed) {
|
|
|
|
return redirect()->action('PollController@viewResults', ['poll' => $poll])->with('alreadyClosed', true);
|
|
|
|
}
|
|
|
|
|
2018-10-16 03:23:19 +02:00
|
|
|
$new = $request->session()->pull('new', false);
|
|
|
|
|
|
|
|
if($request->format() == 'json') {
|
|
|
|
if($new) {
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-10-16 03:56:04 +02:00
|
|
|
//TODO: Implement JSON output
|
|
|
|
return null;
|
2018-10-16 03:23:19 +02:00
|
|
|
} else {
|
2018-10-17 15:12:20 +02:00
|
|
|
return view('view_poll')
|
|
|
|
->with('poll', $poll)
|
|
|
|
->with('new', $new)
|
2018-10-19 20:16:58 +02:00
|
|
|
->with('hasVoted', $this->hasVoted($request, $poll))
|
|
|
|
->with('code', $request->query('code', null));
|
2018-10-16 03:23:19 +02:00
|
|
|
}
|
2018-10-09 02:38:16 +02:00
|
|
|
}
|
|
|
|
|
2018-10-17 15:12:20 +02:00
|
|
|
public function viewResults(Request $request, Poll $poll)
|
|
|
|
{
|
2018-10-18 00:01:03 +02:00
|
|
|
$voted = $request->session()->pull('voted', false);
|
2018-10-19 20:28:33 +02:00
|
|
|
$alreadyClosed = $request->session()->pull('alreadyClosed', false);
|
2018-10-17 15:12:20 +02:00
|
|
|
|
2018-10-18 05:37:08 +02:00
|
|
|
$this->createPieChart($poll);
|
|
|
|
|
2018-10-18 00:01:03 +02:00
|
|
|
return view('view_poll_results')
|
|
|
|
->with('poll', $poll)
|
2018-10-19 20:28:33 +02:00
|
|
|
->with('voted', $voted)
|
|
|
|
->with('alreadyClosed', $alreadyClosed);
|
2018-10-17 15:12:20 +02:00
|
|
|
}
|
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
private static function imageToDataUri($image)
|
|
|
|
{
|
|
|
|
ob_start();
|
|
|
|
|
|
|
|
imagepng($image);
|
|
|
|
$dataUri = "data:image/png;base64," . base64_encode(ob_get_contents());
|
|
|
|
|
|
|
|
ob_end_clean();
|
|
|
|
|
|
|
|
return $dataUri;
|
|
|
|
}
|
|
|
|
|
2018-10-18 05:37:08 +02:00
|
|
|
private function createPieChart(Poll $poll)
|
2018-10-17 15:12:20 +02:00
|
|
|
{
|
2018-10-18 05:37:08 +02:00
|
|
|
$voteCount = $poll->votes->count();
|
|
|
|
|
|
|
|
if(Cache::has($poll->id) && Cache::get($poll->id)['vote_count'] == $voteCount) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$baseColours = [[0xE8, 0x96, 0x3F], [0xAD, 0x3F, 0xE8], [0x3F, 0xE8, 0x6F], [0xE8, 0xE3, 0x3F], [0x3F, 0x64, 0xEB], [0xE8, 0x3F, 0x65], [0x3F, 0xE8, 0xDB]];
|
|
|
|
shuffle($baseColours);
|
|
|
|
|
|
|
|
$supersamplingFactor = 8;
|
|
|
|
|
|
|
|
$width = 512 * $supersamplingFactor;
|
|
|
|
$height = 512 * $supersamplingFactor;
|
|
|
|
$padding = 16 * $supersamplingFactor;
|
|
|
|
|
|
|
|
$chartWidth = $width - 2 * $padding;
|
|
|
|
$chartHeight = $height - 2 * $padding;
|
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
$colourSquareSize = 13;
|
|
|
|
|
2018-10-18 05:37:08 +02:00
|
|
|
$pieChart = imagecreatetruecolor($width, $height);
|
2018-10-18 22:52:19 +02:00
|
|
|
$transparent = imagecolorallocatealpha($pieChart, 0xFF, 0xFF, 0xFF, 0x7F);
|
|
|
|
imagefill($pieChart, 0, 0, $transparent);
|
|
|
|
imagesavealpha($pieChart, true);
|
2018-10-18 05:37:08 +02:00
|
|
|
imageantialias($pieChart, true);
|
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
$colourSquareUris = [];
|
2018-10-18 05:37:08 +02:00
|
|
|
|
|
|
|
$startDegrees = 0;
|
|
|
|
$sortedOptions = $poll->options->sortByDesc(function($option) use($poll) { return $poll->votes->where('poll_option_id', $option->id)->count(); });
|
|
|
|
$nonZeroOptions = $sortedOptions->filter(function($option) use($poll) { return $poll->votes->where('poll_option_id', $option->id)->count() > 0; })->values();
|
|
|
|
debug($nonZeroOptions);
|
2018-10-18 22:52:19 +02:00
|
|
|
for($i = 0; $i < $nonZeroOptions->count(); $i++) {
|
2018-10-18 05:37:08 +02:00
|
|
|
$option = $nonZeroOptions[$i];
|
|
|
|
|
|
|
|
//TODO: Fix gaps
|
|
|
|
$degrees = round($poll->votes->where('poll_option_id', $option->id)->count() / $voteCount * 360);
|
|
|
|
$endDegrees = min($startDegrees + $degrees, 360);
|
|
|
|
|
|
|
|
$c = function($j) use($i, $baseColours, $nonZeroOptions) {
|
|
|
|
return $baseColours[$i % count($baseColours)][$j]
|
|
|
|
+ (255 - $baseColours[$i % count($baseColours)][$j])
|
|
|
|
* floor($i / count($baseColours)) / (floor($nonZeroOptions->count() / count($baseColours)) + 1);
|
|
|
|
};
|
|
|
|
$colour = imagecolorallocate($pieChart, $c(0), $c(1), $c(2));
|
|
|
|
|
|
|
|
debug([$option->text, [$startDegrees, $endDegrees], [$c(0), $c(1), $c(2)]]);
|
|
|
|
|
|
|
|
imagefilledarc($pieChart, $width / 2, $height / 2, $chartWidth, $chartHeight, $startDegrees, $endDegrees, $colour, IMG_ARC_PIE);
|
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
$colourSquare = imagecreatetruecolor($colourSquareSize, $colourSquareSize);
|
|
|
|
$colourSquareColour = imagecolorallocate($colourSquare, $c(0), $c(1), $c(2));
|
|
|
|
imagefill($colourSquare, 0, 0, $colourSquareColour);
|
|
|
|
$colourSquareUris[$option->id] = PollController::imageToDataUri($colourSquare);
|
|
|
|
|
2018-10-18 05:37:08 +02:00
|
|
|
$startDegrees = $endDegrees;
|
|
|
|
}
|
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
debug($colourSquareUris);
|
2018-10-18 05:37:08 +02:00
|
|
|
|
|
|
|
$resized = imagecreatetruecolor($width / $supersamplingFactor, $height / $supersamplingFactor);
|
2018-10-18 22:52:19 +02:00
|
|
|
imagecolortransparent($resized, imagecolorallocatealpha($resized, 0, 0, 0, 0x7F));
|
|
|
|
imagealphablending($resized, false);
|
|
|
|
imagesavealpha($resized, true);
|
2018-10-18 05:37:08 +02:00
|
|
|
imagecopyresampled($resized, $pieChart, 0, 0, 0, 0, $width / $supersamplingFactor, $height / $supersamplingFactor, $width, $height);
|
|
|
|
$pieChart = $resized;
|
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
$dataUri = PollController::imageToDataUri($pieChart);
|
2018-10-18 05:37:08 +02:00
|
|
|
|
2018-10-18 22:52:19 +02:00
|
|
|
Cache::put($poll->id, ['vote_count' => $voteCount, 'pie_chart' => $dataUri, 'colour_squares' => $colourSquareUris], now()->addDays(1));
|
2018-10-17 15:12:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function hasVoted(Request $request, Poll $poll)
|
|
|
|
{
|
|
|
|
if($poll->duplicate_vote_checking == 'cookies') {
|
|
|
|
if($request->session()->exists($poll->id)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else if($poll->duplicate_vote_checking == 'codes') {
|
|
|
|
$code = PollVotingCode::find($request->query('code'));
|
|
|
|
|
|
|
|
if($code == null || $code->used) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-10-09 02:38:16 +02:00
|
|
|
public function vote(Request $request, Poll $poll)
|
|
|
|
{
|
2018-10-19 20:28:33 +02:00
|
|
|
if($poll->closed) {
|
|
|
|
return redirect()->action('PollController@viewResults', ['poll' => $poll])->with('alreadyClosed', true);
|
|
|
|
}
|
|
|
|
|
2018-10-17 15:12:20 +02:00
|
|
|
if($this->hasVoted($request, $poll)) {
|
2018-10-19 20:16:58 +02:00
|
|
|
return view('view_poll')
|
|
|
|
->with('poll', $poll)
|
|
|
|
->with('new', false)
|
|
|
|
->with('hasVoted', true);
|
2018-10-17 15:12:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if($poll->allow_multiple_answers) {
|
|
|
|
$validatedInput = $request->validate([
|
|
|
|
'options' => 'required|distinct',
|
|
|
|
]);
|
|
|
|
} else {
|
|
|
|
$validatedInput = $request->validate([
|
|
|
|
'options' => 'required|distinct|min:1|max:1',
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
foreach($validatedInput['options'] as $option)
|
|
|
|
{
|
|
|
|
if($poll->options()->find($option) == null) {
|
|
|
|
DB::rollBack();
|
|
|
|
|
2018-10-19 20:16:58 +02:00
|
|
|
return redirect()->action('PollController@view', ['poll' => $poll]);
|
2018-10-17 15:12:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$vote = new PollVote;
|
|
|
|
$vote->poll_option_id = $option;
|
|
|
|
$poll->votes()->save($vote);
|
|
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
if($poll->duplicate_vote_checking == 'cookies') {
|
|
|
|
$request->session()->put($poll->id, null);
|
|
|
|
} else if($poll->duplicate_vote_checking == 'codes') {
|
2018-10-19 20:16:58 +02:00
|
|
|
$code = PollVotingCode::find($request->query('code'));
|
|
|
|
|
2018-10-17 15:12:20 +02:00
|
|
|
$code->used = true;
|
|
|
|
$code->save();
|
|
|
|
}
|
|
|
|
|
2018-10-18 00:01:03 +02:00
|
|
|
return redirect()->action('PollController@viewResults', ['poll' => $poll])->with('voted', true);
|
2018-10-09 02:38:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function admin(Request $request, Poll $poll)
|
|
|
|
{
|
2018-10-19 20:16:58 +02:00
|
|
|
$changed = $request->session()->pull('changed', false);
|
2018-10-19 20:28:33 +02:00
|
|
|
$extraCodes = $request->session()->pull('extraCodes', null);
|
2018-10-19 20:16:58 +02:00
|
|
|
|
|
|
|
if($poll->admin_password == null || $request->query('password') != $poll->admin_password) {
|
2018-10-19 20:45:48 +02:00
|
|
|
return redirect()->action('PollController@viewResults', ['poll' => $poll]);
|
2018-10-19 20:16:58 +02:00
|
|
|
}
|
|
|
|
|
2018-10-19 20:28:33 +02:00
|
|
|
return view('edit_poll')->with('poll', $poll)->with('changed', $changed)->with('extraCodes', $extraCodes);
|
2018-10-09 02:38:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function edit(Request $request, Poll $poll)
|
|
|
|
{
|
2018-10-19 20:16:58 +02:00
|
|
|
if($request->has('extra_codes')) {
|
|
|
|
if($poll->duplicate_vote_checking != 'codes') {
|
|
|
|
return redirect()->action('PollController@view', ['poll' => $poll]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$validatedInput = $request->validate([
|
|
|
|
'extra_codes' => 'integer|min:1'
|
|
|
|
]);
|
|
|
|
|
|
|
|
$codes = $poll->createVotingCodes($validatedInput['extra_codes']);
|
|
|
|
|
|
|
|
return redirect()
|
|
|
|
->action('PollController@admin', ['poll' => $poll, 'password' => $poll->admin_password])
|
2018-10-19 20:28:33 +02:00
|
|
|
->with('extraCodes', $codes);
|
2018-10-19 20:16:58 +02:00
|
|
|
} else {
|
|
|
|
$request['allow_multiple_answers'] = $request->has('allow_multiple_answers');
|
|
|
|
$request['hide_results_until_closed'] = $request->has('hide_results_until_closed');
|
|
|
|
$request['automatically_close_poll'] = $request->has('automatically_close_poll');
|
|
|
|
$request['set_admin_password'] = $request->has('set_admin_password');
|
|
|
|
|
|
|
|
if($request['automatically_close_poll'] && $request['automatically_close_poll_datetime'] == 'now') {
|
|
|
|
//HACK: The validation rule 'date' doesn't accept 'now' as a valid date
|
|
|
|
|
|
|
|
$request['automatically_close_poll_datetime'] = Carbon::now()->format('Y-m-d\TH:i');
|
|
|
|
}
|
|
|
|
|
|
|
|
$validatedInput = $request->validate([
|
|
|
|
'hide_results_until_closed' => 'required|boolean',
|
|
|
|
'automatically_close_poll' => 'required|boolean',
|
|
|
|
'automatically_close_poll_datetime' => 'required_if:automatically_close_poll,true|date|after_or_equal:1 minute ago',
|
|
|
|
'set_admin_password' => 'required|boolean',
|
|
|
|
'admin_password' => 'required_if:set_admin_password,true|nullable|string',
|
|
|
|
]);
|
|
|
|
|
|
|
|
$poll->hide_results_until_closed = $validatedInput['hide_results_until_closed'];
|
|
|
|
$poll->closes_at = $validatedInput['automatically_close_poll'] ? Carbon::parse($validatedInput['automatically_close_poll_datetime']) : null;
|
|
|
|
$poll->admin_password = $validatedInput['set_admin_password'] ? $validatedInput['admin_password'] : null;
|
|
|
|
$poll->save();
|
|
|
|
|
|
|
|
if($poll->closed || $poll->admin_password == null) {
|
|
|
|
return redirect()->action('PollController@viewResults', ['poll' => $poll]);
|
|
|
|
} else {
|
|
|
|
return redirect()
|
|
|
|
->action('PollController@admin', ['poll' => $poll, 'password' => $poll->admin_password])
|
|
|
|
->with('changed', true);
|
|
|
|
}
|
|
|
|
}
|
2018-10-09 02:38:16 +02:00
|
|
|
}
|
|
|
|
}
|