From ca63157349bbe656a4420805111861310ae4e39f Mon Sep 17 00:00:00 2001 From: Dominic Bosch Date: Sun, 20 Apr 2014 20:35:52 +0200 Subject: [PATCH] Added user specific function arguments to the event pollers as well --- coffee/components-manager.coffee | 15 +++ coffee/dynamic-modules.coffee | 1 + coffee/event-poller.coffee | 27 +++-- examples/event-pollers/importio.coffee | 16 ++- js/components-manager.js | 20 ++++ js/event-poller.js | 39 ++++--- webpages/handlers/coffee/forge_rule.coffee | 75 +++++++++++-- webpages/handlers/js/forge_rule.js | 110 +++++++++++++++++--- webpages/handlers/templates/forge_rule.html | 2 +- 9 files changed, 258 insertions(+), 47 deletions(-) diff --git a/coffee/components-manager.coffee b/coffee/components-manager.coffee index b3e4867..612c730 100644 --- a/coffee/components-manager.coffee +++ b/coffee/components-manager.coffee @@ -236,6 +236,11 @@ storeRule = ( user, oPayload, callback ) => if oPayload.event_params epModId = rule.event.split( ' -> ' )[ 0 ] db.eventPollers.storeUserParams epModId, user.username, JSON.stringify oPayload.event_params + oFuncArgs = oPayload.event_functions + # if event function arguments were send, store them + for id, args of oFuncArgs + arr = id.split ' -> ' + db.eventPollers.storeUserArguments user.username, rule.id, arr[ 0 ], arr[ 1 ], JSON.stringify args # if action module params were send, store them oParams = oPayload.action_params @@ -287,6 +292,16 @@ commandFunctions = get_event_poller_user_arguments: ( user, oPayload, callback ) -> getModuleUserArguments user, oPayload, db.eventPollers, callback + get_event_poller_function_arguments: ( user, oPayload, callback ) -> + answ = hasRequiredParams [ 'id' ], oPayload + if answ.code isnt 200 + callback answ + else + db.eventPollers.getModuleField oPayload.id, 'functionArgs', ( err, obj ) -> + callback + code: 200 + message: obj + forge_event_poller: ( user, oPayload, callback ) -> forgeModule user, oPayload, db.eventPollers, callback diff --git a/coffee/dynamic-modules.coffee b/coffee/dynamic-modules.coffee index 290a266..1bca5ce 100644 --- a/coffee/dynamic-modules.coffee +++ b/coffee/dynamic-modules.coffee @@ -158,3 +158,4 @@ fTryToLoadModule = ( userId, ruleId, modId, src, dbMod, params, cb ) => funcParams: oFuncParams funcArgs: oFuncArgs logger: sandbox.log + diff --git a/coffee/event-poller.coffee b/coffee/event-poller.coffee index 00cd60c..a7aefc9 100644 --- a/coffee/event-poller.coffee +++ b/coffee/event-poller.coffee @@ -92,27 +92,38 @@ fLoadModule = ( msg ) -> # If user is not yet stored, we open a new object if not listUserModules[msg.user] listUserModules[msg.user] = {} - + + oUser = listUserModules[msg.user] ts = new Date() # We open up a new object for the rule it - listUserModules[msg.user][msg.rule.id] = + oUser[msg.rule.id] = id: msg.rule.event pollfunc: arrName[1] + funcArgs: result.funcArgs event_interval: msg.rule.event_interval * 60 * 1000 module: result.module logger: result.logger timestamp: ts + oUser[msg.rule.id].module.pushEvent = fPushEvent msg.user, msg.rule.id, oUser[msg.rule.id] + log.info "EP | New event module '#{ arrName[0] }' loaded for user #{ msg.user }, in rule #{ msg.rule.id }, starting at #{ ts.toISOString() } and polling every #{ msg.rule.event_interval } minutes" - fCheckAndRun( msg.user, msg.rule.id, ts )() + setTimeout fCheckAndRun( msg.user, msg.rule.id, ts ), 1000 if msg.event is 'new' or not listUserModules[msg.user] or not listUserModules[msg.user][msg.rule.id] fAnonymous() +fPushEvent = ( userId, ruleId, oRule ) -> + ( obj ) -> + db.pushEvent + event: oRule.id + eventid: "polled #{ oRule.id } #{ userId }_#{ ( new Date() ).toISOString() }" + payload: obj + fCheckAndRun = ( userId, ruleId, timestamp ) -> () -> log.info "EP | Check and run user #{ userId }, rule #{ ruleId }" @@ -133,11 +144,11 @@ fCheckAndRun = ( userId, ruleId, timestamp ) -> # eventually not be what they are expected to be fCallFunction = ( userId, ruleId, oRule ) -> try - oRule.module[oRule.pollfunc] ( obj ) -> - db.pushEvent - event: oRule.id - eventid: "polled #{ oRule.id } #{ userId }_#{ ( new Date() ).toISOString() }" - payload: obj + arrArgs = [] + if oRule.funcArgs + for oArg in oRule.funcArgs[oRule.pollfunc] + arrArgs.push oArg.value + oRule.module[oRule.pollfunc].apply null, arrArgs catch err log.info "EP | ERROR in module when polled: #{ oRule.id } #{ userId }: #{err.message}" oRule.logger err.message diff --git a/examples/event-pollers/importio.coffee b/examples/event-pollers/importio.coffee index a2aa5a1..e624333 100644 --- a/examples/event-pollers/importio.coffee +++ b/examples/event-pollers/importio.coffee @@ -3,12 +3,15 @@ Import.io allows to capture data from the web required module params: - apikey +- userGuid - queryGuid ### params.apikey = "Cc8AX35d4B89ozzmn5bpm7k70HRon5rrfUxZvOwkVRj31/oBGHzVfQSRp5mEvlOgxyh7xi+tFSL66iAFo1W/sQ==" params.userGuid = "d19f0d08-bf73-4115-90a8-ac045ad4f225" -params.queryGuid = "4f833315-7aa0-4fcd-b8d0-c65f6a6bafcf" +params.queryGuid = "2a1d789a-4d24-4942-bdca-ffa0e9f99c85" +params.queryGuid = "2a1d789a-4d24-4942-bdca-ffa0e9f99c85" +# params.queryGuid = "4f833315-7aa0-4fcd-b8d0-c65f6a6bafcf" io = new importio params.userGuid, params.apikey, "query.import.io" @@ -23,7 +26,8 @@ exports.queryData = ( pushEvent ) -> else log "Connected!" data = [] - io.query "input": { "input": "query" }, "connectorGuids": [ params.queryGuid ], ( finished, msg ) -> + inp = { "webpage/url": "http://www.meteoblue.com/en/switzerland/weather-sankt-gallen" } + io.query "input": inp, "connectorGuids": [ params.queryGuid ], ( finished, msg ) -> log 'query returned' log msg if msg.type is "MESSAGE" @@ -35,3 +39,11 @@ exports.queryData = ( pushEvent ) -> log 'all work done' log io io = null + io.query({ + "connectorGuids": [ + "2a1d789a-4d24-4942-bdca-ffa0e9f99c85" + ], + "input": { + "webpage/url": "http://www.meteoblue.com/en/switzerland/weather-sankt-gallen" + } + }, getCallbackFunction()); \ No newline at end of file diff --git a/js/components-manager.js b/js/components-manager.js index e076caf..b5daeca 100644 --- a/js/components-manager.js +++ b/js/components-manager.js @@ -327,6 +327,12 @@ Components Manager epModId = rule.event.split(' -> ')[0]; db.eventPollers.storeUserParams(epModId, user.username, JSON.stringify(oPayload.event_params)); } + oFuncArgs = oPayload.event_functions; + for (id in oFuncArgs) { + args = oFuncArgs[id]; + arr = id.split(' -> '); + db.eventPollers.storeUserArguments(user.username, rule.id, arr[0], arr[1], JSON.stringify(args)); + } oParams = oPayload.action_params; for (id in oParams) { params = oParams[id]; @@ -379,6 +385,20 @@ Components Manager get_event_poller_user_arguments: function(user, oPayload, callback) { return getModuleUserArguments(user, oPayload, db.eventPollers, callback); }, + get_event_poller_function_arguments: function(user, oPayload, callback) { + var answ; + answ = hasRequiredParams(['id'], oPayload); + if (answ.code !== 200) { + return callback(answ); + } else { + return db.eventPollers.getModuleField(oPayload.id, 'functionArgs', function(err, obj) { + return callback({ + code: 200, + message: obj + }); + }); + } + }, forge_event_poller: function(user, oPayload, callback) { return forgeModule(user, oPayload, db.eventPollers, callback); }, diff --git a/js/event-poller.js b/js/event-poller.js index 5aef363..653feed 100644 --- a/js/event-poller.js +++ b/js/event-poller.js @@ -9,7 +9,7 @@ Dynamic Modules */ (function() { - var db, dynmod, encryption, fCallFunction, fCheckAndRun, fLoadModule, isRunning, listUserModules, log, logconf, logger, pollLoop; + var db, dynmod, encryption, fCallFunction, fCheckAndRun, fLoadModule, fPushEvent, isRunning, listUserModules, log, logconf, logger, pollLoop; logger = require('./logging'); @@ -84,24 +84,27 @@ Dynamic Modules return log.warn("EP | Strange... no module retrieved: " + arrName[0]); } else { return dynmod.compileString(obj.data, msg.user, msg.rule.id, arrName[0], obj.lang, db.eventPollers, function(result) { - var ts; + var oUser, ts; if (!result.answ === 200) { log.error("EP | Compilation of code failed! " + msg.user + ", " + msg.rule.id + ", " + arrName[0]); } if (!listUserModules[msg.user]) { listUserModules[msg.user] = {}; } + oUser = listUserModules[msg.user]; ts = new Date(); - listUserModules[msg.user][msg.rule.id] = { + oUser[msg.rule.id] = { id: msg.rule.event, pollfunc: arrName[1], + funcArgs: result.funcArgs, event_interval: msg.rule.event_interval * 60 * 1000, module: result.module, logger: result.logger, timestamp: ts }; + oUser[msg.rule.id].module.pushEvent = fPushEvent(msg.user, msg.rule.id, oUser[msg.rule.id]); log.info("EP | New event module '" + arrName[0] + "' loaded for user " + msg.user + ", in rule " + msg.rule.id + ", starting at " + (ts.toISOString()) + " and polling every " + msg.rule.event_interval + " minutes"); - return fCheckAndRun(msg.user, msg.rule.id, ts)(); + return setTimeout(fCheckAndRun(msg.user, msg.rule.id, ts), 1000); }); } }); @@ -111,6 +114,16 @@ Dynamic Modules } }; + fPushEvent = function(userId, ruleId, oRule) { + return function(obj) { + return db.pushEvent({ + event: oRule.id, + eventid: "polled " + oRule.id + " " + userId + "_" + ((new Date()).toISOString()), + payload: obj + }); + }; + }; + fCheckAndRun = function(userId, ruleId, timestamp) { return function() { var oRule; @@ -128,15 +141,17 @@ Dynamic Modules }; fCallFunction = function(userId, ruleId, oRule) { - var err; + var arrArgs, err, oArg, _i, _len, _ref; try { - return oRule.module[oRule.pollfunc](function(obj) { - return db.pushEvent({ - event: oRule.id, - eventid: "polled " + oRule.id + " " + userId + "_" + ((new Date()).toISOString()), - payload: obj - }); - }); + arrArgs = []; + if (oRule.funcArgs) { + _ref = oRule.funcArgs[oRule.pollfunc]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + oArg = _ref[_i]; + arrArgs.push(oArg.value); + } + } + return oRule.module[oRule.pollfunc].apply(null, arrArgs); } catch (_error) { err = _error; log.info("EP | ERROR in module when polled: " + oRule.id + " " + userId + ": " + err.message); diff --git a/webpages/handlers/coffee/forge_rule.coffee b/webpages/handlers/coffee/forge_rule.coffee index 31f58c2..6d0eb93 100644 --- a/webpages/handlers/coffee/forge_rule.coffee +++ b/webpages/handlers/coffee/forge_rule.coffee @@ -89,15 +89,41 @@ fOnLoad = () -> $.post( '/usercommand', obj ) .done fAddEventParams arr[ 0 ] .fail fFailedRequest 'Error fetching event poller params' + fFetchEventFunctionArgs arr + + fFetchEventFunctionArgs = ( arrName ) -> + obj = + command: 'get_event_poller_function_arguments' + payload: JSON.stringify + id: arrName[ 0 ] + $.post( '/usercommand', obj ) + .done ( data ) -> + if data.message + oParams = JSON.parse data.message + if oParams[ arrName[ 1 ] ] + if oParams[ arrName[ 1 ] ].length > 0 + $( '#event_poller_params' ).append $( "" ).text 'Required Function Parameters:' + table = $( '' ).appendTo $( '#event_poller_params' ) + for functionArgument in oParams[ arrName[ 1 ] ] + tr = $( '' ).attr( 'class', 'funcMappings' ).appendTo table + tr.append $( '
' ).css 'width', '20px' + td = $( '' ).appendTo tr + td.append $( '
' ).attr( 'class', 'funcarg' ).text functionArgument + tr.append td + tr.append $( '
' ).text ' : ' + td = $( '' ).appendTo tr + td.append $( '' ).attr 'type', 'text' + tr.append td + .fail fFailedRequest 'Error fetching action invoker function params' fAddEventParams = ( id ) -> ( data ) -> if data.message oParams = JSON.parse data.message - $( '#event_poller_params' ).html '
Required Parameters:' table = $ '' - $( '#event_poller_params' ).append table + i = 0 fAppendParam = ( name, shielded ) -> + i++ tr = $( '' ) tr.append $( '
' ).css 'width', '20px' tr.append $( '' ).attr( 'class', 'key' ).text name @@ -107,6 +133,10 @@ fOnLoad = () -> tr.append $( '' ).text( ' : ' ).append inp table.append tr fAppendParam name, shielded for name, shielded of oParams + if i > 0 + $( '#event_poller_params' ).html 'Required Global Parameters:' + $( '#event_poller_params' ).append table + fFillEventParams id fFillEventParams = ( moduleId ) -> @@ -125,6 +155,23 @@ fOnLoad = () -> $( 'input', par ).change () -> $( this ).attr 'unchanged', 'false' + obj.command = 'get_event_poller_user_arguments' + obj.payload = JSON.stringify + ruleId: $( '#input_id' ).val() + moduleId: moduleId + $.post( '/usercommand', obj ) + .done fAddEventUserArgs moduleId + + fAddEventUserArgs = ( name ) -> + ( data ) -> + for key, arrFuncs of data.message + par = $ "#event_poller_params" + for oFunc in JSON.parse arrFuncs + tr = $( "tr", par ).filter () -> + $( '.funcarg', this ).text() is "#{ oFunc.argument }" + $( "input[type=text]", tr ).val oFunc.value + $( "input[type=checkbox]", tr ).prop 'checked', oFunc.jsselector + # ACTIONS obj = command: 'get_action_invokers' @@ -213,7 +260,6 @@ fOnLoad = () -> td = $( '' ).appendTo tr td.append $( '' ).attr 'type', 'text' tr.append td - tr.append td td = $( '' ).appendTo tr td.append $( '' ).attr( 'type', 'checkbox' ) .attr 'title', 'js-select expression to be resolved on event?' @@ -292,7 +338,8 @@ fOnLoad = () -> $( '#input_id' ).focus() throw new Error 'Please enter a rule name!' - if $( '#input_event' ).val() is '' + eventId = $( '#input_event' ).val() + if eventId is '' $( '#input_event' ).focus() throw new Error 'Please assign an event!' @@ -312,6 +359,13 @@ fOnLoad = () -> else ep[ key ].value = val + evtFuncs = {} + evtFuncs[ eventId ] = [] + $( '#event_poller_params tr.funcMappings' ).each () -> + evtFuncs[ eventId ].push + argument: $( 'div.funcarg', this ).text() + value: $( 'input[type=text]', this ).val() + if $( '#selected_actions tr' ).length is 0 throw new Error 'Please select at least one action or create one!' @@ -337,10 +391,10 @@ fOnLoad = () -> params[ key ].value = val ap[ modName ] = params acts = [] - actParams = {} + actFuncs = {} $( '#selected_actions td.title' ).each () -> actionName = $( this ).text() - actParams[ actionName ] = [] + actFuncs[ actionName ] = [] acts.push actionName par = $( this ).parent() $( '.funcMappings tr', par ).each () -> @@ -350,8 +404,8 @@ fOnLoad = () -> # value: $( 'input[type=text]', this ).val() # regexp: $( 'input[type=checkbox]', this ).is( ':checked' ) # tmp = cryptico.encrypt JSON.stringify( tmp ), strPublicKey - # actParams[ actionName ] = tmp.cipher - actParams[ actionName ].push + # actFuncs[ actionName ] = tmp.cipher + actFuncs[ actionName ].push argument: $( 'div.funcarg', this ).text() value: $( 'input[type=text]', this ).val() jsselector: $( 'input[type=checkbox]', this ).is( ':checked' ) @@ -419,13 +473,14 @@ fOnLoad = () -> command: 'forge_rule' payload: JSON.stringify id: $( '#input_id' ).val() - event: $( '#input_event' ).val() + event: eventId event_params: ep event_interval: mins + event_functions: evtFuncs conditions: conds actions: acts action_params: ap - action_functions: actParams + action_functions: actFuncs $.post( '/usercommand', obj ) .done ( data ) -> $( '#info' ).text data.message diff --git a/webpages/handlers/js/forge_rule.js b/webpages/handlers/js/forge_rule.js index 17798eb..f55b2d1 100644 --- a/webpages/handlers/js/forge_rule.js +++ b/webpages/handlers/js/forge_rule.js @@ -48,7 +48,7 @@ }); fOnLoad = function() { - var editor, fAddActionUserArgs, fAddActionUserParams, fAddEventParams, fAddSelectedAction, fFetchActionFunctionArgs, fFetchActionParams, fFetchEventParams, fFillActionFunction, fFillEventParams, obj; + var editor, fAddActionUserArgs, fAddActionUserParams, fAddEventParams, fAddEventUserArgs, fAddSelectedAction, fFetchActionFunctionArgs, fFetchActionParams, fFetchEventFunctionArgs, fFetchEventParams, fFillActionFunction, fFillEventParams, obj; document.title = 'Rule Forge!'; $('#pagetitle').text('{{{user.username}}}, forge your ECA Rule!'); editor = ace.edit("editor_conditions"); @@ -117,19 +117,56 @@ id: arr[0] }) }; - return $.post('/usercommand', obj).done(fAddEventParams(arr[0])).fail(fFailedRequest('Error fetching event poller params')); + $.post('/usercommand', obj).done(fAddEventParams(arr[0])).fail(fFailedRequest('Error fetching event poller params')); + return fFetchEventFunctionArgs(arr); } }; + fFetchEventFunctionArgs = function(arrName) { + var obj; + obj = { + command: 'get_event_poller_function_arguments', + payload: JSON.stringify({ + id: arrName[0] + }) + }; + return $.post('/usercommand', obj).done(function(data) { + var functionArgument, table, td, tr, _j, _len1, _ref, _results; + if (data.message) { + oParams = JSON.parse(data.message); + if (oParams[arrName[1]]) { + if (oParams[arrName[1]].length > 0) { + $('#event_poller_params').append($("").text('Required Function Parameters:')); + } + table = $('').appendTo($('#event_poller_params')); + _ref = oParams[arrName[1]]; + _results = []; + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + functionArgument = _ref[_j]; + tr = $('').attr('class', 'funcMappings').appendTo(table); + tr.append($('
').css('width', '20px')); + td = $('').appendTo(tr); + td.append($('
').attr('class', 'funcarg').text(functionArgument)); + tr.append(td); + tr.append($('
').text(' : ')); + td = $('').appendTo(tr); + td.append($('').attr('type', 'text')); + _results.push(tr.append(td)); + } + return _results; + } + } + }).fail(fFailedRequest('Error fetching action invoker function params')); + }; fAddEventParams = function(id) { return function(data) { - var fAppendParam, name, shielded, table; + var fAppendParam, i, name, shielded, table; if (data.message) { oParams = JSON.parse(data.message); - $('#event_poller_params').html('
Required Parameters:'); table = $(''); - $('#event_poller_params').append(table); + i = 0; fAppendParam = function(name, shielded) { var inp, tr; + i++; tr = $(''); tr.append($('
').css('width', '20px')); tr.append($('').attr('class', 'key').text(name)); @@ -144,6 +181,10 @@ shielded = oParams[name]; fAppendParam(name, shielded); } + if (i > 0) { + $('#event_poller_params').html('Required Global Parameters:'); + $('#event_poller_params').append(table); + } return fFillEventParams(id); } }; @@ -156,7 +197,7 @@ id: moduleId }) }; - return $.post('/usercommand', obj).done(function(data) { + $.post('/usercommand', obj).done(function(data) { var oParam, par, _results; oParams = JSON.parse(data.message); _results = []; @@ -173,6 +214,38 @@ } return _results; }); + obj.command = 'get_event_poller_user_arguments'; + obj.payload = JSON.stringify({ + ruleId: $('#input_id').val(), + moduleId: moduleId + }); + return $.post('/usercommand', obj).done(fAddEventUserArgs(moduleId)); + }; + fAddEventUserArgs = function(name) { + return function(data) { + var arrFuncs, key, oFunc, par, tr, _ref, _results; + _ref = data.message; + _results = []; + for (key in _ref) { + arrFuncs = _ref[key]; + par = $("#event_poller_params"); + _results.push((function() { + var _j, _len1, _ref1, _results1; + _ref1 = JSON.parse(arrFuncs); + _results1 = []; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + oFunc = _ref1[_j]; + tr = $("tr", par).filter(function() { + return $('.funcarg', this).text() === ("" + oFunc.argument); + }); + $("input[type=text]", tr).val(oFunc.value); + _results1.push($("input[type=checkbox]", tr).prop('checked', oFunc.jsselector)); + } + return _results1; + })()); + } + return _results; + }; }; obj = { command: 'get_action_invokers' @@ -295,7 +368,6 @@ td = $('').appendTo(tr); td.append($('').attr('type', 'text')); tr.append(td); - tr.append(td); td = $('').appendTo(tr); _results.push(td.append($('').attr('type', 'checkbox')).attr('title', 'js-select expression to be resolved on event?')); } @@ -398,7 +470,7 @@ return $(this).closest('tr').remove(); }); $('#but_submit').click(function() { - var actParams, acts, ap, arrInp, conds, d, ep, err, fCheckOverwrite, fParseTime, mins, txtInterval; + var actFuncs, acts, ap, arrInp, conds, d, ep, err, eventId, evtFuncs, fCheckOverwrite, fParseTime, mins, txtInterval; window.scrollTo(0, 0); $('#info').text(''); try { @@ -406,7 +478,8 @@ $('#input_id').focus(); throw new Error('Please enter a rule name!'); } - if ($('#input_event').val() === '') { + eventId = $('#input_event').val(); + if (eventId === '') { $('#input_event').focus(); throw new Error('Please assign an event!'); } @@ -430,6 +503,14 @@ return ep[key].value = val; } }); + evtFuncs = {}; + evtFuncs[eventId] = []; + $('#event_poller_params tr.funcMappings').each(function() { + return evtFuncs[eventId].push({ + argument: $('div.funcarg', this).text(), + value: $('input[type=text]', this).val() + }); + }); if ($('#selected_actions tr').length === 0) { throw new Error('Please select at least one action or create one!'); } @@ -460,15 +541,15 @@ return ap[modName] = params; }); acts = []; - actParams = {}; + actFuncs = {}; $('#selected_actions td.title').each(function() { var actionName, par; actionName = $(this).text(); - actParams[actionName] = []; + actFuncs[actionName] = []; acts.push(actionName); par = $(this).parent(); return $('.funcMappings tr', par).each(function() { - return actParams[actionName].push({ + return actFuncs[actionName].push({ argument: $('div.funcarg', this).text(), value: $('input[type=text]', this).val(), jsselector: $('input[type=checkbox]', this).is(':checked') @@ -543,13 +624,14 @@ command: 'forge_rule', payload: JSON.stringify({ id: $('#input_id').val(), - event: $('#input_event').val(), + event: eventId, event_params: ep, event_interval: mins, + event_functions: evtFuncs, conditions: conds, actions: acts, action_params: ap, - action_functions: actParams + action_functions: actFuncs }) }; return $.post('/usercommand', obj).done(function(data) { diff --git a/webpages/handlers/templates/forge_rule.html b/webpages/handlers/templates/forge_rule.html index bee63b7..466e86c 100644 --- a/webpages/handlers/templates/forge_rule.html +++ b/webpages/handlers/templates/forge_rule.html @@ -2,7 +2,7 @@

EVENT



-
+


CONDITIONS