Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php b/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php
index 4eb5d9d825..ac51e30386 100644
--- a/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php
+++ b/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php
@@ -1,59 +1,75 @@
<?php
final class PhabricatorFilesOnDiskBuiltinFile
extends PhabricatorFilesBuiltinFile {
private $name;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
if ($this->name === null) {
throw new PhutilInvalidStateException('setName');
}
return $this->name;
}
public function getBuiltinDisplayName() {
return $this->getName();
}
public function getBuiltinFileKey() {
$name = $this->getName();
$desc = "disk(name={$name})";
$hash = PhabricatorHash::digestToLength($desc, 40);
return "builtin:{$hash}";
}
public function loadBuiltinFileData() {
$name = $this->getName();
$available = $this->getAllBuiltinFiles();
if (empty($available[$name])) {
throw new Exception(pht('Builtin "%s" does not exist!', $name));
}
return Filesystem::readFile($available[$name]);
}
private function getAllBuiltinFiles() {
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/';
$map = array();
$list = id(new FileFinder($root))
->withType('f')
->withFollowSymlinks(true)
->find();
foreach ($list as $file) {
$map[$file] = $root.$file;
}
return $map;
}
+ public function getProjectBuiltinFiles() {
+ $root = dirname(phutil_get_library_root('phabricator'));
+ $root = $root.'/resources/builtin/projects/';
+
+ $map = array();
+ $list = id(new FileFinder($root))
+ ->withType('f')
+ ->withFollowSymlinks(true)
+ ->find();
+
+ foreach ($list as $file) {
+ $map[$file] = $root.$file;
+ }
+ return $map;
+ }
+
}
diff --git a/src/applications/project/config/PhabricatorProjectConfigOptions.php b/src/applications/project/config/PhabricatorProjectConfigOptions.php
index 36b6f09d86..c61faa64fb 100644
--- a/src/applications/project/config/PhabricatorProjectConfigOptions.php
+++ b/src/applications/project/config/PhabricatorProjectConfigOptions.php
@@ -1,106 +1,108 @@
<?php
final class PhabricatorProjectConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Projects');
}
public function getDescription() {
return pht('Configure Projects.');
}
public function getIcon() {
return 'fa-briefcase';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$default_icons = PhabricatorProjectIconSet::getDefaultConfiguration();
$icons_type = 'project.icons';
$icons_description = $this->deformat(pht(<<<EOTEXT
Allows you to change and customize the available project icons.
You can find a list of available icons in {nav UIExamples > Icons and Images}.
Configure a list of icon specifications. Each icon specification should be
a dictionary, which may contain these keys:
- `key` //Required string.// Internal key identifying the icon.
- `name` //Required string.// Human-readable icon name.
- `icon` //Required string.// Specifies which actual icon image to use.
+ - `image` //Optional string.// Selects a default image. Select an image from
+ `resources/builtins/projects/`.
- `default` //Optional bool.// Selects a default icon. Exactly one icon must
be selected as the default.
- `disabled` //Optional bool.// If true, this icon will no longer be
available for selection when creating or editing projects.
- `special` //Optional string.// Marks an icon as a special icon:
- `milestone` This is the icon for milestones. Exactly one icon must be
selected as the milestone icon.
You can look at the default configuration below for an example of a valid
configuration.
EOTEXT
));
$default_colors = PhabricatorProjectIconSet::getDefaultColorMap();
$colors_type = 'project.colors';
$colors_description = $this->deformat(pht(<<<EOTEXT
Allows you to relabel project colors.
The list of available colors can not be expanded, but the existing colors may
be given labels.
Configure a list of color specifications. Each color specification should be a
dictionary, which may contain these keys:
- `key` //Required string.// The internal key identifying the color.
- `name` //Required string.// Human-readable label for the color.
- `default` //Optional bool.// Selects the default color used when creating
new projects. Exactly one color must be selected as the default.
You can look at the default configuration below for an example of a valid
configuration.
EOTEXT
));
$default_fields = array(
'std:project:internal:description' => true,
);
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": "text"}}',
pht('Valid Setting')),
$this->newOption('projects.fields', $custom_field_type, $default_fields)
->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder project fields.')),
$this->newOption('projects.icons', $icons_type, $default_icons)
->setSummary(pht('Adjust project icons.'))
->setDescription($icons_description),
$this->newOption('projects.colors', $colors_type, $default_colors)
->setSummary(pht('Adjust project colors.'))
->setDescription($colors_description),
);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectEditPictureController.php b/src/applications/project/controller/PhabricatorProjectEditPictureController.php
index f8f1d532a9..97b7fe1792 100644
--- a/src/applications/project/controller/PhabricatorProjectEditPictureController.php
+++ b/src/applications/project/controller/PhabricatorProjectEditPictureController.php
@@ -1,372 +1,375 @@
<?php
final class PhabricatorProjectEditPictureController
extends PhabricatorProjectController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withIDs(array($id))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$manage_uri = $this->getApplicationURI('manage/'.$project->getID().'/');
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_file = true;
$errors = array();
if ($request->isFormPost()) {
$phid = $request->getStr('phid');
$is_default = false;
if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
$phid = null;
$is_default = true;
} else if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
} else {
if ($request->getFileExists('picture')) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['picture'],
array(
'authorPHID' => $viewer->getPHID(),
'canCDN' => true,
));
} else {
$e_file = pht('Required');
$errors[] = pht(
'You must choose a file when uploading a new project picture.');
}
}
if (!$errors && !$is_default) {
if (!$file->isTransformableImage()) {
$e_file = pht('Not Supported');
$errors[] = pht(
'This server only supports these image formats: %s.',
implode(', ', $supported_formats));
} else {
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
$xformed = $xform->executeTransform($file);
}
}
if (!$errors) {
if ($is_default) {
$new_value = null;
} else {
$new_value = $xformed->getPHID();
}
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectImageTransaction::TRANSACTIONTYPE)
->setNewValue($new_value);
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($project, $xactions);
return id(new AphrontRedirectResponse())->setURI($manage_uri);
}
}
$title = pht('Edit Project Picture');
$form = id(new PHUIFormLayoutView())
->setUser($viewer);
- $default_image = PhabricatorFile::loadBuiltin($viewer, 'project.png');
+ $builtin = PhabricatorProjectIconSet::getIconImage(
+ $project->getIcon());
+ $default_image = PhabricatorFile::loadBuiltin($this->getViewer(),
+ 'projects/'.$builtin);
$images = array();
$current = $project->getProfileImagePHID();
$has_current = false;
if ($current) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($current))
->execute();
if ($files) {
$file = head($files);
if ($file->isTransformableImage()) {
$has_current = true;
$images[$current] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Current Picture'),
);
}
}
}
$builtins = array(
'projects/v3/book.png',
'projects/v3/bug.png',
'projects/v3/calendar.png',
'projects/v3/clipboard.png',
'projects/v3/cloud.png',
'projects/v3/creditcard.png',
'projects/v3/database.png',
'projects/v3/desktop.png',
'projects/v3/experimental.png',
'projects/v3/flag.png',
'projects/v3/folder.png',
'projects/v3/lightbulb.png',
'projects/v3/lock.png',
'projects/v3/mail.png',
'projects/v3/marker.png',
'projects/v3/mobile.png',
'projects/v3/organization.png',
'projects/v3/people.png',
'projects/v3/piechart.png',
'projects/v3/robot.png',
'projects/v3/rocket.png',
'projects/v3/servers.png',
'projects/v3/sitemap.png',
'projects/v3/tag.png',
'projects/v3/trash.png',
'projects/v3/truck.png',
'projects/v3/umbrella.png',
);
foreach ($builtins as $builtin) {
$file = PhabricatorFile::loadBuiltin($viewer, $builtin);
$images[$file->getPHID()] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Builtin Image'),
);
}
$images[PhabricatorPHIDConstants::PHID_VOID] = array(
'uri' => $default_image->getBestURI(),
'tip' => pht('Default Picture'),
);
require_celerity_resource('people-profile-css');
Javelin::initBehavior('phabricator-tooltips', array());
$buttons = array();
foreach ($images as $phid => $spec) {
$button = javelin_tag(
'button',
array(
'class' => 'button-grey profile-image-button',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $spec['tip'],
'size' => 300,
),
),
phutil_tag(
'img',
array(
'height' => 50,
'width' => 50,
'src' => $spec['uri'],
)));
$button = array(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'phid',
'value' => $phid,
)),
$button,
);
$button = phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
),
$button);
$buttons[] = $button;
}
if ($has_current) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Current Picture'))
->setValue(array_shift($buttons)));
}
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Use Picture'))
->setValue(
array(
$this->renderDefaultForm($project),
$buttons,
)));
$launch_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'launch-icon-composer',
array(
'launchID' => $launch_id,
'inputID' => $input_id,
));
$compose_button = javelin_tag(
'button',
array(
'class' => 'button-grey',
'id' => $launch_id,
'sigil' => 'icon-composer',
),
pht('Choose Icon and Color...'));
$compose_input = javelin_tag(
'input',
array(
'type' => 'hidden',
'id' => $input_id,
'name' => 'phid',
));
$compose_form = phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
),
array(
$compose_input,
$compose_button,
));
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Custom'))
->setValue($compose_form));
$upload_form = id(new AphrontFormView())
->setUser($viewer)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormFileControl())
->setName('picture')
->setLabel(pht('Upload Picture'))
->setError($e_file)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($manage_uri)
->setValue(pht('Upload Picture')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$upload_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upload New Picture'))
->setForm($upload_form);
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorProject::ITEM_MANAGE);
return $this->newPage()
->setTitle($title)
->setNavigation($nav)
->appendChild(
array(
$form_box,
$upload_box,
));
}
private function renderDefaultForm(PhabricatorProject $project) {
$viewer = $this->getViewer();
$compose_color = $project->getDisplayIconComposeColor();
$compose_icon = $project->getDisplayIconComposeIcon();
$default_builtin = id(new PhabricatorFilesComposeIconBuiltinFile())
->setColor($compose_color)
->setIcon($compose_icon);
$file_builtins = PhabricatorFile::loadBuiltins(
$viewer,
array($default_builtin));
$file_builtin = head($file_builtins);
$default_button = javelin_tag(
'button',
array(
'class' => 'button-grey profile-image-button',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Use Icon and Color'),
'size' => 300,
),
),
phutil_tag(
'img',
array(
'height' => 50,
'width' => 50,
'src' => $file_builtin->getBestURI(),
)));
$inputs = array(
'projectPHID' => $project->getPHID(),
'icon' => $compose_icon,
'color' => $compose_color,
);
foreach ($inputs as $key => $value) {
$inputs[$key] = javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
$default_form = phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
'action' => '/file/compose/',
),
array(
$inputs,
$default_button,
));
return $default_form;
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 393c4569f6..7702b74bd6 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,510 +1,478 @@
<?php
final class PhabricatorProjectTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $isMilestone;
private function setIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function getIsMilestone() {
return $this->isMilestone;
}
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Projects');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
return $types;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
// Prevent creating projects which are both subprojects and milestones,
// since this does not make sense, won't work, and will break everything.
$parent_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectParentTransaction::TRANSACTIONTYPE:
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
if ($xaction->getNewValue() === null) {
continue;
}
if (!$parent_xaction) {
$parent_xaction = $xaction;
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'When creating a project, specify a maximum of one parent '.
'project or milestone project. A project can not be both a '.
'subproject and a milestone.'),
$xaction);
break;
break;
}
}
$is_milestone = $this->getIsMilestone();
$is_parent = $object->getHasSubprojects();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$type = $xaction->getMetadataValue('edge:type');
if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) {
break;
}
if ($is_parent) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a project with subprojects '.
'directly. Members of any subproject are automatically '.
'members of the parent project.'),
$xaction);
}
if ($is_milestone) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a milestone. Members of the '.
'parent project are automatically members of the milestone.'),
$xaction);
}
break;
}
}
return $errors;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectNameTransaction::TRANSACTIONTYPE:
case PhabricatorProjectStatusTransaction::TRANSACTIONTYPE:
case PhabricatorProjectImageTransaction::TRANSACTIONTYPE:
case PhabricatorProjectIconTransaction::TRANSACTIONTYPE:
case PhabricatorProjectColorTransaction::TRANSACTIONTYPE:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
return;
case PhabricatorProjectLockTransaction::TRANSACTIONTYPE:
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
newv($this->getEditorApplicationClass(), array()),
ProjectCanLockProjectsCapability::CAPABILITY);
return;
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$actor_phid = $this->requireActor()->getPHID();
$is_join = (($add === array($actor_phid)) && !$rem);
$is_leave = (($rem === array($actor_phid)) && !$add);
if ($is_join) {
// You need CAN_JOIN to join a project.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_JOIN);
} else if ($is_leave) {
// You usually don't need any capabilities to leave a project.
if ($object->getIsMembershipLocked()) {
// you must be able to edit though to leave locked projects
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
} else {
// You need CAN_EDIT to change members other than yourself.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
return;
}
break;
}
return parent::requireCapabilities($object, $xaction);
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// NOTE: We're using the omnipotent user here because the original actor
// may no longer have permission to view the object.
return id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Project]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
protected function getMailCc(PhabricatorLiskDAO $object) {
return array();
}
public function getMailTagsMap() {
return array(
PhabricatorProjectTransaction::MAILTAG_METADATA =>
pht('Project name, hashtags, icon, image, or color changes.'),
PhabricatorProjectTransaction::MAILTAG_MEMBERS =>
pht('Project membership changes.'),
PhabricatorProjectTransaction::MAILTAG_WATCHERS =>
pht('Project watcher list changes.'),
PhabricatorProjectTransaction::MAILTAG_OTHER =>
pht('Other project activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ProjectReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$name}")
->addHeader('Thread-Topic', "Project {$id}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$uri = '/project/profile/'.$object->getID().'/';
$body->addLinkSection(
pht('PROJECT DETAIL'),
PhabricatorEnv::getProductionURI($uri));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$materialize = false;
$new_parent = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$materialize = true;
break;
}
break;
case PhabricatorProjectParentTransaction::TRANSACTIONTYPE:
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
$materialize = true;
$new_parent = $object->getParentProject();
break;
}
}
if ($new_parent) {
// If we just created the first subproject of this parent, we want to
// copy all of the real members to the subproject.
if (!$new_parent->getHasSubprojects()) {
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$project_members = PhabricatorEdgeQuery::loadDestinationPHIDs(
$new_parent->getPHID(),
$member_type);
if ($project_members) {
$editor = id(new PhabricatorEdgeEditor());
foreach ($project_members as $phid) {
$editor->addEdge($object->getPHID(), $member_type, $phid);
}
$editor->save();
}
}
}
- if ($this->getIsNewObject()) {
- $this->setDefaultProfilePicture($object);
- }
-
// TODO: We should dump an informational transaction onto the parent
// project to show that we created the sub-thing.
if ($materialize) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($object);
}
if ($new_parent) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($new_parent);
}
return parent::applyFinalEffects($object, $xactions);
}
public function addSlug(PhabricatorProject $project, $slug, $force) {
$slug = PhabricatorSlug::normalizeProjectSlug($slug);
$table = new PhabricatorProjectSlug();
$project_phid = $project->getPHID();
if ($force) {
// If we have the `$force` flag set, we only want to ignore an existing
// slug if it's for the same project. We'll error on collisions with
// other projects.
$current = $table->loadOneWhere(
'slug = %s AND projectPHID = %s',
$slug,
$project_phid);
} else {
// Without the `$force` flag, we'll just return without doing anything
// if any other project already has the slug.
$current = $table->loadOneWhere(
'slug = %s',
$slug);
}
if ($current) {
return;
}
return id(new PhabricatorProjectSlug())
->setSlug($slug)
->setProjectPHID($project_phid)
->save();
}
public function removeSlugs(PhabricatorProject $project, array $slugs) {
if (!$slugs) {
return;
}
// We're going to try to delete both the literal and normalized versions
// of all slugs. This allows us to destroy old slugs that are no longer
// valid.
foreach ($this->normalizeSlugs($slugs) as $slug) {
$slugs[] = $slug;
}
$objects = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID = %s AND slug IN (%Ls)',
$project->getPHID(),
$slugs);
foreach ($objects as $object) {
$object->delete();
}
}
public function normalizeSlugs(array $slugs) {
foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug);
}
$slugs = array_unique($slugs);
$slugs = array_values($slugs);
return $slugs;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$member_xaction = null;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() !== $type_edge) {
continue;
}
$edgetype = $xaction->getMetadataValue('edge:type');
if ($edgetype !== $edgetype_member) {
continue;
}
$member_xaction = $xaction;
}
if ($member_xaction) {
$object_phid = $object->getPHID();
if ($object_phid) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getActor())
->withPHIDs(array($object_phid))
->needMembers(true)
->executeOne();
$members = $project->getMemberPHIDs();
} else {
$members = array();
}
$clone_xaction = clone $member_xaction;
$hint = $this->getPHIDTransactionNewValue($clone_xaction, $members);
$rule = new PhabricatorProjectMembersPolicyRule();
$hint = array_fuse($hint);
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
$rule,
$hint);
}
return $copy;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_milestone = $object->isMilestone();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
if ($xaction->getNewValue() !== null) {
$is_milestone = true;
}
break;
}
}
$this->setIsMilestone($is_milestone);
return $results;
}
- private function setDefaultProfilePicture(PhabricatorProject $project) {
- if ($project->isMilestone()) {
- return;
- }
-
- $compose_color = $project->getDisplayIconComposeColor();
- $compose_icon = $project->getDisplayIconComposeIcon();
-
- $builtin = id(new PhabricatorFilesComposeIconBuiltinFile())
- ->setColor($compose_color)
- ->setIcon($compose_icon);
-
- $data = $builtin->loadBuiltinFileData();
-
- $file = PhabricatorFile::newFromFileData(
- $data,
- array(
- 'name' => $builtin->getBuiltinDisplayName(),
- 'profile' => true,
- 'canCDN' => true,
- ));
-
- $project
- ->setProfileImagePHID($file->getPHID())
- ->save();
- }
-
-
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
// Herald rules may run on behalf of other users and need to execute
// membership checks against ancestors.
$project = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
return id(new PhabricatorProjectHeraldAdapter())
->setProject($project);
}
}
diff --git a/src/applications/project/icon/PhabricatorProjectIconSet.php b/src/applications/project/icon/PhabricatorProjectIconSet.php
index 2283026598..f487b216f3 100644
--- a/src/applications/project/icon/PhabricatorProjectIconSet.php
+++ b/src/applications/project/icon/PhabricatorProjectIconSet.php
@@ -1,465 +1,507 @@
<?php
final class PhabricatorProjectIconSet
extends PhabricatorIconSet {
const ICONSETKEY = 'projects';
const SPECIAL_MILESTONE = 'milestone';
public function getSelectIconTitleText() {
return pht('Choose Project Icon');
}
public static function getDefaultConfiguration() {
return array(
array(
'key' => 'project',
'icon' => 'fa-briefcase',
'name' => pht('Project'),
'default' => true,
+ 'image' => 'v3/briefcase.png',
),
array(
'key' => 'tag',
'icon' => 'fa-tags',
'name' => pht('Tag'),
+ 'image' => 'v3/tag.png',
),
array(
'key' => 'policy',
'icon' => 'fa-lock',
'name' => pht('Policy'),
+ 'image' => 'v3/lock.png',
),
array(
'key' => 'group',
'icon' => 'fa-users',
'name' => pht('Group'),
+ 'image' => 'v3/people.png',
),
array(
'key' => 'folder',
'icon' => 'fa-folder',
'name' => pht('Folder'),
+ 'image' => 'v3/folder.png',
),
array(
'key' => 'timeline',
'icon' => 'fa-calendar',
'name' => pht('Timeline'),
+ 'image' => 'v3/calendar.png',
),
array(
'key' => 'goal',
'icon' => 'fa-flag-checkered',
'name' => pht('Goal'),
+ 'image' => 'v3/flag.png',
),
array(
'key' => 'release',
'icon' => 'fa-truck',
'name' => pht('Release'),
+ 'image' => 'v3/truck.png',
),
array(
'key' => 'bugs',
'icon' => 'fa-bug',
'name' => pht('Bugs'),
+ 'image' => 'v3/bug.png',
),
array(
'key' => 'cleanup',
'icon' => 'fa-trash-o',
'name' => pht('Cleanup'),
+ 'image' => 'v3/trash.png',
),
array(
'key' => 'umbrella',
'icon' => 'fa-umbrella',
'name' => pht('Umbrella'),
+ 'image' => 'v3/umbrella.png',
),
array(
'key' => 'communication',
'icon' => 'fa-envelope',
'name' => pht('Communication'),
+ 'image' => 'v3/mail.png',
),
array(
'key' => 'organization',
'icon' => 'fa-building',
'name' => pht('Organization'),
+ 'image' => 'v3/organization.png',
),
array(
'key' => 'infrastructure',
'icon' => 'fa-cloud',
'name' => pht('Infrastructure'),
+ 'image' => 'v3/cloud.png',
),
array(
'key' => 'account',
'icon' => 'fa-credit-card',
'name' => pht('Account'),
+ 'image' => 'v3/creditcard.png',
),
array(
'key' => 'experimental',
'icon' => 'fa-flask',
'name' => pht('Experimental'),
+ 'image' => 'v3/experimental.png',
),
array(
'key' => 'milestone',
'icon' => 'fa-map-marker',
'name' => pht('Milestone'),
'special' => self::SPECIAL_MILESTONE,
+ 'image' => 'v3/marker.png',
),
);
}
protected function newIcons() {
$map = self::getIconSpecifications();
$icons = array();
foreach ($map as $spec) {
$special = idx($spec, 'special');
if ($special === self::SPECIAL_MILESTONE) {
continue;
}
$icons[] = id(new PhabricatorIconSetIcon())
->setKey($spec['key'])
->setIsDisabled(idx($spec, 'disabled'))
->setIcon($spec['icon'])
->setLabel($spec['name']);
}
return $icons;
}
private static function getIconSpecifications() {
return PhabricatorEnv::getEnvConfig('projects.icons');
}
public static function getDefaultIconKey() {
$icons = self::getIconSpecifications();
foreach ($icons as $icon) {
if (idx($icon, 'default')) {
return $icon['key'];
}
}
return null;
}
public static function getIconIcon($key) {
$spec = self::getIconSpec($key);
return idx($spec, 'icon', null);
}
public static function getIconName($key) {
$spec = self::getIconSpec($key);
return idx($spec, 'name', null);
}
+ public static function getIconImage($key) {
+ $spec = self::getIconSpec($key);
+ return idx($spec, 'image', 'v3/briefcase.png');
+ }
+
private static function getIconSpec($key) {
$icons = self::getIconSpecifications();
foreach ($icons as $icon) {
if (idx($icon, 'key') === $key) {
return $icon;
}
}
return array();
}
public static function getMilestoneIconKey() {
$icons = self::getIconSpecifications();
foreach ($icons as $icon) {
if (idx($icon, 'special') === self::SPECIAL_MILESTONE) {
return idx($icon, 'key');
}
}
return null;
}
public static function validateConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht('Configuration must be a list of project icon specifications.'));
}
foreach ($config as $idx => $value) {
if (!is_array($value)) {
throw new Exception(
pht(
'Value for index "%s" should be a dictionary.',
$idx));
}
PhutilTypeSpec::checkMap(
$value,
array(
'key' => 'string',
'name' => 'string',
'icon' => 'string',
+ 'image' => 'optional string',
'special' => 'optional string',
'disabled' => 'optional bool',
'default' => 'optional bool',
));
if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) {
throw new Exception(
pht(
'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '.
'characters long and contain only lowercase letters. For example, '.
'"%s" and "%s" are reasonable keys.',
'tag',
'group'));
}
$special = idx($value, 'special');
$valid = array(
self::SPECIAL_MILESTONE => true,
);
if ($special !== null) {
if (empty($valid[$special])) {
throw new Exception(
pht(
'Icon special attribute "%s" is not valid. Recognized special '.
'attributes are: %s.',
$special,
implode(', ', array_keys($valid))));
}
}
}
$default = null;
$milestone = null;
$keys = array();
foreach ($config as $idx => $value) {
$key = $value['key'];
if (isset($keys[$key])) {
throw new Exception(
pht(
'Project icons must have unique keys, but two icons share the '.
'same key ("%s").',
$key));
} else {
$keys[$key] = true;
}
$is_disabled = idx($value, 'disabled');
+ if (idx($value, 'image')) {
+ $builtin = idx($value, 'image');
+ $builtin_map = id(new PhabricatorFilesOnDiskBuiltinFile())
+ ->getProjectBuiltinFiles();
+ $builtin_map = array_flip($builtin_map);
+
+ $root = dirname(phutil_get_library_root('phabricator'));
+ $image = $root.'/resources/builtin/projects/'.$builtin;
+
+ if (!array_key_exists($image, $builtin_map)) {
+ throw new Exception(
+ pht(
+ 'The project image ("%s") specified for ("%s") '.
+ 'was not found in the folder "resources/builtin/projects/".',
+ $builtin,
+ $key));
+ }
+ }
+
if (idx($value, 'default')) {
if ($default === null) {
if ($is_disabled) {
throw new Exception(
pht(
'The project icon marked as the default icon ("%s") must not '.
'be disabled.',
$key));
}
$default = $value;
} else {
$original_key = $default['key'];
throw new Exception(
pht(
'Two different icons ("%s", "%s") are marked as the default '.
'icon. Only one icon may be marked as the default.',
$key,
$original_key));
}
}
$special = idx($value, 'special');
if ($special === self::SPECIAL_MILESTONE) {
if ($milestone === null) {
if ($is_disabled) {
throw new Exception(
pht(
'The project icon ("%s") with special attribute "%s" must '.
'not be disabled',
$key,
self::SPECIAL_MILESTONE));
}
$milestone = $value;
} else {
$original_key = $milestone['key'];
throw new Exception(
pht(
'Two different icons ("%s", "%s") are marked with special '.
'attribute "%s". Only one icon may be marked with this '.
'attribute.',
$key,
$original_key,
self::SPECIAL_MILESTONE));
}
}
}
if ($default === null) {
throw new Exception(
pht(
'Project icons must include one icon marked as the "%s" icon, '.
'but no such icon exists.',
'default'));
}
if ($milestone === null) {
throw new Exception(
pht(
'Project icons must include one icon marked with special attribute '.
'"%s", but no such icon exists.',
self::SPECIAL_MILESTONE));
}
}
private static function getColorSpecifications() {
return PhabricatorEnv::getEnvConfig('projects.colors');
}
public static function getColorMap() {
$specifications = self::getColorSpecifications();
return ipull($specifications, 'name', 'key');
}
public static function getDefaultColorKey() {
$specifications = self::getColorSpecifications();
foreach ($specifications as $specification) {
if (idx($specification, 'default')) {
return $specification['key'];
}
}
return null;
}
private static function getAvailableColorKeys() {
$list = array();
$specifications = self::getDefaultColorMap();
foreach ($specifications as $specification) {
$list[] = $specification['key'];
}
return $list;
}
public static function getColorName($color_key) {
$map = self::getColorMap();
return idx($map, $color_key);
}
public static function getDefaultColorMap() {
return array(
array(
'key' => PHUITagView::COLOR_RED,
'name' => pht('Red'),
),
array(
'key' => PHUITagView::COLOR_ORANGE,
'name' => pht('Orange'),
),
array(
'key' => PHUITagView::COLOR_YELLOW,
'name' => pht('Yellow'),
),
array(
'key' => PHUITagView::COLOR_GREEN,
'name' => pht('Green'),
),
array(
'key' => PHUITagView::COLOR_BLUE,
'name' => pht('Blue'),
'default' => true,
),
array(
'key' => PHUITagView::COLOR_INDIGO,
'name' => pht('Indigo'),
),
array(
'key' => PHUITagView::COLOR_VIOLET,
'name' => pht('Violet'),
),
array(
'key' => PHUITagView::COLOR_PINK,
'name' => pht('Pink'),
),
array(
'key' => PHUITagView::COLOR_GREY,
'name' => pht('Grey'),
),
array(
'key' => PHUITagView::COLOR_CHECKERED,
'name' => pht('Checkered'),
),
);
}
public static function validateColorConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht('Configuration must be a list of project color specifications.'));
}
$available_keys = self::getAvailableColorKeys();
$available_keys = array_fuse($available_keys);
foreach ($config as $idx => $value) {
if (!is_array($value)) {
throw new Exception(
pht(
'Value for index "%s" should be a dictionary.',
$idx));
}
PhutilTypeSpec::checkMap(
$value,
array(
'key' => 'string',
'name' => 'string',
'default' => 'optional bool',
));
$key = $value['key'];
if (!isset($available_keys[$key])) {
throw new Exception(
pht(
'Color key "%s" is not a valid color key. The supported color '.
'keys are: %s.',
$key,
implode(', ', $available_keys)));
}
}
$default = null;
$keys = array();
foreach ($config as $idx => $value) {
$key = $value['key'];
if (isset($keys[$key])) {
throw new Exception(
pht(
'Project colors must have unique keys, but two icons share the '.
'same key ("%s").',
$key));
} else {
$keys[$key] = true;
}
if (idx($value, 'default')) {
if ($default === null) {
$default = $value;
} else {
$original_key = $default['key'];
throw new Exception(
pht(
'Two different colors ("%s", "%s") are marked as the default '.
'color. Only one color may be marked as the default.',
$key,
$original_key));
}
}
}
if ($default === null) {
throw new Exception(
pht(
'Project colors must include one color marked as the "%s" color, '.
'but no such color exists.',
'default'));
}
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index 44055b7260..780f072678 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,804 +1,800 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $watcherPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $namePrefixes;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $statuses;
private $needSlugs;
private $needMembers;
private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withWatcherPHIDs(array $watcher_phids) {
$this->watcherPHIDs = $watcher_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function withMilestoneNumberBetween($min, $max) {
$this->minMilestoneNumber = $min;
$this->maxMilestoneNumber = $max;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needAncestorMembers($need_ancestor_members) {
$this->needAncestorMembers = $need_ancestor_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
'milestoneNumber' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'milestoneNumber',
'type' => 'int',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'id' => $project->getID(),
'name' => $project->getName(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
$all_graph = $this->getAllReachableAncestors($projects);
// NOTE: Although we may not need much information about ancestors, we
// always need to test if the viewer is a member, because we will return
// ancestor projects to the policy filter via ExtendedPolicy calls. If
// we skip populating membership data on a parent, the policy framework
// will think the user is not a member of the parent project.
$all_sources = array();
foreach ($all_graph as $project) {
// For milestones, we need parent members.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$all_sources[$parent_phid] = $parent_phid;
}
$phid = $project->getPHID();
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
$need_all_edges =
$this->needMembers ||
$this->needWatchers ||
$this->needAncestorMembers;
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
foreach ($all_graph as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
// If we loaded ancestor members, we've already populated membership
// lists above, so we can skip this step.
if (!$this->needAncestorMembers) {
$member_graph = $this->getAllReachableAncestors($membership_projects);
foreach ($all_graph as $phid => $project) {
$is_member = isset($member_graph[$phid]);
$project->setIsUserMember($viewer_phid, $is_member);
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
if ($this->needImages) {
- $default = null;
-
$file_phids = mpull($projects, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($projects as $project) {
$file = idx($files, $project->getProfileImagePHID());
if (!$file) {
- if (!$default) {
- $default = PhabricatorFile::loadBuiltin(
- $this->getViewer(),
- 'project.png');
- }
- $file = $default;
+ $builtin = PhabricatorProjectIconSet::getIconImage(
+ $project->getIcon());
+ $file = PhabricatorFile::loadBuiltin($this->getViewer(),
+ 'projects/'.$builtin);
}
$project->attachProfileImageFile($file);
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->watcherPHIDs !== null) {
$where[] = qsprintf(
$conn,
'w.dst IN (%Ls)',
$this->watcherPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'name LIKE %>',
$name_prefix);
}
$where[] = '('.implode(' OR ', $parts).')';
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = '('.implode(' OR ', $sql).')';
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
if ($this->minMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber >= %d',
$this->minMilestoneNumber);
}
if ($this->maxMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber <= %d',
$this->maxMilestoneNumber);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->watcherPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T w ON w.src = p.phid AND w.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 15, 3:47 PM (15 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
338116
Default Alt Text
(68 KB)

Event Timeline