Nibble

Creating a Quiz (including score calculation and feedback) with Umbraco Contour

After the Setting up a poll on your umbraco site using Contour this is another post in the Umbraco Contour series. This time I’ll create a quiz for my umbraco site using Contour.

This post uses some of the new additions to Contour 1.1.3 so you’ll need to be running that version if you want to try this yourself.

The challenge

With a quiz I mean that I’ll have a number of questions and possible answers. Each of these answers will have a weight (for example, 0 if the answer is incorrect, 1 if the answer is correct) and when a user fills in the quiz he should get feedback on how he did (so how much he scored based on the answers he gave).

Step 1: Setting up the form

Setting up the questions and answers is is a piece of cake, by simply using Contour’s ui to setup the form we have it up in minutes (so I won’t go in detail on how you create a form with Contour, if you want to try it just install Contour directly from the package repo).

image

Once I have a form with some radiobuttonlist fields I’ll also add a hidden field that will store the score

image

Step 2: Making it possible to give a weight to each answer and Calculating + storing the score

 

So once the user has completed the form/quiz I want to calculate the score. To execute some code during the record lifecycle we’ll use a workflow.

A workflow can have multiple settings each of these settings can be of a different type (for example a send email workflow will have a subject setting that is of the type textfield) to be able to give each answer a weight I’ll create a new fieldsetting type that will list all questions with their possible answers and on each answer have an input to provide a weight. Which will look like this:

image

The code for the custom fieldsetting type looks like this (download the entire sourcecode at the bottom of this post:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Umbraco.Forms.Core;
using Umbraco.Forms.Data.Storage;
using System.Web.UI.WebControls;
using System.Web.UI;
 
namespace Contour.Addons.ScoreCalclulator
{
    public class ScoreMapper : FieldSettingType
    {
        private Panel p = new Panel();
        private string _val = "";
 
        public override string Value
        {
            get
            {
                return _val;
            }
            set
            {
                _val = value;
            }
        }
 
        public override System.Web.UI.WebControls.WebControl RenderControl
            (Umbraco.Forms.Core.Attributes.Setting sender, Form form)
        {
            if (!string.IsNullOrEmpty(HttpContext.Current.Request["scoremapperValues"]))
                _val = HttpContext.Current.Request["scoremapperValues"];
 
            Dictionary<string, int> scores = new Dictionary<string, int>();
 
            foreach (string mapping in _val.Split(‘;’))
            {
                int weight = 0;
                if (!string.IsNullOrEmpty(mapping) && mapping.Split(‘,’).Length > 0)
                    scores.Add(
                        mapping.Split(‘,’)[0], 
                        int.TryParse(mapping.Split(‘,’)[1], out weight) ? weight : 0);
            }
 
            string html = "<div id=’scoreMapper’>";
 
            foreach (Field f in form.AllFields)
            {
                if (f.FieldType.SupportsPrevalues && f.PreValueSource.Type.GetPreValues(f).Count > 0)
                {
                    html += string.Format(
                        "<div class=’scoreMapperField’ style=’width:400px’><small>{0}</small>", 
                        f.Caption);
 
                    foreach (PreValue pv in f.PreValueSource.Type.GetPreValues(f))
                    {
                        html += string.Format(
                            "<div class=’scoreMapperFieldPrevalue’><span>{0}</span>", 
                            pv.Value);
                        html += string.Format(
                            "<input type=’hidden’ class=’key’ value=’{0}’/>",
                            pv.Id);
                        html += string.Format(
                            "&nbsp;<input type=’text’ class=’value’ value=’{0}’/>", 
                            scores.ContainsKey(pv.Id.ToString()) ? scores[pv.Id.ToString()].ToString() : "0");
 
                        html += "</div>";
                    }
 
                    html += "</div>";
                }
            }
 
            html += "<input type=’hidden’ id=’scoreMapperValues’ value=’" + _val + "’ name=’scoreMapperValues’/>";
            html += "</div>";
 
            System.Web.UI.Page page = (System.Web.UI.Page)HttpContext.Current.Handler;
 
            page.ClientScript.RegisterClientScriptInclude(
                "Contour.Addons.ScoreCalclulator.scoreMapper.js",
                page.ClientScript.GetWebResourceUrl(
                typeof(ScoreMapper), "Contour.Addons.ScoreCalclulator.scoreMapper.js"));
 
            p.Controls.Add(new LiteralControl(html));
 
            return p;
        }
 
 
    }
}

 

And the contents of the ScoreCalclulator.scoreMapper.js

jQuery(document).ready(function () {
    jQuery(‘#scoreMapper div.scoreMapperFieldPrevalue input.value’).change(function () {
        storeScoreMapperValues();
    });
})
 
function storeScoreMapperValues() {
    var vals = ;
 
    jQuery(‘div.scoreMapperFieldPrevalue’, jQuery(‘#scoreMapper’)).each(function () {
        var id = jQuery(‘input.key’, this).val();
        if (id != ) {
            var score = jQuery(‘input.value’, this).val();
            if (score == ) { score = 0; }
            vals += id + ‘,’ + score + ‘;’;
        }
    });
 
    jQuery(‘#scoreMapperValues’).val(vals);
}

Now I’ll use this custom field setting type on a custom workflow wich will be used to calculate the score. Besides the custom weight mapper setting the workflow also has a field picker that makes it possible to select a field on the form (I’ll use it to select the field where I want to store the score). And I’ll also add a textfield where I will prompt for a session variable key that I’ll set with the score.

So entire workflow will look like this

image

And the code (again you can download the full sourcecode at the bottom of this post):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Umbraco.Forms.Data.Storage;
using Umbraco.Forms.Core.Services;
using Umbraco.Forms.Core;
 
namespace Contour.Addons.ScoreCalclulator
{
    public class EnergyQuizScoreCalculator : Umbraco.Forms.Core.WorkflowType
    {      
        [Umbraco.Forms.Core.Attributes.Setting(
            "Score mappings", 
            description = "Setup how much each answer scores", 
            control = "Contour.Addons.ScoreCalclulator.ScoreMapper", 
            assembly = "Contour.Addons.ScoreCalclulator")]
        public string scoremappings { get; set; }
 
        [Umbraco.Forms.Core.Attributes.Setting("Field to update", 
            description = "Field that will be populated with the score", 
            control = "Umbraco.Forms.Core.FieldSetting.FieldPicker")]
        public string field { get; set; }
 
        [Umbraco.Forms.Core.Attributes.Setting("Session key to set", 
            description = "If supplied a session variable will be set", 
            control = "Umbraco.Forms.Core.FieldSetting.TextField")]
        public string sessionkey { get; set; }
 
        public EnergyQuizScoreCalculator()
        {
            this.Name = "Score Calculator";
            this.Id = new Guid("EAFAC6F1-F2D2-43A4-8940-AB7A6F7AE83F");
            this.Description = "Calculates a score based on the questions/correct answers";
        }
 
 
        public override List<Exception> ValidateSettings()
        {
            List<Exception> exceptions = new List<Exception>();
            return exceptions;
        }
 
 
        public override Umbraco.Forms.Core.Enums.WorkflowExecutionStatus Execute(Record record, RecordEventArgs e)
        {
            //calc score
            int score = 0;
 
            Dictionary<string, int> scores = new Dictionary<string, int>();
 
            foreach (string mapping in scoremappings.Split(‘;’))
            {
                if (!string.IsNullOrEmpty(mapping) && mapping.Split(‘,’).Length > 0)
                {
                    int weight = 0;
                    scores.Add(
                        mapping.Split(‘,’)[0], 
                        int.TryParse(mapping.Split(‘,’)[1], out weight) ? weight : 0);
                }
            }
 
            foreach (RecordField rf in record.RecordFields.Values)
            {
                if (rf.Values.Count > 0 && scores.ContainsKey(rf.Values[0].ToString()))
                    score += scores[rf.Values[0].ToString()];
            }
 
            //set score
            foreach (RecordField rf in record.RecordFields.Values)
            {
                if (rf.Field.Id ==  new Guid(field))
                {
                    rf.Values.Clear();
                    rf.Values.Add(score);
                    break;
                }
            }
 
            if(!string.IsNullOrEmpty(sessionkey))
                HttpContext.Current.Session[sessionkey] = score.ToString();
 
            FormStorage fs = new FormStorage();
            Form f = fs.GetForm(record.Form);
            RecordStorage rs = new RecordStorage();
            rs.UpdateRecord(record, f);
            rs.UpdateRecordXml(record, f);
 
            fs.Dispose();
            rs.Dispose();
 
            return Umbraco.Forms.Core.Enums.WorkflowExecutionStatus.Completed;
 
        }
    }
}

 

Once my custom workflow type is ready I simply need to place the assembly containing the workflow type in my bin directory and I should now be able to choose a ‘Score Calculator’ workflow type and add this to my form.

Step 3: Displaying the score

Once the workflow is setup the last step is to provide details on the score to the user filling out the quiz.

In the workflow I set a session variable with the score, so I’ll use the bracket syntax supported by Contour in the message on submit field to display this to the user. (in this case the key is moviequizscore)

image

The Result

Your content editors will be able to setup custom online quizzes, they will have full control over the number of questions/answers and how much weight they give each answer all from a nice UI directly integrated in the umbraco backend.

Want to learn more on how to extend Contour, check out the developer docs on the project page http://our.umbraco.org/projects/umbraco-pro/contour and the sourcecode for all default components in Contour (field types, workflow types, prevalues and data types) http://our.umbraco.org/projects/umbraco-contour-shared-source

Download the sourcecode described in this post here:

(simply drop the assembly Contour.Addons.ScoreCalclulator.dll your bin directory and you should also have the score calculator workflow)

10 Comments so far

  1. John on December 29th, 2010

    Is there anything else that needs to be done to get the score to save with the entry? If I debug I can see the “score” being set with “rf.Values.Add(score)” but the Entries section never has a value set. I’m using the latest version, 1.1.4.1

    Thanks

  2. Tim Geyssens on January 3rd, 2011

    @John, nope just attach the workflow (and I think it needs to be set on the approved workflow)

  3. John on January 4th, 2011

    @Tim
    Changing the workflow type from Submitted to Approved did the trick. Thanks so much!

  4. keilo on November 15th, 2011

    Thanks for the detailed documentation.

    What I am exploring however is a bit different, how can you show the next question based on the answer of the previous question, sort of like a trouble shooting/expert system. At the end of the the questions, user will be directed to a PDF download that is related with the final answer.

    Is this possible with CONTOUR? If yes I would like to purchase right away and will appreciate your views.

  5. Marianne on October 25th, 2012

    Hi,

    I’m using the score calculation for a multiple choice quiz and it seems that the score calculation is only using 1 score per question. Is it possible to get it to sum up on all the scores from selected answers to a question?

  6. eyal on March 14th, 2013

    hi,

    How a quiz can be linked to registered member of the site? How the quiz can be opened only to certain group of members?

  7. Nikola on September 2nd, 2013

    Hi! I’ve created a quiz for a client using Contour, since they already had a license for it, and now they want to be able to add images to each question.

    Does anyone have any idea how this could be achieved?

    Any help would be appreciated.

  8. Tim Geyssens on September 11th, 2013

    @Nikola check this http://contourstrikesagain.codeplex.com/ it has a mediapicker type you can use

  9. Nathan on November 13th, 2014

    This has stopped working completely :( Is there some other way to calculate the score that works?

  10. Craig on November 17th, 2014

    Hi, brilliant stuff.

    I wonder if there’s a simple way to show the user a block of pass/fail information.

    Great work though!

Leave a Reply