Problem/Motivation

Tracker has the following controllers:

  • TrackerPage
  • TrackerUserRecent
  • TrackerUserTab

but they are all just including tracker.pages.inc.

Proposed resolution

Adds a single new controller that contains the code and also some code improvements, DI and so on.

Remaining tasks

User interface changes

API changes

The old controllers and functions are deprecated. Technically, none of them ever were an API, they are internal per BC policy.

Data model changes

Release notes snippet

CommentFileSizeAuthor
#53 3015650-53-interdiff.txt5.34 KBkim.pepper
#53 3015650-53.patch21 KBkim.pepper
#49 3015650-49-interdiff.txt2.55 KBkim.pepper
#49 3015650-49.patch16.31 KBkim.pepper
#46 3015650-46-interdiff.txt1.83 KBkim.pepper
#46 3015650-46.patch16.29 KBkim.pepper
#39 3015650-39-interdiff.txt796 bytesBerdir
#39 3015650-39.patch14.99 KBBerdir
#36 3015650-36-interdiff.txt2.66 KBBerdir
#36 3015650-36.patch14.99 KBBerdir
#34 3015650-34.patch15.04 KBandypost
#34 interdiff.txt915 bytesandypost
#29 3015650-29.patch14.15 KBandypost
#29 interdiff.txt3.11 KBandypost
#26 3015650-tracker-26.patch19.43 KBandypost
#26 interdiff.txt3.52 KBandypost
#25 3015650-tracker-25.patch19.92 KBkim.pepper
#25 3015650-tracker-25-interdiff.txt2.73 KBkim.pepper
#21 3015650-tracker-21.patch19.39 KBkim.pepper
#19 3015650-tracker-19.patch111.17 KBkim.pepper
#19 3015650-tracker-19-interdiff.txt1.16 KBkim.pepper
#17 3015650-tracker-17.patch19.41 KBkim.pepper
#17 3015650-tracker-17-interdiff.txt4.85 KBkim.pepper
#14 3015650-tracker-14.patch19.41 KBkim.pepper
#14 3015650-tracker-14-interdiff.txt1.25 KBkim.pepper
#12 10-12-interdiff.txt9.37 KBkim.pepper
#12 3015650-tracker-12.patch20.66 KBkim.pepper
#10 interdiff_8-10.txt528 bytesvacho
#10 3015650-10.patch15.2 KBvacho
#8 3015650-tracker-8.patch15.15 KBkim.pepper
#8 3015650-tracker-8-interdiff.txt4.58 KBkim.pepper
#7 3015650-7.patch13.27 KBandypost
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

kim.pepper created an issue. See original summary.

kim.pepper’s picture

Issue tags: +DrupalSouth 2018

Tagging for DrupalSouth

jibran’s picture

We can modernize the module file as well maybe in followup.

andypost’s picture

Meanwhile it is time to brainstorm on tracker fate #1261120-12: Deprecate Tracker module in D10 and move to contrib in D11

andypost’s picture

Eric115’s picture

Assigned: Unassigned » Eric115
andypost’s picture

Status: Active » Needs review
FileSize
13.27 KB

just to make ball roll

kim.pepper’s picture

  1. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,157 @@
    +      $query = \Drupal::database()->select('tracker_user', 't')
    
  2. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,157 @@
    +      $query = \Drupal::service('database.replica')->select('tracker_node', 't')
    
  3. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,157 @@
    +      $result = \Drupal::service('comment.statistics')->read($nodes, 'node', FALSE);
    
  4. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,157 @@
    +      $dateFormatter = \Drupal::service('date.formatter');
    

    We can inject these.

  5. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,157 @@
    +        ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
    

    We can use PagerSelectExtender::class instead.

  6. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,157 @@
    +  public function buildContent(AccountInterface $account = NULL) {
    

    Missing docs.

kim.pepper’s picture

  1. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,226 @@
    +      $nodes = Node::loadMultiple(array_keys($tracker_data));
    

    We should typehint this with
    /** @var \Drupal\node\NodeInterface[] $nodes */

  2. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,226 @@
    +        $node->last_activity = $tracker_data[$node->id()]->changed;
    ...
    +        if ($node->comment_count) {
    +          $comments = $node->comment_count;
    ...
    +            'data-history-node-last-comment-timestamp' => $node->last_comment_timestamp,
    ...
    +              '@time' => $this->dateFormatter->formatTimeDiffSince($node->last_activity),
    

    Should we be using get() and set() for these properties?

vacho’s picture

Solving point 1 from #9 comment.

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,227 @@
    +      // Enrich the node data.
    +      $result = $this->commentStatistics->read($nodes, 'node', FALSE);
    +      foreach ($result as $statistics) {
    +        // The node ID may not be unique; there can be multiple comment fields.
    +        // Make comment_count the total of all comments.
    +        $nid = $statistics->entity_id;
    +        if (empty($nodes[$nid]->comment_count)
    +          || !is_numeric($nodes[$nid]->comment_count)) {
    +          $nodes[$nid]->comment_count = $statistics->comment_count;
    +        }
    +        else {
    +          $nodes[$nid]->comment_count += $statistics->comment_count;
    +        }
    +        // Make the last comment timestamp reflect the latest comment.
    +        if (!isset($nodes[$nid]->last_comment_timestamp)) {
    +          $nodes[$nid]->last_comment_timestamp = $statistics->last_comment_timestamp;
    +        }
    +        else {
    +          $nodes[$nid]->last_comment_timestamp = max($nodes[$nid]->last_comment_timestamp, $statistics->last_comment_timestamp);
    +        }
    +      }
    

    https://api.drupal.org/api/drupal/core%21modules%21comment%21comment.mod... also loads comment statistics and adds it to the comment field. Makes me wonder if the code here is even still necessary?

  2. +++ b/core/modules/tracker/src/Controller/TrackerBase.php
    @@ -0,0 +1,227 @@
    +              '#url' => $node->urlInfo(),
    

    urlInfo() is deprecated, should use toUrl()

  3. +++ b/core/modules/tracker/src/Controller/TrackerPage.php
    @@ -2,19 +2,16 @@
     /**
      * Controller for tracker.page route.
      */
    -class TrackerPage extends ControllerBase {
    +class TrackerPage extends TrackerBase {
     
    

    I'm trying to understand why we really still need those old classes?

    Could we maybe rename TrackerBase to TrackerController and actually deprecate the child classes, and update the routes to call the parent directly?

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
20.66 KB
9.37 KB
  1. Not sure about that either. Didn't make any changes in this patch.
  2. Fixed
  3. Moved to a TrackerController, and updated routing.yml. I kept the old controllers and added class deprecation.

I also added change record for removing tracker.pages.inc.

Status: Needs review » Needs work

The last submitted patch, 12: 3015650-tracker-12.patch, failed testing. View results

kim.pepper’s picture

Reverting changes to core/modules/migrate_drupal/tests/fixtures/drupal7.php

Status: Needs review » Needs work

The last submitted patch, 14: 3015650-tracker-14.patch, failed testing. View results

Berdir’s picture

+++ b/core/modules/tracker/src/Controller/TrackerController.php
@@ -85,7 +139,7 @@ public static function create(ContainerInterface $container) {
    *   The render array.
    */
-  public function buildContent(AccountInterface $account = NULL) {
+  protected function buildContent(AccountInterface $account = NULL) {
     if ($account) {
       $query = $this->database->select('tracker_user', 't')

Couldn't we keep it public, rename $account to $user and then point the routes directly to buildContent()? They should be able to deal with an optional argument that's only set on one route just fine?

+++ b/core/modules/tracker/src/Controller/TrackerController.php
@@ -0,0 +1,281 @@
+      // Load nodes into an array with the same order as $tracker_data.
+      /** @var \Drupal\node\NodeInterface[] $nodes */
+      $nodes = Node::loadMultiple(array_keys($tracker_data));

should probably inject the entity type manager and load through the storage handler?

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
4.85 KB
19.41 KB

Yep. Sounds good.

  1. Fixed
  2. Fixed

Status: Needs review » Needs work

The last submitted patch, 17: 3015650-tracker-17.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
1.16 KB
111.17 KB

Need to rename the 'account' param to 'user' as @Berdir suggested above.

jibran’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

Needs rebase.

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
19.39 KB

Re-roll of #19

jibran’s picture

Assigned: Eric115 » Unassigned
Issue tags: -Needs reroll
+++ b/core/modules/tracker/src/Controller/TrackerController.php
@@ -0,0 +1,269 @@
+          'type' => node_get_type_label($node),

We have ETM injected already let's add nodeType storage property and get rid of this call.
$type = $this->nodeTypeStorage->load($node->bundle());

jibran’s picture

Sorry one more nit.

+++ b/core/modules/tracker/src/Controller/TrackerController.php
@@ -0,0 +1,269 @@
+    $cache_tags = Cache::mergeTags($cache_tags, $this->entityTypeManager()->getDefinition('node')->getListCacheTags());

We have injected nodeStorage so no need to use ETM method.

jibran’s picture

And one more nit.

+++ b/core/modules/tracker/src/Controller/TrackerController.php
@@ -0,0 +1,269 @@
+              '#account' => $node->getOwner(),
...
+        if ($node->getOwner()) {
+          $cache_tags = Cache::mergeTags($cache_tags, $node->getOwner()->getCacheTags());

We can have $owner

kim.pepper’s picture

  1. #22 Fixed
  2. #23Not sure whether I understand how to get a definition from node storage?? I added a property for nodeTypeDefinition instead.
  3. #24Added a variable
jibran’s picture

Status: Needs review » Needs work

This is a nice cleanup but unfortunately, there are some redundant changes. :)

  1. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -209,10 +193,9 @@ public function buildContent(AccountInterface $user = NULL) {
    -          'type' => $nodeType ? $nodeType->label() : FALSE,
    +          'type' => $node->getEntityType()->getBundleLabel(),
    

    These are not the same thing.
    The removed line will show, 'Page' or 'Article' and the added line will show 'Content Type'.

  2. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -251,7 +234,7 @@ public function buildContent(AccountInterface $user = NULL) {
    -    $cache_tags = Cache::mergeTags($cache_tags, $this->nodeTypeDefinition->getListCacheTags());
    +    $cache_tags = Cache::mergeTags($cache_tags, $this->nodeStorage->getEntityType()->getListCacheTags());
    

    This is a redundant change.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andypost’s picture

Status: Needs work » Needs review
FileSize
3.11 KB
14.15 KB

reroll and fix #27.1 - reverted back to node_get_type_label() is not deprecated and has todo to implement access from entity to bundle entity, so I did not injected node type storage - it supposed to be retrieved from $node

#27.2 is not redundant - controllers should not cache the entity definition (value object) inside properties

Berdir’s picture

  1. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,271 @@
    +        $nid = $statistics->entity_id;
    +        if (empty($nodes[$nid]->comment_count)
    +          || !is_numeric($nodes[$nid]->comment_count)) {
    +          $nodes[$nid]->comment_count = $statistics->comment_count;
    +        }
    +        else {
    +          $nodes[$nid]->comment_count += $statistics->comment_count;
    +        }
    +        // Make the last comment timestamp reflect the latest comment.
    +        if (!isset($nodes[$nid]->last_comment_timestamp)) {
    +          $nodes[$nid]->last_comment_timestamp = $statistics->last_comment_timestamp;
    +        }
    +        else {
    +          $nodes[$nid]->last_comment_timestamp = max($nodes[$nid]->last_comment_timestamp, $statistics->last_comment_timestamp);
    +        }
    

    Maybe we could store this comment metadata in $tracker_data instead of assigning to to arbitrary top-level node properties, which relies on __get() magic. We could also explicitly initialize those two array keys in the loop with $tracker_data += [...]; to avoid those if/else cases.

  2. +++ b/core/modules/tracker/src/Controller/TrackerUserTab.php
    @@ -2,27 +2,20 @@
        */
       public function getContent(UserInterface $user) {
    -    module_load_include('inc', 'tracker', 'tracker.pages');
    -    return tracker_page($user);
    

    We should also add @deprecated and @trigger_error to the old functions in tracker.pages? No need to call the controller from them, just leave the old code.

andypost’s picture

@Berdir good point about #30.1 but .2 makes no sense because trigger_error() added to class - so it someone using it as parent should get message

andypost’s picture

OTOH whole class needs deprecation to be caught by sniffers

Berdir’s picture

"Old functions in tracker.pages" (.inc).

The controllers are just wrappers around the old functions in the include file, I'm saying we should deprecate them too.

andypost’s picture

FileSize
915 bytes
15.04 KB

Patch deprecates old function
btw This statistics loaded in comment_entity_storage_load() so this properties probably just adjusted here

Berdir’s picture

> btw This statistics loaded in comment_entity_storage_load() so this properties probably just adjusted here

Yeah, but while not very obvious, that hook stores the information on the comment fields, of which there might be multiple. I assume this code was rewritten then to still store that information as summary of all comment fields that might exist on the node. So it's similar to that but actually not the same anyway and we could also store it in our own data array.

Berdir’s picture

Overlooked that $tracker_data is a list of objects too, so the initialize-with-defaults parts doesn't work but I still prefer to not put stuff onto the node entities like that, and combined with the ?? operator, we can clean it up a bit further (the current code relies on __get('last_comment_timestamp') not triggering a notice.

mikelutz’s picture

mikelutz’s picture

Status: Needs review » Needs work

I think moving the logic is fine, and I'm fine to move it without unnecessary refactoring. A couple nits.

  1. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    + * Base class for tracker controllers.
    

    While technically being used as a base class to the deprecated clases, this isn't really a base class, it is THE tracker controller.

  2. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +   * Constructs a TrackerBase object.
    

    I'm guessing you started planning to make an abstract base class that the other three could extend and changed you r mind.

  3. +++ b/core/modules/tracker/src/Controller/TrackerPage.php
    @@ -2,19 +2,18 @@
    +@trigger_error(__NAMESPACE__ . '\TrackerPage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\tracker\Controller\TrackerController instead. See https://www.drupal.org/node/3030645', E_USER_DEPRECATED);
    
    +++ b/core/modules/tracker/src/Controller/TrackerUserRecent.php
    @@ -2,38 +2,20 @@
    +@trigger_error(__NAMESPACE__ . '\TrackerUserRecent is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\tracker\Controller\TrackerController instead. See https://www.drupal.org/node/3030645', E_USER_DEPRECATED);
    
    +++ b/core/modules/tracker/src/Controller/TrackerUserTab.php
    @@ -2,27 +2,20 @@
    +@trigger_error(__NAMESPACE__ . '\TrackerUserTab is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\tracker\Controller\TrackerController instead. See https://www.drupal.org/node/3030645', E_USER_DEPRECATED);
    

    I'm supposed to make you write legacy tests to prove these errors are triggered, but even I confess I don't see the value..

  4. +++ b/core/modules/tracker/tracker.pages.inc
    @@ -19,8 +19,14 @@
    + *
    + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
    + *   \Drupal\tracker\Controller\TrackerController::buildContent() instead.
    + *
    + * @see https://www.drupal.org/node/3030645
      */
     function tracker_page($account = NULL) {
    +  @trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\tracker\Controller\TrackerController::buildContent() instead. See https://www.drupal.org/node/3030645', E_USER_DEPRECATED);
    

    Can we deprecate this whole file a la https://www.drupal.org/core/deprecation#how-file in addition to the function?

Berdir’s picture

Status: Needs work » Needs review
FileSize
14.99 KB
796 bytes

1. & 2. Fixed the comments.

3. & 4. Yeah, per https://www.drupal.org/core/d8-bc-policy, both controllers and and specifically also "Locations of procedural code" mentions that filenames and location of functions in them is internal. We've started being way more strict than that page recently, specifically on constructors, but I also think that this is enough. The chance that someone used these very specific controllers/functions that aren't reusable in any way is basically zero.

Berdir’s picture

Issue summary: View changes
mikelutz’s picture

Status: Needs review » Reviewed & tested by the community

I guess my argument on adding the error to the file is specifically that require_once(\Drupal::service('module_handler').getModule('tracker').getPath() . "/tracker.pages.inc"); will work in D8 with no error triggered and throw a fatal in D9. That all having been said, we still have to load all the legacy include files in D8 and I assume we are intending to remove them for D9, So so be it. Perhaps we should update https://www.drupal.org/core/deprecation#how-file to say simply to deprecate all the functions, and not say we need to trigger the error in the file itself.

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +    return $user->getAccountName();
    

    should we be using getDisplayName here instead - the docs of getAccountName say

    Only display this name to admins and to the user who owns this account, and only in the context of the name used to login. For any other display purposes, use
    \Drupal\Core\Session\AccountInterface::getDisplayName() instead.

  2. +++ b/core/modules/tracker/tracker.pages.inc
    @@ -19,8 +19,14 @@
     function tracker_page($account = NULL) {
    +  @trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\tracker\Controller\TrackerController::buildContent() instead. See https://www.drupal.org/node/3030645', E_USER_DEPRECATED);
    

    I think we need a deprecation test for this

larowlan’s picture

http://grep.xnddx.ru/search?text=tracker_page&filename= shows its just portfolio_theme, which had core committed in an earlier version

xjm’s picture

Issue tags: +mwds2019
andypost’s picture

Status: Needs review » Needs work

NW for #42.2 at least

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
16.29 KB
1.83 KB

Fixes for #42.

jibran’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC as #42 has been addressed.

dpi’s picture

Status: Reviewed & tested by the community » Needs review

Changes look good,

Minor feedback:

  1. +++ b/core/modules/tracker/tracker.routing.yml
    @@ -9,18 +9,18 @@ tracker.page:
    +    _title_callback: '\Drupal\tracker\Controller\TrackerController::getTitle'
    
  2. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +  public function getTitle(UserInterface $user) {
    

The title only applies to one of the routes, I suggest renaming the method, or removing it entirely: setting the title in controller instead, with ['#title'] in the render array.

Falling under "code improvements" / modernization:

  1. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +        $cache_tags = Cache::mergeTags($cache_tags, $node->getCacheTags());
    
  2. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +          $cache_tags = Cache::mergeTags($cache_tags, $owner->getCacheTags());
    
  3. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +    $cache_tags = Cache::mergeTags($cache_tags, $this->nodeStorage->getEntityType()->getListCacheTags());
    
  4. +++ b/core/modules/tracker/src/Controller/TrackerController.php
    @@ -0,0 +1,265 @@
    +    $page['#cache']['contexts'][] = 'user.node_grants:view';
    

Many of these merge calls and other '#cache' usage can be converted to: creating a \Drupal\Core\Cache\CacheableMetadata object at the top, adding all cache metadata in between, then apply-ing just before the render array is returned. Code would be simpler to read.

kim.pepper’s picture

Thanks @dpi!

Re: 'Minor feedback:'

Not entirely sure how we determine whether to set the title or not from inside buildContent($user = NULL). There are two routes that pass a $user but only one of them sets a title.

Re: "code improvements" / modernization:

Good idea! I've not spent much time with this, so let me know if this works.

dpi’s picture

Status: Needs review » Reviewed & tested by the community

After looking at how titles are determined/set, it appears the way it is implemented may be more straightforward.

Thanks for the cacheable metadata object, looks good!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/tracker/tests/src/Kernel/TrackerLegacyTest.php
@@ -0,0 +1,44 @@
+    module_load_include('inc', 'tracker', 'tracker.pages');
+    $this->assertNotEmpty(tracker_page());

One of the dangers of not having tracker_page() call out to the new controller is that this test isn't really enough. If we committed this change we'd have no coverage of the code in tracker_page() beyond that it returns a value that's not empty. Beyond this the change looks great - nice to see this finally cleaned up.

alexpott’s picture

+++ b/core/modules/tracker/tracker.pages.inc
@@ -19,8 +19,14 @@
   if ($account) {
     $query = \Drupal::database()->select('tracker_user', 't')
       ->extend('Drupal\Core\Database\Query\PagerSelectExtender')

This could be return \Drupal::classResolver(TrackerController::class)->buildContent($account); and then everything is using the same code.

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
21 KB
5.34 KB

Ah yes! Good idea.

Let's see what the bot says. :-)

jibran’s picture

Status: Needs review » Reviewed & tested by the community

Yeah, this looks better now.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 3ed7550 and pushed to 8.8.x. Thanks!

+++ b/core/modules/tracker/src/Controller/TrackerController.php
@@ -0,0 +1,264 @@
+  public function getTitle(UserInterface $user) {
+    return $user->getDisplayName();
+  }

+++ b/core/modules/tracker/src/Controller/TrackerUserTab.php
@@ -2,27 +2,20 @@
-  public function getTitle(UserInterface $user) {
-    return $user->getAccountName();
+    return $this->buildContent($user);
   }
 

So this change to use display name is actually a bug fix. It's been discussed on #2629286: Use getDisplayName() for user names consistently - an issue where progress has been hard. Given that and the fact that there is no logic in either ::getTitle() method I think we should proceed here.

  • alexpott committed 3ed7550 on 8.8.x
    Issue #3015650 by kim.pepper, andypost, Berdir, vacho, jibran, mikelutz...
alexpott’s picture

Title: Remove tracker.pages.inc » Deprecate tracker_page() and allow tracker.pages.inc to be removed in Drupal 9

Fixing the issue title to be closer to what has occurred.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.