private static function loadPackagesForPaths(PhabricatorRepository $repository, array $paths, $limit = 0) { $fragments = array(); foreach ($paths as $path) { foreach (self::splitPath($path) as $fragment) { $fragments[$fragment][$path] = true; } } $package = new PhabricatorOwnersPackage(); $path = new PhabricatorOwnersPath(); $conn = $package->establishConnection('r'); $repository_clause = qsprintf($conn, 'AND p.repositoryPHID = %s', $repository->getPHID()); // NOTE: The list of $paths may be very large if we're coming from // the OwnersWorker and processing, e.g., an SVN commit which created a new // branch. Break it apart so that it will fit within 'max_allowed_packet', // and then merge results in PHP. $rows = array(); foreach (array_chunk(array_keys($fragments), 128) as $chunk) { $rows[] = queryfx_all($conn, 'SELECT pkg.id, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.path IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $chunk, $repository_clause); } $rows = array_mergev($rows); $ids = self::findLongestPathsPerPackage($rows, $fragments); if (!$ids) { return array(); } arsort($ids); if ($limit) { $ids = array_slice($ids, 0, $limit, $preserve_keys = true); } $ids = array_keys($ids); $packages = $package->loadAllWhere('id in (%Ld)', $ids); $packages = array_select_keys($packages, $ids); return $packages; }
public function save() { // TODO: Transactions! $ret = parent::save(); if ($this->unsavedOwners) { $new_owners = array_fill_keys($this->unsavedOwners, true); $cur_owners = array(); foreach ($this->loadOwners() as $owner) { if (empty($new_owners[$owner->getUserPHID()])) { $owner->delete(); continue; } $cur_owners[$owner->getUserPHID()] = true; } $add_owners = array_diff_key($new_owners, $cur_owners); foreach ($add_owners as $phid => $ignored) { $owner = new PhabricatorOwnersOwner(); $owner->setPackageID($this->getID()); $owner->setUserPHID($phid); $owner->save(); } unset($this->unsavedOwners); } if ($this->unsavedPaths) { $new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path'); $cur_paths = $this->loadPaths(); foreach ($cur_paths as $key => $path) { if (empty($new_paths[$path->getRepositoryPHID()][$path->getPath()])) { $path->delete(); unset($cur_paths[$key]); } } $cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath'); foreach ($new_paths as $repository_phid => $paths) { foreach ($paths as $path => $ignored) { if (empty($cur_paths[$repository_phid][$path])) { $obj = new PhabricatorOwnersPath(); $obj->setPackageID($this->getID()); $obj->setRepositoryPHID($repository_phid); $obj->setPath($path); $obj->save(); } } } unset($this->unsavedPaths); } return $ret; }
public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: $old = $this->getOldValue(); $new = $this->getNewValue(); return $this->renderTextCorpusChangeDetails($viewer, $old, $new); case self::TYPE_PATHS: $old = $this->getOldValue(); $new = $this->getNewValue(); $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); list($rem, $add) = $diffs; $rows = array(); foreach ($rem as $ref) { $rows[] = array('class' => 'diff-removed', 'change' => '-') + $ref; } foreach ($add as $ref) { $rows[] = array('class' => 'diff-added', 'change' => '+') + $ref; } $rowc = array(); foreach ($rows as $key => $row) { $rowc[] = $row['class']; $rows[$key] = array($row['change'], $row['excluded'] ? pht('Exclude') : pht('Include'), $viewer->renderHandle($row['repositoryPHID']), $row['path']); } $table = id(new AphrontTableView($rows))->setRowClasses($rowc)->setHeaders(array(null, pht('Type'), pht('Repository'), pht('Path')))->setColumnClasses(array(null, null, null, 'wide')); return $table; } return parent::renderChangeDetails($viewer); }
protected function applyCustomExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: case PhabricatorOwnersPackageTransaction::TYPE_PRIMARY: case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: return; case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); // TODO: needOwners this $owners = $object->loadOwners(); $owners = mpull($owners, null, 'getUserPHID'); $rem = array_diff($old, $new); foreach ($rem as $phid) { if (isset($owners[$phid])) { $owners[$phid]->delete(); unset($owners[$phid]); } } $add = array_diff($new, $old); foreach ($add as $phid) { $owners[$phid] = id(new PhabricatorOwnersOwner())->setPackageID($object->getID())->setUserPHID($phid)->save(); } // TODO: Attach owners here return; case PhabricatorOwnersPackageTransaction::TYPE_PATHS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); // TODO: needPaths this $paths = $object->loadPaths(); $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); list($rem, $add) = $diffs; $set = PhabricatorOwnersPath::getSetFromTransactionValue($rem); foreach ($paths as $path) { $ref = $path->getRef(); if (PhabricatorOwnersPath::isRefInSet($ref, $set)) { $path->delete(); } } foreach ($add as $ref) { $path = PhabricatorOwnersPath::newFromRef($ref)->setPackageID($object->getID())->save(); } return; } return parent::applyCustomExternalTransaction($object, $xaction); }
public function save() { if ($this->getID()) { $is_new = false; } else { $is_new = true; } $this->openTransaction(); $ret = parent::save(); $add_owners = array(); $remove_owners = array(); $all_owners = array(); if ($this->unsavedOwners) { $new_owners = array_fill_keys($this->unsavedOwners, true); $cur_owners = array(); foreach ($this->loadOwners() as $owner) { if (empty($new_owners[$owner->getUserPHID()])) { $remove_owners[$owner->getUserPHID()] = true; $owner->delete(); continue; } $cur_owners[$owner->getUserPHID()] = true; } $add_owners = array_diff_key($new_owners, $cur_owners); $all_owners = array_merge(array($this->getPrimaryOwnerPHID() => true), $new_owners, $remove_owners); foreach ($add_owners as $phid => $ignored) { $owner = new PhabricatorOwnersOwner(); $owner->setPackageID($this->getID()); $owner->setUserPHID($phid); $owner->save(); } unset($this->unsavedOwners); } $add_paths = array(); $remove_paths = array(); $touched_repos = array(); if ($this->unsavedPaths) { $new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path'); $cur_paths = $this->loadPaths(); foreach ($cur_paths as $key => $path) { if (empty($new_paths[$path->getRepositoryPHID()][$path->getPath()])) { $touched_repos[$path->getRepositoryPHID()] = true; $remove_paths[$path->getRepositoryPHID()][$path->getPath()] = true; $path->delete(); unset($cur_paths[$key]); } } $cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath'); foreach ($new_paths as $repository_phid => $paths) { // get repository object for path validation $repository = id(new PhabricatorRepository())->loadOneWhere('phid = %s', $repository_phid); if (!$repository) { continue; } foreach ($paths as $path => $ignored) { $path = ltrim($path, '/'); // build query to validate path $drequest = DiffusionRequest::newFromDictionary(array('repository' => $repository, 'path' => $path)); $query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest); $query->needValidityOnly(true); $valid = $query->loadPaths(); $is_directory = true; if (!$valid) { switch ($query->getReasonForEmptyResultSet()) { case DiffusionBrowseQuery::REASON_IS_FILE: $valid = true; $is_directory = false; break; case DiffusionBrowseQuery::REASON_IS_EMPTY: $valid = true; break; } } if ($is_directory && substr($path, -1) != '/') { $path .= '/'; } if (substr($path, 0, 1) != '/') { $path = '/' . $path; } if (empty($cur_paths[$repository_phid][$path]) && $valid) { $touched_repos[$repository_phid] = true; $add_paths[$repository_phid][$path] = true; $obj = new PhabricatorOwnersPath(); $obj->setPackageID($this->getID()); $obj->setRepositoryPHID($repository_phid); $obj->setPath($path); $obj->save(); } } } unset($this->unsavedPaths); } $this->saveTransaction(); if ($is_new) { $mail = new PackageCreateMail($this); } else { $mail = new PackageModifyMail($this, array_keys($add_owners), array_keys($remove_owners), array_keys($all_owners), array_keys($touched_repos), $add_paths, $remove_paths); } $mail->send(); return $ret; }
public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $views = array('owned' => 'Owned Packages', 'all' => 'All Packages', 'search' => 'Search Results'); if (empty($views[$this->view])) { reset($views); $this->view = key($views); } if ($this->view != 'search') { unset($views['search']); } $nav = new AphrontSideNavView(); foreach ($views as $key => $name) { $nav->addNavItem(phutil_render_tag('a', array('href' => '/owners/view/' . $key . '/', 'class' => $this->view == $key ? 'aphront-side-nav-selected' : null), phutil_escape_html($name))); } $package = new PhabricatorOwnersPackage(); $owner = new PhabricatorOwnersOwner(); $path = new PhabricatorOwnersPath(); switch ($this->view) { case 'search': $packages = array(); $conn_r = $package->establishConnection('r'); $where = array('1 = 1'); $join = array(); if ($request->getStr('name')) { $where[] = qsprintf($conn_r, 'p.name LIKE %~', $request->getStr('name')); } if ($request->getStr('path')) { $join[] = qsprintf($conn_r, 'JOIN %T path ON path.packageID = p.id', $path->getTableName()); $where[] = qsprintf($conn_r, 'path.path LIKE %~', $request->getStr('path')); } if ($request->getArr('owner')) { $join[] = qsprintf($conn_r, 'JOIN %T o ON o.packageID = p.id', $owner->getTableName()); $where[] = qsprintf($conn_r, 'o.userPHID IN (%Ls)', $request->getArr('owner')); } $data = queryfx_all($conn_r, 'SELECT p.* FROM %T p %Q WHERE %Q GROUP BY p.id', $package->getTableName(), implode(' ', $join), '(' . implode(') AND (', $where) . ')'); $packages = $package->loadAllFromArray($data); $header = 'Search Results'; $nodata = 'No packages match your query.'; break; case 'owned': $data = queryfx_all($package->establishConnection('r'), 'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID WHERE o.userPHID = %s GROUP BY p.id', $package->getTableName(), $owner->getTableName(), $user->getPHID()); $packages = $package->loadAllFromArray($data); $header = 'Owned Packages'; $nodata = 'No owned packages'; break; case 'all': $packages = $package->loadAll(); $header = 'All Packages'; $nodata = 'There are no defined packages.'; break; } $content = $this->renderPackageTable($packages, $header, $nodata); $filter = new AphrontListFilterView(); $filter->addButton(phutil_render_tag('a', array('href' => '/owners/new/', 'class' => 'green button'), 'Create New Package')); $owners_search_value = array(); if ($request->getArr('owner')) { $phids = $request->getArr('owner'); $phid = reset($phids); $handles = id(new PhabricatorObjectHandleData(array($phid)))->loadHandles(); $owners_search_value = array($phid => $handles[$phid]->getFullName()); } $form = id(new AphrontFormView())->setUser($user)->setAction('/owners/view/search/')->appendChild(id(new AphrontFormTextControl())->setName('name')->setLabel('Name')->setValue($request->getStr('name')))->appendChild(id(new AphrontFormTokenizerControl())->setDatasource('/typeahead/common/users/')->setLimit(1)->setName('owner')->setLabel('Owner')->setValue($owners_search_value))->appendChild(id(new AphrontFormTextControl())->setName('path')->setLabel('Path')->setValue($request->getStr('path')))->appendChild(id(new AphrontFormSubmitControl())->setValue('Search for Packages')); $filter->appendChild($form); $nav->appendChild($filter); $nav->appendChild($content); return $this->buildStandardPageResponse($nav, array('title' => 'Package Index', 'tab' => 'index')); }
public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $package = new PhabricatorOwnersPackage(); $owner = new PhabricatorOwnersOwner(); $path = new PhabricatorOwnersPath(); $repository_phid = ''; if ($request->getStr('repository') != '') { $repository_phid = id(new PhabricatorRepositoryQuery())->setViewer($user)->withCallsigns(array($request->getStr('repository')))->executeOne()->getPHID(); } switch ($this->view) { case 'search': $packages = array(); $conn_r = $package->establishConnection('r'); $where = array('1 = 1'); $join = array(); $having = ''; if ($request->getStr('name')) { $where[] = qsprintf($conn_r, 'p.name LIKE %~', $request->getStr('name')); } if ($repository_phid || $request->getStr('path')) { $join[] = qsprintf($conn_r, 'JOIN %T path ON path.packageID = p.id', $path->getTableName()); if ($repository_phid) { $where[] = qsprintf($conn_r, 'path.repositoryPHID = %s', $repository_phid); } if ($request->getStr('path')) { $where[] = qsprintf($conn_r, '(path.path LIKE %~ AND NOT path.excluded) OR %s LIKE CONCAT(REPLACE(path.path, %s, %s), %s)', $request->getStr('path'), $request->getStr('path'), '_', '\\_', '%'); $having = 'HAVING MAX(path.excluded) = 0'; } } if ($request->getArr('owner')) { $join[] = qsprintf($conn_r, 'JOIN %T o ON o.packageID = p.id', $owner->getTableName()); $where[] = qsprintf($conn_r, 'o.userPHID IN (%Ls)', $request->getArr('owner')); } $data = queryfx_all($conn_r, 'SELECT p.* FROM %T p %Q WHERE %Q GROUP BY p.id %Q', $package->getTableName(), implode(' ', $join), '(' . implode(') AND (', $where) . ')', $having); $packages = $package->loadAllFromArray($data); $header = pht('Search Results'); $nodata = pht('No packages match your query.'); break; case 'owned': $data = queryfx_all($package->establishConnection('r'), 'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID WHERE o.userPHID = %s GROUP BY p.id', $package->getTableName(), $owner->getTableName(), $user->getPHID()); $packages = $package->loadAllFromArray($data); $header = pht('Owned Packages'); $nodata = pht('No owned packages'); break; case 'projects': $projects = id(new PhabricatorProjectQuery())->setViewer($user)->withMemberPHIDs(array($user->getPHID()))->withStatus(PhabricatorProjectQuery::STATUS_ANY)->execute(); $owner_phids = mpull($projects, 'getPHID'); if ($owner_phids) { $data = queryfx_all($package->establishConnection('r'), 'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID WHERE o.userPHID IN (%Ls) GROUP BY p.id', $package->getTableName(), $owner->getTableName(), $owner_phids); } else { $data = array(); } $packages = $package->loadAllFromArray($data); $header = pht('Owned Packages'); $nodata = pht('No owned packages'); break; case 'all': $packages = $package->loadAll(); $header = pht('All Packages'); $nodata = pht('There are no defined packages.'); break; } $content = $this->renderPackageTable($packages, $header, $nodata); $filter = new AphrontListFilterView(); $owners_search_value = array(); if ($request->getArr('owner')) { $phids = $request->getArr('owner'); $phid = reset($phids); $handles = $this->loadViewerHandles(array($phid)); $owners_search_value = array($handles[$phid]); } $callsigns = array('' => pht('(Any Repository)')); $repositories = id(new PhabricatorRepositoryQuery())->setViewer($user)->setOrder(PhabricatorRepositoryQuery::ORDER_CALLSIGN)->execute(); foreach ($repositories as $repository) { $callsigns[$repository->getCallsign()] = $repository->getCallsign() . ': ' . $repository->getName(); } $form = id(new AphrontFormView())->setUser($user)->setAction('/owners/view/search/')->setMethod('GET')->appendChild(id(new AphrontFormTextControl())->setName('name')->setLabel(pht('Name'))->setValue($request->getStr('name')))->appendChild(id(new AphrontFormTokenizerControl())->setDatasource(new PhabricatorProjectOrUserDatasource())->setLimit(1)->setName('owner')->setLabel(pht('Owner'))->setValue($owners_search_value))->appendChild(id(new AphrontFormSelectControl())->setName('repository')->setLabel(pht('Repository'))->setOptions($callsigns)->setValue($request->getStr('repository')))->appendChild(id(new AphrontFormTextControl())->setName('path')->setLabel(pht('Path'))->setValue($request->getStr('path')))->appendChild(id(new AphrontFormSubmitControl())->setValue(pht('Search for Packages'))); $filter->appendChild($form); $nav = $this->buildSideNavView(); $nav->appendChild($filter); $nav->appendChild($content); return $this->buildApplicationPage(array($nav), array('title' => pht('Package Index'))); }
public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $package = new PhabricatorOwnersPackage(); $owner = new PhabricatorOwnersOwner(); $path = new PhabricatorOwnersPath(); $repository_phid = ''; if ($request->getStr('repository') != '') { $repository_phid = id(new PhabricatorRepository())->loadOneWhere('callsign = %s', $request->getStr('repository'))->getPHID(); } switch ($this->view) { case 'search': $packages = array(); $conn_r = $package->establishConnection('r'); $where = array('1 = 1'); $join = array(); if ($request->getStr('name')) { $where[] = qsprintf($conn_r, 'p.name LIKE %~', $request->getStr('name')); } if ($repository_phid || $request->getStr('path')) { $join[] = qsprintf($conn_r, 'JOIN %T path ON path.packageID = p.id', $path->getTableName()); if ($repository_phid) { $where[] = qsprintf($conn_r, 'path.repositoryPHID = %s', $repository_phid); } if ($request->getStr('path')) { $where[] = qsprintf($conn_r, 'path.path LIKE %~ OR %s LIKE CONCAT(path.path, %s)', $request->getStr('path'), $request->getStr('path'), '%'); } } if ($request->getArr('owner')) { $join[] = qsprintf($conn_r, 'JOIN %T o ON o.packageID = p.id', $owner->getTableName()); $where[] = qsprintf($conn_r, 'o.userPHID IN (%Ls)', $request->getArr('owner')); } $data = queryfx_all($conn_r, 'SELECT p.* FROM %T p %Q WHERE %Q GROUP BY p.id', $package->getTableName(), implode(' ', $join), '(' . implode(') AND (', $where) . ')'); $packages = $package->loadAllFromArray($data); $header = 'Search Results'; $nodata = 'No packages match your query.'; break; case 'owned': $data = queryfx_all($package->establishConnection('r'), 'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID WHERE o.userPHID = %s GROUP BY p.id', $package->getTableName(), $owner->getTableName(), $user->getPHID()); $packages = $package->loadAllFromArray($data); $header = 'Owned Packages'; $nodata = 'No owned packages'; break; case 'all': $packages = $package->loadAll(); $header = 'All Packages'; $nodata = 'There are no defined packages.'; break; } $content = $this->renderPackageTable($packages, $header, $nodata); $filter = new AphrontListFilterView(); $filter->addButton(phutil_render_tag('a', array('href' => '/owners/new/', 'class' => 'green button'), 'Create New Package')); $owners_search_value = array(); if ($request->getArr('owner')) { $phids = $request->getArr('owner'); $phid = reset($phids); $handles = id(new PhabricatorObjectHandleData(array($phid)))->loadHandles(); $owners_search_value = array($phid => $handles[$phid]->getFullName()); } $callsigns = array('' => '(Any Repository)'); $repositories = id(new PhabricatorRepository())->loadAllWhere('1 = 1 ORDER BY callsign'); foreach ($repositories as $repository) { $callsigns[$repository->getCallsign()] = $repository->getCallsign() . ': ' . $repository->getName(); } $form = id(new AphrontFormView())->setUser($user)->setAction('/owners/view/search/')->setMethod('GET')->appendChild(id(new AphrontFormTextControl())->setName('name')->setLabel('Name')->setValue($request->getStr('name')))->appendChild(id(new AphrontFormTokenizerControl())->setDatasource('/typeahead/common/usersorprojects/')->setLimit(1)->setName('owner')->setLabel('Owner')->setValue($owners_search_value))->appendChild(id(new AphrontFormSelectControl())->setName('repository')->setLabel('Repository')->setOptions($callsigns)->setValue($request->getStr('repository')))->appendChild(id(new AphrontFormTextControl())->setName('path')->setLabel('Path')->setValue($request->getStr('path')))->appendChild(id(new AphrontFormSubmitControl())->setValue('Search for Packages')); $filter->appendChild($form); return $this->buildStandardPageResponse(array($filter, $content), array('title' => 'Package Index')); }
protected function validateTransaction(PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField($object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError($type, pht('Required'), pht('Package name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; case PhabricatorOwnersPackageTransaction::TYPE_PATHS: if (!$xactions) { continue; } $old = mpull($object->getPaths(), 'getRef'); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); // Check that we have a list of paths. if (!is_array($new)) { $errors[] = new PhabricatorApplicationTransactionValidationError($type, pht('Invalid'), pht('Path specification must be a list of paths.'), $xaction); continue; } // Check that each item in the list is formatted properly. $type_exception = null; foreach ($new as $key => $value) { try { PhutilTypeSpec::checkMap($value, array('repositoryPHID' => 'string', 'path' => 'string', 'excluded' => 'optional wild')); } catch (PhutilTypeCheckException $ex) { $errors[] = new PhabricatorApplicationTransactionValidationError($type, pht('Invalid'), pht('Path specification list contains invalid value ' . 'in key "%s": %s.', $key, $ex->getMessage()), $xaction); $type_exception = $ex; } } if ($type_exception) { continue; } // Check that any new paths reference legitimate repositories which // the viewer has permission to see. list($rem, $add) = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); if ($add) { $repository_phids = ipull($add, 'repositoryPHID'); $repositories = id(new PhabricatorRepositoryQuery())->setViewer($this->getActor())->withPHIDs($repository_phids)->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($add as $ref) { $repository_phid = $ref['repositoryPHID']; if (isset($repositories[$repository_phid])) { continue; } $errors[] = new PhabricatorApplicationTransactionValidationError($type, pht('Invalid'), pht('Path specification list references repository PHID "%s", ' . 'but that is not a valid, visible repository.', $repository_phid)); } } } break; } return $errors; }
public function newChangeDetailView() { $old = $this->getOldValue(); $new = $this->getNewValue(); $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); list($rem, $add) = $diffs; $rows = array(); foreach ($rem as $ref) { $rows[] = array('class' => 'diff-removed', 'change' => '-') + $ref; } foreach ($add as $ref) { $rows[] = array('class' => 'diff-added', 'change' => '+') + $ref; } $rowc = array(); foreach ($rows as $key => $row) { $rowc[] = $row['class']; $rows[$key] = array($row['change'], $row['excluded'] ? pht('Exclude') : pht('Include'), $this->renderHandle($row['repositoryPHID']), $row['path']); } $table = id(new AphrontTableView($rows))->setViewer($this->getViewer())->setRowClasses($rowc)->setHeaders(array(null, pht('Type'), pht('Repository'), pht('Path')))->setColumnClasses(array(null, null, null, 'wide')); return $table; }