diff --git a/control/Controller.php b/control/Controller.php index 43a93b8ae..79fda01de 100644 --- a/control/Controller.php +++ b/control/Controller.php @@ -71,6 +71,20 @@ class Controller extends RequestHandler implements TemplateGlobalProvider { public function init() { if($this->basicAuthEnabled) BasicAuth::protect_site_if_necessary(); + // Directly access the session variable just in case the Group or Member tables don't yet exist + if(Member::config()->log_last_visited) { + Deprecation::notice( + '4.0', + 'Member::$LastVisited is deprecated. From 4.0 onwards you should implement this as a custom extension' + ); + if(Session::get('loggedInAs') && Security::database_is_ready() && ($member = Member::currentUser())) { + DB::prepared_query( + sprintf('UPDATE "Member" SET "LastVisited" = %s WHERE "ID" = ?', DB::get_conn()->now()), + array($member->ID) + ); + } + } + // This is used to test that subordinate controllers are actually calling parent::init() - a common bug $this->baseInitCalled = true; } diff --git a/control/Cookie.php b/control/Cookie.php index 76de0e3e0..4b4043df7 100644 --- a/control/Cookie.php +++ b/control/Cookie.php @@ -61,6 +61,15 @@ class Cookie { return self::get_inst()->getAll($includeUnsent); } + /** + * @deprecated + */ + public static function forceExpiry($name, $path = null, $domain = null) { + Deprecation::notice('4.0', 'Use Cookie::force_expiry instead.'); + + return self::force_expiry($name, $path, $domain); + } + /** * @param string * @param string @@ -69,4 +78,20 @@ class Cookie { public static function force_expiry($name, $path = null, $domain = null, $secure = false, $httpOnly = true) { return self::get_inst()->forceExpiry($name, $path, $domain, $secure, $httpOnly); } + + /** + * @deprecated + */ + public static function set_report_errors($reportErrors) { + Deprecation::notice('4.0', 'Use "Cookie.report_errors" config setting instead'); + Config::inst()->update('Cookie', 'report_errors', $reportErrors); + } + + /** + * @deprecated + */ + public static function report_errors() { + Deprecation::notice('4.0', 'Use "Cookie.report_errors" config setting instead'); + return Config::inst()->get('Cookie', 'report_errors'); + } } diff --git a/core/Object.php b/core/Object.php index 9943a5d03..2aa240af8 100755 --- a/core/Object.php +++ b/core/Object.php @@ -394,6 +394,58 @@ abstract class Object { return $default; } + /** + * @deprecated + */ + public static function get_static($class, $name, $uncached = false) { + Deprecation::notice('4.0', 'Replaced by Config#get'); + return Config::inst()->get($class, $name, Config::FIRST_SET); + } + + /** + * @deprecated + */ + public static function set_static($class, $name, $value) { + Deprecation::notice('4.0', 'Replaced by Config#update'); + Config::inst()->update($class, $name, $value); + } + + /** + * @deprecated + */ + public static function uninherited_static($class, $name, $uncached = false) { + Deprecation::notice('4.0', 'Replaced by Config#get'); + return Config::inst()->get($class, $name, Config::UNINHERITED); + } + + /** + * @deprecated + */ + public static function combined_static($class, $name, $ceiling = false) { + if ($ceiling) throw new Exception('Ceiling argument to combined_static is no longer supported'); + + Deprecation::notice('4.0', 'Replaced by Config#get'); + return Config::inst()->get($class, $name); + } + + /** + * @deprecated + */ + public static function addStaticVars($class, $properties, $replace = false) { + Deprecation::notice('4.0', 'Replaced by Config#update'); + foreach($properties as $prop => $value) self::add_static_var($class, $prop, $value, $replace); + } + + /** + * @deprecated + */ + public static function add_static_var($class, $name, $value, $replace = false) { + Deprecation::notice('4.0', 'Replaced by Config#remove and Config#update'); + + if ($replace) Config::inst()->remove($class, $name); + Config::inst()->update($class, $name, $value); + } + /** * Return TRUE if a class has a specified extension. * This supports backwards-compatible format (static Object::has_extension($requiredExtension)) @@ -441,7 +493,7 @@ abstract class Object { * instances, not existing ones (including all instances created through {@link singleton()}). * * @see http://doc.silverstripe.org/framework/en/trunk/reference/dataextension - * @param string $class Class that should be extended - has to be a subclass of {@link Object} + * @param string $classOrExtension Class that should be extended - has to be a subclass of {@link Object} * @param string $extension Subclass of {@link Extension} with optional parameters * as a string, e.g. "Versioned" or "Translatable('Param')" */ diff --git a/dev/Profiler.php b/dev/Profiler.php new file mode 100644 index 000000000..15f57fe14 --- /dev/null +++ b/dev/Profiler.php @@ -0,0 +1,239 @@ +description = array(); + $this->startTime = array(); + $this->endTime = array(); + $this->initTime = 0; + $this->cur_timer = ""; + $this->stack = array(); + $this->trail = ""; + $this->trace = ""; + $this->count = array(); + $this->running = array(); + $this->initTime = $this->getMicroTime(); + $this->output_enabled = $output_enabled; + $this->trace_enabled = $trace_enabled; + $this->startTimer('unprofiled'); + } + + // Public Methods + + public static function init() { + Deprecation::notice('4.0', 'The Profiler class is deprecated, use third party tools like XHProf instead'); + if(!self::$inst) self::$inst = new Profiler(true,true); + } + + public static function mark($name, $level2 = "", $desc = "") { + if($level2 && $_GET['debug_profile'] > 1) $name .= " $level2"; + + if(!self::$inst) self::$inst = new Profiler(true,true); + + self::$inst->startTimer($name, $desc); + } + public static function unmark($name, $level2 = "", $desc = "") { + if($level2 && $_GET['debug_profile'] > 1) $name .= " $level2"; + + if(!self::$inst) self::$inst = new Profiler(true,true); + + self::$inst->stopTimer($name, $desc); + } + public static function show($showTrace = false) { + if(!self::$inst) self::$inst = new Profiler(true,true); + + echo "
"; + echo "

" + . "(Click to close)

"; + self::$inst->printTimers(); + if($showTrace) self::$inst->printTrace(); + echo "
"; + } + + /** + * Start an individual timer + * This will pause the running timer and place it on a stack. + * @param string $name name of the timer + * @param string optional $desc description of the timer + */ + public function startTimer($name, $desc="" ){ + $this->trace.="start $name\n"; + $n=array_push( $this->stack, $this->cur_timer ); + $this->__suspendTimer( $this->stack[$n-1] ); + $this->startTime[$name] = $this->getMicroTime(); + $this->cur_timer=$name; + $this->description[$name] = $desc; + if (!array_key_exists($name,$this->count)) + $this->count[$name] = 1; + else + $this->count[$name]++; + } + + /** + * Stop an individual timer + * Restart the timer that was running before this one + * @param string $name name of the timer + */ + public function stopTimer($name){ + $this->trace.="stop $name\n"; + $this->endTime[$name] = $this->getMicroTime(); + if (!array_key_exists($name, $this->running)) + $this->running[$name] = $this->elapsedTime($name); + else + $this->running[$name] += $this->elapsedTime($name); + $this->cur_timer=array_pop($this->stack); + $this->__resumeTimer($this->cur_timer); + } + + /** + * measure the elapsed time of a timer without stoping the timer if + * it is still running + */ + public function elapsedTime($name){ + // This shouldn't happen, but it does once. + if (!array_key_exists($name,$this->startTime)) + return 0; + + if(array_key_exists($name,$this->endTime)){ + return ($this->endTime[$name] - $this->startTime[$name]); + } else { + $now=$this->getMicroTime(); + return ($now - $this->startTime[$name]); + } + }//end start_time + + /** + * Measure the elapsed time since the profile class was initialised + * + */ + public function elapsedOverall(){ + $oaTime = $this->getMicroTime() - $this->initTime; + return($oaTime); + }//end start_time + + /** + * print out a log of all the timers that were registered + * + */ + public function printTimers($enabled=false) + { + if($this->output_enabled||$enabled){ + $TimedTotal = 0; + $tot_perc = 0; + ksort($this->description); + print("
\n");
+			$oaTime = $this->getMicroTime() - $this->initTime;
+			echo"============================================================================\n";
+			echo "                              PROFILER OUTPUT\n";
+			echo"============================================================================\n";
+			print( "Calls                    Time  Routine\n");
+			echo"-----------------------------------------------------------------------------\n";
+			while (list ($key, $val) = each ($this->description)) {
+				$t = $this->elapsedTime($key);
+				$total = $this->running[$key];
+				$count = $this->count[$key];
+				$TimedTotal += $total;
+				$perc = ($total/$oaTime)*100;
+				$tot_perc+=$perc;
+				// $perc=sprintf("%3.2f", $perc );
+				$lines[ sprintf( "%3d    %3.4f ms (%3.2f %%)  %s\n", $count, $total*1000, $perc, $key) ] = $total;
+			}
+			arsort($lines);
+			foreach($lines as $line => $total) {
+				echo $line;
+			}
+
+			echo "\n";
+
+			$missed=$oaTime-$TimedTotal;
+			$perc = ($missed/$oaTime)*100;
+			$tot_perc+=$perc;
+			// $perc=sprintf("%3.2f", $perc );
+			printf( "       %3.4f ms (%3.2f %%)  %s\n", $missed*1000,$perc, "Missed");
+
+			echo"============================================================================\n";
+
+			printf( "       %3.4f ms (%3.2f %%)  %s\n", $oaTime*1000,$tot_perc, "OVERALL TIME");
+
+			echo"============================================================================\n";
+
+			print("
"); + } + } + + public function printTrace( $enabled=false ) + { + if($this->trace_enabled||$enabled){ + print("
");
+			print("Trace\n$this->trace\n\n");
+			print("
"); + } + } + + /// Internal Use Only Functions + + /** + * Get the current time as accuratly as possible + * + */ + public function getMicroTime(){ + $tmp=explode(' ', microtime()); + $rt=$tmp[0]+$tmp[1]; + return $rt; + } + + /** + * resume an individual timer + * + */ + public function __resumeTimer($name){ + $this->trace.="resume $name\n"; + $this->startTime[$name] = $this->getMicroTime(); + } + + /** + * suspend an individual timer + * + */ + public function __suspendTimer($name){ + $this->trace.="suspend $name\n"; + $this->endTime[$name] = $this->getMicroTime(); + if (!array_key_exists($name, $this->running)) + $this->running[$name] = $this->elapsedTime($name); + else + $this->running[$name] += $this->elapsedTime($name); + } +} diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 5519e0f76..4db25575e 100644 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -446,6 +446,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase { * {@link loadFixture()} */ public function clearFixtures() { + $this->fixtures = array(); $this->getFixtureFactory()->clear(); } diff --git a/dev/YamlFixture.php b/dev/YamlFixture.php index 356ecbc77..165ab910b 100644 --- a/dev/YamlFixture.php +++ b/dev/YamlFixture.php @@ -83,6 +83,12 @@ class YamlFixture extends Object { */ protected $fixtureString; + /** + * @var FixtureFactory + * @deprecated 3.1 Use writeInto() and FixtureFactory instead + */ + protected $factory; + /** * @param String Absolute file path, or relative path to {@link Director::baseFolder()} */ @@ -117,6 +123,68 @@ class YamlFixture extends Object { return $this->fixtureString; } + /** + * Get the ID of an object from the fixture. + * + * @deprecated 4.0 Use writeInto() and FixtureFactory accessors instead + * + * @param $className The data class, as specified in your fixture file. Parent classes won't work + * @param $identifier The identifier string, as provided in your fixture file + */ + public function idFromFixture($className, $identifier) { + Deprecation::notice('4.0', 'Use writeInto() and FixtureFactory accessors instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + return $this->factory->getId($className, $identifier); + + } + + /** + * Return all of the IDs in the fixture of a particular class name. + * + * @deprecated 4.0 Use writeInto() and FixtureFactory accessors instead + * + * @return A map of fixture-identifier => object-id + */ + public function allFixtureIDs($className) { + Deprecation::notice('4.0', 'Use writeInto() and FixtureFactory accessors instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + return $this->factory->getIds($className); + } + + /** + * Get an object from the fixture. + * + * @deprecated 4.0 Use writeInto() and FixtureFactory accessors instead + * + * @param $className The data class, as specified in your fixture file. Parent classes won't work + * @param $identifier The identifier string, as provided in your fixture file + */ + public function objFromFixture($className, $identifier) { + Deprecation::notice('4.0', 'Use writeInto() and FixtureFactory accessors instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + return $this->factory->get($className, $identifier); + } + + /** + * Load a YAML fixture file into the database. + * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. + * + * Caution: In order to support reflexive relations which need a valid object ID, + * the record is written twice: first after populating all non-relational fields, + * then again after populating all relations (has_one, has_many, many_many). + * + * @deprecated 4.0 Use writeInto() and FixtureFactory instance instead + */ + public function saveIntoDatabase(DataModel $model) { + Deprecation::notice('4.0', 'Use writeInto() and FixtureFactory instance instead'); + + if(!$this->factory) $this->factory = Injector::inst()->create('FixtureFactory'); + $this->writeInto($this->factory); + } + /** * Persists the YAML data in a FixtureFactory, * which in turn saves them into the database. diff --git a/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md b/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md index c3a00d601..e52c38171 100644 --- a/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md +++ b/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md @@ -28,7 +28,7 @@ explicitly logging in or by invoking the "remember me" functionality. public function updateCMSFields(FieldList $fields) { $fields->addFieldsToTab('Root.Main', array( ReadonlyField::create('LastVisited', 'Last visited'), - ReadonlyField::create('NumVisits', 'Number of visits') + ReadonlyField::create('NumVisit', 'Number of visits') )); } diff --git a/docs/en/04_Changelogs/3.2.0.md b/docs/en/04_Changelogs/3.2.0.md index a3206cfda..dad837b06 100644 --- a/docs/en/04_Changelogs/3.2.0.md +++ b/docs/en/04_Changelogs/3.2.0.md @@ -25,30 +25,14 @@ removes from both draft and live simultaneously. * Most of the `Image` manipulation methods have been renamed -## Deprecated classes/methods removed +## Deprecated classes/methods + +The following functionality deprecated in 3.0 has been removed: -* `ToggleField` was deprecated in 3.1, and has been removed. Use custom Javascript with `ReadonlyField` instead. -* `ExactMatchMultiFilter` was deprecated in 3.1, and has been removed. Use `ExactMatchFilter` instead. -* `NegationFilter` was deprecated in 3.1, and has been removed. Use `ExactMatchFilter:not` instead. -* `StartsWithMultiFilter` was deprecated in 3.1, and has been removed. Use `StartsWithFilter` instead. -* `ScheduledTask` and subclasses like `DailyTask` were deprecated in 3.1, and have been removed. - Use custom code instead, or a module like silverstripe-crontask: https://github.com/silverstripe-labs/silverstripe-crontask -* `Cookie::forceExpiry()` was removed. Use `Cookie::force_expiry()` instead -* `Object` statics removal: `get_static()`, `set_static()`, `uninherited_static()`, `combined_static()`, - `addStaticVars()` and `add_static_var()` removed. Use the Config methods instead. -* `GD` methods removed: `setGD()`, `getGD()`, `hasGD()`. Use `setImageResource()`, `getImageResource()`, and `hasImageResource()` instead -* `DataExtension::get_extra_config()` removed, no longer supports `extraStatics` or `extraDBFields`. Define your - statics on the class directly. * `DataList::getRange()` removed. Use `limit()` instead. * `SQLMap` removed. Call `map()` on a `DataList` or use `SS_Map` directly instead. -* `Profiler` removed. Use xhprof or xdebug for profiling instead. -* `Aggregate` removed. Call aggregate methods on a `DataList` instead e.g. `Member::get()->max('LastEdited')` -* `MySQLDatabase::set_connection_charset()` removed. Use `MySQLDatabase.connection_charset` config setting instead -* `SQLConditionalExpression/SQLQuery` `select()`, `limit()`, `orderby()`, `groupby()`, `having()`, `from()`, `leftjoin()`, `innerjoin()`, `where()` and `whereAny()` removed. +* `SQLQuery` methods `select()`, `limit()`, `orderby()`, `groupby()`, `having()`, `from()`, `leftjoin()`, `innerjoin()`, `where()` and `whereAny()` removed. Use `set*()` and `add*()` methods instead. -* Template `<% control $MyList %>` syntax removed. Use `<% loop $MyList %>` instead. -* Removed `Member.LastVisited` and `Member.NumVisits` properties, see - [Howto: Track Member Logins](/extending/how_tos/track_member_logins) to restore functionality as custom code ## New and changed API @@ -199,6 +183,27 @@ ## Upgrading Notes +### Disable `LastVisited` and `NumVisits` counter + +These fields were deprecated in 3.1 due to performance concerns, and should be disabled unless required by +your application. + +In order to disable these functions you can add the following yml to your configuration: + + :::yaml + --- + Name: disablevisits + --- + Member: + log_num_visits: false + log_last_visited: false + + +This functionality will be removed in 4.0 + +[Howto: Track Member Logins](/developer-guides/extending/how_tos/track_member_logins) to restore functionality +as custom code + ### UploadField "Select from files" shows files in all folders by default In order to list files in a single folder by default (previous default behaviour), diff --git a/email/Mailer.php b/email/Mailer.php index 0e44df151..cdc15bcec 100644 --- a/email/Mailer.php +++ b/email/Mailer.php @@ -491,3 +491,113 @@ class Mailer extends Object { } } +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function htmlEmail($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false, + $plainContent = false) { + + Deprecation::notice('4.0', 'Use Email->sendHTML() instead'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->sendHTML($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function plaintextEmail($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false) { + Deprecation::notice('4.0', 'Use Email->sendPlain() instead'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->sendPlain($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function encodeMultipart($parts, $contentType, $headers = false) { + Deprecation::notice('4.0', 'Use Email->$this->encodeMultipart() instead'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->encodeMultipart($parts, $contentType, $headers = false); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function wrapImagesInline($htmlContent) { + Deprecation::notice('4.0', 'Functionality removed from core'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->wrapImagesInline($htmlContent); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function wrapImagesInline_rewriter($url) { + Deprecation::notice('4.0', 'Functionality removed from core'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->wrapImagesInline_rewriter($url); + +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function processHeaders($headers, $body = false) { + Deprecation::notice('4.0', 'Set headers through Email->addCustomHeader()'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->processHeaders($headers, $url); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") { + Deprecation::notice('4.0', 'Please add files through Email->attachFile()'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->encodeFileForEmail($file, $destFileName, $disposition, $extraHeaders); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function QuotedPrintable_encode($quotprint) { + Deprecation::notice('4.0', 'No longer available, handled internally'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->QuotedPrintable_encode($quotprint); +} + +/** + * @package framework + * @subpackage email + * @deprecated 3.1 + */ +function validEmailAddr($emailAddress) { + Deprecation::notice('4.0', 'Use Email->validEmailAddr() instead'); + + $mailer = Injector::inst()->create('Mailer'); + return $mailer->validEmailAddr($emailAddress); +} diff --git a/filesystem/GD.php b/filesystem/GD.php index c50b5a1a8..623923cec 100644 --- a/filesystem/GD.php +++ b/filesystem/GD.php @@ -86,10 +86,26 @@ class GDBackend extends Object implements Image_Backend { $this->height = imagesy($resource); } + /** + * @deprecated + */ + public function setGD($gd) { + Deprecation::notice('4.0', 'Use GD::setImageResource instead'); + return $this->setImageResource($gd); + } + public function getImageResource() { return $this->gd; } + /** + * @deprecated + */ + public function getGD() { + Deprecation::notice('4.0', 'GD::getImageResource instead'); + return $this->getImageResource(); + } + /** * @param string $filename * @return boolean @@ -217,6 +233,16 @@ class GDBackend extends Object implements Image_Backend { return $this->gd ? true : false; } + /** + * @deprecated + */ + public function hasGD() { + Deprecation::notice('4.0', 'GD::hasImageResource instead', + Deprecation::SCOPE_CLASS); + return $this->hasImageResource(); + } + + /** * Resize an image, skewing it as necessary. */ diff --git a/forms/DropdownField.php b/forms/DropdownField.php index bc9f9ad46..861a26686 100644 --- a/forms/DropdownField.php +++ b/forms/DropdownField.php @@ -123,8 +123,22 @@ class DropdownField extends FormField { * @param string $value The current value * @param Form $form The parent form */ - public function __construct($name, $title=null, $source=array(), $value='', $form=null) { + public function __construct($name, $title=null, $source=array(), $value='', $form=null, $emptyString=null) { $this->setSource($source); + + if($emptyString === true) { + Deprecation::notice('4.0', + 'Please use setHasEmptyDefault(true) instead of passing a boolean true $emptyString argument', + Deprecation::SCOPE_GLOBAL); + } + if(is_string($emptyString)) { + Deprecation::notice('4.0', 'Please use setEmptyString() instead of passing a string emptyString argument.', + Deprecation::SCOPE_GLOBAL); + } + + if($emptyString) $this->setHasEmptyDefault(true); + if(is_string($emptyString)) $this->setEmptyString($emptyString); + parent::__construct($name, ($title===null) ? $name : $title, $value, $form); } diff --git a/forms/ToggleField.php b/forms/ToggleField.php new file mode 100644 index 000000000..01f9f0d35 --- /dev/null +++ b/forms/ToggleField.php @@ -0,0 +1,101 @@ +labelMore = _t('ToggleField.MORE', 'more'); + $this->labelLess = _t('ToggleField.LESS', 'less'); + + $this->startClosed(true); + + parent::__construct($name, $title, $value); + } + + public function Field($properties = array()) { + $content = ''; + + Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); + Requirements::javascript(FRAMEWORK_DIR . "/javascript/ToggleField.js"); + + if($this->startClosed) $this->addExtraClass('startClosed'); + + $valforInput = $this->value ? Convert::raw2att($this->value) : ""; + $rawInput = Convert::html2raw($valforInput); + + if($this->charNum) $reducedVal = substr($rawInput,0,$this->charNum); + else $reducedVal = DBField::create_field('Text',$rawInput)->{$this->truncateMethod}(); + + // only create togglefield if the truncated content is shorter + if(strlen($reducedVal) < strlen($rawInput)) { + $content = << + $reducedVal +  $this->labelMore + +
+ $this->value +  $this->labelLess +
+
+ +HTML; + } else { + $this->dontEscape = true; + $content = parent::Field(); + } + + return $content; + } + + /** + * Determines if the field should render open or closed by default. + * + * @param boolean + */ + public function startClosed($bool) { + ($bool) ? $this->addExtraClass('startClosed') : $this->removeExtraClass('startClosed'); + } + + public function Type() { + return "toggleField"; + } +} + diff --git a/model/Aggregate.php b/model/Aggregate.php new file mode 100644 index 000000000..f49b9b49b --- /dev/null +++ b/model/Aggregate.php @@ -0,0 +1,190 @@ +XML_val(aggregate_function, array(field)) - For templates + * $aggregate->aggregate_function(field) - For PHP + * + * Aggregate functions are uppercased by this class, but are otherwise assumed to be valid SQL functions. Some + * examples: Min, Max, Avg + * + * Aggregates are often used as portions of a cacheblock key. They are therefore cached themselves, in the 'aggregate' + * cache, although the invalidation logic prefers speed over keeping valid data. + * The aggregate cache is cleared through {@link DataObject::flushCache()}, which in turn is called on + * {@link DataObject->write()} and other write operations. + * This means most write operations to the database will invalidate the cache correctly. + * Use {@link Aggregate::flushCache()} to manually clear. + * + * NOTE: The cache logic uses tags, and so a backend that supports tags is required. Currently only the File + * backend (and the two-level backend with the File backend as the slow store) meets this requirement + * + * @deprecated 3.1 Use DataList to aggregate data + * + * @author hfried + * @package framework + * @subpackage core + */ +class Aggregate extends ViewableData { + + private static $cache = null; + + /** Build & cache the cache object */ + protected static function cache() { + return self::$cache ? self::$cache : (self::$cache = SS_Cache::factory('aggregate')); + } + + /** + * Clear the aggregate cache for a given type, or pass nothing to clear all aggregate caches. + * {@link $class} is just effective if the cache backend supports tags. + */ + public static function flushCache($class=null) { + $cache = self::cache(); + $capabilities = $cache->getBackend()->getCapabilities(); + if($capabilities['tags'] && (!$class || $class == 'DataObject')) { + $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('aggregate')); + } elseif($capabilities['tags']) { + $tags = ClassInfo::ancestry($class); + foreach($tags as &$tag) { + $tag = preg_replace('/[^a-zA-Z0-9_]/', '_', $tag); + } + $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags); + } else { + $cache->clean(Zend_Cache::CLEANING_MODE_ALL); + } + } + + /** + * Constructor + * + * @deprecated 3.1 Use DataList to aggregate data + * + * @param string $type The DataObject type we are building an aggregate for + * @param string $filter (optional) An SQL filter to apply to the selected rows before calculating the aggregate + */ + public function __construct($type, $filter = '') { + Deprecation::notice('4.0', 'Call aggregate methods on a DataList directly instead. In templates' + . ' an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead' + . ' (check partial-caching.md documentation for more details.)'); + $this->type = $type; + $this->filter = $filter; + parent::__construct(); + } + + /** + * Build the SQLSelect to calculate the aggregate + * This is a seperate function so that subtypes of Aggregate can change just this bit + * @param string $attr - the SQL field statement for selection (i.e. "MAX(LastUpdated)") + * @return SQLSelect + */ + protected function query($attr) { + $query = DataList::create($this->type)->where($this->filter); + $query->setSelect($attr); + $query->setOrderBy(array()); + $singleton->extend('augmentSQL', $query); + return $query; + } + + /** + * Entry point for being called from a template. + * + * This gets the aggregate function + * + */ + public function XML_val($name, $args = null, $cache = false) { + $func = strtoupper( strpos($name, 'get') === 0 ? substr($name, 3) : $name ); + $attribute = $args ? $args[0] : 'ID'; + + $table = null; + + foreach (ClassInfo::ancestry($this->type, true) as $class) { + $fields = DataObject::database_fields($class, false); + if (array_key_exists($attribute, $fields)) { $table = $class; break; } + } + + if (!$table) user_error("Couldn't find table for field $attribute in type {$this->type}", E_USER_ERROR); + + $query = $this->query("$func(\"$table\".\"$attribute\")"); + + // Cache results of this specific SQL query until flushCache() is triggered. + $sql = $query->sql($parameters); + $cachekey = sha1($sql.'-'.var_export($parameters, true)); + $cache = self::cache(); + + if (!($result = $cache->load($cachekey))) { + $result = (string)$query->execute()->value(); if (!$result) $result = '0'; + $cache->save($result, null, array('aggregate', preg_replace('/[^a-zA-Z0-9_]/', '_', $this->type))); + } + + return $result; + } + + /** + * Entry point for being called from PHP. + */ + public function __call($method, $arguments) { + return $this->XML_val($method, $arguments); + } +} + +/** + * A subclass of Aggregate that calculates aggregates for the result of a has_many query. + * + * @deprecated + * + * @author hfried + * @package framework + * @subpackage core + */ +class Aggregate_Relationship extends Aggregate { + + /** + * Constructor + * + * @param DataObject $object The object that has_many somethings that we're calculating the aggregate for + * @param string $relationship The name of the relationship + * @param string $filter (optional) An SQL filter to apply to the relationship rows before calculating the + * aggregate + */ + public function __construct($object, $relationship, $filter = '') { + $this->object = $object; + $this->relationship = $relationship; + + $this->has_many = $object->has_many($relationship); + $this->many_many = $object->many_many($relationship); + + if (!$this->has_many && !$this->many_many) { + user_error("Could not find relationship $relationship on object class {$object->class} in" + . " Aggregate Relationship", E_USER_ERROR); + } + + parent::__construct($this->has_many ? $this->has_many : $this->many_many[1], $filter); + } + + protected function query($attr) { + if ($this->has_many) { + $query = $this->object->getComponentsQuery($this->relationship, $this->filter); + } + else { + $query = $this->object->getManyManyComponentsQuery($this->relationship, $this->filter); + } + + $query->setSelect($attr); + $query->setGroupBy(array()); + + $singleton = singleton($this->type); + $singleton->extend('augmentSQL', $query); + + return $query; + } +} diff --git a/model/DataExtension.php b/model/DataExtension.php index 7465207fa..c1837c3bf 100644 --- a/model/DataExtension.php +++ b/model/DataExtension.php @@ -7,6 +7,28 @@ */ abstract class DataExtension extends Extension { + public static function get_extra_config($class, $extension, $args) { + if(method_exists($extension, 'extraDBFields')) { + $extraStaticsMethod = 'extraDBFields'; + } elseif(method_exists($extension, 'extraStatics')) { + $extraStaticsMethod = 'extraStatics'; + } else { + return null; + } + + Deprecation::notice('4.0', + "$extraStaticsMethod deprecated. Just define statics on your extension, or use get_extra_config", + Deprecation::SCOPE_GLOBAL); + + $statics = Injector::inst() + ->get($extension, true, $args) + ->$extraStaticsMethod($class, $extension); + + if ($statics) { + return $statics; + } + } + public static function unload_extra_statics($class, $extension) { throw new Exception('unload_extra_statics gone'); } diff --git a/model/DataList.php b/model/DataList.php index 880da4c17..ed0c5f0c7 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -489,6 +489,13 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab throw new InvalidArgumentException("Bad field expression $field"); } + if (!$this->inAlterDataQueryCall) { + Deprecation::notice( + '4.0', + 'getRelationName is mutating, and must be called inside an alterDataQuery block' + ); + } + if(strpos($field,'.') === false) { return '"'.$field.'"'; } diff --git a/model/DataObject.php b/model/DataObject.php index 4a30167ea..85ba65b4f 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1633,6 +1633,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity ->sort($sort); } + /** + * @deprecated + */ + public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") { + Deprecation::notice('4.0', "Use getComponents to get a filtered DataList for an object's relation"); + return $this->getComponents($componentName, $filter, $sort, $join, $limit); + } + /** * Find the foreign class of a relation on this DataObject, regardless of the relation type. * @@ -3135,6 +3143,36 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } + /** + * @deprecated + */ + public function Aggregate($class = null) { + Deprecation::notice('4.0', 'Call aggregate methods on a DataList directly instead. In templates' + . ' an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead' + . ' (check partial-caching.md documentation for more details.)'); + + if($class) { + $list = new DataList($class); + $list->setDataModel(DataModel::inst()); + } else if(isset($this)) { + $list = new DataList(get_class($this)); + $list->setDataModel($this->model); + } else { + throw new \InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed" + . " a classname"); + } + return $list; + } + + /** + * @deprecated + */ + public function RelationshipAggregate($relationship) { + Deprecation::notice('4.0', 'Call aggregate methods on a relationship directly instead.'); + + return $this->$relationship(); + } + /** * Return the first item matching the given query. * All calls to get_one() are cached. @@ -3183,6 +3221,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return DataObject $this */ public function flushCache($persistent = true) { + if($persistent) Aggregate::flushCache($this->class); + if($this->class == 'DataObject') { DataObject::$_cache_get_one = array(); return $this; diff --git a/model/connect/MySQLDatabase.php b/model/connect/MySQLDatabase.php index 272f96f1f..11bb03307 100644 --- a/model/connect/MySQLDatabase.php +++ b/model/connect/MySQLDatabase.php @@ -12,7 +12,7 @@ class MySQLDatabase extends SS_Database { /** * Default connection charset (may be overridden in $databaseConfig) - * + * * @config * @var String */ @@ -49,6 +49,14 @@ class MySQLDatabase extends SS_Database { } } + /** + * @deprecated 4.0 Use "MySQLDatabase.connection_charset" config setting instead + */ + public static function set_connection_charset($charset = 'utf8') { + Deprecation::notice('4.0', 'Use "MySQLDatabase.connection_charset" config setting instead'); + Config::inst()->update('MySQLDatabase', 'connection_charset', $charset); + } + /** * Sets the SQL mode * diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php new file mode 100644 index 000000000..88e7caa51 --- /dev/null +++ b/search/filters/ExactMatchMultiFilter.php @@ -0,0 +1,20 @@ +DirectMembers(); // Remove the default foreign key filter in prep for re-applying a filter containing all children groups. @@ -261,7 +273,7 @@ class Group extends DataObject { } // Now set all children groups as a new foreign key $groups = Group::get()->byIDs($this->collateFamilyIDs()); - $result = $result->forForeignID($groups->column('ID'))->where($filter); + $result = $result->forForeignID($groups->column('ID'))->where($filter)->sort($sort)->limit($limit); return $result; } diff --git a/security/Member.php b/security/Member.php index 28a368766..d0428a6fb 100644 --- a/security/Member.php +++ b/security/Member.php @@ -12,8 +12,8 @@ * @property string $RememberLoginToken * @property string $TempIDHash * @property string $TempIDExpired - * @property int $NumVisit - * @property string $LastVisited Date and time of last visit + * @property int $NumVisit @deprecated 4.0 + * @property string $LastVisited @deprecated 4.0 * @property string $AutoLoginHash * @property string $AutoLoginExpired * @property string $PasswordEncryption @@ -35,6 +35,8 @@ class Member extends DataObject implements TemplateGlobalProvider { 'TempIDExpired' => 'SS_Datetime', // Expiry of temp login 'Password' => 'Varchar(160)', 'RememberLoginToken' => 'Varchar(160)', // Note: this currently holds a hash, not a token. + 'NumVisit' => 'Int', // @deprecated 4.0 + 'LastVisited' => 'SS_Datetime', // @deprecated 4.0 'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset 'AutoLoginExpired' => 'SS_Datetime', // This is an arbitrary code pointing to a PasswordEncryptor instance, @@ -81,6 +83,24 @@ class Member extends DataObject implements TemplateGlobalProvider { */ private static $notify_password_change = false; + /** + * Flag whether or not member visits should be logged (count only) + * + * @deprecated 4.0 + * @var bool + * @config + */ + private static $log_last_visited = true; + + /** + * Flag whether we should count number of visits + * + * @deprecated 4.0 + * @var bool + * @config + */ + private static $log_num_visits = true; + /** * All searchable database columns * in this object, currently queried @@ -120,7 +140,7 @@ class Member extends DataObject implements TemplateGlobalProvider { 'TempIDHash', 'TempIDExpired', 'Salt', - 'NumVisit' + 'NumVisit', // @deprecated 4.0 ); /** @@ -446,6 +466,8 @@ class Member extends DataObject implements TemplateGlobalProvider { // This lets apache rules detect whether the user has logged in if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0); + $this->addVisit(); + if($remember) { // Store the hash and give the client the cookie with the token. $generator = new RandomGenerator(); @@ -474,6 +496,19 @@ class Member extends DataObject implements TemplateGlobalProvider { $this->extend('memberLoggedIn'); } + /** + * @deprecated 4.0 + */ + public function addVisit() { + if($this->config()->log_num_visits) { + Deprecation::notice( + '4.0', + 'Member::$NumVisit is deprecated. From 4.0 onwards you should implement this as a custom extension' + ); + $this->NumVisit++; + } + } + /** * Trigger regeneration of TempID. * @@ -549,7 +584,7 @@ class Member extends DataObject implements TemplateGlobalProvider { $member->RememberLoginToken = $hash; Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true); - $member->NumVisit++; + $member->addVisit(); $member->write(); // Audit logging hook @@ -707,6 +742,7 @@ class Member extends DataObject implements TemplateGlobalProvider { )); $fields->removeByName(static::config()->hidden_fields); + $fields->removeByName('LastVisited'); $fields->removeByName('FailedLoginCount'); @@ -1315,7 +1351,9 @@ class Member extends DataObject implements TemplateGlobalProvider { _t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'), i18n::get_existing_translations() )); + $mainFields->removeByName($self->config()->hidden_fields); + $mainFields->makeFieldReadonly('LastVisited'); if( ! $self->config()->lock_out_after_incorrect_logins) { $mainFields->removeByName('FailedLoginCount'); @@ -1417,6 +1455,8 @@ class Member extends DataObject implements TemplateGlobalProvider { $labels['Surname'] = _t('Member.SURNAME', 'Surname'); $labels['Email'] = _t('Member.EMAIL', 'Email'); $labels['Password'] = _t('Member.db_Password', 'Password'); + $labels['NumVisit'] = _t('Member.db_NumVisit', 'Number of Visits'); + $labels['LastVisited'] = _t('Member.db_LastVisited', 'Last Visited Date'); $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date'); $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale'); diff --git a/security/RandomGenerator.php b/security/RandomGenerator.php index e9bb7aca9..52ac1a858 100644 --- a/security/RandomGenerator.php +++ b/security/RandomGenerator.php @@ -74,4 +74,16 @@ class RandomGenerator { return hash($algorithm, $this->generateEntropy()); } + /** + * @deprecated + */ + public function generateHash($algorithm = 'whirlpool') { + Deprecation::notice('4.0', + 'RandomGenerator::generateHash is deprecated because of a confusing name that hints the output is secure, '. + 'while in fact it is just a random string. Use RandomGenerator::randomToken instead.', + Deprecation::SCOPE_METHOD + ); + + return $this->randomToken($algorithm); + } } diff --git a/tasks/DailyTask.php b/tasks/DailyTask.php new file mode 100644 index 000000000..a4a966544 --- /dev/null +++ b/tasks/DailyTask.php @@ -0,0 +1,17 @@ +Usage + * + * Implement a daily task by extending DailyTask and implementing process(). + * + * + * class MyTask extends DailyTask { + * function process() { + * // implement your task here + * } + * } + * + * + * You can also implement the index() method to overwrite which singleton classes are instantiated and processed. + * By default, all subclasses of the task are instantiated and used. For the DailyTask class, this means + * that an instance of each subclass of DailyTask will be created. + * + * You can test your task from the command line by running the following command + * (replace is the classname of your task): + * + * framework/cli-script.php / + * + * To perform all Daily tasks, run from the command line: + * + * cli-script.php /DailyTask + * + * Example Cron Definition + * + * + * # Quarter-hourly task (every hour at 25 minutes past) (remove space between first * and /15) + * * /15 * * * * www-data /webroot/framework/cli-script.php /QuarterHourlyTask > /var/log/quarterhourlytask.log + * + * # HourlyTask (every hour at 25 minutes past) + * 25 * * * * www-data /webroot/framework/cli-script.php /HourlyTask > /var/log/hourlytask.log + * + * # DailyTask (every day at 6:25am) + * 25 6 * * * www-data /webroot/framework/cli-script.php /DailyTask > /var/log/dailytask.log + * + * # WeelkyTask (every Monday at 6:25am) + * 25 6 1 * * www-data /webroot/framework/cli-script.php /WeeklyTask > /var/log/weeklytask.log + * + * + * @deprecated 3.1 + * + * @todo Improve documentation + * @package framework + * @subpackage cron + */ +abstract class ScheduledTask extends CliController { + // this class exists as a logical extension + + public function init() { + Deprecation::notice( + '3.1', + 'ScheduledTask, QuarterHourlyTask, HourlyTask, DailyTask, MonthlyTask, WeeklyTask and ' . + 'YearlyTask are deprecated, please extend from BuildTask or CliController, ' . + 'and invoke them in self-defined frequencies through Unix cronjobs etc.' + ); + + parent::init(); + } +} diff --git a/tasks/WeeklyTask.php b/tasks/WeeklyTask.php new file mode 100644 index 000000000..08ff4d10a --- /dev/null +++ b/tasks/WeeklyTask.php @@ -0,0 +1,16 @@ + "Int" + ); + + private static $has_one = array('Bar' => 'AggregateTest_Bar'); + private static $belongs_many_many = array('Bazi' => 'AggregateTest_Baz'); +} + +/** + * @deprecated + */ +class AggregateTest_Fab extends AggregateTest_Foo { + private static $db = array( + "Fab" => "Int" + ); +} + +/** + * @deprecated + */ +class AggregateTest_Fac extends AggregateTest_Fab { + private static $db = array( + "Fac" => "Int" + ); +} + + +/** + * @deprecated + */ +class AggregateTest_Bar extends DataObject implements TestOnly { + private static $db = array( + "Bar" => "Int" + ); + + private static $has_many = array( + "Foos" => "AggregateTest_Foo" + ); +} + + +/** + * @deprecated + */ +class AggregateTest_Baz extends DataObject implements TestOnly { + private static $db = array( + "Baz" => "Int" + ); + + private static $many_many = array( + "Foos" => "AggregateTest_Foo" + ); +} + +/** + * @deprecated + */ +class AggregateTest extends SapphireTest { + protected static $fixture_file = 'AggregateTest.yml'; + + protected $extraDataObjects = array( + 'AggregateTest_Foo', + 'AggregateTest_Fab', + 'AggregateTest_Fac', + 'AggregateTest_Bar', + 'AggregateTest_Baz' + ); + + protected $originalDeprecation; + + public function setUp() { + parent::setUp(); + // This test tests code that was deprecated after 2.4 + $this->originalDeprecation = Deprecation::dump_settings(); + Deprecation::notification_version('2.4'); + } + + public function tearDown() { + Deprecation::restore_settings($this->originalDeprecation); + parent::tearDown(); + } + + /** + * Test basic aggregation on a passed type + */ + public function testTypeSpecifiedAggregate() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + + // Template style access + $this->assertEquals($foo->Aggregate('AggregateTest_Foo')->XML_val('Max', array('Foo')), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Fab')->XML_val('Max', array('Fab')), 3); + + // PHP style access + $this->assertEquals($foo->Aggregate('AggregateTest_Foo')->Max('Foo'), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Fab')->Max('Fab'), 3); + } + /* */ + + /** + * Test basic aggregation on a given dataobject + * @return unknown_type + */ + public function testAutoTypeAggregate() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + $fab = $this->objFromFixture('AggregateTest_Fab', 'fab1'); + + // Template style access + $this->assertEquals($foo->Aggregate()->XML_val('Max', array('Foo')), 9); + $this->assertEquals($fab->Aggregate()->XML_val('Max', array('Fab')), 3); + + // PHP style access + $this->assertEquals($foo->Aggregate()->Max('Foo'), 9); + $this->assertEquals($fab->Aggregate()->Max('Fab'), 3); + } + /* */ + + /** + * Test base-level field access - was failing due to use of custom_database_fields, not just database_fields + * @return unknown_type + */ + public function testBaseFieldAggregate() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + + $this->assertEquals( + $this->formatDate($foo->Aggregate('AggregateTest_Foo')->Max('LastEdited')), + $this->formatDate(DataObject::get_one('AggregateTest_Foo', '', '', '"LastEdited" DESC')->LastEdited) + ); + + $this->assertEquals( + $this->formatDate($foo->Aggregate('AggregateTest_Foo')->Max('Created')), + $this->formatDate(DataObject::get_one('AggregateTest_Foo', '', '', '"Created" DESC')->Created) + ); + } + /* */ + + /** + * Test aggregation takes place on the passed type & it's children only + */ + public function testChildAggregate() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + + // For base classes, aggregate is calculcated on it and all children classes + $this->assertEquals($foo->Aggregate('AggregateTest_Foo')->Max('Foo'), 9); + + // For subclasses, aggregate is calculated for that subclass and it's children only + $this->assertEquals($foo->Aggregate('AggregateTest_Fab')->Max('Foo'), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + + } + /* */ + + /** + * Test aggregates are cached properly + */ + public function testCache() { + $this->markTestIncomplete(); + } + /* */ + + /** + * Test cache is correctly flushed on write + */ + public function testCacheFlushing() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + $fab = $this->objFromFixture('AggregateTest_Fab', 'fab1'); + + // For base classes, aggregate is calculcated on it and all children classes + $this->assertEquals($fab->Aggregate('AggregateTest_Foo')->Max('Foo'), 9); + + // For subclasses, aggregate is calculated for that subclass and it's children only + $this->assertEquals($fab->Aggregate('AggregateTest_Fab')->Max('Foo'), 9); + $this->assertEquals($fab->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + + $foo->Foo = 12; + $foo->write(); + + // For base classes, aggregate is calculcated on it and all children classes + $this->assertEquals($fab->Aggregate('AggregateTest_Foo')->Max('Foo'), 12); + + // For subclasses, aggregate is calculated for that subclass and it's children only + $this->assertEquals($fab->Aggregate('AggregateTest_Fab')->Max('Foo'), 9); + $this->assertEquals($fab->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + + $fab->Foo = 15; + $fab->write(); + + // For base classes, aggregate is calculcated on it and all children classes + $this->assertEquals($fab->Aggregate('AggregateTest_Foo')->Max('Foo'), 15); + + // For subclasses, aggregate is calculated for that subclass and it's children only + $this->assertEquals($fab->Aggregate('AggregateTest_Fab')->Max('Foo'), 15); + $this->assertEquals($fab->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + } + /* */ + + /** + * Test basic relationship aggregation + */ + public function testRelationshipAggregate() { + $bar1 = $this->objFromFixture('AggregateTest_Bar', 'bar1'); + $this->assertEquals($bar1->RelationshipAggregate('Foos')->Max('Foo'), 8); + + $baz1 = $this->objFromFixture('AggregateTest_Baz', 'baz1'); + $this->assertEquals($baz1->RelationshipAggregate('Foos')->Max('Foo'), 8); + } + /* */ + + /** + * Copied from DataObject::__construct(), special case for MSSQLDatabase. + * + * @param String + * @return String + */ + protected function formatDate($dateStr) { + $dateStr = preg_replace('/:[0-9][0-9][0-9]([ap]m)$/i', ' \\1', $dateStr); + return date('Y-m-d H:i:s', strtotime($dateStr)); + } +} diff --git a/tests/model/AggregateTest.yml b/tests/model/AggregateTest.yml new file mode 100644 index 000000000..4a8bc7242 --- /dev/null +++ b/tests/model/AggregateTest.yml @@ -0,0 +1,46 @@ +AggregateTest_Bar: + bar1: + Bar: 1 + bar2: + Bar: 2 +AggregateTest_Foo: + foo1: + Foo: 1 + Bar: =>AggregateTest_Bar.bar1 + foo2: + Foo: 2 + Bar: =>AggregateTest_Bar.bar1 + foo3: + Foo: 3 + Bar: =>AggregateTest_Bar.bar2 +AggregateTest_Fab: + fab1: + Foo: 7 + Fab: 1 + Bar: =>AggregateTest_Bar.bar1 + fab2: + Foo: 8 + Fab: 2 + Bar: =>AggregateTest_Bar.bar1 + fab3: + Foo: 9 + Fab: 3 + Bar: =>AggregateTest_Bar.bar2 +AggregateTest_Fac: + fac1: + Foo: 4 + Fac: 1 + fac2: + Foo: 5 + Fac: 2 + fac3: + Foo: 6 + Fac: 3 +AggregateTest_Baz: + baz1: + Baz: 1 + Foos: =>AggregateTest_Foo.foo1,=>AggregateTest_Foo.foo2,=>AggregateTest_Fab.fab1,=>AggregateTest_Fab.fab2 + baz2: + Baz: 2 + Foos: =>AggregateTest_Foo.foo3,=>AggregateTest_Fab.fab3 + \ No newline at end of file diff --git a/view/SSTemplateParser.php b/view/SSTemplateParser.php index fdae5ccfc..62f38277c 100644 --- a/view/SSTemplateParser.php +++ b/view/SSTemplateParser.php @@ -3751,6 +3751,15 @@ class SSTemplateParser extends Parser implements TemplateParser { '}; $scope->popScope(); '; } + /** + * The deprecated closed block handler for control blocks + * @deprecated + */ + function ClosedBlock_Handle_Control(&$res) { + Deprecation::notice('4.0', '<% control %> is deprecated. Use <% with %> or <% loop %> instead.'); + return $this->ClosedBlock_Handle_Loop($res); + } + /** * The closed block handler for with blocks */ diff --git a/view/SSTemplateParser.php.inc b/view/SSTemplateParser.php.inc index c2fedbe09..bc7640669 100644 --- a/view/SSTemplateParser.php.inc +++ b/view/SSTemplateParser.php.inc @@ -931,6 +931,15 @@ class SSTemplateParser extends Parser implements TemplateParser { '}; $scope->popScope(); '; } + /** + * The deprecated closed block handler for control blocks + * @deprecated + */ + function ClosedBlock_Handle_Control(&$res) { + Deprecation::notice('4.0', '<% control %> is deprecated. Use <% with %> or <% loop %> instead.'); + return $this->ClosedBlock_Handle_Loop($res); + } + /** * The closed block handler for with blocks */