Quiz Bowl


A real-time, simultaneous multi-player, online
quiz manager

Catalyst

package QuizBowl::Web;

use Moose;
use namespace::autoclean;

use Catalyst::Runtime;

use Catalyst qw/
  -Debug
  ConfigLoader
  Static::Simple
  Session
  Session::State::Cookie
  Session::Store::DBIC
  Authentication
  /;...

Sessions

__PACKAGE__->config(
    name => 'QuizBowl::Web',
    'Plugin::Session' => {
        dbic_class => 'DB::Session',
        expires    => 60 * 60 * 24 * 7 * 2,    # 2 weeks
        id_field   => 'session_id',
    },
);

Authentication

"Plugin::Authentication":
  default_realm: users
  realms:
    users:
      credential:
        class: Password
        password_field: password
        password_type: salted_hash
        password_salt_len: 4
        password_hash_type: SHA-1
      store:
        class: "DBIx::Class"
        user_model: "DB::User"
 sub login : Local {
    my ( $self, $c ) = @_; 
    if (
        $c->req->param('email')
        && $c->authenticate( ...

REST

package QuizBowl::Web::Controller::REST;

use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller::REST' }
with 'Catalyst::TraitFor::Controller::DBIC::DoesPaging';
with 'Catalyst::TraitFor::Controller::DoesExtPaging';

__PACKAGE__->config(
    'default'                => 'application/json',
    'map'                    => {
        'text/html'                => 'YAML::HTML',
        'text/xml'                 => 'XML::Simple',
        'application/xml'          => 'XML::Simple',
        'text/x-yaml'              => 'YAML',
        'application/json'         => 'JSON',
...

REST Easy

my $users_rs = $c->model('DB::User')->search( {} );
my $paginated_rs = $self->page_and_sort( $c, $users_rs );
my $data_r = $self->ext_paginate( $paginated_rs, 'REST_data' );

Database

package QuizBowl::Schema::Result::User;

use Moose;
use MooseX::NonMoose;
extends 'QuizBowl::Schema::Result';

__PACKAGE__->table('user_account');

__PACKAGE__->add_columns(
    user_id => {
        data_type   => 'serial',
        is_nullable => 0,
    },...

Constraints/RElationships

__PACKAGE__->set_primary_key('user_id');

__PACKAGE__->add_unique_constraint( unique_email => ['email'], );

__PACKAGE__->has_many(
    event_registrations => 'QuizBowl::Schema::Result::EventUser',
    'user_id',
);

__PACKAGE__->many_to_many(
    registered_events => 'event_registrations',
    'player',
);

Database Tracing

Always remember!
  1. DBIC_TRACE=1
  2. DBIC_TRACE_PROFILE=console

Email

my $email = Email::Simple->create(
    header => [
        To      => $user->email_with_name,
        From    => 'system@example.com',
        Subject => 'Quiz Bowl Password Help',
    ],  
    body => sprintf( 'Reset password %s', $uri->as_string() ),
);  

sendmail($email);

Templates

[% USE date %]...<tbody>
    [% WHILE ( event = events_rs.next() ) %]
    <tr>
        <td>[% event.id %]</td>
        <td>[% event.name %]</td>
        <td>[% date.format(event.start_time, '%b %e %l:%S %p') %]</td>
        <td>
            [% IF c.user_exists %]
            <a href="[% c.uri_for_action('/event/run', [event.id]) %]"
...

Real-time Web

var socket = io.connect();

socket.on('roll call requested', function(){
    $('.panel').hide();
    $('#waiting_panel').show();
    $('#round_number').text('Roll Call');
    $('#question_level').text('Press Ready');
});

socket.on('user list updated', user_list_updated);
socket.on('round started', round_started);

socket.on('reconnecting', function(){
    $.growl('Whoops, trying to reconnect...');
});

Perl Socket.IO

use QuizBowl::SocketIO;
use PocketIO;
use Plack::Builder;

builder {    # ...
    mount '/socket.io' => builder {
        PocketIO->new(
            instance => QuizBowl::SocketIO->new(),
            method => 'run'
        );
    };
};

PocketIO

package QuizBowl::SocketIO;

sub run {
    my $self = shift;
    return sub {
        my $self = shift;
        $self->on( 'submit answer' => \&submit_answer );
    };
}

sub submit_answer {
    my $self = shift;
    my $answer = shift;
}
// in JavaScript:
$('#submit_button').click(function(){
    QuizBowl.socket.emit('submit answer', $('#answer').val());
    return false;
});

Rooms

  • Host events simultaneously
# Join a room
$self->join($event_id);

# Send to everyone in room
$self->sockets->in($event_id)->emit(
    'answer submitted',
    {
        user_id           => $user_id,
        event_question_id => $eq->id,
    }
);

# Send to everyone in room _except_ me
$self->broadcast->to($event_id)->emit(
    'growl',
    sprintf( 'Round will close in %i seconds', $seconds )
);

Future

  • Self-service
    • Question builder
    • Event manager
  • Cleanup
    • Finish bootstrap conversion
    • Restructure PocketIO app
    • Permissions
  • Switch frameworks
  • Wing on Postgres :)

Quiz Bowl

By mcsnolte

Quiz Bowl

Overview of some of the tech in Quiz Bowl (2013.)

  • 1,990