Skip to content
This repository has been archived by the owner on Feb 13, 2022. It is now read-only.

Commit

Permalink
Merge pull request #39 from praekelt/feature/issue-39-implement-front…
Browse files Browse the repository at this point in the history
…-end-functionality-for-boolean-operators-for-advanced-surveys

Implement front end functionality for boolean operators for advanced surveys
  • Loading branch information
nathanbegbie committed Nov 7, 2017
2 parents ec18947 + 02b9082 commit 6af4245
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 1 deletion.
6 changes: 6 additions & 0 deletions molo/surveys/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ def clean(self, value):


class RuleSelectBlock(blocks.CharBlock):
def __init__(self, *args, **kwargs):
super(RuleSelectBlock, self).__init__(*args, **kwargs)
self.field.widget = SelectAndHiddenWidget()

def js_initializer(self):
return 'newRuleAdded'

class Meta:
icon = 'cog'
Expand Down
37 changes: 37 additions & 0 deletions molo/surveys/migrations/0018_add_combination_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-11-07 07:01
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
import molo.surveys.blocks
import wagtail.wagtailcore.blocks
import wagtail.wagtailcore.fields


class Migration(migrations.Migration):

dependencies = [
('wagtail_personalisation', '0013_auto_20171026_1748'),
('surveys', '0017_make_count_positive_integer'),
]

operations = [
migrations.CreateModel(
name='CombinationRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', wagtail.wagtailcore.fields.StreamField([(b'Rule', molo.surveys.blocks.RuleSelectBlock()), (b'Operator', wagtail.wagtailcore.blocks.ChoiceBlock(choices=[(b'and', 'And'), (b'or', 'Or')])), (b'NestedLogic', wagtail.wagtailcore.blocks.StructBlock([(b'rule_1', molo.surveys.blocks.RuleSelectBlock(required=True)), (b'operator', wagtail.wagtailcore.blocks.ChoiceBlock(choices=[(b'and', 'And'), (b'or', 'Or')])), (b'rule_2', molo.surveys.blocks.RuleSelectBlock(required=True))]))])),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='surveys_combinationrule_related', related_query_name='%(app_label)s_%(class)ss', to='wagtail_personalisation.Segment')),
],
options={
'verbose_name': 'Rule Combination',
},
),
migrations.AlterField(
model_name='articletagrule',
name='date_to',
field=models.DateTimeField(blank=True, help_text='All times are UTC. Leave both fields blank to search all time.', null=True),
),
]
29 changes: 29 additions & 0 deletions molo/surveys/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from wagtail.wagtailcore.blocks.stream_block import StreamBlockValidationError
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel,
FieldRowPanel,
Expand Down Expand Up @@ -391,5 +392,33 @@ def description(self):
'particular combination of rules'),
}

def clean(self):
super(CombinationRule, self).clean()
if isinstance(self.body.stream_data[0], dict):
newData = [block['type'] for block in self.body.stream_data]
elif isinstance(self.body.stream_data[0], tuple):
newData = [block[0] for block in self.body.stream_data]

if (len(newData) - 1) % 2 != 0:
raise StreamBlockValidationError(non_block_errors=[_(
'Rule Combination must follow the <Rule/NestedLogic>'
'<Operator> <Rule/NestedLogic> pattern.')])

iterations = (len(newData) - 1) / 2
for i in range(iterations):
first_rule_index = i * 2
operator_index = (i * 2) + 1
second_rule_index = (i * 2) + 2

if not (
(newData[first_rule_index] == 'Rule' or
newData[first_rule_index] == 'NestedLogic') and
(newData[operator_index] == 'Operator') and
(newData[second_rule_index] == 'Rule' or
newData[second_rule_index] == 'NestedLogic')):
raise StreamBlockValidationError(non_block_errors=[_(
'Rule Combination must follow the <Rule/NestedLogic> '
'<Operator> <Rule/NestedLogic> pattern.')])

class Meta:
verbose_name = _('Rule Combination')
201 changes: 200 additions & 1 deletion molo/surveys/static/js/survey-admin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
$(function(){
console.log('loaded')
var multi_step_checkbox = $('#id_multi_step');
var multi_step_label = $('label[for=id_multi_step]');

Expand Down Expand Up @@ -52,4 +51,204 @@ $(function(){
enableDisplayDirect();
}
});

window.rules = [
'Time Rule',
'Day Rule',
'Referral Rule',
'Visit Count Rule',
'Article Tag Rule',
'Query Rule',
'Device Rule',
'User Is Logged In Rule',
'Comment Data Rule',
'Profile Data Rule',
'Survey Submission Data Rule',
'Group Membership Rule',
]

window.ruleIndex = {};
window.ruleFields = [];

window.initRuleIndex = function(){
rules.map(function(ruleTitle) {
var ruleInfo = {
ruleName: ruleTitle,
form: $('[id*="' + ruleTitle.replace(/ /g, "").toLowerCase() + '"]').filter('ul'),
blockIds: [],
updateBlock: function(){
this.blockIds = $.map(this.form.children('li').not('.deleted'), function(arg, index){
return arg.id
});
},
deleteBlock: function(id){
var index = this.blockIds.indexOf(id);
this.updateBlock();
var deletedOptionValue = this.ruleName.replace(/ /g, "") + "_" + String(index);
deleteRuleFromField(deletedOptionValue);
},
}
ruleInfo.updateBlock();
window.ruleIndex[ruleInfo.form.attr("id")] = ruleInfo;
})
}



window.extractRuleInfo = function(){
var ruleValueTextPairs = []
$.each(ruleIndex, function(index_outer, form){
$.each(form.blockIds, function(index_inner, value) {
ruleValueTextPairs.push({
"value": form.ruleName.replace(" ", "") + "_" + String(index_inner),
"text": form.ruleName + " " + String(index_inner + 1),
});
});
});
return ruleValueTextPairs;
}

window.addUpdateTrigger = function(obj, form_id, block_id){
obj.click(function(){
window.deleteFromRuleIndex(form_id, block_id);
});
}

window.addActionToExistingDeleteButtons = function(){
var forms = $('[id*="rule_related-FORMS"]')
.not("#id_surveys_combinationrule_related-FORMS")
.filter("ul");

forms.map(function(index, form) {
$("#" + form.id)
.children("li")
.not(".deleted")
.each(function(index, block) {
var deleteButton = $("#" + block.id).find('button[title^="Delete"]');
window.addUpdateTrigger(deleteButton, form.id, block.id);
});
});
};

window.deleteFromRuleIndex = function(form_id, block_id){
var block = ruleIndex[form_id]
block.deleteBlock(block_id);
};

window.attachActionToRuleCreators = function() {
var some_rules = $('[id*="rule_related-ADD"]').filter("a").not("#id_surveys_combinationrule_related-ADD");
$.map(some_rules, function(val, i) {
val.addEventListener("click", function() {
var id = $(this).attr('id');
var form_id = id.replace('ADD', 'FORMS');
var form = $("#" + form_id);
var newBlock = form.children(':last');
// upate Index of Rules
ruleIndex[form_id].updateBlock();
updateRuleOptions();
// add event listener to new Delete Button
var newDeleteButton = newBlock.find('button[title^="Delete"]');
window.addUpdateTrigger(newDeleteButton, form.attr("id"), newBlock.attr("id"));
}, false);
});
};

var createSelectManager = function(id){
var selectRuleField = {
hiddenInput: $('#' + id + '_0'),
select: $('#' + id + '_1'),
repopulateOptions: function(){
var ruleInfo = extractRuleInfo()
var select = this.select;
select.children().remove();
select.append($("<option value=''>-------</option>"));
$.each(ruleInfo, function(index, option) {
select.append($('<option>', {
"value": option["value"],
"text": option["text"],
}));
});
},
populateSelect: function(){
var existingValue = this.hiddenInput.val();
this.repopulateOptions();
if(existingValue){
this.select.val(existingValue);
}
this.updateHiddenField();
},
updateHiddenField:function(){
this.hiddenInput.val(this.select.val())
},
deleteSelect: function(optionValue){
var existingValue = this.hiddenInput.val();

var existingRuleType = existingValue.split("_")[0]
var existingRuleOrder = existingValue.split("_")[1]

var deletedRuleType = optionValue.split("_")[0]
var deletedRuleOrder = optionValue.split("_")[1]

//check if the value is the one being deleted
if(existingRuleType===deletedRuleType){
if(existingRuleOrder===deletedRuleOrder){
this.repopulateOptions();
this.select.val("");
this.updateHiddenField();
}
else if(deletedRuleOrder<existingRuleOrder){
var newSelectedValue = existingRuleType + "_" + String(existingRuleOrder - 1);
this.repopulateOptions();
this.select.val(newSelectedValue);
this.updateHiddenField();
}
else{
this.populateSelect()
}
}
else{
this.populateSelect()
}
}
}
selectRuleField.populateSelect();
return selectRuleField
}

window.newRuleAdded = function(id){
ruleFields.push(createSelectManager(id));
};

window.deleteRuleFromField = function(deletedOption){
$.each(ruleFields, function(index, selectManager){
selectManager.deleteSelect(deletedOption);
});
}

window.updateRuleOptions = function(){
$.each(ruleFields, function(index, selectManager){
selectManager.populateSelect();
});
}

// Prevent User from creating more than one rule combination
window.addHideToRuleCombinationFunctionality = function(){
$('#id_surveys_combinationrule_related-ADD').click(function(){
var id = $(this).attr("id");
var form_id = id.replace("ADD", "FORMS");
var form = $("#" + form_id);
var deleteButton = form
.children(":last")
.find('button[title^="Delete"]');
deleteButton.click(function(){
$("#id_surveys_combinationrule_related-ADD").toggle();
});
$("#id_surveys_combinationrule_related-ADD").toggle();
});
}
addHideToRuleCombinationFunctionality();

initRuleIndex();
attachActionToRuleCreators();
addActionToExistingDeleteButtons();
})

0 comments on commit 6af4245

Please sign in to comment.