diff --git a/_config.php b/_config.php index 6dae167..32ae9cf 100644 --- a/_config.php +++ b/_config.php @@ -1,5 +1,6 @@ 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); \ No newline at end of file +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); +} + diff --git a/code/DMSDocument.php b/code/DMSDocument.php index 15a170a..6b1c062 100755 --- a/code/DMSDocument.php +++ b/code/DMSDocument.php @@ -365,7 +365,8 @@ class DMSDocument extends DataObject implements DMSDocumentInterface { * @return DataList List of Document objects */ 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(); + //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 parent::delete(); } + + /** * Relate an existing file on the filesystem to the document. * 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 $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) //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 + $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 $relationFinder = new ShortCodeRelationFinder(); $relationList = $relationFinder->getList($this->ID); @@ -554,7 +576,6 @@ class DMSDocument extends DataObject implements DMSDocumentInterface { $gridFieldConfig ); - $referencesGrid = GridField::create( 'References', _t('DMSDocument.RelatedReferences', 'Related References'), @@ -562,13 +583,35 @@ class DMSDocument extends DataObject implements DMSDocumentInterface { $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'=>'$FilenameWithoutID')); + + $versionsGrid = GridField::create( + 'Versions', + _t('DMSDocument.Versions', 'Versions'), + $this->getVersions(), + $versionsGridFieldConfig + ); + $extraTasks .= '
  • Versions
  • '; + $extraFields = $versionsGrid->addExtraClass('find-versions'); + } + $fields->add(new LiteralField('BottomTaskSelection', '
    ')); $embargoValue = 'None'; @@ -599,7 +642,8 @@ class DMSDocument extends DataObject implements DMSDocumentInterface { )->addExtraClass('expiry'), $uploadField->addExtraClass('replace'), $pagesGrid->addExtraClass('find-usage'), - $referencesGrid->addExtraClass('find-references') + $referencesGrid->addExtraClass('find-references'), + $extraFields )->setName("ActionsPanel")->addExtraClass('dmsupload ss-uploadfield')); $this->extend('updateCMSFields', $fields); @@ -745,9 +789,18 @@ class DMSDocument_Controller extends Controller { */ protected function getDocumentFromID($request) { $doc = null; - $id = Convert::raw2sql(intval($request->param('ID'))); - $doc = DataObject::get_by_id('DMSDocument', $id); - $this->extend('updateDocumentFromID', $doc, $request); + + $id = Convert::raw2sql($request->param('ID')); + + 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; } @@ -761,22 +814,28 @@ class DMSDocument_Controller extends Controller { $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 - $pages = $doc->Pages(); - if ($pages->Count() > 0) { - foreach($pages as $page) { - if ($page->CanView()) { - $canView = true; //just one canView is enough to know that we can view the file - break; + if (method_exists($doc, 'Pages')) { + $pages = $doc->Pages(); + if ($pages->Count() > 0) { + foreach($pages as $page) { + if ($page->CanView()) { + $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 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) { $path = $doc->getFullPath(); if ( is_file($path) ) { diff --git a/code/DMSDocument_versions.php b/code/DMSDocument_versions.php new file mode 100644 index 0000000..e209972 --- /dev/null +++ b/code/DMSDocument_versions.php @@ -0,0 +1,211 @@ + '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}"); + } + } + +} + +?> \ No newline at end of file diff --git a/tests/DMSVersioningTest.php b/tests/DMSVersioningTest.php new file mode 100755 index 0000000..d389a0c --- /dev/null +++ b/tests/DMSVersioningTest.php @@ -0,0 +1,91 @@ +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"); + + } + + + +} \ No newline at end of file