Skip to content

Loading…

Improved sortable behavior #359

Merged
merged 9 commits into from

4 participants

@rozwell

Changes:

  • added native moving between scopes when scope value was changed and rank not (if rank was changed - we assume superior change)
  • regular saving objects with empty/null scope was setting correct rank but methods like insertAtBottom() didn't allow inserting in null scope (lack of consistency?) It is now possible to use those methods in null scope (why not?). See: native sorting in sfPropelORMPlugin admin generator
  • removeFromList() was clearing both, scope and rank column and breaking good structure in null scope. Now it moves object to the end of null scope
  • swapWith() can now fully operate between scopes - previously it would only exchange ranks, which was fine for the same scope, but would break the structure in both scopes (no exception was thrown)

There are some other issues like when scope column is a FK and onDelete="setnull", removing related/scope object will only clean the scope value leaving ranks untouched.

@travisbot

This pull request fails (merged 07ff600 into 82372dd).

@rozwell

@travisbot You're right, I will have to adjust and add tests too.
But lets wait for someone to comment if this is a good way to go or shall we change something else?

Edit: Why does removeFromList() sets rank to null but there is no "AND SORTABLE_RANK IS NOT NULL" condition in any query?
Especially in non-scope tables where this could be expected(?)

This behavior needs more solid concept.
For now I'm confused.

@travisbot

This pull request fails (merged 9df7198 into 82372dd).

@willdurand
Propel member

@rozwell travisbot is... a bot :p

Your work seems ok, but I don't really know this behavior so.. let's see other comments, and try to fix the test suite.

@rozwell

@willdurand I know ;)
Unit tests are easy to fix (just 2 failed) and I will add a few more too if these changes are ok.

@willdurand
Propel member

It looks fine to me, but as I said, I'm not expert on this. Maybe we should try to expose what the behavior should do, and how before to change everything. What do you think?

@rozwell

I took one more look at this behavior and after my modifications we actually need to talk about those 2 things only:

  1. removeFromList() - I'd suggest to remove this method since we can just change the scope to null or if scope isn't used it seems better to add other column to manage visibility like is_visible so there is no need for adding NOT NULL condition mentioned above.
    I've got in mind admin generator so this may affect my judgment since I also see how to manage null ranks.

  2. onDelete="setnull" - removing related object will only clean the scope value leaving ranks untouched and since we sort in null scope too, shouldn't they be moved at the end of it? (makes most sense)

@willdurand
Propel member

I think you are changing the behavior of ... this behavior :)

  1. I mean, a null scope should not mean objects are not in a list. The "null" scope is actually a scope, like a "global" scope, or a "default" one I guess. So the removeFromList() makes sense. But I agree with the is_visible, it could a nice option.

  2. You're right!

Sorry for the delay, I tried to learn this behavior a bit more.

@travisbot

This pull request fails (merged fb30d63 into 8b02639).

@travisbot

This pull request fails (merged 5b06917 into 8b02639).

@travisbot

This pull request fails (merged cce76e3d into 8b02639).

@travisbot

This pull request fails (merged 957b90a into 8b02639).

@travisbot

This pull request fails (merged 7e169b3 into 8b02639).

@rozwell

@willdurand is there any reason to have boolean use_scope parameter for scope, when you have to set scope_column anyway?
Isn't setting scope_column enough?

@willdurand
Propel member

well, I think you can enable/disable the use of scopes easily.

@fzaninotto
Propel member

This PR needs unit tests that fail if the builder is not modified and passes otherwise - this will reveal the interest of tour modification.

@rozwell

@fzaninotto can you explain with more details?

rozwell added some commits
@rozwell rozwell sortable behavior: changed removeFromList() to work with new scope ap…
…proach, little bugfix in preUpdate
2efa390
@rozwell rozwell sortable behavior: tests for new scope approach (includes fixed typo …
…in SortableBehaviorObjectBuilderModifierWithScopeTest::testInsertAtRank())
3af6a72
@travisbot

This pull request passes (merged 3af6a72 into 441f969).

@rozwell

Little summary of changes:

  • added native moving between scopes when scope value was changed and rank not (if rank was changed - we assume superior change)
  • methods like insertAtBottom() now works with null scope
  • removeFromList() now moves object to the end of null scope (if scope is used)
  • swapWith() can now fully operate between scopes

TODO:
When scope column is a FK and onDelete="setnull", removing related object should move all objects to null scope (maybe trigger
removeFromList() ?) - is there a proper way to do so?

@rozwell

Seems ready to be merged.
Any thoughts?

@willdurand
Propel member

So null is a scope then?

@fzaninotto
Propel member

ok, I'm +1. Good patch.

@willdurand willdurand merged commit 19dc671 into propelorm:master

1 check passed

Details default The Travis build passed
@willdurand
Propel member

Great, merged! Thank you so much.

@willdurand
Propel member

Can you port this PR on Propel2? :)

@rozwell

Sure, but after I fix onDelete="setnull" issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 10, 2012
  1. @rozwell
Commits on May 12, 2012
  1. @rozwell
Commits on Aug 12, 2012
  1. @rozwell

    Merge with Propel/master

    rozwell committed
  2. @rozwell
  3. @rozwell
  4. @rozwell
Commits on Aug 16, 2012
  1. @rozwell
  2. @rozwell

    sortable behavior: changed removeFromList() to work with new scope ap…

    rozwell committed
    …proach, little bugfix in preUpdate
  3. @rozwell

    sortable behavior: tests for new scope approach (includes fixed typo …

    rozwell committed
    …in SortableBehaviorObjectBuilderModifierWithScopeTest::testInsertAtRank())
View
93 generator/lib/behavior/sortable/SortableBehaviorObjectBuilderModifier.php
@@ -84,6 +84,21 @@ public function preInsert($builder)
";
}
+ public function preUpdate($builder)
+ {
+ if ($this->behavior->useScope()) {
+ $this->setBuilder($builder);
+
+ return "// if scope has changed and rank was not modified (if yes, assuming superior action)
+// insert object to the end of new scope and cleanup old one
+if (\$this->isColumnModified({$this->peerClassname}::SCOPE_COL) && !\$this->isColumnModified({$this->peerClassname}::RANK_COL)) {
+ {$this->peerClassname}::shiftRank(-1, \$this->{$this->getColumnGetter()}() + 1, null, \$this->oldScope, \$con);
+ \$this->insertAtBottom(\$con);
+}
+";
+ }
+ }
+
public function preDelete($builder)
{
$useScope = $this->behavior->useScope();
@@ -97,13 +112,24 @@ public function preDelete($builder)
public function objectAttributes($builder)
{
- return "
+ $script = "
/**
* Queries to be executed in the save transaction
* @var array
*/
protected \$sortableQueries = array();
";
+ if ($this->behavior->useScope()) {
+ $script .= "
+/**
+ * The old scope value.
+ * @var int
+ */
+protected \$oldScope;
+";
+ }
+
+ return $script;
}
public function objectMethods($builder)
@@ -136,6 +162,19 @@ public function objectMethods($builder)
return $script;
}
+ public function objectFilter(&$script, $builder)
+ {
+ if ($this->behavior->useScope()) {
+ $methodName = $this->getColumnSetter('scope_column');
+ $search = "if (\$this->{$this->getColumnAttribute('scope_column')} !== \$v) {";
+ $replace = $search . "
+ // sortable behavior
+ \$this->oldScope = \$this->{$this->getColumnGetter('scope_column')}();
+";
+ $script = str_replace($search, $replace, $script);
+ }
+ }
+
/**
* Get the wraps for getter/setter, if the rank column has not the default name
*
@@ -143,7 +182,7 @@ public function objectMethods($builder)
*/
protected function addRankAccessors(&$script)
{
- $script .= "
+ $script .= "
/**
* Wrap the getter for rank value
*
@@ -174,7 +213,8 @@ public function setRank(\$v)
*/
protected function addScopeAccessors(&$script)
{
- $script .= "
+ $script .= "
+
/**
* Wrap the getter for scope value
*
@@ -309,12 +349,6 @@ protected function addInsertAtRank(&$script)
*/
public function insertAtRank(\$rank, PropelPDO \$con = null)
{";
- if ($useScope) {
- $script .= "
- if (null === \$this->{$this->getColumnGetter('scope_column')}()) {
- throw new PropelException('The scope must be defined before inserting an object in a suite');
- }";
- }
$script .= "
\$maxRank = {$this->queryClassname}::create()->getMaxRank(" . ($useScope ? "\$this->{$this->getColumnGetter('scope_column')}(), " : '') . "\$con);
if (\$rank < 1 || \$rank > \$maxRank + 1) {
@@ -351,12 +385,6 @@ protected function addInsertAtBottom(&$script)
*/
public function insertAtBottom(PropelPDO \$con = null)
{";
- if ($useScope) {
- $script .= "
- if (null === \$this->{$this->getColumnGetter('scope_column')}()) {
- throw new PropelException('The scope must be defined before inserting an object in a suite');
- }";
- }
$script .= "
\$this->{$this->getColumnSetter()}({$this->queryClassname}::create()->getMaxRank(" . ($useScope ? "\$this->{$this->getColumnGetter('scope_column')}(), " : '') . "\$con) + 1);
@@ -454,7 +482,17 @@ public function swapWith(\$object, PropelPDO \$con = null)
\$con = Propel::getConnection({$this->peerClassname}::DATABASE_NAME);
}
\$con->beginTransaction();
- try {
+ try {";
+ if ($this->behavior->useScope()) {
+ $script .= "
+ \$oldScope = \$this->{$this->getColumnGetter('scope_column')}();
+ \$newScope = \$object->{$this->getColumnGetter('scope_column')}();
+ if (\$oldScope != \$newScope) {
+ \$this->{$this->getColumnSetter('scope_column')}(\$newScope);
+ \$object->{$this->getColumnSetter('scope_column')}(\$oldScope);
+ }";
+ }
+$script .= "
\$oldRank = \$this->{$this->getColumnGetter()}();
\$newRank = \$object->{$this->getColumnGetter()}();
\$this->{$this->getColumnSetter()}(\$newRank);
@@ -598,13 +636,27 @@ protected function addRemoveFromList(&$script)
$useScope = $this->behavior->useScope();
$script .= "
/**
- * Removes the current object from the list.
+ * Removes the current object from the list".($useScope ? ' (moves it to the null scope)' : '').".
* The modifications are not persisted until the object is saved.
*
+ * @param PropelPDO \$con optional connection
+ *
* @return {$this->objectClassname} the current object
*/
-public function removeFromList()
-{
+public function removeFromList(PropelPDO \$con = null)
+{";
+ if ($useScope) {
+ $script .= "
+ // check if object is already removed
+ if (\$this->{$this->getColumnGetter('scope_column')}() === null) {
+ throw new PropelException('Object is already removed (has null scope)');
+ }
+
+ // move the object to the end of null scope
+ \$this->{$this->getColumnSetter('scope_column')}(null);
+// \$this->insertAtBottom(\$con);";
+ } else {
+ $script .= "
// Keep the list modification query for the save() transaction
\$this->sortableQueries []= array(
'callable' => array(self::PEER, 'shiftRank'),
@@ -612,9 +664,6 @@ public function removeFromList()
);
// remove the object from the list
\$this->{$this->getColumnSetter('rank_column')}(null);";
- if ($useScope) {
- $script .= "
- \$this->{$this->getColumnSetter('scope_column')}(null);";
}
$script .= "
View
104 ...tsuite/generator/behavior/sortable/SortableBehaviorObjectBuilderModifierWithScopeTest.php
@@ -120,7 +120,7 @@ public function testInsertAtRank()
$t->setScopeValue(1);
$t->insertAtRank(2);
$this->assertEquals(2, $t->getRank(), 'insertAtRank() sets the position');
- $this->assertTrue($t->isNew(), 'insertAtTop() doesn\'t save the object');
+ $this->assertTrue($t->isNew(), 'insertAtRank() doesn\'t save the object');
$t->save();
$expected = array(1 => 'row1', 2 => 'new', 3 => 'row2', 4 => 'row3', 5 => 'row4');
$this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'insertAtRank() shifts the entire suite');
@@ -128,6 +128,22 @@ public function testInsertAtRank()
$this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtRank() leaves other suites unchanged');
}
+ public function testInsertAtRankNoScope()
+ {
+ $t = new Table12();
+ $t->setTitle('new');
+ $t->insertAtRank(2);
+ $this->assertEquals(2, $t->getRank(), 'insertAtRank() sets the position');
+ $this->assertTrue($t->isNew(), 'insertAtRank() doesn\'t save the object');
+ $t->save();
+ $expected = array(1 => 'row7', 2 => 'new', 3 => 'row8', 4 => 'row9', 5 => 'row10');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'insertAtRank() shifts the entire suite');
+ $expected = array(1 => 'row1', 2 => 'row2', 3 => 'row3', 4 => 'row4');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'insertAtRank() leaves other suites unchanged');
+ $expected = array(1 => 'row5', 2 => 'row6');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtRank() leaves other suites unchanged');
+ }
+
/**
* @expectedException PropelException
*/
@@ -148,15 +164,6 @@ public function testInsertAtOverMaxRank()
$t->insertAtRank(6);
}
- /**
- * @expectedException PropelException
- */
- public function testInsertAtNoScope()
- {
- $t = new Table12();
- $t->insertAtRank(3);
- }
-
public function testInsertAtBottom()
{
$t = new Table12();
@@ -172,13 +179,20 @@ public function testInsertAtBottom()
$this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtBottom() leaves other suites unchanged');
}
- /**
- * @expectedException PropelException
- */
public function testInsertAtBottomNoScope()
{
$t = new Table12();
+ $t->setTitle('new');
$t->insertAtBottom();
+ $this->assertEquals(5, $t->getRank(), 'insertAtBottom() sets the position to the last');
+ $this->assertTrue($t->isNew(), 'insertAtTop() doesn\'t save the object');
+ $t->save();
+ $expected = array(1 => 'row7', 2 => 'row8', 3 => 'row9', 4 => 'row10', 5 => 'new');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'insertAtBottom() does not shift the entire suite');
+ $expected = array(1 => 'row1', 2 => 'row2', 3 => 'row3', 4 => 'row4');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'insertAtRank() leaves other suites unchanged');
+ $expected = array(1 => 'row5', 2 => 'row6');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtRank() leaves other suites unchanged');
}
public function testInsertAtTop()
@@ -196,6 +210,22 @@ public function testInsertAtTop()
$this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtTop() leaves other suites unchanged');
}
+ public function testInsertAtTopNoScope()
+ {
+ $t = new Table12();
+ $t->setTitle('new');
+ $t->insertAtTop();
+ $this->assertEquals(1, $t->getRank(), 'insertAtTop() sets the position to 1');
+ $this->assertTrue($t->isNew(), 'insertAtTop() doesn\'t save the object');
+ $t->save();
+ $expected = array(1 => 'new', 2 => 'row7', 3 => 'row8', 4 => 'row9', 5 => 'row10');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'insertAtTop() shifts the entire suite');
+ $expected = array(1 => 'row1', 2 => 'row2', 3 => 'row3', 4 => 'row4');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'insertAtRank() leaves other suites unchanged');
+ $expected = array(1 => 'row5', 2 => 'row6');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtRank() leaves other suites unchanged');
+ }
+
public function testMoveToRank()
{
$t2 = Table12Peer::retrieveByRank(2, 1);
@@ -215,6 +245,27 @@ public function testMoveToRank()
$this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'moveToRank() can move down');
}
+ public function testMoveToRankNoScope()
+ {
+ $t2 = Table12Peer::retrieveByRank(2);
+ $t2->moveToRank(3);
+ $expected = array(1 => 'row7', 2 => 'row9', 3 => 'row8', 4 => 'row10');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'moveToRank() can move up');
+ $expected = array(1 => 'row1', 2 => 'row2', 3 => 'row3', 4 => 'row4');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'insertAtRank() leaves other suites unchanged');
+ $expected = array(1 => 'row5', 2 => 'row6');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'insertAtRank() leaves other suites unchanged');
+ $t2->moveToRank(1);
+ $expected = array(1 => 'row8', 2 => 'row7', 3 => 'row9', 4 => 'row10');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'moveToRank() can move to the first rank');
+ $t2->moveToRank(4);
+ $expected = array(1 => 'row7', 2 => 'row9', 3 => 'row10', 4 => 'row8');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'moveToRank() can move to the last rank');
+ $t2->moveToRank(2);
+ $expected = array(1 => 'row7', 2 => 'row8', 3 => 'row9', 4 => 'row10');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'moveToRank() can move down');
+ }
+
/**
* @expectedException PropelException
*/
@@ -253,6 +304,19 @@ public function testSwapWith()
$this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'swapWith() leaves other suites unchanged');
}
+ public function testSwapWithBetweenScopes()
+ {
+ $t2 = Table12Peer::retrieveByRank(2, 1);
+ $t4 = Table12Peer::retrieveByRank(4);
+ $t2->swapWith($t4);
+ $expected = array(1 => 'row7', 2 => 'row8', 3 => 'row9', 4 => 'row2');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'swapWith() swaps ranks of the two objects between scopes and leaves the other ranks unchanged');
+ $expected = array(1 => 'row1', 2 => 'row10', 3 => 'row3', 4 => 'row4');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'swapWith() swaps ranks of the two objects between scopes and leaves the other ranks unchanged');
+ $expected = array(1 => 'row5', 2 => 'row6');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'swapWith() leaves rest of suites unchanged');
+ }
+
public function testMoveUp()
{
$t3 = Table12Peer::retrieveByRank(3, 1);
@@ -320,16 +384,26 @@ public function testRemoveFromList()
$t2 = Table12Peer::retrieveByRank(2, 1);
$res = $t2->removeFromList();
$this->assertTrue($res instanceof Table12, 'removeFromList() returns the current object');
- $this->assertNull($res->getRank(), 'removeFromList() resets the object\'s rank');
Table12Peer::clearInstancePool();
$expected = array(1 => 'row1', 2 => 'row2', 3 => 'row3', 4 => 'row4');
$this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'removeFromList() does not change the list until the object is saved');
$t2->save();
Table12Peer::clearInstancePool();
$expected = array(1 => 'row1', 2 => 'row3', 3 => 'row4');
- $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'removeFromList() changes the list once the object is saved');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(1), 'removeFromList() changes the list and moves object to null scope once the object is saved');
+ $expected = array(1 => 'row7', 2 => 'row8', 3 => 'row9', 4 => 'row10', 5 => 'row2');
+ $this->assertEquals($expected, $this->getFixturesArrayWithScope(), 'removeFromList() moves object to the end of null scope');
$expected = array(1 => 'row5', 2 => 'row6');
$this->assertEquals($expected, $this->getFixturesArrayWithScope(2), 'removeFromList() leaves other suites unchanged');
}
+ /**
+ * @expectedException PropelException
+ */
+ public function testRemoveFromListNoScope()
+ {
+ $t2 = Table12Peer::retrieveByRank(2);
+ $t2->removeFromList();
+ }
+
}
View
9 ...estsuite/generator/behavior/sortable/SortableBehaviorPeerBuilderModifierWithScopeTest.php
@@ -42,6 +42,7 @@ public function testGetMaxRank()
Table12Peer::doDeleteAll();
$this->assertNull(Table12Peer::getMaxRank(1), 'getMaxRank() returns null for empty tables');
}
+
public function testRetrieveByRank()
{
$t = Table12Peer::retrieveByRank(5, 1);
@@ -94,21 +95,25 @@ public function testDoSelectOrderByRank()
public function testRetrieveList()
{
+ $this->assertEquals(4, count(Table12Peer::retrieveList(null)), 'retrieveList() returns the list of objects in the scope');
$this->assertEquals(4, count(Table12Peer::retrieveList(1)), 'retrieveList() returns the list of objects in the scope');
$this->assertEquals(2, count(Table12Peer::retrieveList(2)), 'retrieveList() returns the list of objects in the scope');
}
public function testCountList()
{
+ $this->assertEquals(4, Table12Peer::countList(null), 'countList() returns the list of objects in the scope');
$this->assertEquals(4, Table12Peer::countList(1), 'countList() returns the list of objects in the scope');
$this->assertEquals(2, Table12Peer::countList(2), 'countList() returns the list of objects in the scope');
}
public function testDeleteList()
{
- $this->assertEquals(4, Table12Peer::deleteList(1), 'deleteList() returns the list of objects in the scope');
+ $this->assertEquals(4, Table12Peer::deleteList(null), 'deleteList() returns the list of deleted objects in the scope');
+ $this->assertEquals(6, Table12Peer::doCount(new Criteria()), 'deleteList() deletes the objects in the scope');
+ $this->assertEquals(4, Table12Peer::deleteList(1), 'deleteList() returns the list of deleted objects in the scope');
$this->assertEquals(2, Table12Peer::doCount(new Criteria()), 'deleteList() deletes the objects in the scope');
- $this->assertEquals(2, Table12Peer::deleteList(2), 'deleteList() returns the list of objects in the scope');
+ $this->assertEquals(2, Table12Peer::deleteList(2), 'deleteList() returns the list of deleted objects in the scope');
$this->assertEquals(0, Table12Peer::doCount(new Criteria()), 'deleteList() deletes the objects in the scope');
}
}
View
30 test/tools/helpers/bookstore/behavior/BookstoreSortableTestBase.php
@@ -36,11 +36,11 @@ protected function populateTable11()
protected function populateTable12()
{
/* List used for tests
- scope=1 scope=2
- row1 row5
- row2 row6
- row3
- row4
+ scope=1 scope=2 scope=null
+ row1 row5 row7
+ row2 row6 row8
+ row3 row9
+ row4 row10
*/
Table12Peer::doDeleteAll();
$t1 = new Table12();
@@ -73,6 +73,22 @@ protected function populateTable12()
$t6->setScopeValue(2);
$t6->setTitle('row6');
$t6->save();
+ $t7 = new Table12();
+ $t7->setRank(1);
+ $t7->setTitle('row7');
+ $t7->save();
+ $t8 = new Table12();
+ $t8->setRank(2);
+ $t8->setTitle('row8');
+ $t8->save();
+ $t9 = new Table12();
+ $t9->setRank(3);
+ $t9->setTitle('row9');
+ $t9->save();
+ $t10 = new Table12();
+ $t10->setRank(4);
+ $t10->setTitle('row10');
+ $t10->save();
}
protected function getFixturesArray()
@@ -91,9 +107,7 @@ protected function getFixturesArray()
protected function getFixturesArrayWithScope($scope = null)
{
$c = new Criteria();
- if ($scope !== null) {
- $c->add(Table12Peer::SCOPE_COL, $scope);
- }
+ $c->add(Table12Peer::SCOPE_COL, $scope);
$c->addAscendingOrderByColumn(Table12Peer::RANK_COL);
$ts = Table12Peer::doSelect($c);
$ret = array();
Something went wrong with that request. Please try again.