gocodebox/lifterlms

View on GitHub
assets/js/builder/Models/Question.js

Summary

Maintainability
A
45 mins
Test Coverage
/**
 * Quiz Question
 *
 * @since    3.16.0
 * @version  3.27.0
 */
define( [
        'Models/Image',
        'Collections/Questions',
        'Collections/QuestionChoices',
        'Models/QuestionType',
        'Models/_Relationships',
        'Models/_Utilities'
    ], function(
        Image,
        Questions,
        QuestionChoices,
        QuestionType,
        Relationships,
        Utilities
    ) {

        return Backbone.Model.extend( _.defaults( {

            /**
             * Model relationships
             *
             * @type  {Object}
             */
            relationships: {
                parent: {
                    model: 'llms_quiz',
                    type: 'model',
                },
                children: {
                    choices: {
                        class: 'QuestionChoices',
                        model: 'choice',
                        type: 'collection',
                    },
                    image: {
                        class: 'Image',
                        model: 'image',
                        type: 'model',
                    },
                    questions: {
                        class: 'Questions',
                        conditional: function( model ) {
                            var type = model.get( 'question_type' ),
                            type_id  = _.isString( type ) ? type : type.get( 'id' );
                            return ( 'group' === type_id );
                        },
                        model: 'llms_question',
                        type: 'collection',
                    },
                    question_type: {
                        class: 'QuestionType',
                        lookup: function( val ) {
                            if ( _.isString( val ) ) {
                                return window.llms_builder.questions.get( val );
                            }
                            return val;
                        },
                        model: 'question_type',
                        type: 'model',
                    },
                }
            },

            /**
             * Model defaults
             *
             * @return   obj
             * @since    3.16.0
             * @version  3.16.0
             */
            defaults: function() {
                return {
                    id: _.uniqueId( 'temp_' ),
                    choices: [],
                    content: '',
                    description_enabled: 'no',
                    image: {},
                    multi_choices: 'no',
                    menu_order: 1,
                    points: 1,
                    question_type: 'generic',
                    questions: [], // for question groups
                    parent_id: '',
                    title: '',
                    type: 'llms_question',
                    video_enabled: 'no',
                    video_src: '',

                    _expanded: false,
                }
            },

            /**
             * Initializer
             *
             * @param    obj   data     object of data for the model
             * @param    obj   options  additional options
             * @return   void
             * @since    3.16.0
             * @version  3.16.0
             */
            initialize: function( data, options ) {

                var self = this;

                this.startTracking();
                this.init_relationships( options );

                if ( false !== this.get( 'question_type' ).choices ) {

                    this._ensure_min_choices();

                    // when a choice is removed, maybe add back some defaults so we always have the minimum
                    this.listenTo( this.get( 'choices' ), 'remove', function() {
                        // new items are added at index 0 when there's only 1 item in the collection, not sure why exactly...
                        setTimeout( function() {
                            self._ensure_min_choices();
                        }, 0 );
                    } );

                }

                // ensure question types that don't support points don't record default 1 point in database
                if ( ! this.get( 'question_type' ).get( 'points' ) ) {
                    this.set( 'points', 0 );
                }

                _.delay( function( self ) {
                    self.on( 'change:points', self.get_parent().update_points, self.get_parent() );
                }, 1, this );

            },

            /**
             * Add a new question choice
             *
             * @param    obj   data     object of choice data
             * @param    obj   options  additional options
             * @since    3.16.0
             * @version  3.16.0
             */
            add_choice: function( data, options ) {

                var max = this.get( 'question_type' ).get_max_choices();
                if ( this.get( 'choices' ).size() >= max ) {
                    return;
                }

                data    = data || {};
                options = options || {};

                data.choice_type = this.get( 'question_type' ).get_choice_type();
                data.question_id = this.get( 'id' );
                options.parent   = this;

                var choice = this.get( 'choices' ).add( data, options );

                Backbone.pubSub.trigger( 'question-add-choice', choice, this );

            },

            /**
             * Collapse question_type attribute during full syncs to save to database
             * Not needed because question types cannot be adjusted after question creation
             * Called from sync controller
             *
             * @param    obj      atts       flat object of attributes to be saved to db
             * @param    string   sync_type  full or partial
             *                                 full indicates a force resync or that the model isn't persisted yet
             * @return   obj
             * @since    3.16.0
             * @version  3.16.0
             */
            before_save: function( atts, sync_type  ) {
                if ( 'full' === sync_type ) {
                    atts.question_type = this.get( 'question_type' ).get( 'id' );
                }
                return atts;
            },

            /**
             * Retrieve the model's parent (if set)
             *
             * @return   obj|false
             * @since    3.16.0
             * @version  3.16.0
             */
            get_parent: function() {

                var rels = this.get_relationships();

                if ( rels.parent ) {
                    if ( this.collection && this.collection.parent ) {
                        return this.collection.parent;
                    } else if ( rels.parent.reference ) {
                        return rels.parent.reference;
                    }
                }

                return false;

            },

            /**
             * Retrieve the translated post type name for the model's type
             *
             * @param    bool     plural  if true, returns the plural, otherwise returns singular
             * @return   string
             * @since    3.27.0
             * @version  3.27.0
             */
            get_l10n_type: function( plural ) {

                if ( plural ) {
                    return LLMS.l10n.translate( 'questions' );
                }

                return LLMS.l10n.translate( 'question' );
            },

            /**
             * Gets the index of the question within it's parent
             * Question numbers skip content elements
             * & content elements skip questions
             *
             * @return   int
             * @since    3.16.0
             * @version  3.16.0
             */
            get_type_index: function() {

                // current models type, used to check the predicate in the filter function below
                var curr_type = this.get( 'question_type' ).get( 'id' ),
                questions;

                questions = this.collection.filter( function( question ) {

                    var type = question.get( 'question_type' ).get( 'id' );

                    // if current model is not content, return all non-content questions
                    if ( curr_type !== 'content' ) {
                        return ( 'content' !== type );
                    }

                    // current model is content, return only content questions
                    return 'content' === type;

                } );

                return questions.indexOf( this );

            },

            /**
             * Gets iterator for the given type
             * Questions use numbers and content uses alphabet
             *
             * @return   mixed
             * @since    3.16.0
             * @version  3.16.0
             */
            get_type_iterator: function() {

                var index = this.get_type_index();

                if ( -1 === index ) {
                    return '';
                }

                if ( 'content' === this.get( 'question_type' ).get( 'id' ) ) {
                    var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split( '' );
                    return alphabet[ index ];
                }

                return index + 1;

            },

            get_qid: function() {

                var parent = this.get_parent_question(),
                prefix     = '';

                if ( parent ) {

                    prefix = parent.get_qid() + '.';

                }

                // return short_id + this.get_type_iterator();
                return prefix + this.get_type_iterator();

            },

            /**
             * Retrieve the parent question (if the question is in a question group)
             *
             * @return   obj|false
             * @since    3.16.0
             * @version  3.16.0
             */
            get_parent_question: function() {

                if ( this.is_in_group() ) {

                    return this.collection.parent;

                }

                return false;

            },

            /**
             * Retrieve the parent quiz
             *
             * @return   obj
             * @since    3.16.0
             * @version  3.16.0
             */
            get_parent_quiz: function() {
                return this.get_parent();
            },

            /**
             * Points getter
             * ensures that 0 is always returned if the question type doesn't support points
             *
             * @return   int
             * @since    3.16.0
             * @version  3.16.0
             */
            get_points: function() {

                if ( ! this.get( 'question_type' ).get( 'points' ) ) {
                    return 0;
                }

                return this.get( 'points' );

            },

            /**
             * Retrieve the questions percentage value within the quiz
             *
             * @return   string
             * @since    3.16.0
             * @version  3.16.0
             */
            get_points_percentage: function() {

                var total = this.get_parent().get( '_points' ),
                points    = this.get( 'points' );

                if ( 0 === total ) {
                    return '0%';
                }

                return ( ( points / total ) * 100 ).toFixed( 2 ) + '%';

            },

            /**
             * Determine if the question belongs to a question group
             *
             * @return   {Boolean}
             * @since    3.16.0
             * @version  3.16.0
             */
            is_in_group: function() {

                return ( 'question' === this.collection.parent.get( 'type' ) );

            },

            _ensure_min_choices: function() {

                var choices = this.get( 'choices' );
                while ( choices.size() < this.get( 'question_type' ).get_min_choices() ) {
                    this.add_choice();
                }

            },

        }, Relationships, Utilities ) );

} );