Skip to content

Commit

Permalink
Merge pull request #31 from kjac/field-statistics
Browse files Browse the repository at this point in the history
Field statistics
  • Loading branch information
kjac committed May 2, 2016
2 parents 92356af + cbbd0cb commit 260f026
Show file tree
Hide file tree
Showing 38 changed files with 690 additions and 49 deletions.
9 changes: 8 additions & 1 deletion Docs/extend_field_advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@ Have a look at the [`UploadField`](../Source/Solution/FormEditor/Fields/UploadFi
## Supporting receipt emails
In case the value stored by your field is not an email address, but you need your field to work as a source for receipt email addresses, you can implement the interface [`IEmailField`](../Source/Solution/FormEditor/Fields/IEmailField.cs) to translate your field value into the appropriate email addresses.

Have a look at the [`MemberInfoField`](../Source/Solution/FormEditor/Fields/MemberInfoField.cs) for an example.
Have a look at the [`MemberInfoField`](../Source/Solution/FormEditor/Fields/MemberInfoField.cs) for an example.

## Supporting statistics
You can easily include the submitted values for your field in the built-in statistics. All you need to do is implement an interface and Form Editor will do the rest.

**Please note:** The statistics interfaces will most likely change over time, which might cause breaking changes for you when you upgrade Form Editor.

At the time of writing, the only type of statistics that's supported is field value frequency statistics. The typical fields for this type of statistics are fields with predefined value ranges. To support field value frequency statistics, your field must implement [`IValueFrequencyStatisticsField`](../Source/Solution/FormEditor/Fields/Statistics/IValueFrequencyStatisticsField.cs).
Binary file added Docs/img/statistics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions Docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,9 @@ You must supply the Elastic connection string as `FormEditor.ElasticIndex` in th
</connectionStrings>
```

## Supporting statistics
To make your index work with the build-in statistics it must implement the [`IStatisticsIndex`](../Source/Solution/FormEditor/Storage/Statistics/IStatisticsIndex.cs) interface. Nothing else is required - Form Editor will automatically enable statistics if it's backed by an index that implements this interface.

**Please note:** The `IStatisticsIndex` interface will most likely change over time, which might cause breaking changes for you when you upgrade Form Editor.

None of the sample indexes currently support statistics, but you can have a look at the [`default index implementation`](../Source/Solution/FormEditor/Storage/Index.cs) for inspiration.
90 changes: 88 additions & 2 deletions Docs/submissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var formData = form.GetSubmittedValues(
);
```

## Putting it all together
### Sample template
Here's a full sample template that outputs the most recent form submissions to the users:

```xml
Expand Down Expand Up @@ -84,8 +84,94 @@ Here's a full sample template that outputs the most recent form submissions to t
</html>
```

## How about statistics?
If you'd like to work with the form submissions statistics on your frontend - you can! Once again, we start by retrieving the form model:

```cs
// get the form model (named "form" on the content type)
var form = Model.Content.GetPropertyValue<FormModel>("form");
```

The form model exposes the statistics for field value frequency through the method `GetFieldValueFrequencyStatistics()`. It can be used with the following parameters, all of which are optional:

- `IEnumerable<string> fieldNames`: The "form safe names" of the fields to retrieve statistics for. Default is all supported fields.
- `IPublishedContent content`: The content that holds the form. Default is current page.

Mind you, only a subset of the built-in Form Editor fields support field value frequency statistics, namely the ones that have a predefined value range (like radio button group, select box, etc).

### Sample template
Here's a full sample template that renders a bar chart (using [Google chart tools](https://developers.google.com/chart/)) of the field value frequencies for the field "genres":

```xml
@using FormEditor;
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@{
Layout = null;

// get the form model (named "form" on the content type)
var form = Model.Content.GetPropertyValue<FormModel>("form");

// get the field value frequency statistics for the field "genres" (if it exists)
var statistics = form.GetFieldValueFrequencyStatistics(new[] { "genres" });
var fieldValueFrequencies = statistics.FieldValueFrequencies.FirstOrDefault();
}
<!DOCTYPE html>
<html>
<head>
<title>@Model.Content.Name</title>
<link rel="stylesheet" href="http://getbootstrap.com/dist/css/bootstrap.min.css" />
@if(fieldValueFrequencies != null)
{

@* read all about Google chart tools at https://developers.google.com/chart/ *@
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script>
google.charts.load("current", {packages: ["corechart", "bar"]});
google.charts.setOnLoadCallback(drawChart);

function drawChart() {

@* this is the chart data. the first entry contains the chart legend - gonna leave that empty. *@
var chartData = [
["", ""]
];

@* add the field values and their frequencies *@
@foreach(var fieldValueFrequency in fieldValueFrequencies.Frequencies)
{
@: chartData.push(["@fieldValueFrequency.Value", @fieldValueFrequency.Frequency]);
}

@* do the Google charts stuff *@
var data = google.visualization.arrayToDataTable(chartData);
var options = {
chartArea: {width: "60%"},
hAxis: {
minValue: 0,
title: "Total number of submissions: @statistics.TotalRows"
},
legend: "none"
};
var chart = new google.visualization.BarChart(document.getElementById("chart"));
chart.draw(data, options);
}
</script>
}
</head>
<body>
@* this is where the chart will be rendered *@
<div id="chart"></div>
</body>
</html>
```

The output should come out something like this:

![Frontend rendering of statistics](img/statistics.png)


## Wait... what about async?
Nope, sorry. There's no public endpoint for retrieving form submissions asynchronously. It would be a major security problem to have that.
Nope, sorry. There's no public endpoint for retrieving form submissions or statistics asynchronously. It would be a major security problem to have that.

## Next step
Onwards to [extending Form Editor](extend.md).
2 changes: 1 addition & 1 deletion Grunt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"grunt-template": "^0.2.3"
},
"meta": {
"version": "0.13.1.2"
"version": "0.13.2.1"
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Some highlights include:
* Full control over the frontend rendering.
* Full support for asynchronous postback, e.g. for AngularJS.
* Easily extendable with custom fields.
* Built-in statistics for form submissions.
* Editors can add texts and images alongside the form fields.
* reCAPTCHA support.
* Fully localizable.
Expand Down
6 changes: 3 additions & 3 deletions Samples/Advanced custom field/My.Fields.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Examine.0.1.68.0\lib\Examine.dll</HintPath>
</Reference>
<Reference Include="FormEditor, Version=0.13.1.2, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FormEditor.Binaries.0.13.1.2\lib\net40\FormEditor.dll</HintPath>
<Reference Include="FormEditor, Version=0.13.2.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FormEditor.Binaries.0.13.2.1\lib\net40\FormEditor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="HtmlAgilityPack, Version=1.4.9.0, Culture=neutral, PublicKeyToken=bd319b19eaf3b43a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
Expand Down
2 changes: 1 addition & 1 deletion Samples/Advanced custom field/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<package id="ClientDependency" version="1.8.4" targetFramework="net45" />
<package id="ClientDependency-Mvc5" version="1.8.0.0" targetFramework="net45" />
<package id="Examine" version="0.1.68.0" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.1.2" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.2.1" targetFramework="net45" />
<package id="HtmlAgilityPack" version="1.4.9" targetFramework="net45" />
<package id="ImageProcessor" version="2.3.0.0" targetFramework="net45" />
<package id="ImageProcessor.Web" version="4.4.0.0" targetFramework="net45" />
Expand Down
6 changes: 3 additions & 3 deletions Samples/Custom validation condition/My.Conditions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@
<HintPath>..\packages\Examine.0.1.68.0\lib\Examine.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FormEditor, Version=0.13.1.2, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FormEditor.Binaries.0.13.1.2\lib\net40\FormEditor.dll</HintPath>
<Reference Include="FormEditor, Version=0.13.2.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FormEditor.Binaries.0.13.2.1\lib\net40\FormEditor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="HtmlAgilityPack">
<HintPath>..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll</HintPath>
Expand Down
2 changes: 1 addition & 1 deletion Samples/Custom validation condition/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<package id="ClientDependency" version="1.8.4" targetFramework="net45" />
<package id="ClientDependency-Mvc5" version="1.8.0.0" targetFramework="net45" />
<package id="Examine" version="0.1.68.0" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.1.2" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.2.1" targetFramework="net45" />
<package id="HtmlAgilityPack" version="1.4.9" targetFramework="net45" />
<package id="ImageProcessor" version="2.3.0.0" targetFramework="net45" />
<package id="ImageProcessor.Web" version="4.4.0.0" targetFramework="net45" />
Expand Down
6 changes: 3 additions & 3 deletions Samples/Elastic storage index/FormEditor.ElasticIndex.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
<HintPath>..\packages\Examine.0.1.68.0\lib\Examine.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FormEditor, Version=0.13.1.2, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FormEditor.Binaries.0.13.1.2\lib\net40\FormEditor.dll</HintPath>
<Reference Include="FormEditor, Version=0.13.2.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FormEditor.Binaries.0.13.2.1\lib\net40\FormEditor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="HtmlAgilityPack, Version=1.4.9.0, Culture=neutral, PublicKeyToken=bd319b19eaf3b43a, processorArchitecture=MSIL">
<HintPath>..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll</HintPath>
Expand Down
2 changes: 1 addition & 1 deletion Samples/Elastic storage index/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<package id="ClientDependency-Mvc5" version="1.8.0.0" targetFramework="net45" />
<package id="Elasticsearch.Net" version="1.6.0" targetFramework="net45" />
<package id="Examine" version="0.1.68.0" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.1.2" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.2.1" targetFramework="net45" />
<package id="HtmlAgilityPack" version="1.4.9" targetFramework="net45" />
<package id="ImageProcessor" version="2.3.0.0" targetFramework="net45" />
<package id="ImageProcessor.Web" version="4.4.0.0" targetFramework="net45" />
Expand Down
6 changes: 3 additions & 3 deletions Samples/Event handling/My.Events.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
<Reference Include="Examine">
<HintPath>..\packages\Examine.0.1.68.0\lib\Examine.dll</HintPath>
</Reference>
<Reference Include="FormEditor, Version=0.13.1.2, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FormEditor.Binaries.0.13.1.2\lib\net40\FormEditor.dll</HintPath>
<Reference Include="FormEditor, Version=0.13.2.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FormEditor.Binaries.0.13.2.1\lib\net40\FormEditor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="HtmlAgilityPack">
<HintPath>..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll</HintPath>
Expand Down
2 changes: 1 addition & 1 deletion Samples/Event handling/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<package id="ClientDependency" version="1.8.4" targetFramework="net45" />
<package id="ClientDependency-Mvc5" version="1.8.0.0" targetFramework="net45" />
<package id="Examine" version="0.1.68.0" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.1.2" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.2.1" targetFramework="net45" />
<package id="HtmlAgilityPack" version="1.4.9" targetFramework="net45" />
<package id="ImageProcessor" version="2.3.0.0" targetFramework="net45" />
<package id="ImageProcessor.Web" version="4.4.0.0" targetFramework="net45" />
Expand Down
6 changes: 3 additions & 3 deletions Samples/SQL storage index/FormEditor.SqlIndex.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Examine.0.1.68.0\lib\Examine.dll</HintPath>
</Reference>
<Reference Include="FormEditor, Version=0.13.1.2, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FormEditor.Binaries.0.13.1.2\lib\net40\FormEditor.dll</HintPath>
<Reference Include="FormEditor, Version=0.13.2.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FormEditor.Binaries.0.13.2.1\lib\net40\FormEditor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="HtmlAgilityPack, Version=1.4.9.0, Culture=neutral, PublicKeyToken=bd319b19eaf3b43a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
Expand Down
2 changes: 1 addition & 1 deletion Samples/SQL storage index/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<package id="ClientDependency" version="1.8.4" targetFramework="net45" />
<package id="ClientDependency-Mvc5" version="1.8.0.0" targetFramework="net45" />
<package id="Examine" version="0.1.68.0" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.1.2" targetFramework="net45" />
<package id="FormEditor.Binaries" version="0.13.2.1" targetFramework="net45" />
<package id="HtmlAgilityPack" version="1.4.9" targetFramework="net45" />
<package id="ImageProcessor" version="2.3.0.0" targetFramework="net45" />
<package id="ImageProcessor.Web" version="4.4.0.0" targetFramework="net45" />
Expand Down
65 changes: 61 additions & 4 deletions Source/Solution/FormEditor/Api/PropertyEditorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
using System.Web.Hosting;
using System.Web.Http;
using FormEditor.Fields;
using FormEditor.Fields.Statistics;
using FormEditor.Storage;
using FormEditor.Storage.Statistics;
using FormEditor.Umbraco;
using FormEditor.Validation.Conditions;
using Newtonsoft.Json;
Expand Down Expand Up @@ -147,7 +149,9 @@ public object GetData(int id, int page, string sortField, bool sortDescending, s
return null;
}

var allFields = GetAllFieldsForDisplay(model, document);
var preValues = ContentHelper.GetPreValues(document, FormModel.PropertyEditorAlias);
var allFields = GetAllFieldsForDisplay(model, document, preValues);
var statisticsEnabled = ContentHelper.StatisticsEnabled(preValues);

var index = IndexHelper.GetIndex(id);
var fullTextIndex = index as IFullTextIndex;
Expand Down Expand Up @@ -180,7 +184,8 @@ public object GetData(int id, int page, string sortField, bool sortDescending, s
totalPages = totalPages,
sortField = result.SortField,
sortDescending = result.SortDescending,
supportsSearch = fullTextIndex != null
supportsSearch = fullTextIndex != null,
supportsStatistics = statisticsEnabled && index is IStatisticsIndex && allFields.StatisticsFields().Any()
};
}

Expand All @@ -191,12 +196,64 @@ public object GetMediaUrl(int id)
return image != null ? new { id = image.Id, url = image.Url } : null;
}

internal static List<FieldWithValue> GetAllFieldsForDisplay(FormModel model, IContent document)
public object GetFieldValueFrequencyStatistics(int id)
{
var document = ContentHelper.GetById(id);
if(document == null)
{
return null;
}
var model = ContentHelper.GetFormModel(document);
if(model == null)
{
return null;
}

var statisticsFields = model.AllValueFields().OfType<IValueFrequencyStatisticsField>().ToList();
if(statisticsFields.Any() == false)
{
return null;
}

var index = IndexHelper.GetIndex(id) as IStatisticsIndex;
if(index == null)
{
return null;
}

var fieldValueFrequencyStatistics = index.GetFieldValueFrequencyStatistics(statisticsFields.StatisticsFieldNames());

return new
{
totalRows = fieldValueFrequencyStatistics.TotalRows,
fields = fieldValueFrequencyStatistics.FieldValueFrequencies
.Where(f => statisticsFields.Any(v => v.FormSafeName == f.Field))
.Select(f =>
{
var field = statisticsFields.First(v => v.FormSafeName == f.Field);
return new
{
name = field.Name,
formSafeName = field.FormSafeName,
multipleValuesPerEntry = field.MultipleValuesPerEntry,
values = f.Frequencies.Select(v => new
{
value = v.Value,
frequency = v.Frequency
})
};
})
};
}

internal static List<FieldWithValue> GetAllFieldsForDisplay(FormModel model, IContent document, IDictionary<string, PreValue> preValues = null)
{
var allFields = model.AllValueFields().ToList();

preValues = preValues ?? ContentHelper.GetPreValues(document, FormModel.PropertyEditorAlias);

// show logged IPs?
if (ContentHelper.IpDisplayEnabled(document) && ContentHelper.IpLoggingEnabled(document))
if (ContentHelper.IpDisplayEnabled(preValues) && ContentHelper.IpLoggingEnabled(preValues))
{
// IPs are being logged, add a single line text field to retrieve IPs as a string
allFields.Add(new TextBoxField { Name = "IP", FormSafeName = "_ip" });
Expand Down
20 changes: 18 additions & 2 deletions Source/Solution/FormEditor/Fields/FieldWithFieldValues.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using FormEditor.Fields.Statistics;
using Umbraco.Core.Models;

namespace FormEditor.Fields
{
public abstract class FieldWithFieldValues : FieldWithMandatoryValidation
public abstract class FieldWithFieldValues : FieldWithMandatoryValidation, IValueFrequencyStatisticsField
{
public FieldValue[] FieldValues { get; set; }

Expand All @@ -20,7 +21,7 @@ protected internal override bool ValidateSubmittedValue(IEnumerable<Field> allCo
return true;
}

var submittedFieldValues = SubmittedValue.Split(',');
var submittedFieldValues = ExtractSubmittedValues();
FieldValues.ToList().ForEach(f => f.Selected = submittedFieldValues.Contains(f.Value));

// make sure all submitted values are actually defined as a field value (maybe some schmuck tampered with the options client side)
Expand All @@ -36,5 +37,20 @@ public virtual bool IsMultiSelectEnabled
{
get { return false; }
}

public IEnumerable<string> SubmittedValues
{
get { return ExtractSubmittedValues(); }
}

private string[] ExtractSubmittedValues()
{
return SubmittedValue != null ? SubmittedValue.Split(',') : new string[] { };
}

public virtual bool MultipleValuesPerEntry
{
get { return IsMultiSelectEnabled; }
}
}
}
Loading

0 comments on commit 260f026

Please sign in to comment.