From 01bbeb975e70579f2d795c95d6e4a0ae2520f4be Mon Sep 17 00:00:00 2001 From: marian Date: Sun, 27 Mar 2016 16:40:34 +0300 Subject: [PATCH] Made a start on implementing the 'Attendees Question' feature. - Created a migration (based on @daveearley 's migration) to add the question_types, questions, question_options, event_question and question_ticket tables. - Created a seeder for the question_types table. - Implemented event question add modal. - Done a bit of refactoring to the ajax form submission error handling. - Questions are now displayed on the event customization page, under the Order form tab. --- .../Controllers/EventQuestionsController.php | 149 ++++++++++++++++++ app/Http/Controllers/MyBaseController.php | 7 +- .../Requests/StoreEventQuestionRequest.php | 31 ++++ app/Http/routes.php | 5 + app/Models/Question.php | 13 ++ app/Models/QuestionOption.php | 35 ++++ app/Models/Ticket.php | 2 +- ...3_26_103318_create_attendees_questions.php | 113 +++++++++++++ database/seeds/DatabaseSeeder.php | 3 + database/seeds/QuestionTypesSeeder.php | 60 +++++++ public/assets/javascript/backend.js | 90 ++++++++--- .../views/ManageEvent/Customize.blade.php | 17 ++ .../ManageEvent/Modals/EditQuestion.blade.php | 120 ++++++++++++++ 13 files changed, 624 insertions(+), 21 deletions(-) create mode 100644 app/Http/Controllers/EventQuestionsController.php create mode 100644 app/Http/Requests/StoreEventQuestionRequest.php create mode 100644 app/Models/QuestionOption.php create mode 100644 database/migrations/2016_03_26_103318_create_attendees_questions.php create mode 100644 database/seeds/QuestionTypesSeeder.php create mode 100644 resources/views/ManageEvent/Modals/EditQuestion.blade.php diff --git a/app/Http/Controllers/EventQuestionsController.php b/app/Http/Controllers/EventQuestionsController.php new file mode 100644 index 00000000..67ace573 --- /dev/null +++ b/app/Http/Controllers/EventQuestionsController.php @@ -0,0 +1,149 @@ +get('event_id'); + + // Get the event or display a 'not found' warning. + $event = Event::findOrFail($event_id); + + // Initialize an empty array for the question options. + $question_options = [ + (object)[ + 'name' => '', + ], + ]; + + return view('ManageEvent.Modals.EditQuestion', [ + 'form_url' => route('event.question.store', [ + 'event_id' => $event_id, + ]), + 'event' => $event, + 'question' => [], + 'modal_id' => $request->get('modal_id'), + 'question_types' => QuestionType::all(), + 'question_options' => $question_options, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @access public + * @param StoreEventQuestionRequest $request + * @return \Illuminate\Http\Response + */ + public function store(StoreEventQuestionRequest $request) + { + // Get the event ID. + $event_id = $request->get('event_id'); + + // Get the event or display a 'not found' warning. + $event = Event::findOrFail($event_id); + + // Create question. + $question = Question::createNew(false, false, true); + $question->title = $request->get('title'); + $question->instructions = $request->get('instructions'); + $question->is_required = $request->get('title'); + $question->question_type_id = $request->get('question_type_id'); + $question->save(); + + // Get options. + $options = $request->get('option'); + + // Add options. + if ($options && is_array($options)) { + foreach ($options as $option_name) { + if (trim($option_name) !== '') { + $question->options()->create([ + 'name' => $option_name, + ]); + } + } + } + + // Get tickets. + $ticket_ids = $request->get('tickets'); + + // Add ticket <-> question entry. + if ($ticket_ids && is_array($ticket_ids)) { + foreach ($ticket_ids as $ticket_id) { + Ticket::find($ticket_id)->questions()->attach($question->id); + } + } + + $event->questions()->attach($question->id); + + return response()->json([ + 'status' => 'success', + 'message' => 'Successfully Created Question. Refreshing page...', + 'runThis' => 'reloadPageDelayed();', + ]); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function show($id) + { + // + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function edit($id) + { + // + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\Response + */ + public function update(Request $request, $id) + { + // + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function destroy($id) + { + // + } +} \ No newline at end of file diff --git a/app/Http/Controllers/MyBaseController.php b/app/Http/Controllers/MyBaseController.php index 6ec2847f..e19edc6f 100644 --- a/app/Http/Controllers/MyBaseController.php +++ b/app/Http/Controllers/MyBaseController.php @@ -36,8 +36,11 @@ class MyBaseController extends Controller */ public function getEventViewData($event_id, $additional_data = []) { + $event = Event::scope()->findOrFail($event_id); + return array_merge([ - 'event' => Event::scope()->findOrFail($event_id), - ], $additional_data); + 'event' => $event, + 'questions' => $event->questions()->get(), + ], $additional_data); } } diff --git a/app/Http/Requests/StoreEventQuestionRequest.php b/app/Http/Requests/StoreEventQuestionRequest.php new file mode 100644 index 00000000..130983b2 --- /dev/null +++ b/app/Http/Requests/StoreEventQuestionRequest.php @@ -0,0 +1,31 @@ + 'required', + 'option' => 'array', + ]; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index eaf99cd8..af9497e1 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -399,6 +399,11 @@ Route::group(['middleware' => ['auth', 'first.run']], function () { 'uses' => 'EventTicketQuestionsController@postCreateQuestion', ]); + /** + * Event questions. + */ + Route::resource('question', 'EventQuestionsController'); + /* * ------- * Attendees diff --git a/app/Models/Question.php b/app/Models/Question.php index 91ab4e7d..a50eb1a6 100644 --- a/app/Models/Question.php +++ b/app/Models/Question.php @@ -16,6 +16,7 @@ class Question extends MyBaseModel /** * The events associated with the question. * + * @access public * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function events() @@ -26,10 +27,22 @@ class Question extends MyBaseModel /** * The type associated with the question. * + * @access public * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function question_types() { return $this->hasOne('\App\Models\QuestionType'); } + + /** + * The options associated with the question. + * + * @access public + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function options() + { + return $this->hasMany('\App\Models\QuestionOption'); + } } diff --git a/app/Models/QuestionOption.php b/app/Models/QuestionOption.php new file mode 100644 index 00000000..b5af3674 --- /dev/null +++ b/app/Models/QuestionOption.php @@ -0,0 +1,35 @@ +belongsTo('\App\Models\Question'); + } +} diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 3a869cf1..94716859 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -61,7 +61,7 @@ class Ticket extends MyBaseModel */ public function questions() { - return $this->belongsToMany('\App\Models\Question', 'ticket_question'); + return $this->belongsToMany('\App\Models\Question'); } /** diff --git a/database/migrations/2016_03_26_103318_create_attendees_questions.php b/database/migrations/2016_03_26_103318_create_attendees_questions.php new file mode 100644 index 00000000..f9f1eaf7 --- /dev/null +++ b/database/migrations/2016_03_26_103318_create_attendees_questions.php @@ -0,0 +1,113 @@ +increments('id'); + $table->string('alias'); + $table->string('name'); + $table->boolean('has_options')->default(false); + $table->boolean('allow_multiple')->default(false); + }); + + /** + * The questions. + */ + Schema::create('questions', function (Blueprint $table) + { + $table->increments('id'); + + $table->string('title', 255); + $table->text('instructions'); + + $table->unsignedInteger('question_type_id'); + $table->unsignedInteger('account_id')->index(); + + $table->tinyInteger('is_required')->default(0); + + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('question_type_id')->references('id')->on('question_types'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + /** + * Used for the questions that allow options (checkbox, radio, dropdown). + */ + Schema::create('question_options', function (Blueprint $table) + { + $table->increments('id'); + $table->string('name'); + $table->integer('question_id')->unsigned()->index(); + + $table->foreign('question_id')->references('id')->on('questions')->onDelete('cascade'); + }); + + /** + * Event / Question pivot table. + */ + Schema::create('event_question', function(Blueprint $table) + { + $table->increments('id'); + $table->integer('event_id')->unsigned()->index(); + $table->integer('question_id')->unsigned()->index(); + + $table->foreign('event_id')->references('id')->on('events')->onDelete('cascade'); + $table->foreign('question_id')->references('id')->on('questions')->onDelete('cascade'); + }); + + /** + * Question / Ticket pivot table. + */ + Schema::create('question_ticket', function (Blueprint $table) + { + $table->increments('id'); + $table->integer('question_id')->unsigned()->index(); + $table->integer('ticket_id')->unsigned()->index(); + + $table->foreign('ticket_id')->references('id')->on('tickets')->onDelete('cascade'); + $table->foreign('question_id')->references('id')->on('questions')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @access public + * @return void + */ + public function down() + { + $tables = [ + 'question_types', + 'questions', + 'question_options', + 'event_question', + 'question_ticket', + ]; + + DB::statement('SET FOREIGN_KEY_CHECKS=0;'); + + foreach ($tables as $table) { + Schema::drop($table); + } + + DB::statement('SET FOREIGN_KEY_CHECKS=1;'); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index 6104c4b3..03c4e0bc 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -19,5 +19,8 @@ class DatabaseSeeder extends Seeder // $this->call('UserTableSeeder'); $this->call('CountriesSeeder'); $this->command->info('Seeded the countries!'); + + $this->call('QuestionTypesSeeder'); + $this->command->info('Seeded the question types!'); } } diff --git a/database/seeds/QuestionTypesSeeder.php b/database/seeds/QuestionTypesSeeder.php new file mode 100644 index 00000000..0603987e --- /dev/null +++ b/database/seeds/QuestionTypesSeeder.php @@ -0,0 +1,60 @@ +insert([ + [ + 'id' => 1, + 'alias' => 'text', + 'name' => 'Single-line text box', + 'has_options' => 0, + 'allow_multiple' => 0, + ], + [ + 'id' => 2, + 'alias' => 'textarea', + 'name' => 'Multi-line text box', + 'has_options' => 0, + 'allow_multiple' => 0, + ], + [ + 'id' => 3, + 'alias' => 'dropdown', + 'name' => 'Dropdown (single selection)', + 'has_options' => 1, + 'allow_multiple' => 0, + ], + [ + 'id' => 4, + 'alias' => 'dropdown_multiple', + 'name' => 'Dropdown (multiple selection)', + 'has_options' => 1, + 'allow_multiple' => 1, + ], + [ + 'id' => 5, + 'alias' => 'checkbox', + 'name' => 'Checkbox', + 'has_options' => 1, + 'allow_multiple' => 1, + ], + [ + 'id' => 6, + 'alias' => 'radio', + 'name' => 'Radio input', + 'has_options' => 1, + 'allow_multiple' => 0, + ], + ]); + } +} diff --git a/public/assets/javascript/backend.js b/public/assets/javascript/backend.js index 6313a935..c328f715 100644 --- a/public/assets/javascript/backend.js +++ b/public/assets/javascript/backend.js @@ -8543,6 +8543,12 @@ $(function () { }, error: function (data, statusText, xhr, $form) { + // Form validation error. + if (422 == data.status) { + processFormErrors($form, $.parseJSON(data.responseText)); + return; + } + showMessage('Whoops!, it looks like something went wrong on our servers.\n\ Please try again, or contact support if the problem persists.'); @@ -8582,24 +8588,7 @@ $(function () { break; case 'error': - $.each(data.messages, function (index, error) { - var $input = $(':input[name=' + index + ']', $form); - - if ($input.prop('type') === 'file') { - $('#input-' + $input.prop('name')).append('
' + error + '
') - .parent() - .addClass('has-error'); - } else { - $input.after('
' + error + '
') - .parent() - .addClass('has-error'); - } - - }); - - var $submitButton = $form.find('input[type=submit]'); - toggleSubmitDisabled($submitButton); - + processFormErrors($form, data.messages); break; default: @@ -8862,6 +8851,71 @@ $(function () { }); +function changeQuestionType(select) +{ + var select = $(select); + var selected = select.find(':selected'); + + if (selected.data('has-options') == '1') { + $('#question-options').removeClass('hide'); + } else { + $('#question-options').addClass('hide'); + } +} + +function submitQuestionForm() +{ + $('#edit-question-form').submit(); +} + +function addQuestionOption() +{ + var tbody = $('#question-options tbody'); + var questionOption = $('#question-option-template').html(); + + tbody.append(questionOption); +} + +function removeQuestionOption(removeBtn) +{ + var removeBtn = $(removeBtn); + var tbody = removeBtn.parents('tbody'); + + if (tbody.find('tr').length > 1) { + removeBtn.parents('tr').remove(); + } else { + alert('You must have at least one option.'); + } +} + +function processFormErrors($form, errors) +{ + $.each(errors, function (index, error) + { + var $input = $(':input[name=' + index + ']', $form); + + if ($input.prop('type') === 'file') { + $('#input-' + $input.prop('name')).append('
' + error + '
') + .parent() + .addClass('has-error'); + } else { + $input.after('
' + error + '
') + .parent() + .addClass('has-error'); + } + + }); + + var $submitButton = $form.find('input[type=submit]'); + toggleSubmitDisabled($submitButton); +} + +function reloadPageDelayed() +{ + setTimeout(function () { + location.reload(); + }, 2000); +} /** * diff --git a/resources/views/ManageEvent/Customize.blade.php b/resources/views/ManageEvent/Customize.blade.php index 6b3e2a0f..8df3a896 100644 --- a/resources/views/ManageEvent/Customize.blade.php +++ b/resources/views/ManageEvent/Customize.blade.php @@ -483,7 +483,24 @@ +

Attendees questions

+ @if ($questions) + + + @foreach ($questions as $question) + + + + @endforeach + +
{{ $question->title }}
+ @endif + diff --git a/resources/views/ManageEvent/Modals/EditQuestion.blade.php b/resources/views/ManageEvent/Modals/EditQuestion.blade.php new file mode 100644 index 00000000..58c68c0f --- /dev/null +++ b/resources/views/ManageEvent/Modals/EditQuestion.blade.php @@ -0,0 +1,120 @@ + + \ No newline at end of file