API-CHANGE: adding document versioning

This commit is contained in:
Julian Seidenberg 2012-11-21 18:24:46 +13:00
parent 3643d3f3a6
commit 1bd5d929ef
4 changed files with 386 additions and 18 deletions

View File

@ -1,5 +1,6 @@
<?php <?php
define('DMS_DIR', 'dms'); define('DMS_DIR', 'dms');
if (!file_exists(BASE_PATH . DIRECTORY_SEPARATOR . DMS_DIR)) user_error("DMS directory named incorrectly. Please install the DMS module into a folder named: ".DMS_DIR);
Object::add_extension('SiteTree','DMSSiteTreeExtension'); Object::add_extension('SiteTree','DMSSiteTreeExtension');
Object::add_extension('HtmlEditorField_Toolbar','DocumentHtmlEditorFieldToolbar'); Object::add_extension('HtmlEditorField_Toolbar','DocumentHtmlEditorFieldToolbar');
@ -7,4 +8,10 @@ CMSMenu::remove_menu_item('DMSDocumentAddController');
ShortcodeParser::get('default')->register('dms_document_link', array('DMSDocument_Controller', 'dms_link_shortcode_handler')); ShortcodeParser::get('default')->register('dms_document_link', array('DMSDocument_Controller', 'dms_link_shortcode_handler'));
if (!file_exists(BASE_PATH . DIRECTORY_SEPARATOR . DMS_DIR)) user_error("DMS directory named incorrectly. Please install the DMS module into a folder named: ".DMS_DIR); DMSDocument_versions::$enable_versions = true;
if (DMSDocument_versions::$enable_versions) {
//using the same db relations for the versioned documents, as for the actual documents
Config::inst()->update('DMSDocument_versions', 'db', DMSDocument::$db);
}

View File

@ -365,7 +365,8 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
* @return DataList List of Document objects * @return DataList List of Document objects
*/ */
function getVersions() { function getVersions() {
// TODO: Implement getVersions() method. if (!DMSDocument_versions::$enable_versions) user_error("DMSDocument versions are disabled",E_USER_WARNING);
return DMSDocument_versions::get_versions($this);
} }
/** /**
@ -405,10 +406,22 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
$this->removeAllPages(); $this->removeAllPages();
//get rid of any versions have saved for this DMSDocument, too
if (DMSDocument_versions::$enable_versions) {
$versions = $this->getVersions();
if ($versions->Count() > 0) {
foreach($versions as $v) {
$v->delete();
}
}
}
//delete the dataobject //delete the dataobject
parent::delete(); parent::delete();
} }
/** /**
* Relate an existing file on the filesystem to the document. * Relate an existing file on the filesystem to the document.
* Copies the file to the new destination, as defined in {@link get_DMS_path()}. * Copies the file to the new destination, as defined in {@link get_DMS_path()}.
@ -427,6 +440,12 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
//copy the file into place //copy the file into place
$fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath; $fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath;
//version the existing file (copy it to a new "very specific" filename
if (DMSDocument_versions::$enable_versions) {
DMSDocument_versions::create_version($this);
}
copy($fromPath, $toPath); //this will overwrite the existing file (if present) copy($fromPath, $toPath); //this will overwrite the existing file (if present)
//write the filename of the stored document //write the filename of the stored document
@ -510,6 +529,9 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
$fields = new FieldList(); //don't use the automatic scaffolding, it is slow and unnecessary here $fields = new FieldList(); //don't use the automatic scaffolding, it is slow and unnecessary here
$extraTasks = ''; //additional text to inject into the list of tasks at the bottom of a DMSDocument CMSfield
$extraFields = FormField::create('Empty');
//get list of shortcode page relations //get list of shortcode page relations
$relationFinder = new ShortCodeRelationFinder(); $relationFinder = new ShortCodeRelationFinder();
$relationList = $relationFinder->getList($this->ID); $relationList = $relationFinder->getList($this->ID);
@ -554,7 +576,6 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
$gridFieldConfig $gridFieldConfig
); );
$referencesGrid = GridField::create( $referencesGrid = GridField::create(
'References', 'References',
_t('DMSDocument.RelatedReferences', 'Related References'), _t('DMSDocument.RelatedReferences', 'Related References'),
@ -562,13 +583,35 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
$gridFieldConfig $gridFieldConfig
); );
if (DMSDocument_versions::$enable_versions) {
$versionsGridFieldConfig = GridFieldConfig::create()->addComponents(
new GridFieldToolbarHeader(),
new GridFieldSortableHeader(),
new GridFieldDataColumns(),
new GridFieldPaginator(30)
);
$versionsGridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(Config::inst()->get('DMSDocument_versions', 'display_fields'))
->setFieldCasting(array('LastChanged'=>"Datetime->Ago"))
->setFieldFormatting(array('FilenameWithoutID'=>'<a target=\'_blank\' class=\'file-url\' href=\'$Link\'>$FilenameWithoutID</a>'));
$versionsGrid = GridField::create(
'Versions',
_t('DMSDocument.Versions', 'Versions'),
$this->getVersions(),
$versionsGridFieldConfig
);
$extraTasks .= '<li class="ss-ui-button" data-panel="find-versions">Versions</li>';
$extraFields = $versionsGrid->addExtraClass('find-versions');
}
$fields->add(new LiteralField('BottomTaskSelection', $fields->add(new LiteralField('BottomTaskSelection',
'<div id="Actions" class="field actions"><label class="left">Actions</label><ul>'. '<div id="Actions" class="field actions"><label class="left">Actions</label><ul>'.
'<li class="ss-ui-button" data-panel="embargo">Embargo</li>'. '<li class="ss-ui-button" data-panel="embargo">Embargo</li>'.
'<li class="ss-ui-button" data-panel="expiry">Expiry</li>'. '<li class="ss-ui-button" data-panel="expiry">Expiry</li>'.
'<li class="ss-ui-button" data-panel="replace">Replace</li>'. '<li class="ss-ui-button" data-panel="replace">Replace</li>'.
'<li class="ss-ui-button" data-panel="find-usage">Find usage</li>'. '<li class="ss-ui-button" data-panel="find-usage">Usage</li>'.
'<li class="ss-ui-button" data-panel="find-references">Find references</li>'. '<li class="ss-ui-button" data-panel="find-references">References</li>'.
$extraTasks.
'</ul></div>')); '</ul></div>'));
$embargoValue = 'None'; $embargoValue = 'None';
@ -599,7 +642,8 @@ class DMSDocument extends DataObject implements DMSDocumentInterface {
)->addExtraClass('expiry'), )->addExtraClass('expiry'),
$uploadField->addExtraClass('replace'), $uploadField->addExtraClass('replace'),
$pagesGrid->addExtraClass('find-usage'), $pagesGrid->addExtraClass('find-usage'),
$referencesGrid->addExtraClass('find-references') $referencesGrid->addExtraClass('find-references'),
$extraFields
)->setName("ActionsPanel")->addExtraClass('dmsupload ss-uploadfield')); )->setName("ActionsPanel")->addExtraClass('dmsupload ss-uploadfield'));
$this->extend('updateCMSFields', $fields); $this->extend('updateCMSFields', $fields);
@ -745,9 +789,18 @@ class DMSDocument_Controller extends Controller {
*/ */
protected function getDocumentFromID($request) { protected function getDocumentFromID($request) {
$doc = null; $doc = null;
$id = Convert::raw2sql(intval($request->param('ID')));
$doc = DataObject::get_by_id('DMSDocument', $id); $id = Convert::raw2sql($request->param('ID'));
$this->extend('updateDocumentFromID', $doc, $request);
if (strpos($id, 'version') === 0) { //versioned document
$id = str_replace('version','',$id);
$doc = DataObject::get_by_id('DMSDocument_versions', $id);
$this->extend('updateVersionFromID', $doc, $request);
} else { //normal document
$doc = DataObject::get_by_id('DMSDocument', $id);
$this->extend('updateDocumentFromID', $doc, $request);
}
return $doc; return $doc;
} }
@ -761,22 +814,28 @@ class DMSDocument_Controller extends Controller {
$canView = false; $canView = false;
//Runs through all pages that this page links to and sets canView to true if the user can view ONE of these pages //Runs through all pages that this page links to and sets canView to true if the user can view ONE of these pages
$pages = $doc->Pages(); if (method_exists($doc, 'Pages')) {
if ($pages->Count() > 0) { $pages = $doc->Pages();
foreach($pages as $page) { if ($pages->Count() > 0) {
if ($page->CanView()) { foreach($pages as $page) {
$canView = true; //just one canView is enough to know that we can view the file if ($page->CanView()) {
break; $canView = true; //just one canView is enough to know that we can view the file
break;
}
} }
} else {
//if the document isn't on any page, then allow viewing of the document (because there is no canView() to consult)
$canView = true;
} }
} else {
//if the document isn't on any page, then allow viewing of the document (because there is no canView() to consult)
$canView = true;
} }
// check for embargo or expiry // check for embargo or expiry
if ($doc->isHidden()) $canView = false; if ($doc->isHidden()) $canView = false;
//admins can always download any document, even if otherwise hidden
$member = Member::currentUser();
if ($member && Permission::checkMember($member, 'ADMIN')) $canView = true;
if ($canView) { if ($canView) {
$path = $doc->getFullPath(); $path = $doc->getFullPath();
if ( is_file($path) ) { if ( is_file($path) ) {

View File

@ -0,0 +1,211 @@
<?php
/**
* DataObject to store versions of uploaded Documents. Versions are only created when replacing a document, not on every
* save of the DMSDocument dataobject. So, versions store the various versions of the underlying Document, not the
* DataObject with information about that object.
*/
class DMSDocument_versions extends DataObject {
static $enable_versions = true; //flag that turns on or off versions of documents when replacing them
static $db = array(
'VersionCounter' => 'Int',
'VersionViewCount' => 'Int'
); //config system call in _config creates this to mirror DMSDocument
static $has_one = array(
'Document' => 'DMSDocument' //ID of the original DMSDocument object this is a version of
);
static $defaults = array(
'VersionCounter' => 0
);
static $display_fields = array(
'VersionCounter' => 'VersionCounter',
'FilenameWithoutID' => 'Filename',
'LastChanged' => 'LastChanged'
);
static $summary_fields = array(
'VersionCounter',
'FilenameWithoutID'
);
static $field_labels = array(
'FilenameWithoutID'=>'Filename'
);
static $default_sort = array(
'LastChanged' => 'DESC'
);
/**
* Creates a new version of a document by moving the current file and renaming it to the versioned filename.
* This method assumes that the method calling this is just about to upload a new file to replace the old file.
* @static
* @param DMSDocument $doc
* @return bool Success or failure
*/
static function create_version(DMSDocument $doc) {
$success = false;
$existingPath = $doc->getFullPath();
if (is_file($existingPath)) {
$version = new DMSDocument_versions($doc); //create a copy of the current DMSDocument as a version
$previousVersionCounter = 0;
$newestExistingVersion = self::get_versions($doc)->sort(array('Created'=>'DESC','ID'=>'DESC'))->limit(1);
if ($newestExistingVersion && $newestExistingVersion->Count() > 0) {
$previousVersionCounter = $newestExistingVersion->first()->VersionCounter;
}
//change the filename field to a field containing the new soon-to-be versioned file
$version->VersionCounter = $previousVersionCounter + 1; //start versions at 1
$newFilename = $version->generateVersionedFilename($doc, $version->VersionCounter);
$version->Filename = $newFilename;
//add a relation back to the origin ID;
$version->DocumentID = $doc->ID;
$id = $version->write();
if (!empty($id)) {
rename($existingPath, $version->getFullPath());
$success = true;
}
}
return $success;
}
public function delete() {
$path = $this->getFullPath();
if (file_exists($path)) unlink($path);
parent::delete();
}
/**
* Returns a DataList of all previous Versions of a document (check the LastEdited date of each
* object to find the correct one)
* @static
* @param DMSDocument $doc
* @return DataList List of Document objects
*/
static function get_versions(DMSDocument $doc) {
if (!DMSDocument_versions::$enable_versions) user_error("DMSDocument versions are disabled",E_USER_WARNING);
return DMSDocument_versions::get()->filter(array('DocumentID' => $doc->ID));
}
public function __construct($record = null, $isSingleton = false, $model = null) {
//check what the constructor was passed
$dmsObject = null;
if ($record && is_subclass_of($record,'DMSDocumentInterface')) {
$dmsObject = $record;
$record = null; //cancel the record creation to just create an empty object
}
//create the object
parent::__construct($record, $isSingleton, $model);
//copy the DMSDocument object, if passed into the constructor
if ($dmsObject) {
foreach(array_keys(DataObject::custom_database_fields($dmsObject->ClassName)) as $key) {
$this->$key = $dmsObject->$key;
}
}
}
/**
* Returns a link to download this document from the DMS store
* @return String
*/
function getLink() {
return Controller::join_links(Director::baseURL(),'dmsdocument/version'.$this->ID);
}
/**
* Document versions are always hidden from outside viewing. Only admins can download them
* @return bool
*/
function isHidden() {
return true;
}
/**
* Returns the full filename of the document stored in this object. Can optionally specify which filename to use at the end
* @return string
*/
function getFullPath($filename = null) {
if (!$filename) $filename = $this->Filename;
return DMS::get_dms_path() . DIRECTORY_SEPARATOR . $this->Folder . DIRECTORY_SEPARATOR . $filename;
}
function getFilenameWithoutID() {
$filenameParts = explode('~',$this->Filename);
$filename = array_pop($filenameParts);
return $filename;
}
/**
* Creates a new filename for the current Document's file when replacing the current file with a new file
* @param $filename The original filename to generate the versioned filename from
* @return String The new filename
*/
protected function generateVersionedFilename(DMSDocument $doc, $versionCounter) {
$filename = $doc->Filename;
do {
$versionPaddingString = str_pad($versionCounter, 4, '0', STR_PAD_LEFT); //add leading zeros to make sorting accurate up to 10,000 documents
$newVersionFilename = preg_replace('/([0-9]+~)(.*?)/','$1~'.$versionPaddingString.'~$2',$filename);
if ($newVersionFilename == $filename || empty($newVersionFilename)) { //sanity check for crazy document names
user_error('Cannot generate new document filename for file: '.$filename,E_USER_ERROR);
}
$versionCounter++; //increase the counter for the next loop run, if necessary
} while(file_exists($this->getFullPath($newVersionFilename)));
return $newVersionFilename;
}
/**
* Return the extension of the file associated with the document
*/
function getExtension() {
return strtolower(pathinfo($this->Filename, PATHINFO_EXTENSION));
}
function getSize() {
$size = $this->getAbsoluteSize();
return ($size) ? File::format_size($size) : false;
}
/**
* Return the size of the file associated with the document
*/
function getAbsoluteSize() {
return filesize($this->getFullPath());
}
/**
* An alias to DMSDocument::getSize()
*/
function getFileSizeFormatted(){
return $this->getSize();
}
/**
*/
function trackView(){
if ($this->ID > 0) {
$count = $this->VersionViewCount + 1;
DB::query("UPDATE \"DMSDocument_versions\" SET \"VersionViewCount\"='$count' WHERE \"ID\"={$this->ID}");
}
}
}
?>

91
tests/DMSVersioningTest.php Executable file
View File

@ -0,0 +1,91 @@
<?php
class DMSVersioningTest extends SapphireTest {
static $testFile = 'dms/tests/DMS-test-lorum-file.pdf';
static $testFile2 = 'dms/tests/DMS-test-document-2.pdf';
//store values to reset back to after this test runs
static $dmsFolderOld;
static $dmsFolderSizeOld;
function setUp() {
parent::setUp();
self::$dmsFolderOld = DMS::$dmsFolder;
self::$dmsFolderSizeOld = DMS::$dmsFolderSize;
//use a test DMS folder, so we don't overwrite the live one
DMS::$dmsFolder = 'dms-assets-test-versions';
//clear out the test folder (in case a broken test doesn't delete it)
$this->delete(BASE_PATH . DIRECTORY_SEPARATOR . 'dms-assets-test-versions');
}
function tearDown() {
parent::tearDown();
$d = DataObject::get("DMSDocument");
foreach($d as $d1) {
$d1->delete();
}
$t = DataObject::get("DMSTag");
foreach($t as $t1) {
$t1->delete();
}
//delete the test folder after the test runs
$this->delete(BASE_PATH . DIRECTORY_SEPARATOR . 'dms-assets-test-versions');
//set the old DMS folder back again
DMS::$dmsFolder = self::$dmsFolderOld;
DMS::$dmsFolderSize = self::$dmsFolderSizeOld;
}
public function delete($path) {
if (file_exists($path) || is_dir($path)) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $file) {
if (in_array($file->getBasename(), array('.', '..'))) {
continue;
} elseif ($file->isDir()) {
rmdir($file->getPathname());
} elseif ($file->isFile() || $file->isLink()) {
unlink($file->getPathname());
}
}
rmdir($path);
}
}
function testDMSVersionStorage() {
$dms = DMS::inst();
$document = $dms->storeDocument(self::$testFile);
$this->assertNotNull($document, "Document object created");
$this->assertTrue(file_exists(DMS::get_dms_path() . DIRECTORY_SEPARATOR . $document->Folder . DIRECTORY_SEPARATOR . $document->Filename),"Document file copied into DMS folder");
$document->replaceDocument(self::$testFile2);
$document->replaceDocument(self::$testFile);
$document->replaceDocument(self::$testFile2);
$document->replaceDocument(self::$testFile);
$versionsList = $document->getVersions();
$this->assertEquals(4, $versionsList->Count(),"4 Versions created");
$versionsArray = $versionsList->toArray();
$this->assertEquals($versionsArray[0]->VersionCounter, 1,"Correct version count");
$this->assertEquals($versionsArray[1]->VersionCounter, 2,"Correct version count");
$this->assertEquals($versionsArray[2]->VersionCounter, 3,"Correct version count");
$this->assertEquals($versionsArray[3]->VersionCounter, 4,"Correct version count");
}
}