diff --git a/src/applications/conduit/method/project/ConduitAPI_project_Method.php b/src/applications/conduit/method/project/ConduitAPI_project_Method.php index a5762bcaa7..96bdc7f921 100644 --- a/src/applications/conduit/method/project/ConduitAPI_project_Method.php +++ b/src/applications/conduit/method/project/ConduitAPI_project_Method.php @@ -1,54 +1,54 @@ buildProjectInfoDictionaries(array($project)); return idx($results, $project->getPHID()); } protected function buildProjectInfoDictionaries(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); if (!$projects) { return array(); } $result = array(); foreach ($projects as $project) { - $member_phids = mpull($project->getAffiliations(), 'getUserPHID'); + $member_phids = $project->getMemberPHIDs(); $member_phids = array_values($member_phids); $result[$project->getPHID()] = array( 'id' => $project->getID(), 'phid' => $project->getPHID(), 'name' => $project->getName(), 'members' => $member_phids, 'dateCreated' => $project->getDateCreated(), 'dateModified' => $project->getDateModified(), ); } return $result; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 5f781a5609..9bdcd97a72 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,369 +1,365 @@ id = idx($data, 'id'); $this->page = idx($data, 'page'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProject())->load($this->id); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (!$profile) { $profile = new PhabricatorProjectProfile(); } $picture = $profile->loadProfileImageURI(); - - $members = mpull($project->loadAffiliations(), null, 'getUserPHID'); + $members = $project->loadMemberPHIDs(); + $member_map = array_fill_keys($members, true); $nav_view = new AphrontSideNavFilterView(); $uri = new PhutilURI('/project/view/'.$project->getID().'/'); $nav_view->setBaseURI($uri); $external_arrow = "\xE2\x86\x97"; $tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID(); $slug = PhabricatorSlug::normalize($project->getName()); $phriction_uri = '/w/projects/'.$slug; $edit_uri = '/project/edit/'.$project->getID().'/'; $nav_view->addFilter('dashboard', 'Dashboard'); $nav_view->addSpacer(); $nav_view->addFilter('feed', 'Feed'); $nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri); $nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri); $nav_view->addFilter('people', 'People'); $nav_view->addFilter('about', 'About'); $nav_view->addSpacer(); $nav_view->addFilter(null, "Edit Project\xE2\x80\xA6", $edit_uri); $this->page = $nav_view->selectFilter($this->page, 'dashboard'); require_celerity_resource('phabricator-profile-css'); switch ($this->page) { case 'dashboard': $content = $this->renderTasksPage($project, $profile); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $project->getPHID(), )); $query->setLimit(50); $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $content .= $this->renderStories($stories); break; case 'about': $content = $this->renderAboutPage($project, $profile); break; case 'people': $content = $this->renderPeoplePage($project, $profile); break; case 'feed': $content = $this->renderFeedPage($project, $profile); break; default: throw new Exception("Unimplemented filter '{$this->page}'."); } $content = '
'.$content.'
'; $nav_view->appendChild($content); $header = new PhabricatorProfileHeaderView(); $header->setName($project->getName()); $header->setDescription( phutil_utf8_shorten($profile->getBlurb(), 1024)); $header->setProfilePicture($picture); $action = null; - if (empty($members[$user->getPHID()])) { + if (empty($member_map[$user->getPHID()])) { $action = phabricator_render_form( $user, array( 'action' => '/project/update/'.$project->getID().'/join/', 'method' => 'post', ), phutil_render_tag( 'button', array( 'class' => 'green', ), 'Join Project')); } else { $action = javelin_render_tag( 'a', array( 'href' => '/project/update/'.$project->getID().'/leave/', 'sigil' => 'workflow', 'class' => 'grey button', ), 'Leave Project...'); } $header->addAction($action); $header->appendChild($nav_view); return $this->buildStandardPageResponse( $header, array( 'title' => $project->getName().' Project', )); } private function renderAboutPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $viewer = $this->getRequest()->getUser(); $blurb = $profile->getBlurb(); $blurb = phutil_escape_html($blurb); $blurb = str_replace("\n", '
', $blurb); $phids = array_merge( array($project->getAuthorPHID()), $project->getSubprojectPHIDs() ); $phids = array_unique($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $timestamp = phabricator_datetime($project->getDateCreated(), $viewer); $about = '

About

Creator '.$handles[$project->getAuthorPHID()]->renderLink().'
Created '.$timestamp.'
PHID '.phutil_escape_html($project->getPHID()).'
Blurb '.$blurb.'
'; if ($project->getSubprojectPHIDs()) { $table = $this->renderSubprojectTable( $handles, $project->getSubprojectPHIDs()); $subproject_list = $table->render(); } else { $subproject_list = '

No subprojects.

'; } $about .= '
'. '

Subprojects

'. '
'. $subproject_list. '
'. '
'; return $about; } private function renderPeoplePage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { - $affiliations = $project->loadAffiliations(); - - $phids = mpull($affiliations, 'getUserPHID'); - $handles = id(new PhabricatorObjectHandleData($phids)) + $member_phids = $project->loadMemberPHIDs(); + $handles = id(new PhabricatorObjectHandleData($member_phids)) ->loadHandles(); $affiliated = array(); - foreach ($affiliations as $affiliation) { - $user = $handles[$affiliation->getUserPHID()]->renderLink(); - $role = phutil_escape_html($affiliation->getRole()); - $affiliated[] = '
  • '.$user.' — '.$role.'
  • '; + foreach ($handles as $phids => $handle) { + $affiliated[] = '
  • '.$handle->renderLink().'
  • '; } if ($affiliated) { $affiliated = ''; } else { $affiliated = '

    No one is affiliated with this project.

    '; } return '
    '. '

    People

    '. '
    '. $affiliated. '
    '. '
    '; } private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); $query->setViewer($this->getRequest()->getUser()); $query->setLimit(100); $stories = $query->execute(); if (!$stories) { return 'There are no stories about this project.'; } return $this->renderStories($stories); } private function renderStories(array $stories) { assert_instances_of($stories, 'PhabricatorFeedStory'); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $view = $builder->buildView(); return '
    '. '

    Activity Feed

    '. '
    '. $view->render(). '
    '. '
    '; } private function renderTasksPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = id(new ManiphestTaskQuery()) ->withProjects(array($project->getPHID())) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10) ->setCalculateRows(true); $tasks = $query->execute(); $count = $query->getRowCount(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_filter($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $task_views = array(); foreach ($tasks as $task) { $view = id(new ManiphestTaskSummaryView()) ->setTask($task) ->setHandles($handles) ->setUser($this->getRequest()->getUser()); $task_views[] = $view->render(); } if (empty($tasks)) { $task_views = 'No open tasks.'; } else { $task_views = implode('', $task_views); } $open = number_format($count); $more_link = phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/?projects='.$project->getPHID(), ), "View All Open Tasks \xC2\xBB"); $content = '

    '. "Open Tasks ({$open})". '

    '. '
    '. $task_views. ''. '
    '; return $content; } private function renderSubprojectTable( array $handles, array $subprojects_phids) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $rows = array(); foreach ($subprojects_phids as $subproject_phid) { $phid = $handles[$subproject_phid]->getPHID(); $rows[] = array( phutil_escape_html($handles[$phid]->getFullName()), phutil_render_tag( 'a', array( 'class' => 'small grey button', 'href' => $handles[$phid]->getURI(), ), 'View Project Profile'), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Name', '', )); $table->setColumnClasses( array( 'pri', 'action right', )); return $table; } } diff --git a/src/applications/project/controller/PhabricatorProjectUpdateController.php b/src/applications/project/controller/PhabricatorProjectUpdateController.php index 29e589042c..32adf77bc8 100644 --- a/src/applications/project/controller/PhabricatorProjectUpdateController.php +++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php @@ -1,113 +1,114 @@ id = $data['id']; $this->action = $data['action']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProject())->load($this->id); if (!$project) { return new Aphront404Response(); } $process_action = false; switch ($this->action) { case 'join': $process_action = $request->isFormPost(); break; case 'leave': $process_action = $request->isDialogFormPost(); break; default: return new Aphront404Response(); } $project_uri = '/project/view/'.$project->getID().'/'; if ($process_action) { $xactions = array(); + switch ($this->action) { case 'join': - $affils = $project->loadAffiliations(); - $affils = mpull($affils, null, 'getUserPHID'); - if (empty($affils[$user->getPHID()])) { - $affils[$user->getPHID()] = true; + $member_phids = $project->loadMemberPHIDs(); + $member_map = array_fill_keys($member_phids, true); + if (empty($member_map[$user->getPHID()])) { + $member_map[$user->getPHID()] = true; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_MEMBERS); - $xaction->setNewValue(array_keys($affils)); + $xaction->setNewValue(array_keys($member_map)); $xactions[] = $xaction; } break; case 'leave': - $affils = $project->loadAffiliations(); - $affils = mpull($affils, null, 'getUserPHID'); - if (isset($affils[$user->getPHID()])) { - unset($affils[$user->getPHID()]); + $member_phids = $project->loadMemberPHIDs(); + $member_map = array_fill_keys($member_phids, true); + if (isset($member_map[$user->getPHID()])) { + unset($member_map[$user->getPHID()]); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_MEMBERS); - $xaction->setNewValue(array_keys($affils)); + $xaction->setNewValue(array_keys($member_map)); $xactions[] = $xaction; } break; } if ($xactions) { $editor = new PhabricatorProjectEditor($project); $editor->setUser($user); $editor->applyTransactions($xactions); } return id(new AphrontRedirectResponse())->setURI($project_uri); } $dialog = null; switch ($this->action) { case 'leave': $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle('Really leave project?'); $dialog->appendChild( '

    Your tremendous contributions to this project will be sorely '. 'missed. Are you sure you want to leave?

    '); $dialog->addCancelButton($project_uri); $dialog->addSubmitButton('Leave Project'); break; default: return new Aphront404Response(); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index c279d0a953..47a44ccc4b 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -1,103 +1,111 @@ true, self::CONFIG_SERIALIZATION => array( 'subprojectPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_PROJ); } public function setSubprojectPHIDs(array $phids) { $this->subprojectPHIDs = $phids; $this->subprojectsNeedUpdate = true; return $this; } public function loadProfile() { $profile = id(new PhabricatorProjectProfile())->loadOneWhere( 'projectPHID = %s', $this->getPHID()); return $profile; } + public function getMemberPHIDs() { + return mpull($this->getAffiliations(), 'getUserPHID'); + } + + public function loadMemberPHIDs() { + return mpull($this->loadAffiliations(), 'getUserPHID'); + } + public function getAffiliations() { if ($this->affiliations === null) { throw new Exception('Attach affiliations first!'); } return $this->affiliations; } public function attachAffiliations(array $affiliations) { assert_instances_of($affiliations, 'PhabricatorProjectAffiliation'); $this->affiliations = $affiliations; return $this; } public function loadAffiliations() { $affils = PhabricatorProjectAffiliation::loadAllForProjectPHIDs( array($this->getPHID())); return $affils[$this->getPHID()]; } public function setPhrictionSlug($slug) { // NOTE: We're doing a little magic here and stripping out '/' so that // project pages always appear at top level under projects/ even if the // display name is "Hack / Slash" or similar (it will become // 'hack_slash' instead of 'hack/slash'). $slug = str_replace('/', ' ', $slug); $slug = PhabricatorSlug::normalize($slug); $this->phrictionSlug = $slug; return $this; } public function save() { $result = parent::save(); if ($this->subprojectsNeedUpdate) { // If we've changed the project PHIDs for this task, update the link // table. PhabricatorProjectSubproject::updateProjectSubproject($this); $this->subprojectsNeedUpdate = false; } return $result; } }