1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-20 20:40:56 +01:00

Implement custom fields in Projects

Summary:
Ref T4379. Ref T3794. Fixes T4010. This brings CustomFields to projects.

My primary goal is to get rid of the special casing around project profiles and profile editing, so all edits are ApplicationTransactions. Particularly, I want to make the "blurb/description" field a custom field which goes through builtin infrastructure.

A distant secondary goal is that this is a feature which users like/want because users like/want features.

Test Plan: Added a custom field and examined it in the edit, view, and search interfaces.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T3794, T4010, T4379

Differential Revision: https://secure.phabricator.com/D8180
This commit is contained in:
epriestley 2014-02-10 14:31:34 -08:00
parent b99f72216d
commit 7c8a875c19
9 changed files with 214 additions and 45 deletions

View file

@ -1830,9 +1830,12 @@ phutil_register_library_map(array(
'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php',
'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php',
'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php',
'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
'PhabricatorProjectCreateController' => 'applications/project/controller/PhabricatorProjectCreateController.php',
'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php',
'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php',
'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php',
'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php',
@ -4557,6 +4560,7 @@ phutil_register_library_map(array(
1 => 'PhabricatorFlaggableInterface',
2 => 'PhabricatorPolicyInterface',
3 => 'PhabricatorSubscribableInterface',
4 => 'PhabricatorCustomFieldInterface',
),
'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardController' => 'PhabricatorProjectController',
@ -4567,8 +4571,15 @@ phutil_register_library_map(array(
1 => 'PhabricatorPolicyInterface',
),
'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorProjectConfiguredCustomField' =>
array(
0 => 'PhabricatorProjectCustomField',
1 => 'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectController' => 'PhabricatorController',
'PhabricatorProjectCreateController' => 'PhabricatorProjectController',
'PhabricatorProjectCustomField' => 'PhabricatorCustomField',
'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',

View file

@ -0,0 +1,43 @@
<?php
final class PhabricatorProjectConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht("Projects");
}
public function getDescription() {
return pht("Configure Projects.");
}
public function getOptions() {
// This is intentionally blank for now, until we can move more Project
// logic to custom fields.
$default_fields = array();
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
return array(
$this->newOption('projects.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Projects fields.'))
->setDescription(
pht(
"Array of custom fields for Projects."))
->addExample(
'{"mycompany:motto": {"name": "Project Motto", '.
'"type": "string"}}',
pht('Valid Setting')),
$this->newOption('projects.fields', $custom_field_type, $default_fields)
->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass())
->setDescription(pht("Select and reorder project fields.")),
);
}
}

View file

@ -266,6 +266,11 @@ final class PhabricatorProjectProfileController
? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
: phutil_tag('em', array(), pht('None')));
$field_list = PhabricatorCustomField::getObjectFields(
$project,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($project, $viewer, $view);
$view->addSectionHeader(pht('Description'));
$view->addTextContent(
PhabricatorMarkupEngine::renderOneObject(

View file

@ -10,12 +10,11 @@ final class PhabricatorProjectProfileEditController
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer = $request->getUser();
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
@ -30,49 +29,80 @@ final class PhabricatorProjectProfileEditController
$profile = $project->getProfile();
$e_name = true;
$field_list = PhabricatorCustomField::getObjectFields(
$project,
PhabricatorCustomField::ROLE_EDIT);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($project);
$view_uri = $this->getApplicationURI('view/'.$project->getID().'/');
$e_name = true;
$e_edit = null;
$v_name = $project->getName();
$v_desc = $profile->getBlurb();
$validation_exception = null;
$errors = array();
if ($request->isFormPost()) {
$xactions = array();
$e_name = null;
$v_name = $request->getStr('name');
$v_desc = $request->getStr('blurb');
$v_view = $request->getStr('can_view');
$v_edit = $request->getStr('can_edit');
$v_join = $request->getStr('can_join');
$xactions = $field_list->buildFieldTransactionsFromRequest(
new PhabricatorProjectTransaction(),
$request);
$type_name = PhabricatorProjectTransaction::TYPE_NAME;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
->setTransactionType($type_name)
->setNewValue($request->getStr('name'));
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($request->getStr('can_view'));
->setNewValue($v_view);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($request->getStr('can_edit'));
->setTransactionType($type_edit)
->setNewValue($v_edit);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY)
->setNewValue($request->getStr('can_join'));
->setNewValue($v_join);
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
->setContinueOnNoEffect(true);
$profile->setBlurb($request->getStr('blurb'));
try {
$editor->applyTransactions($project, $xactions);
if (!strlen($project->getName())) {
$e_name = pht('Required');
$errors[] = pht('Project name is required.');
} else {
$e_name = null;
}
if (!$errors) {
$project->save();
$profile->setProjectPHID($project->getPHID());
// TODO: Move this into a custom field.
$profile->setBlurb($request->getStr('blurb'));
if (!$profile->getProjectPHID()) {
$profile->setProjectPHID($project->getPHID());
}
$profile->save();
return id(new AphrontRedirectResponse())
->setURI('/project/view/'.$project->getID().'/');
return id(new AphrontRedirectResponse())->setURI($view_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $ex->getShortMessage($type_name);
$e_edit = $ex->getShortMessage($type_edit);
$project->setViewPolicy($v_view);
$project->setEditPolicy($v_edit);
$project->setJoinPolicy($v_join);
}
}
@ -81,29 +111,33 @@ final class PhabricatorProjectProfileEditController
$action = '/project/edit/'.$project->getID().'/';
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setViewer($viewer)
->setObject($project)
->execute();
$form = new AphrontFormView();
$form
->setID('project-edit-form')
->setUser($user)
->setUser($viewer)
->setAction($action)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($project->getName())
->setValue($v_name)
->setError($e_name))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Description'))
->setName('blurb')
->setValue($profile->getBlurb()))
->setValue($v_desc));
$field_list->appendFieldsToForm($form);
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setUser($viewer)
->setName('can_view')
->setCaption(pht('Members can always view a project.'))
->setPolicyObject($project)
@ -111,14 +145,15 @@ final class PhabricatorProjectProfileEditController
->setCapability(PhabricatorPolicyCapability::CAN_VIEW))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setUser($viewer)
->setName('can_edit')
->setPolicyObject($project)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT))
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setError($e_edit))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setUser($viewer)
->setName('can_join')
->setCaption(
pht('Users who can edit a project can always join a project.'))
@ -127,18 +162,16 @@ final class PhabricatorProjectProfileEditController
->setCapability(PhabricatorPolicyCapability::CAN_JOIN))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/project/view/'.$project->getID().'/')
->addCancelButton($view_uri)
->setValue(pht('Save')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setValidationException($validation_exception)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs($this->buildSideNavView())
->addTextCrumb(
$project->getName(),
'/project/view/'.$project->getID().'/')
->addTextCrumb($project->getName(), $view_uri)
->addTextCrumb(pht('Edit Project'), $this->getApplicationURI());
return $this->buildApplicationPage(

View file

@ -0,0 +1,31 @@
<?php
final class PhabricatorProjectConfiguredCustomField
extends PhabricatorProjectCustomField
implements PhabricatorStandardCustomFieldInterface {
public function getStandardCustomFieldNamespace() {
return 'project';
}
public function createFields() {
return PhabricatorStandardCustomField::buildStandardFields(
$this,
PhabricatorEnv::getEnvConfig(
'projects.custom-field-definitions',
array()));
}
public function newStorageObject() {
return new PhabricatorProjectCustomFieldStorage();
}
protected function newStringIndexStorage() {
return new PhabricatorProjectCustomFieldStringIndex();
}
protected function newNumericIndexStorage() {
return new PhabricatorProjectCustomFieldNumericIndex();
}
}

View file

@ -0,0 +1,7 @@
<?php
abstract class PhabricatorProjectCustomField
extends PhabricatorCustomField {
}

View file

@ -246,7 +246,7 @@ final class PhabricatorProjectQuery
if ($this->memberPHIDs) {
return 'GROUP BY p.id';
} else {
return '';
return $this->buildApplicationSearchGroupClause($conn_r);
}
}
@ -270,6 +270,8 @@ final class PhabricatorProjectQuery
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER);
}
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
return implode(' ', $joins);
}
@ -278,4 +280,8 @@ final class PhabricatorProjectQuery
return 'PhabricatorApplicationProject';
}
protected function getApplicationSearchObjectPHIDColumn() {
return 'p.phid';
}
}

View file

@ -3,6 +3,10 @@
final class PhabricatorProjectSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getCustomFieldObject() {
return new PhabricatorProject();
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
@ -11,6 +15,8 @@ final class PhabricatorProjectSearchEngine
$this->readUsersFromRequest($request, 'members'));
$saved->setParameter('status', $request->getStr('status'));
$this->readCustomFieldsFromRequest($request, $saved);
return $saved;
}
@ -28,20 +34,22 @@ final class PhabricatorProjectSearchEngine
$query->withStatus($status);
}
$this->applyCustomFieldsToQuery($query, $saved);
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {
PhabricatorSavedQuery $saved) {
$phids = $saved_query->getParameter('memberPHIDs', array());
$phids = $saved->getParameter('memberPHIDs', array());
$member_handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($phids)
->execute();
$status = $saved_query->getParameter('status');
$status = $saved->getParameter('status');
$form
->appendChild(
@ -56,6 +64,8 @@ final class PhabricatorProjectSearchEngine
->setName('status')
->setOptions($this->getStatusOptions())
->setValue($status));
$this->appendCustomFieldsToForm($form, $saved);
}
protected function getURI($path) {

View file

@ -4,7 +4,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface {
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
@ -19,6 +20,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
private $memberPHIDs = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $profile = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
public static function initializeNewProject(PhabricatorUser $actor) {
return id(new PhabricatorProject())
@ -168,4 +170,25 @@ final class PhabricatorProject extends PhabricatorProjectDAO
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}