Welcome Guest, Not a member yet? Register   Sign In
Multi-Step Form Data Storing
#1

I have a rather large mobile form that i've split up into 6 steps.  It's been a while since i've created a wizard type flow process, and wondered what is the best practice way to set this up these days?   The biggest question is on how to store the data between steps.  The form itself should be easy enough to go through and fill out in one sitting in probably 5 mins or so.  I don't want to provide a mechanism for a user to only complete a partial form and come back later.

So my question is do I store the data in the session as they progress through the steps, then save it all to the database at the end?  This seems to be the way to go versus saving to database after each step, but just wondering if that's a best practice in this situation.

Thanks!
Reply
#2

(This post was last modified: 08-20-2016, 01:29 AM by ivantcholakov. Edit Reason: clarification )

I use data within session in such a case.

In an online store ordering is separated in several pages - basket/cart content, client data (names, address, email), payment method, final preview and sending the order, and a flash confirmation page saying that order has been sent. Pages usually have "Back" button (a link to the previous page) and "Continue" button (a submit type button of the current form).

This page sequence is served by a special model "Order_draft" that stores in the session only, it has nothing to do with the database. There is another model "Orders" that manages the orders within the database. When the client clicks on the button "Send Order", the collected data from Order_draft is used by "Orders" for creation of a new order within database. After that, data within Order_draft gets cleared, and based on the newly created order, confirmation/notification email messages are sent.

I would propose to you something similar. Make a tandem of models "Something_draft" (session data) and "Something" (data within database). Let "Something_draft" collect the data from the forms, let "Something" create and manage the submitted data, clear "Something_draft" data on every final submission.
Reply
#3

(08-20-2016, 01:27 AM)ivantcholakov Wrote: I use data within session in such a case.

In an online store ordering is separated in several pages - basket/cart content, client data (names, address, email), payment method, final preview and sending the order, and a flash confirmation page saying that order has been sent. Pages usually have "Back" button (a link to the previous page) and "Continue" button (a submit type button of the current form).

This page sequence is served by a special model "Order_draft" that stores in the session only, it has nothing to do with the database. There is another model "Orders" that manages the orders within the database. When the client clicks on the button "Send Order", the collected data from Order_draft is used by "Orders" for creation of a new order within database. After that, data within Order_draft gets cleared, and based on the newly created order, confirmation/notification email messages are sent.

I would propose to you something similar. Make a tandem of models "Something_draft" (session data) and "Something" (data within database). Let "Something_draft" collect the data from the forms, let "Something" create and manage the submitted data, clear "Something_draft" data on every final submission.

Thank you for the reply. That sounds like an excellent solution.
Reply
#4

I have used javascript solutions in the past to group related questions in a form into a step. Basicly it still is one large form and javascript makes it more userfriendly with different steps.

I ended up using this jquery plugin: http://www.jquery-steps.com
Reply
#5

A JavaScript is a possible option too, however server-side validation should be implemented somehow. JavaScript validation alone could be bypassed.
Reply
#6

Great tips, guys! I've also been struggling with multi-step forms. This is really helpful!
Reply
#7

(08-20-2016, 01:27 AM)ivantcholakov Wrote: I use data within session in such a case.

In an online store ordering is separated in several pages - basket/cart content, client data (names, address, email), payment method, final preview and sending the order, and a flash confirmation page saying that order has been sent. Pages usually have "Back" button (a link to the previous page) and "Continue" button (a submit type button of the current form).

This page sequence is served by a special model "Order_draft" that stores in the session only, it has nothing to do with the database. There is another model "Orders" that manages the orders within the database. When the client clicks on the button "Send Order", the collected data from Order_draft is used by "Orders" for creation of a new order within database. After that, data within Order_draft gets cleared, and based on the newly created order, confirmation/notification email messages are sent.

I would propose to you something similar. Make a tandem of models "Something_draft" (session data) and "Something" (data within database). Let "Something_draft" collect the data from the forms, let "Something" create and manage the submitted data, clear "Something_draft" data on every final submission.

iivantcholakov,

if you don't mind, could you show me an example of what your draft model looks like? just wondering if you are referencing the input->post in your model or doing that in the controller. I like your idea a lot, just trying to figure it out.

thanks.
Reply
#8

(This post was last modified: 08-26-2016, 09:23 PM by ivantcholakov.)

@jlq10
Real code, just as it is. It is not perfect, for making money it is good enough. There is project-specific supported data, you can remove it.

Order_draft model
Code:
<?php defined('BASEPATH') OR exit('No direct script access allowed');

class Order_draft extends Basket {

    protected $client = null;
    protected $payment_methods = null;
    protected $order_statuses = null;
    protected $orders = null;

    protected $session = null;

    protected $session_key = 'order_draft';
    protected $has_session_data = null;
    protected $data = null;

    protected $ci = null;

    public function __construct() {

        parent::__construct();

        $this->ci = get_instance();

        $this->ci->load->model('client');
        $this->client = $this->ci->client;

        $this->ci->load->model('payment_methods');
        $this->payment_methods = $this->ci->payment_methods;

        $this->ci->load->model('order_statuses');
        $this->order_statuses = $this->ci->order_statuses;

        $this->ci->load->model('orders');
        $this->orders = $this->ci->orders;

        $this->session = $this->ci->session;

        $this->has_session_data = $this->session->userdata($this->session_key) !== NULL;

        $this->data = $this->session->userdata($this->session_key);

        if ($this->data === NULL) {
            $this->destroy();
        }
    }

    public function has_session_data() {

        return $this->has_session_data;
    }

    // tc - Terms & Conditions, a boolean value.
    public function set_tc_accepted($value) {

        $this->data['tc_accepted'] = !empty($value);
        return $this->_save_data();
    }

    public function tc_accepted() {

        return !empty($this->data['tc_accepted']);
    }

    // Added by Ivan Tcholakov, 17-MAR-2013.
    public function tc_required() {

        return true;
    }

    public function set_client($data = array()) {

        if (!is_array($data)) {
            return false;
        }

        $empty_data = $this->client->get_empty();
        $keys = array_keys($empty_data);
        $data = array_only($data, $keys);
        $data = array_merge($empty_data, $data);

        $this->data['client'] = $data;
        return $this->_save_data();
    }

    public function client() {

        return $this->data['client'];
    }

    public function set_payment_method($payment_method_id) {

        $data = $this->payment_methods->get($payment_method_id);

        if (empty($data)) {
            return false;
        }

        $this->data['payment_method'] = $data;
        return $this->_save_data();
    }

    public function payment_method() {

        return $this->data['payment_method'];
    }

    public function payment_method_id() {

        $payment_method = $this->payment_method();
        return $payment_method['id'];
    }

    public function set_payment_note($payment_note) {

        $this->data['payment_note'] = $payment_note;
        return $this->_save_data();
    }

    public function payment_note() {

        return $this->data['payment_note'];
    }

    public function set_bank_account($bank_account) {

        $this->data['bank_account'] = $bank_account;
        return $this->_save_data();
    }

    public function bank_account() {

        return $this->data['bank_account'];
    }

    public function set_transport_required($required) {

        $this->data['transport_required'] = !empty($required);
        return $this->_save_data();
    }

    public function transport_required() {

        return $this->data['transport_required'];
    }

    public function set_installation_required($required) {

        $this->data['installation_required'] = !empty($required);
        return $this->_save_data();
    }

    public function installation_required() {

        return $this->data['installation_required'];
    }

    public function set_notes($notes) {

        $this->data['notes'] = $notes;
        return $this->_save_data();
    }

    public function notes() {

        return $this->data['notes'];
    }

    public function charges() {

        return $this->data['charges'];
    }

    public function total_charges() {

        return $this->data['total_charges'];
    }

    public function total_charge_items() {

        return $this->data['total_charge_items'];
    }

    public function grand_total() {

        return $this->data['grand_total'];
    }

    public function status() {

        return $this->data['status'];
    }

    public function set_order_id($order_id) {

        $this->data['order_id'] = $order_id;
        return $this->_save_data();
    }

    public function order_id() {

        return $this->data['order_id'];
    }

    public function set_order_number($order_number) {

        $this->data['order_number'] = $order_number;
        return $this->_save_data();
    }

    public function order_number() {

        return $this->data['order_number'];
    }

    public function set_date($date) {

        $this->data['date'] = $date;
        return $this->_save_data();
    }

    public function date() {

        return $this->data['date'];
    }

    public function is_sent() {

        return !empty($this->data['order_id']);
    }

    // tc - Terms & Conditions, a boolean value.
    public function set_bnp_tc_accepted($value) {

        $this->data['bnp_tc_accepted'] = !empty($value);
        return $this->_save_data();
    }

    public function bnp_tc_accepted() {

        return isset($this->data['bnp_tc_accepted']) ? !empty($this->data['bnp_tc_accepted']) : false;
    }

    public function set_bnp_pricing_scheme($bnp_pricing_scheme) {

        $bnp_pricing_scheme = $bnp_pricing_scheme == '' ? null : (int) $bnp_pricing_scheme;
        $this->data['bnp_pricing_scheme'] = $bnp_pricing_scheme;
        return $this->_save_data();
    }

    public function bnp_pricing_scheme() {

        return isset($this->data['bnp_pricing_scheme']) ? $this->data['bnp_pricing_scheme'] : null;
    }

    public function set_bnp_maturity($bnp_maturity) {

        $bnp_maturity = $bnp_maturity == '' ? null : (int) $bnp_maturity;
        $this->data['bnp_maturity'] = $bnp_maturity;
        return $this->_save_data();
    }

    public function bnp_maturity() {

        return isset($this->data['bnp_maturity']) ? $this->data['bnp_maturity'] : null;
    }

    public function set_bnp_pricing_variant($bnp_pricing_variant) {

        $bnp_pricing_variant = $bnp_pricing_variant == '' ? null : (int) $bnp_pricing_variant;
        $this->data['bnp_pricing_variant'] = $bnp_pricing_variant;
        return $this->_save_data();
    }

    public function bnp_pricing_variant() {

        return isset($this->data['bnp_pricing_variant']) ? $this->data['bnp_pricing_variant'] : null;
    }

    public function set_bnp_success($bnp_success) {

        $bnp_success = $bnp_success == '' ? null : !empty($bnp_success);
        $this->data['bnp_success'] = $bnp_success;
        return $this->_save_data();
    }

    public function bnp_success() {

        return isset($this->data['bnp_success']) ? $this->data['bnp_success'] : null;
    }

    public function set_bnp_error_code($bnp_error_code) {

        $bnp_error_code = $bnp_error_code == '' ? null : (int) $bnp_error_code;
        $this->data['bnp_error_code'] = $bnp_error_code;
        return $this->_save_data();
    }

    public function bnp_error_code() {

        return isset($this->data['bnp_error_code']) ? $this->data['bnp_error_code'] : null;
    }

    public function set_bnp_error_message($bnp_error_message) {

        $bnp_error_message = $bnp_error_message == '' ? null : (string) $bnp_error_message;
        $this->data['bnp_error_message'] = $bnp_error_message;
        return $this->_save_data();
    }

    public function bnp_error_message() {

        return isset($this->data['bnp_error_message']) ? $this->data['bnp_error_message'] : null;
    }

    public function set_bnp_installment_amount($bnp_installment_amount) {

        $bnp_installment_amount = $bnp_installment_amount == '' ? null : (float) $bnp_installment_amount;
        $this->data['bnp_installment_amount'] = $bnp_installment_amount;
        return $this->_save_data();
    }

    public function bnp_installment_amount() {

        return isset($this->data['bnp_installment_amount']) ? $this->data['bnp_installment_amount'] : null;
    }

    public function set_bnp_total_repayment_amount($bnp_total_repayment_amount) {

        $bnp_total_repayment_amount = $bnp_total_repayment_amount == '' ? null : (float) $bnp_total_repayment_amount;
        $this->data['bnp_total_repayment_amount'] = $bnp_total_repayment_amount;
        return $this->_save_data();
    }

    public function bnp_total_repayment_amount() {

        return isset($this->data['bnp_total_repayment_amount']) ? $this->data['bnp_total_repayment_amount'] : null;
    }

    public function set_bnp_nir($bnp_nir) {

        $bnp_nir = $bnp_nir == '' ? null : (float) $bnp_nir;
        $this->data['bnp_nir'] = $bnp_nir;
        return $this->_save_data();
    }

    public function bnp_nir() {

        return isset($this->data['bnp_nir']) ? $this->data['bnp_nir'] : null;
    }

    public function set_bnp_apr($bnp_apr) {

        $bnp_apr = $bnp_apr == '' ? null : (float) $bnp_apr;
        $this->data['bnp_apr'] = $bnp_apr;
        return $this->_save_data();
    }

    public function bnp_apr() {

        return isset($this->data['bnp_apr']) ? $this->data['bnp_apr'] : null;
    }

    public function destroy() {

        parent::destroy();

        $this->data = array(
            'tc_accepted' => $this->tc_required() ? false : true,
            'client' => $this->client->get_empty(),
            'payment_method' => $this->payment_methods->get_default(),
            'payment_note' => null,
            'bank_account' => null,
            'status' => $this->order_statuses->get_default(),
            'notes' => null,
            'charges' => array(),
            'total_charges' => 0,
            'total_charge_items' => 0,
            'grand_total' => $this->total(),
            'order_id' => null,
            'order_number' => null,
            'date' => null,
            'transport_required' => false,
            'installation_required' => false,
            'bnp_pricing_scheme' => null,
            'bnp_maturity' => null,
            'bnp_pricing_variant' => null,
            'bnp_success' => null,
            'bnp_error_code' => null,
            'bnp_error_message' => null,
            'bnp_installment_amount' => null,
            'bnp_total_repayment_amount' => null,
            'bnp_nir' => null,
            'bnp_apr' => null,
            'bnp_tc_accepted' => null,
        );

        return $this->_save_data();
    }

    protected function _save_data() {

        $this->data['charges'] = array();
        $this->data['total_charges'] = 0;
        $this->data['total_charge_items'] = 0;
        $this->data['grand_total'] = $this->total();

        foreach ($this->data['charges'] as $charge) {

            $subtotal = (float) $charge['subtotal'];
            $this->data['total_charges'] += $subtotal;
            $this->data['total_charge_items']++;
            $this->data['grand_total'] += $subtotal;
        }

        $this->session->set_userdata($this->session_key, $this->data);
        return true;
    }

}

Orders model - you would need to write something similar in pure CodeIgniter style:

Code:
<?php defined('BASEPATH') OR exit('No direct script access allowed');

class Orders extends Core_Model {

    protected $check_for_existing_fields = true;
    public $protected_attributes = array('id');

    protected $_table = 'orders';
    protected $return_type = 'array';

    protected $soft_delete = true;

    public $before_create = array('set_created_at');

    protected $ci = null;

    public function __construct() {

        parent::__construct();

        $this->ci = get_instance();

        $this->ci->load->model('order_items');
        $this->order_items = $this->ci->order_items;

        $this->ci->load->model('order_charges');
        $this->order_charges = $this->ci->order_charges;
    }

    public function new_order_number() {

        return (int) $this->with_deleted()->select('MAX(order_number)')->as_value()->first() + 1;
    }

    public function create() {

        $this->ci->load->model('order_draft');
        $this->order_draft = $this->ci->order_draft;

        $this->ci->load->model('products');
        $this->products = $this->ci->products;

        $client = $this->order_draft->client();

        $payment_method = $this->order_draft->payment_method_id();

        $status = $this->order_draft->status();
        $status  = $status ['id'];

        $date = date('Y-m-d');

        $public_access_code = Random::uuid();

        $this->db->trans_start();

        $order_number = $this->new_order_number();

        $data = array(

            'order_number' => $order_number,
            'date' => $date,

            'first_name' => $client['first_name'],
            'last_name' => $client['last_name'],
            'country' => $client['country'],
            'city' => $client['city'],
            'district' => $client['district'],
            'municipality' => $client['municipality'],
            'postcode' => $client['postcode'],
            'street' => $client['street'],
            'street_number' => $client['street_number'],
            'quarter' => $client['quarter'],
            'block' => $client['block'],
            'floor' => $client['floor'],
            'apartment' => $client['apartment'],
            'phone' => $client['phone'],
            'email' => $client['email'],

            'notes' => $this->order_draft->notes(),

            'payment_method' => $payment_method,
            'bnp_pricing_scheme_id' => $this->order_draft->bnp_pricing_scheme() > 0 ? (int) $this->order_draft->bnp_pricing_scheme() : null,
            'payment_note' => $this->order_draft->payment_note(),
            'bank_account' => $this->order_draft->bank_account(),

            'transport_please' => $this->order_draft->transport_required() ? 1 : 0,
            'install_please' => $this->order_draft->installation_required() ? 1 : 0,

            'total_items' => $this->order_draft->total_items(),
            'total' => $this->order_draft->total(),
            'total_charge_items' => $this->order_draft->total_charge_items(),
            'total_charges' => $this->order_draft->total_charges(),
            'grand_total' => $this->order_draft->grand_total(),

            'status' => $status,

            'public_access_code' => $public_access_code,
        );

        // Added by Ivan Tcholakov, 07-AUG-2016.
        if ($payment_method == 3) {

            $data = array_merge($data, array(
                'success' => $this->order_draft->bnp_success() ? 1 : 0,
                'error_code' => (int) $this->order_draft->bnp_error_code(),
                'error_message' => $this->order_draft->bnp_error_message(),
                'initial_sum' => $this->order_draft->grand_total(),
                'number_of_payments' => $this->order_draft->bnp_maturity(),
                'pricing_variant_id' => $this->order_draft->bnp_pricing_variant(),
                'monthly_payment_with_processing_fee' => $this->order_draft->bnp_installment_amount(),
                'total_ammount_of_payments' => $this->order_draft->bnp_total_repayment_amount(),
                'annual_rate' => $this->order_draft->bnp_nir(),
                'annual_rate_of_charge' => $this->order_draft->bnp_apr(),
            ));
        }

        $order_id = $this->insert($data);

        if ($order_id !== false) {

            $products = array();
            $items = $this->order_draft->contents();

            foreach ($items as $item) {

                $product = array();
                $product['order_id'] = $order_id;
                $product['product_id'] = $item['id'];
                $product['quantity'] = $item['qty'];
                $product['price'] = $item['price'];
                $product['subtotal'] = $item['subtotal'];
                $product['name'] = $item['name'];
                $product['image'] = $item['image'];
                $product['category_id'] = (int) $this->products->select('category_id')->as_value()->get($item['id']);

                $bnp_data = $this->products->select('bnp_category_id, bnp_type_id, bnp_special_category_id, bnp_special_type_id')->get($item['id']);
                $product['bnp_category_id'] = empty($bnp_data['bnp_category_id']) ? null : (int) $bnp_data['bnp_category_id'];
                $product['bnp_type_id'] = empty($bnp_data['bnp_type_id']) ? null : (int) $bnp_data['bnp_type_id'];
                $product['bnp_special_category_id'] = empty($bnp_data['bnp_special_category_id']) ? null : (int) $bnp_data['bnp_special_category_id'];
                $product['bnp_special_type_id'] = empty($bnp_data['bnp_special_type_id']) ? null : (int) $bnp_data['bnp_special_type_id'];

                $products[] = $product;
            }

            $this->order_items->insert_many($products);

            $charges = array();
            $items = $this->order_draft->charges();

            foreach ($items as $item) {

                $charge = array();
                $charge['order_id'] = $order_id;
                $charge['subtotal'] = $item['subtotal'];
                $charge['name'] = $item['name'];

                $charges[] = $charge;
            }

            $this->order_charges->insert_many($charges);
        }

        $this->db->trans_complete();

        if ($this->db->trans_status() === false || $order_id === false) {

            log_message('error', 'Order creation was not successfull.');
            return false;
        }

        $this->order_draft->set_order_id($order_id);
        $this->order_draft->set_order_number($order_number);
        $this->order_draft->set_date($date);

        return $order_id;
    }

    public function items($order_id) {

        return $this->order_items->get_many_by('order_id', (int) $order_id);
    }

    public function charges($order_id) {

        return $this->order_charges->get_many_by('order_id', (int) $order_id);
    }

    public function has_charge($order_id, $key) {

        $order_id = (int) $order_id;
        $key = (string) $key;

        if (empty($order_id) || $key == '') {
            return false;
        }

        $name = $this->order_charges->get_name($key);

        $result = $this->order_charges->select('id')->where('order_id', $order_id)->where('name', $name)->first();

        return $result != '';
    }

    public function get_charge($order_id, $key) {

        $order_id = (int) $order_id;
        $key = (string) $key;

        if (empty($order_id) || $key == '') {
            return false;
        }

        $name = $this->order_charges->get_name($key);

        $result = $this->order_charges->select('subtotal')->where('order_id', $order_id)->where('name', $name)->as_value()->first();

        return $result;
    }

    public function update_charge($order_id, $key, $value) {

        $order_id = (int) $order_id;
        $key = (string) $key;
        $value = price_parse($value);

        if (empty($order_id) || $key == '') {
            return false;
        }

        $name = $this->order_charges->get_name($key);

        $this->order_charges->where('order_id', $order_id)->where('name', $name)->delete_many_by();

        if (empty($value)) {
            return true;
        }

        if ($this->order_charges->insert(array('order_id' => $order_id, 'name' => $name, 'subtotal' => $value)) === false) {
            return false;
        }

        return $this->update_calculation($order_id);
    }

    public function update_calculation($order_id) {

        $order_id = (int) $order_id;

        $data = array();

        $data['total_items'] = $this->order_items->total_items($order_id);
        $data['total'] = $this->order_items->total($order_id);

        $data['total_charge_items'] =  $this->order_charges->total_items($order_id);
        $data['total_charges'] =  $this->order_charges->total($order_id);

        $data['grand_total'] = $data['total'] + $data['total_charges'];

        return $this->update($order_id, $data);
    }

    protected function set_created_at($row) {

        // This is a MySQL-specific trick for setting the creation time.
        $row['created_at'] = null;

        return $row;
    }

}

Edit: The abstract class Basket, it turns the Cart library into a model:

Code:
<?php defined('BASEPATH') OR exit('No direct script access allowed');

abstract class Basket extends CI_Model {

    protected $cart = null;
    protected $products = null;

    protected $ci = null;

    public function __construct() {

        parent::__construct();

        $this->ci = get_instance();

        $this->ci->load->helper('price');

        $this->ci->load->library('cart');
        $this->cart = $this->ci->cart;

        $this->ci->load->model('products');
        $this->products = $this->ci->products;
    }

    public function insert($items = array()) {

        if (!is_array($items) || count($items) === 0) {

            log_message('error', 'The insert method must be passed an array containing data.');
            return false;
        }

        $items_to_insert = array();

        if (isset($items['id'])) {

            if ($this->insert_check($items)) {
                $items_to_insert = $items;
            }

        } else {

            foreach ($items as $item) {

                if ($this->insert_check($item)) {
                    $items_to_insert[] = $item;
                }
            }
        }

        $result = $this->cart->insert($items_to_insert);
        $this->_save_data();
        return $result;
    }

    protected function insert_check(& $item) {

        if (is_array($item) && isset($item['id'])) {

            $name = isset($item['name']) ? trim($item['name']) : '';

            if (!empty($item['id'])) {

                $product = $this->products->select('name, price')->get($item['id']);

                if (!empty($product)) {

                    if ($name == '') {
                        $name = trim($product['name']);
                    }

                    if (!isset($item['price'])) {

                        $price = $product['price'];
                        $item['price'] = $price;
                    }

                } else {

                    return false;
                }
            }

            if ($name != '') {

                $item['name'] = $name;

                if (!isset($item['qty'])) {
                    $item['qty'] = 1;
                }

                if (!isset($item['price'])) {
                    return false;
                }

                return true;
            }
        }

        return false;
    }

    public function update($items = array()) {

        $result = $this->cart->update($items);
        $this->_save_data();
        return $result;
    }

    public function total() {

        return $this->cart->total();
    }

    public function remove($rowid) {

        $result = $this->cart->remove($rowid);
        $this->_save_data();
        return $result;
    }

    public function total_items() {

        return $this->cart->total_items();
    }

    public function contents($newest_first = false) {

        $items = $this->cart->contents($newest_first);

        if (!empty($items)) {
            foreach ($items as & $item) {
                $item['image'] = $this->products->select('image')->as_value()->get($item['id']);
            }
        }

        return $items;
    }

    public function get_item($rowid) {

         $item = $this->cart->get_item($rowid);

         if (!empty($item)) {
             $item['image'] = $this->products->select('image')->as_value()->get($item['id']);
         }

         return $item;
    }

    public function has_options($rowid = '') {

        return $this->cart->has_options($rowid);
    }

    public function product_options($rowid = '') {

        return $this->cart->product_options($rowid);
    }

    public function format_number($n = '') {

        return ($n === '') ? '' : price_format($n);
    }

    protected function destroy() {

        $this->cart->destroy();
    }

    abstract protected function _save_data();

}
Reply




Theme © iAndrew 2016 - Forum software by © MyBB