1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-15 17:21:10 +01:00

Provide wiki pages for projects

Summary:
Provide tighter integration between Projects and Phriction. Partly, I have most
of a rewrite for the Projects homepage ready but it's not currently possible to
publish feed stories about a project so all the feeds are empty/boring. This
partly makes them more useful and partly just provides a tool integration point.

  - When you create a project, all the wiki pages in projects/<project_name>/*
are associated with it.
  - Publish updates to those pages as being related to the project so they'll
show up in project feeds.
  - Show a project link on those pages.

This is very "convention over configuration" but I think it's the right
approach. We could provide some sort of, like, "@project=derp" tag to let you
associated arbitrary pages to projects later, but just letting you move pages is
probably far better.

Test Plan:
  - Ran upgrade scripts against stupidly named projects ("der", "  der", "  der
", "der (2)", "  der (2) (2)", etc). Ended up with uniquely named projects.
  - Ran unit tests.
  - Created /projects/ wiki documents and made sure they displayed correctly.
  - Verified feed stories publish as project-related.
  - Edited projects, including perfomring a name-colliding edit.
  - Created projects, including performing a name-colliding create.

Reviewers: btrahan, jungejason

Reviewed By: btrahan

CC: aran, epriestley, btrahan

Maniphest Tasks: T681

Differential Revision: 1231
This commit is contained in:
epriestley 2011-12-17 11:58:55 -08:00
parent c80d1480d5
commit 21ba07d5bd
20 changed files with 427 additions and 15 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE phabricator_project.project
ADD phrictionSlug varchar(512);

View file

@ -0,0 +1,117 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
echo "Ensuring project names are unique enough...\n";
$projects = id(new PhabricatorProject())->loadAll();
$slug_map = array();
foreach ($projects as $project) {
$project->setPhrictionSlug($project->getName());
$slug = $project->getPhrictionSlug();
if ($slug == '/') {
$project_id = $project->getID();
echo "Project #{$project_id} doesn't have a meaningful name...\n";
$project->setName(trim('Unnamed Project '.$project->getName()));
}
$slug_map[$slug][] = $project->getID();
}
foreach ($slug_map as $slug => $similar) {
if (count($similar) <= 1) {
continue;
}
echo "Too many projects are similar to '{$slug}'...\n";
foreach (array_slice($similar, 1, null, true) as $key => $project_id) {
$project = $projects[$project_id];
$old_name = $project->getName();
$new_name = rename_project($project, $projects);
echo "Renaming project #{$project_id} ".
"from '{$old_name}' to '{$new_name}'.\n";
$project->setName($new_name);
}
}
$update = $projects;
while ($update) {
$size = count($update);
foreach ($update as $key => $project) {
$id = $project->getID();
$name = $project->getName();
$project->setPhrictionSlug($name);
$slug = $project->getPhrictionSlug();
echo "Updating project #{$id} '{$name}' ({$slug})...";
try {
queryfx(
$project->establishConnection('w'),
'UPDATE %T SET name = %s, phrictionSlug = %s WHERE id = %d',
$project->getTableName(),
$name,
$slug,
$project->getID());
unset($update[$key]);
echo "okay.\n";
} catch (AphrontQueryDuplicateKeyException $ex) {
echo "failed, will retry.\n";
}
}
if (count($update) == $size) {
throw new Exception(
"Failed to make any progress while updating projects. Schema upgrade ".
"has failed. Go manually fix your project names to be unique (they are ".
"probably ridiculous?) and then try again.");
}
}
echo "Done.\n";
/**
* Rename the project so that it has a unique slug, by appending (2), (3), etc.
* to its name.
*/
function rename_project($project, $projects) {
$suffix = 2;
while (true) {
$new_name = $project->getName().' ('.$suffix.')';
$project->setPhrictionSlug($new_name);
$new_slug = $project->getPhrictionSlug();
$okay = true;
foreach ($projects as $other) {
if ($other->getID() == $project->getID()) {
continue;
}
if ($other->getPhrictionSlug() == $new_slug) {
$okay = false;
break;
}
}
if ($okay) {
break;
} else {
$suffix++;
}
}
return $new_name;
}

View file

@ -0,0 +1,2 @@
ALTER TABLE phabricator_project.project
ADD UNIQUE KEY (phrictionSlug);

View file

@ -575,7 +575,9 @@ phutil_register_library_map(array(
'PhabricatorProjectController' => 'applications/project/controller/base',
'PhabricatorProjectCreateController' => 'applications/project/controller/create',
'PhabricatorProjectDAO' => 'applications/project/storage/base',
'PhabricatorProjectEditor' => 'applications/project/editor/project',
'PhabricatorProjectListController' => 'applications/project/controller/list',
'PhabricatorProjectNameCollisionException' => 'applications/project/exception/namecollison',
'PhabricatorProjectProfile' => 'applications/project/storage/profile',
'PhabricatorProjectProfileController' => 'applications/project/controller/profile',
'PhabricatorProjectProfileEditController' => 'applications/project/controller/profileedit',

View file

@ -98,7 +98,19 @@ class PhrictionDocumentController
}
$page_title = $content->getTitle();
$phids = array($content->getAuthorPHID());
$project_phid = null;
if (PhrictionDocument::isProjectSlug($slug)) {
$project = id(new PhabricatorProject())->loadOneWhere(
'phrictionSlug = %s',
PhrictionDocument::getProjectSlugIdentifier($slug));
$project_phid = $project->getPHID();
}
$phids = array_filter(
array(
$content->getAuthorPHID(),
$project_phid,
));
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$age = time() - $content->getDateCreated();
@ -112,10 +124,21 @@ class PhrictionDocumentController
$when = "{$age} days ago";
}
$project_info = null;
if ($project_phid) {
$project_info =
'<br />This document is about the project '.
$handles[$project_phid]->renderLink().'.';
}
$byline =
'<div class="phriction-byline">'.
"Last updated {$when} by ".
$handles[$content->getAuthorPHID()]->renderLink().'.'.
$project_info.
'</div>';
$engine = PhabricatorMarkupEngine::newPhrictionMarkupEngine();

View file

@ -15,6 +15,7 @@ phutil_require_module('phabricator', 'applications/phriction/constants/documents
phutil_require_module('phabricator', 'applications/phriction/controller/base');
phutil_require_module('phabricator', 'applications/phriction/storage/content');
phutil_require_module('phabricator', 'applications/phriction/storage/document');
phutil_require_module('phabricator', 'applications/project/storage/project');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/form/error');

View file

@ -201,12 +201,28 @@ final class PhrictionDocumentEditor {
$document->attachContent($new_content);
PhabricatorSearchPhrictionIndexer::indexDocument($document);
$project_phid = null;
$slug = $document->getSlug();
if (PhrictionDocument::isProjectSlug($slug)) {
$project = id(new PhabricatorProject())->loadOneWhere(
'phrictionSlug = %s',
PhrictionDocument::getProjectSlugIdentifier($slug));
if ($project) {
$project_phid = $project->getPHID();
}
}
$related_phids = array(
$document->getPHID(),
$this->user->getPHID(),
);
if ($project_phid) {
$related_phids[] = $project_phid;
}
id(new PhabricatorFeedStoryPublisher())
->setRelatedPHIDs(
array(
$document->getPHID(),
$this->user->getPHID(),
))
->setRelatedPHIDs($related_phids)
->setStoryAuthorPHID($this->user->getPHID())
->setStoryTime(time())
->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PHRICTION)
@ -215,6 +231,7 @@ final class PhrictionDocumentEditor {
'phid' => $document->getPHID(),
'action' => $feed_action,
'content' => phutil_utf8_shorten($new_content->getContent(), 140),
'project' => $project_phid,
))
->publish();

View file

@ -13,6 +13,7 @@ phutil_require_module('phabricator', 'applications/phriction/constants/changetyp
phutil_require_module('phabricator', 'applications/phriction/constants/documentstatus');
phutil_require_module('phabricator', 'applications/phriction/storage/content');
phutil_require_module('phabricator', 'applications/phriction/storage/document');
phutil_require_module('phabricator', 'applications/project/storage/project');
phutil_require_module('phabricator', 'applications/search/index/indexer/phriction');
phutil_require_module('phutil', 'utils');

View file

@ -135,4 +135,25 @@ class PhrictionDocument extends PhrictionDAO {
return $this->contentObject;
}
public static function isProjectSlug($slug) {
$slug = self::normalizeSlug($slug);
$prefix = 'projects/';
if ($slug == $prefix) {
// The 'projects/' document is not itself a project slug.
return false;
}
return !strncmp($slug, $prefix, strlen($prefix));
}
public static function getProjectSlugIdentifier($slug) {
if (!self::isProjectSlug($slug)) {
throw new Exception("Slug '{$slug}' is not a project slug!");
}
$slug = self::normalizeSlug($slug);
$parts = explode('/', $slug);
return $parts[1].'/';
}
}

View file

@ -26,6 +26,7 @@ class PhrictionDocumentTestCase extends PhabricatorTestCase {
'' => '/',
'/' => '/',
'//' => '/',
'&&&' => '/',
'/derp/' => 'derp/',
'derp' => 'derp/',
'derp//derp' => 'derp/derp/',
@ -72,4 +73,47 @@ class PhrictionDocumentTestCase extends PhabricatorTestCase {
}
}
public function testProjectSlugs() {
$slugs = array(
'/' => false,
'zebra/' => false,
'projects/' => false,
'projects/a/' => true,
'projects/a/b/' => true,
'stuff/projects/a/' => false,
);
foreach ($slugs as $slug => $expect) {
$this->assertEqual(
$expect,
PhrictionDocument::isProjectSlug($slug),
"Is '{$slug}' a project slug?");
}
}
public function testProjectSlugIdentifiers() {
$slugs = array(
'projects/' => null,
'derp/' => null,
'projects/a/' => 'a/',
'projects/a/b/' => 'a/',
);
foreach ($slugs as $slug => $expect) {
$ex = null;
$result = null;
try {
$result = PhrictionDocument::getProjectSlugIdentifier($slug);
} catch (Exception $e) {
$ex = $e;
}
if ($expect === null) {
$this->assertEqual(true, (bool)$ex, "Slug '{$slug}' is invalid.");
} else {
$this->assertEqual($expect, $result, "Slug '{$slug}' identifier.");
}
}
}
}

View file

@ -33,18 +33,20 @@ class PhabricatorProjectCreateController
$errors = array();
if ($request->isFormPost()) {
$project->setName($request->getStr('name'));
try {
$editor = new PhabricatorProjectEditor($project);
$editor->setUser($user);
$editor->setName($request->getStr('name'));
$editor->save();
} catch (PhabricatorProjectNameCollisionException $ex) {
$e_name = 'Not Unique';
$errors[] = $ex->getMessage();
}
$project->setStatus(PhabricatorProjectStatus::ONGOING);
$profile->setBlurb($request->getStr('blurb'));
if (!strlen($project->getName())) {
$e_name = 'Required';
$errors[] = 'Project name is required.';
} else {
$e_name = null;
}
if (!$errors) {
$project->save();
$profile->setProjectPHID($project->getPHID());

View file

@ -11,6 +11,7 @@ phutil_require_module('phabricator', 'aphront/response/dialog');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/project/constants/status');
phutil_require_module('phabricator', 'applications/project/controller/base');
phutil_require_module('phabricator', 'applications/project/editor/project');
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/profile');
phutil_require_module('phabricator', 'applications/project/storage/project');

View file

@ -56,7 +56,17 @@ class PhabricatorProjectProfileEditController
$errors = array();
$state = null;
if ($request->isFormPost()) {
$project->setName($request->getStr('name'));
try {
$editor = new PhabricatorProjectEditor($project);
$editor->setUser($user);
$editor->setName($request->getStr('name'));
$editor->save();
} catch (PhabricatorProjectNameCollisionException $ex) {
$e_name = 'Not Unique';
$errors[] = $ex->getMessage();
}
$project->setStatus($request->getStr('status'));
$project->setSubprojectPHIDs($request->getArr('set_subprojects'));
$profile->setBlurb($request->getStr('blurb'));

View file

@ -13,6 +13,7 @@ phutil_require_module('phabricator', 'applications/files/transform');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'applications/project/constants/status');
phutil_require_module('phabricator', 'applications/project/controller/base');
phutil_require_module('phabricator', 'applications/project/editor/project');
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/profile');
phutil_require_module('phabricator', 'applications/project/storage/project');

View file

@ -0,0 +1,103 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorProjectEditor {
private $project;
private $user;
private $projectName;
public function __construct(PhabricatorProject $project) {
$this->project = $project;
}
public function setName($name) {
$this->projectName = $name;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function save() {
if (!$this->user) {
throw new Exception('Call setUser() before save()!');
}
$project = $this->project;
$is_new = !$project->getID();
if ($is_new) {
$project->setAuthorPHID($this->user->getPHID());
}
if (($this->projectName !== null) &&
($this->projectName !== $project->getName())) {
$project->setName($this->projectName);
$project->setPhrictionSlug($this->projectName);
$this->validateName($project);
}
try {
$project->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// We already validated the slug, but might race. Try again to see if
// that's the issue. If it is, we'll throw a more specific exception. If
// not, throw the original exception.
$this->validateName($project);
throw $ex;
}
// TODO: If we rename a project, we should move its Phriction page. Do
// that once Phriction supports document moves.
return $this;
}
private function validateName(PhabricatorProject $project) {
$slug = $project->getPhrictionSlug();
$name = $project->getName();
if ($slug == '/') {
throw new PhabricatorProjectNameCollisionException(
"Project names must be unique and contain some letters or numbers.");
}
$id = $project->getID();
$collision = id(new PhabricatorProject())->loadOneWhere(
'(name = %s OR phrictionSlug = %s) AND id %Q %nd',
$name,
$slug,
$id ? '!=' : 'IS NOT',
$id ? $id : null);
if ($collision) {
$other_name = $collision->getName();
$other_id = $collision->getID();
throw new PhabricatorProjectNameCollisionException(
"Project names must be unique. The name '{$name}' is too similar to ".
"the name of another project, '{$other_name}' (Project ID: ".
"{$other_id}). Choose a unique name.");
}
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/project/exception/namecollison');
phutil_require_module('phabricator', 'applications/project/storage/project');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorProjectEditor.php');

View file

@ -0,0 +1,25 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Thrown when you try to save a project with a name too similar to another
* project.
*/
final class PhabricatorProjectNameCollisionException extends Exception {
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorProjectNameCollisionException.php');

View file

@ -23,6 +23,7 @@ class PhabricatorProject extends PhabricatorProjectDAO {
protected $status = PhabricatorProjectStatus::UNKNOWN;
protected $authorPHID;
protected $subprojectPHIDs = array();
protected $phrictionSlug;
private $subprojectsNeedUpdate;
@ -59,6 +60,19 @@ class PhabricatorProject extends PhabricatorProjectDAO {
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 = PhrictionDocument::normalizeSlug($slug);
$this->phrictionSlug = $slug;
return $this;
}
public function save() {
$result = parent::save();

View file

@ -8,6 +8,7 @@
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/storage/phid');
phutil_require_module('phabricator', 'applications/phriction/storage/document');
phutil_require_module('phabricator', 'applications/project/constants/status');
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/base');