Commit 458b0297 authored by Ivan Tyagov's avatar Ivan Tyagov

Initial import.

parents
#vi
*.swp
*~
<html>
<head>
<!-- Use proper path -->
<script type="text/javascript" src="../../../../lib/require/require.js"> </script>
<script type="text/javascript" src="../../../../lib/jquery/jquery.js"> </script>
<script type="text/javascript" src="../../renderjs.js"></script>
<head>
<body>
<div id="say-hello"
data-gadget="say-hello.html"
data-gadget-cacheable="0"
data-gadget-cache-id="say-hello"></div>
</body>
</html>
\ No newline at end of file
Hello from the gadget!
\ No newline at end of file
<html>
<head>
<script type="text/javascript" src="../../../../lib/require/require.js"> </script>
<script type="text/javascript" src="../../../../lib/jquery/jquery.js"> </script>
<script type="text/javascript" src="../../renderjs.js"></script>
<head>
<body>
<div id="init-gadget"
data-gadget="init-gadget.html"
data-gadget-cacheable="0"
data-gadget-cache-id="init-gadget"
data-gadget-property="{&quot;name&quot;: &quot;Ivan&quot;}"></div>
</body>
</html>
\ No newline at end of file
<p>Hello to <span id="name"></span> from the gadget which can be initialized via data-gadget-property attribute from parent gadget!</p>
<script type="text/javascript" language="javascript">
//<![CDATA[
name = RenderJs.GadgetIndex.getGadgetById("init-gadget").name;
$("#name").html(name);
//]]>
</script>
\ No newline at end of file
<div>
A (gadget)
<button onclick="RenderJs.GadgetIndex.getGadgetById('A').jsCall1()">A.jsCall1 -> B.jsCall1</button>
<button onclick="$('#A').trigger('htmlEvent1')">HTML event 1</button>
<button onclick="$('#A').trigger('htmlEvent2')">HTML event 2</button>
<p id="hide" style="display:none;"> hidden text on myownHTMlEvent1</p>
</div>
<script type="text/javascript" language="javascript">
//<![CDATA[
$(document).ready(function() {
gadget = RenderJs.GadgetIndex.getGadgetById("A");
gadget.jsCall1 = function (){alert("A.jscall1");};
gadget.jsCall2 = function (){alert("A.jscall2");};
gadget.myOwnHtmlEvent1 = function (){
$("#hide").toggle();
alert("A.myOwnHtmlEvent1");
};
});
//]]>
</script>
\ No newline at end of file
<div>
B (gadget)
<button onclick="RenderJs.GadgetIndex.getGadgetById('B').jsCall2();">B.jsCall2 -> A.jsCall2</button>
</div>
<script type="text/javascript" language="javascript">
//<![CDATA[
$(document).ready(function() {
gadget = RenderJs.GadgetIndex.getGadgetById("B");
gadget.jsCall1 = function (){alert("B.jscall1");};
gadget.jsCall2 = function (){alert("B.jscall2");};
gadget.htmlEvent1 = function (){alert("B.htmlEvent1");};
gadget.htmlEvent2 = function (){alert("B.htmlEvent2");};
});
//]]>
</script>
\ No newline at end of file
<html>
<head>
<script type="text/javascript" src="../../../../lib/require/require.js"> </script>
<script type="text/javascript" src="../../../../lib/jquery/jquery.js"> </script>
<script type="text/javascript" src="../../renderjs.js"></script>
<head>
<body>
<div id="A"
data-gadget="A.html"
data-gadget-cacheable="0"
data-gadget-cache-id="A"></div>
<div id="B"
data-gadget="B.html"
data-gadget-cacheable="0"
data-gadget-cache-id="B"></div>
<div data-gadget=""
id="main-interactor"
data-gadget-connection="[
{&quot;source&quot;: &quot;A.jsCall1&quot;, &quot;destination&quot;: &quot;B.jsCall1&quot;},
{&quot;source&quot;: &quot;A.htmlEvent1&quot;, &quot;destination&quot;: &quot;B.htmlEvent1&quot;},
{&quot;source&quot;: &quot;A.htmlEvent1&quot;, &quot;destination&quot;: &quot;A.myOwnHtmlEvent1&quot;},
{&quot;source&quot;: &quot;A.htmlEvent2&quot;, &quot;destination&quot;: &quot;B.htmlEvent2&quot;},
{&quot;source&quot;: &quot;B.jsCall2&quot;, &quot;destination&quot;: &quot;A.jsCall2&quot;}]">
</div>
<script type="text/javascript" language="javascript">
//<![CDATA[
$(document).ready(function() {
RenderJs.GadgetIndex.getRootGadget().getDom().bind("ready", function () {
RenderJs.InteractionGadget.bind($("#main-interactor"));});
});
//]]>
</script>
</body>
</html>
<html>
<head>
<script type="text/javascript" src="../../../../lib/require/require.js"> </script>
<script type="text/javascript" src="../../../../lib/jquery/jquery.js"> </script>
<script type="text/javascript" src="../../renderjs.js"></script>
<head>
<body>
<div id="recursive"
data-gadget="recursive.html"
data-gadget-cacheable="0"
data-gadget-cache-id="recursive"></div>
</body>
</html>
\ No newline at end of file
<p>
Hello from the recursive contained gadget!
</p>
<p>
Hello from the recursive gadget!
Below a new gadget will be loaded
</p>
<div data-gadget="recursive-contained.html"
data-gadget-cacheable="0"
data-gadget-cache-id="recursive-contained"></div>
\ No newline at end of file
/*global console, require, $, localStorage, document */
"use strict";
/*
* RenderJs - Generic Gadget library renderer.
* http://www.renderjs.org/documentation
*/
// by default RenderJs will render all gadgets when page is loaded
// still it's possible to override this and use explicit gadget rendering
var RENDERJS_ENABLE_IMPLICIT_GADGET_RENDERING = true;
if (typeof require !== 'undefined') {
// example of how we can use requirejs to load external libraries
//require(["../../../../lib/jstorage/jstorage.js"], function (util) {
//});
}
// fallback for IE
if (typeof console === "undefined" || typeof console.log === "undefined") {
console = {};
console.log = function () {};
}
var RenderJs = (function () {
// a variable indicating if current gadget loading is over or not
var is_ready = false;
return {
bootstrap: function (root) {
/* initial load application gadget */
var gadget_id;
gadget_id = root.attr("id");
if (gadget_id!==undefined) {
// bootstart root gadget only if it is indeed a gadget
RenderJs.loadGadgetFromUrl(root);
}
RenderJs.load(root);
},
load: function (root) {
/* Load gadget layout by traversing DOM */
var gadget_list;
gadget_list = root.find("[data-gadget]");
// Load chilren
gadget_list.each(function () {
RenderJs.loadGadgetFromUrl($(this));
});
},
updateAndRecurse: function (gadget, data) {
/* Update current gadget and recurse down */
gadget.append(data);
// a gadget may contain sub gadgets
this.load(gadget);
},
loadGadgetFromUrl: function (gadget) {
/* Load gadget's SPECs from URL */
var url, gadget_id, gadget_property, cacheable, cache_id,
app_cache, data, gadget_js;
url = gadget.attr("data-gadget");
gadget_id = gadget.attr("id");
// register gadget in javascript namespace
gadget_js = new RenderJs.Gadget(gadget_id, gadget);
RenderJs.GadgetIndex.registerGadget(gadget_js);
// update Gadget's instance with contents of "data-gadget-property"
gadget_property = gadget.attr("data-gadget-property");
if (gadget_property !== undefined) {
gadget_property = $.parseJSON(gadget_property);
$.each(gadget_property, function (key, value) {
gadget_js[key] = value;
});
}
if (url !== undefined && url !== "") {
cacheable = gadget.attr("data-gadget-cacheable");
cache_id = gadget.attr("data-gadget-cache-id");
if (cacheable !== undefined && cache_id !== undefined) {
cacheable = Boolean(parseInt(cacheable, 10));
}
//cacheable = false ; // to develop faster
if (cacheable) {
// get from cache if possible, use last part from URL as
// cache_key
app_cache = RenderJs.Cache.get(cache_id, undefined);
if (app_cache === undefined || app_cache === null) {
// not in cache so we pull from network and cache
$.ajax({
url: url,
yourCustomData: {
"gadget_id": gadget_id,
"cache_id": cache_id
},
success: function (data) {
cache_id = this.yourCustomData.cache_id;
gadget_id = this.yourCustomData.gadget_id;
RenderJs.Cache.set(cache_id, data);
RenderJs.GadgetIndex.getGadgetById(gadget_id).
setReady();
RenderJs.updateAndRecurse(gadget, data);
RenderJs.checkAndTriggerReady();
}
});
} else {
// get from cache
data = app_cache;
gadget_js.setReady();
this.updateAndRecurse(gadget, data);
this.checkAndTriggerReady();
}
} else {
// not to be cached
$.ajax({
url: url,
yourCustomData: {"gadget_id": gadget_id},
success: function (data) {
gadget_id = this.yourCustomData.gadget_id;
RenderJs.GadgetIndex.getGadgetById(gadget_id).
setReady();
RenderJs.updateAndRecurse(gadget, data);
RenderJs.checkAndTriggerReady();
}
});
}
} else {
// gadget is an inline one so no need to load it from network
gadget_js.setReady();
RenderJs.checkAndTriggerReady();
}
},
isReady: function (value) {
/*
* Get rendering status
*/
return is_ready;
},
setReady: function (value) {
/*
* Update rendering status
*/
is_ready = value;
},
checkAndTriggerReady: function () {
/*
* Trigger "ready" event only if all gadgets were marked as "ready"
*/
var is_gadget_list_loaded;
is_gadget_list_loaded = RenderJs.GadgetIndex.isGadgetListLoaded();
if (is_gadget_list_loaded) {
if (!RenderJs.isReady()) {
RenderJs.GadgetIndex.getRootGadget().getDom().
trigger("ready");
RenderJs.setReady(true);
}
}
return is_gadget_list_loaded;
},
update: function (root) {
/* update gadget with data from remote source */
root.find("[gadget]").each(function (i, v) {
RenderJs.updateGadgetData($(this));
});
},
updateGadgetWithDataHandler: function (result) {
var data_handler;
data_handler = this.yourCustomData.data_handler;
if (data_handler !== undefined) {
eval(data_handler + "(result)");
}
},
updateGadgetData: function (gadget) {
/* Do real gagdet update here */
var data_source, data_handler;
data_source = gadget.attr("data-gadget-source");
data_handler = gadget.attr("data-gadget-handler");
// acquire data and pass it to method handler
if (data_source !== undefined && data_source !== "") {
$.ajax({
url: data_source,
dataType: "json",
yourCustomData: {"data_handler": data_handler},
success: RenderJs.updateGadgetWithDataHandler
});
}
},
addGadget: function (dom_id, gadget, gadget_data_handler,
gadget_data_source) {
/*
* add new gadget and render it
*/
var html_string, tab_container, tab_gadget;
tab_container = $('#' + dom_id);
tab_container.empty();
html_string = [
'<div class="gadget" ',
'data-gadget="' + gadget + '"',
'data-gadget-handler="' + gadget_data_handler + '" ',
'data-gadget-source="' + gadget_data_source + '"></div>'
].join('\n');
tab_container.append(html_string);
tab_gadget = tab_container.find(".gadget");
// render new gadget
RenderJs.setReady(false);
RenderJs.loadGadgetFromUrl(tab_gadget);
// clear previous events
RenderJs.GadgetIndex.getRootGadget().getDom().bind(
"ready",
function () {
if (!is_ready) {
RenderJs.updateGadgetData(tab_gadget);
is_ready = true;
}
}
);
return tab_gadget;
},
Cache: (function () {
/*
* Generic cache implementation that can fall back to local
* namespace storage if no "modern" storage like localStorage
* is available
*/
return {
ROOT_CACHE_ID: 'APP_CACHE',
getCacheId: function (cache_id) {
/*
* We should have a way to 'purge' localStorage by setting a
* ROOT_CACHE_ID in all browser instances
*/
return this.ROOT_CACHE_ID + cache_id;
},
hasLocalStorage: function () {
/*
* Feature test if localStorage is supported
*/
var mod;
mod = 'localstorage_test_12345678';
try {
localStorage.setItem(mod, mod);
localStorage.removeItem(mod);
return true;
} catch (e) {
return false;
}
},
get: function (cache_id, default_value) {
/* Get cache key value */
cache_id = this.getCacheId(cache_id);
if (this.hasLocalStorage()) {
return this.LocalStorageCachePlugin.
get(cache_id, default_value);
}
//fallback to javscript namespace cache
return this.NameSpaceStorageCachePlugin.
get(cache_id, default_value);
},
set: function (cache_id, data) {
/* Set cache key value */
cache_id = this.getCacheId(cache_id);
if (this.hasLocalStorage()) {
this.LocalStorageCachePlugin.set(cache_id, data);
} else {
this.NameSpaceStorageCachePlugin.set(cache_id, data);
}
},
LocalStorageCachePlugin: (function () {
/*
* This plugin saves using HTML5 localStorage.
*/
return {
get: function (cache_id, default_value) {
/* Get cache key value */
if (cache_id in localStorage) {
return JSON.parse(localStorage.getItem(cache_id));
}
else {
return default_value;
}
},
set: function (cache_id, data) {
/* Set cache key value */
localStorage.setItem(cache_id, JSON.stringify(data));
}
};
}()),
NameSpaceStorageCachePlugin: (function () {
/*
* This plugin saves within current page namespace.
*/
var namespace = {};
return {
get: function (cache_id, default_value) {
/* Get cache key value */
return namespace[cache_id];
},
set: function (cache_id, data) {
/* Set cache key value */
namespace[cache_id] = data;
}
};
}())
};
}()),
Gadget: (function (gadget_id, dom) {
/*
* Javascript Gadget representation
*/
this.id = gadget_id;
this.dom = dom;
this.is_ready = false;
this.getId = function () {
return this.id;
};
this.getDom = function () {
return this.dom;
};
this.isReady = function () {
/*
* Return True if remote gadget is loaded into DOM.
*/
return this.is_ready;
};
this.setReady = function () {
/*
* Return True if remote gadget is loaded into DOM.
*/
this.is_ready = true;
};
}),
TabbularGadget: (function () {
/*
* Generic tabular gadget
*/
return {
toggleVisibility: function (visible_dom) {
/*
* Set tab as active visually and mark as not active rest.
*/
$(".selected").addClass("not_selected");
$(".selected").removeClass("selected");
visible_dom.addClass("selected");
visible_dom.removeClass("not_selected");
},
addNewTabGadget: function (dom_id, gadget, gadget_data_handler,
gadget_data_source) {
/*
* add new gadget and render it
*/
var tab_gadget;
tab_gadget = RenderJs.addGadget(
dom_id, gadget, gadget_data_handler, gadget_data_source
);
// XXX: we should unregister all gadgets (if any we replace now in DOM)
}
};
}()),
GadgetIndex: (function () {
/*
* Generic gadget index placeholder
*/
var gadget_list = [];
return {
getGadgetList: function () {
/*
* Return list of registered gadgets
*/
return gadget_list;
},
registerGadget: function (gadget) {
/*
* Register gadget
*/
gadget_list.push(gadget);
},
unregisterGadget: function (gadget) {
/*
* Unregister gadget
*/
var index = $.inArray(gadget, gadget_list);
if (index !== -1) {
gadget_list.splice(index, 1);
}
},
getGadgetById: function (gadget_id) {
/*
* Get gadget javascript representation by its Id
*/
var gadget;
gadget = undefined;
$(RenderJs.GadgetIndex.getGadgetList()).each(
function (index, value) {
if (value.getId() === gadget_id) {
gadget = value;
}
}
);
return gadget;
},
getRootGadget: function () {
/*
* Return root gadget (always first one in list)
*/
return this.getGadgetList()[0];
},
isGadgetListLoaded: function () {
/*
* Return True if all gadgets were loaded from network or
* cache
*/
var result;
result = true;
$(this.getGadgetList()).each(
function (index, value) {
if (value.isReady() === false) {
result = false;
}
}
);
return result;
}
};
}()),
InteractionGadget : (function () {
/*
* Basic gadget interaction gadget implementation.
*/
return {
bind: function (gadget_dom) {
/*
* Bind event between gadgets.
*/
var gadget_id, gadget_connection_list,
createMethodInteraction = function (
original_source_method_id, source_gadget_id,
source_method_id, destination_gadget_id,
destination_method_id) {
var interaction = function () {
RenderJs.GadgetIndex.getGadgetById(
source_gadget_id)[original_source_method_id].
apply(null, arguments);
RenderJs.GadgetIndex.getGadgetById(
destination_gadget_id)[destination_method_id]();
};
return interaction;
},
createTriggerInteraction = function (
destination_gadget_id, destination_method_id) {
var interaction = function () {
RenderJs.GadgetIndex.getGadgetById(
destination_gadget_id)[destination_method_id].
apply(null, arguments);
};
return interaction;
};
gadget_id = gadget_dom.attr("id");
gadget_connection_list = gadget_dom.attr("data-gadget-connection");
gadget_connection_list = $.parseJSON(gadget_connection_list);
$.each(gadget_connection_list, function (key, value) {
var source, source_gadget_id, source_method_id,
source_gadget, destination, destination_gadget_id,
destination_method_id, destination_gadget,
original_source_method_id;
source = value.source.split(".");
source_gadget_id = source[0];
source_method_id = source[1];
source_gadget = RenderJs.GadgetIndex.
getGadgetById(source_gadget_id);
destination = value.destination.split(".");
destination_gadget_id = destination[0];
destination_method_id = destination[1];
destination_gadget = RenderJs.GadgetIndex.
getGadgetById(destination_gadget_id);
if (source_gadget.hasOwnProperty(source_method_id)) {
// direct javascript use case
original_source_method_id = "original_" +
source_method_id;
source_gadget[original_source_method_id] =
source_gadget[source_method_id];
source_gadget[source_method_id] =
createMethodInteraction(
original_source_method_id,
source_gadget_id,
source_method_id,
destination_gadget_id,
destination_method_id
);
}
else {
// this is a custom event attached to HTML gadget
// representation
source_gadget.dom.bind(
source_method_id,
createTriggerInteraction(
destination_gadget_id, destination_method_id
)
);
}
});
}
};
}())
};
}());
// impliticly call RenderJs bootstrap
$(document).ready(function () {
if (RENDERJS_ENABLE_IMPLICIT_GADGET_RENDERING) {
RenderJs.bootstrap($('body'));
}
});
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<link rel="stylesheet" href="../../../lib/qunit/qunit.css" type="text/css"/>
<script src="../../../lib/jquery/jquery.js"> </script>
<script src="../../../lib/qunit/qunit.js" type="text/javascript"></script>
<script type="text/javascript" src="../renderjs.js"></script>
<script type="text/javascript" src="../../../lib/jstorage/jstorage.js"></script>
<script type="text/javascript" src="renderjs_test.js"></script>
</head>
<body>
<h1 id="qunit-header">QUnit RenderJS test suite</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture"> </div>
<script type="text/javascript">
//<![CDATA[
$(document).ready(setupRenderJSTest());
//]]>
</script>
</body>
</html>
\ No newline at end of file
<div>A (gadget)</div>
<script type="text/javascript" language="javascript">
//<![CDATA[
$(document).ready(function() {
gadget = RenderJs.GadgetIndex.getGadgetById("A");
gadget.inc = function (){counter = counter +1;};
});
//]]>
</script>
\ No newline at end of file
<div> B (gadget) </div>
<script type="text/javascript" language="javascript">
//<![CDATA[
$(document).ready(function() {
gadget = RenderJs.GadgetIndex.getGadgetById("B");
gadget.inc = function (){counter = counter + 1};
gadget.htmlEvent1 = function (){counter = counter + 1;};
});
//]]>
</script>
\ No newline at end of file
<div id="A"
data-gadget="interactions/A.html"
data-gadget-cacheable="0"
data-gadget-cache-id="A"></div>
<div id="B"
data-gadget="interactions/B.html"
data-gadget-cacheable="0"
data-gadget-cache-id="B"></div>
<div data-gadget=""
id="main-interactor"
data-gadget-connection="[
{&quot;source&quot;: &quot;A.inc&quot;, &quot;destination&quot;: &quot;B.inc&quot;},
{&quot;source&quot;: &quot;A.htmlEvent1&quot;, &quot;destination&quot;: &quot;B.htmlEvent1&quot;}]"></div>
test!!!
\ No newline at end of file
/*
* RenderJs tests
*/
counter = 0;
function cleanUp () {
/*
* Clean up namespace between tests
*/
// re-init GadgetIndex
$.each(RenderJs.GadgetIndex.getGadgetList(), function () {
RenderJs.GadgetIndex.unregisterGadget(this);
});
}
function setupRenderJSTest(){
/*
* Main RenderJS test entry point
*/
module("Cache");
test('Cache', function () {
cache_id = 'my_test';
data = {'gg':1};
RenderJs.Cache.set(cache_id, data);
deepEqual(data, RenderJs.Cache.get(cache_id));
// test return default value if key is missing works
equal("no such key", RenderJs.Cache.get("no_such_key", "no such key"));
});
module("GadgetIndex");
test('GadgetIndex', function () {
cleanUp();
$("#qunit-fixture").append('<div data-gadget="" id="new">X</div>');
RenderJs.bootstrap($("#qunit-fixture"));
RenderJs.GadgetIndex.getRootGadget().getDom().one("ready", function (){
RenderJs.update($("#qunit-fixture"));
});
equal(RenderJs.GadgetIndex.getGadgetList().length, 2);
equal(true, RenderJs.GadgetIndex.isGadgetListLoaded());
equal($("#qunit-fixture").attr("id"), RenderJs.GadgetIndex.getRootGadget().getDom().attr("id"));
equal(RenderJs.GadgetIndex.getGadgetById("qunit-fixture"), RenderJs.GadgetIndex.getRootGadget());
// unregister gadget all gadgets from this test not to mess with rest of tests
RenderJs.GadgetIndex.unregisterGadget(RenderJs.GadgetIndex.getGadgetById("qunit-fixture"));
equal(RenderJs.GadgetIndex.getGadgetList().length, 1);
equal(RenderJs.GadgetIndex.getGadgetById("new"), RenderJs.GadgetIndex.getRootGadget());
RenderJs.GadgetIndex.unregisterGadget(RenderJs.GadgetIndex.getGadgetById("new"));
equal(RenderJs.GadgetIndex.getGadgetList().length, 0);
});
module("addGadget");
test('addGadget', function () {
cleanUp();
RenderJs.addGadget("qunit-fixture", "loading/test-gadget.html", "", "");
equal($("#qunit-fixture").children(".gadget").length, 1);
equal(RenderJs.GadgetIndex.getGadgetList().length, 1);
});
module("GadgetInitialization");
test('GadgetInitialization', function () {
cleanUp();
$("#qunit-fixture").append('<div data-gadget="" id="new-init" data-gadget-property="{&quot;name&quot;: &quot;Ivan&quot;, &quot;age&quot;: 33}">X</div>');
RenderJs.bootstrap($("#qunit-fixture"));
RenderJs.GadgetIndex.getRootGadget().getDom().one("ready", function (){
RenderJs.update($("#qunit-fixture"));
});
// test that gadget get a proper initialization from data-gadget-property
equal('Ivan', RenderJs.GadgetIndex.getGadgetById("new-init").name);
equal(33, RenderJs.GadgetIndex.getGadgetById("new-init").age);
});
module("GadgetReadyEvent");
test('GadgetReadyEvent', function () {
cleanUp();
RenderJs.addGadget("qunit-fixture", "interactions/index.html", "", "");
stop();
// we need to wait for all gadgets loading ...
RenderJs.GadgetIndex.getRootGadget().getDom().bind("ready", function () {
start();
equal(true, RenderJs.GadgetIndex.isGadgetListLoaded());
equal(true, RenderJs.isReady());
});
});
module("InteractionGadget");
test('InteractionGadget', function () {
cleanUp();
RenderJs.addGadget("qunit-fixture", "interactions/index.html", "", "");
stop();
// we need to wait for all gadgets loading ...
RenderJs.GadgetIndex.getRootGadget().getDom().bind("ready", function () {
RenderJs.InteractionGadget.bind($("#main-interactor"));
start();
equal(0, counter);
// A.inc will call B.inc, both will increase counter by 1
RenderJs.GadgetIndex.getGadgetById("A").inc();
equal(2, counter);
// fire pure HTML event on A and test it calls respective B method
$('#A').trigger('htmlEvent1');
equal(3, counter);
});
});
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment