Commit 1321863c authored by Stuart Langley's avatar Stuart Langley Committed by Commit Bot

Fix breadcrumbs for "Computers" and nested computers.

This patch updates line_location.js to support deciphering paths that fall
under Google Drive > Computers.

Adds an integration test which ensures that Computers entries can be navigated,
which has the side effect of also testing that the breadcrumbs are working as
breadcrumbs are used to work out where the navigation has actually navigated to.

Bug: 884020
Change-Id: I8ea14eb6716a5060005442e65d07125b41a9c4a3
Reviewed-on: https://chromium-review.googlesource.com/c/1312178
Commit-Queue: Stuart Langley <slangley@chromium.org>
Reviewed-by: default avatarLuciano Pacheco <lucmult@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604761}
parent 244ff236
...@@ -201,6 +201,7 @@ WRAPPED_INSTANTIATE_TEST_CASE_P( ...@@ -201,6 +201,7 @@ WRAPPED_INSTANTIATE_TEST_CASE_P(
TestCase("fileDisplayDriveOffline").Offline().EnableDriveFs(), TestCase("fileDisplayDriveOffline").Offline().EnableDriveFs(),
TestCase("fileDisplayDriveOnline").EnableDriveFs(), TestCase("fileDisplayDriveOnline").EnableDriveFs(),
TestCase("fileDisplayDriveOnline").DisableDriveFs(), TestCase("fileDisplayDriveOnline").DisableDriveFs(),
TestCase("fileDisplayComputers").EnableDriveFs(),
TestCase("fileDisplayMtp"), TestCase("fileDisplayMtp"),
TestCase("fileDisplayUsb"), TestCase("fileDisplayUsb"),
TestCase("fileSearch"), TestCase("fileSearch"),
......
...@@ -115,7 +115,7 @@ struct AddEntriesMessage { ...@@ -115,7 +115,7 @@ struct AddEntriesMessage {
}; };
// Represents the different types of entries (e.g. file, folder). // Represents the different types of entries (e.g. file, folder).
enum EntryType { FILE, DIRECTORY, TEAM_DRIVE }; enum EntryType { FILE, DIRECTORY, TEAM_DRIVE, COMPUTER };
// Represents whether an entry appears in 'Share with Me' or not. // Represents whether an entry appears in 'Share with Me' or not.
enum SharedOption { NONE, SHARED, SHARED_WITH_ME, NESTED_SHARED_WITH_ME }; enum SharedOption { NONE, SHARED, SHARED_WITH_ME, NESTED_SHARED_WITH_ME };
...@@ -214,6 +214,7 @@ struct AddEntriesMessage { ...@@ -214,6 +214,7 @@ struct AddEntriesMessage {
const std::string& target_path, const std::string& target_path,
const std::string& mime_type, const std::string& mime_type,
const std::string& team_drive_name, const std::string& team_drive_name,
const std::string& computer_name,
SharedOption shared_option, SharedOption shared_option,
const base::Time& last_modified_time, const base::Time& last_modified_time,
const EntryCapabilities& capabilities, const EntryCapabilities& capabilities,
...@@ -223,6 +224,7 @@ struct AddEntriesMessage { ...@@ -223,6 +224,7 @@ struct AddEntriesMessage {
source_file_name(source_file_name), source_file_name(source_file_name),
target_path(target_path), target_path(target_path),
team_drive_name(team_drive_name), team_drive_name(team_drive_name),
computer_name(computer_name),
mime_type(mime_type), mime_type(mime_type),
last_modified_time(last_modified_time), last_modified_time(last_modified_time),
capabilities(capabilities), capabilities(capabilities),
...@@ -234,6 +236,7 @@ struct AddEntriesMessage { ...@@ -234,6 +236,7 @@ struct AddEntriesMessage {
std::string target_path; // Target file or directory path. std::string target_path; // Target file or directory path.
std::string name_text; // Display file name. std::string name_text; // Display file name.
std::string team_drive_name; // Name of team drive this entry is in. std::string team_drive_name; // Name of team drive this entry is in.
std::string computer_name; // Name of the computer this entry is in.
std::string mime_type; // File entry content mime type. std::string mime_type; // File entry content mime type.
base::Time last_modified_time; // Entry last modified time. base::Time last_modified_time; // Entry last modified time.
EntryCapabilities capabilities; // Entry permissions. EntryCapabilities capabilities; // Entry permissions.
...@@ -250,6 +253,8 @@ struct AddEntriesMessage { ...@@ -250,6 +253,8 @@ struct AddEntriesMessage {
converter->RegisterStringField("nameText", &TestEntryInfo::name_text); converter->RegisterStringField("nameText", &TestEntryInfo::name_text);
converter->RegisterStringField("teamDriveName", converter->RegisterStringField("teamDriveName",
&TestEntryInfo::team_drive_name); &TestEntryInfo::team_drive_name);
converter->RegisterStringField("computerName",
&TestEntryInfo::computer_name);
converter->RegisterStringField("mimeType", &TestEntryInfo::mime_type); converter->RegisterStringField("mimeType", &TestEntryInfo::mime_type);
converter->RegisterCustomField("sharedOption", converter->RegisterCustomField("sharedOption",
&TestEntryInfo::shared_option, &TestEntryInfo::shared_option,
...@@ -270,6 +275,8 @@ struct AddEntriesMessage { ...@@ -270,6 +275,8 @@ struct AddEntriesMessage {
*type = DIRECTORY; *type = DIRECTORY;
else if (value == "team_drive") else if (value == "team_drive")
*type = TEAM_DRIVE; *type = TEAM_DRIVE;
else if (value == "Computer")
*type = COMPUTER;
else else
return false; return false;
return true; return true;
...@@ -456,6 +463,10 @@ class LocalTestVolume : public TestVolume { ...@@ -456,6 +463,10 @@ class LocalTestVolume : public TestVolume {
NOTREACHED() << "Can't create a team drive in a local volume: " NOTREACHED() << "Can't create a team drive in a local volume: "
<< target_path.value(); << target_path.value();
break; break;
case AddEntriesMessage::COMPUTER:
NOTREACHED() << "Can't create a computer in a local volume: "
<< target_path.value();
break;
} }
ASSERT_TRUE(UpdateModifiedTime(entry)); ASSERT_TRUE(UpdateModifiedTime(entry));
...@@ -572,13 +583,13 @@ class FakeTestVolume : public LocalTestVolume { ...@@ -572,13 +583,13 @@ class FakeTestVolume : public LocalTestVolume {
// Note: must be kept in sync with BASIC_FAKE_ENTRY_SET defined in the // Note: must be kept in sync with BASIC_FAKE_ENTRY_SET defined in the
// integration_tests/file_manager JS code. // integration_tests/file_manager JS code.
CreateEntry(AddEntriesMessage::TestEntryInfo( CreateEntry(AddEntriesMessage::TestEntryInfo(
AddEntriesMessage::FILE, "text.txt", "hello.txt", std::string(), AddEntriesMessage::FILE, "text.txt", "hello.txt", "text/plain",
"text/plain", AddEntriesMessage::SharedOption::NONE, base::Time::Now(), std::string(), std::string(), AddEntriesMessage::SharedOption::NONE,
AddEntriesMessage::EntryCapabilities(), false)); base::Time::Now(), AddEntriesMessage::EntryCapabilities(), false));
CreateEntry(AddEntriesMessage::TestEntryInfo( CreateEntry(AddEntriesMessage::TestEntryInfo(
AddEntriesMessage::DIRECTORY, std::string(), "A", std::string(), AddEntriesMessage::DIRECTORY, std::string(), "A", std::string(),
std::string(), AddEntriesMessage::SharedOption::NONE, base::Time::Now(), std::string(), std::string(), AddEntriesMessage::SharedOption::NONE,
AddEntriesMessage::EntryCapabilities(), false)); base::Time::Now(), AddEntriesMessage::EntryCapabilities(), false));
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
return true; return true;
} }
...@@ -588,21 +599,21 @@ class FakeTestVolume : public LocalTestVolume { ...@@ -588,21 +599,21 @@ class FakeTestVolume : public LocalTestVolume {
return false; return false;
CreateEntry(AddEntriesMessage::TestEntryInfo( CreateEntry(AddEntriesMessage::TestEntryInfo(
AddEntriesMessage::DIRECTORY, "", "DCIM", std::string(), "", AddEntriesMessage::DIRECTORY, "", "DCIM", std::string(), std::string(),
AddEntriesMessage::SharedOption::NONE, base::Time::Now(), "", AddEntriesMessage::SharedOption::NONE, base::Time::Now(),
AddEntriesMessage::EntryCapabilities(), false)); AddEntriesMessage::EntryCapabilities(), false));
CreateEntry(AddEntriesMessage::TestEntryInfo( CreateEntry(AddEntriesMessage::TestEntryInfo(
AddEntriesMessage::FILE, "image2.png", "image2.png", std::string(), AddEntriesMessage::FILE, "image2.png", "image2.png", std::string(),
"image/png", AddEntriesMessage::SharedOption::NONE, base::Time::Now(), std::string(), "image/png", AddEntriesMessage::SharedOption::NONE,
AddEntriesMessage::EntryCapabilities(), false)); base::Time::Now(), AddEntriesMessage::EntryCapabilities(), false));
CreateEntry(AddEntriesMessage::TestEntryInfo( CreateEntry(AddEntriesMessage::TestEntryInfo(
AddEntriesMessage::FILE, "image3.jpg", "DCIM/image3.jpg", std::string(), AddEntriesMessage::FILE, "image3.jpg", "DCIM/image3.jpg", std::string(),
"image/jpeg", AddEntriesMessage::SharedOption::NONE, base::Time::Now(), std::string(), "image/jpeg", AddEntriesMessage::SharedOption::NONE,
AddEntriesMessage::EntryCapabilities(), false)); base::Time::Now(), AddEntriesMessage::EntryCapabilities(), false));
CreateEntry(AddEntriesMessage::TestEntryInfo( CreateEntry(AddEntriesMessage::TestEntryInfo(
AddEntriesMessage::FILE, "text.txt", "DCIM/hello.txt", std::string(), AddEntriesMessage::FILE, "text.txt", "DCIM/hello.txt", std::string(),
"text/plain", AddEntriesMessage::SharedOption::NONE, base::Time::Now(), std::string(), "text/plain", AddEntriesMessage::SharedOption::NONE,
AddEntriesMessage::EntryCapabilities(), false)); base::Time::Now(), AddEntriesMessage::EntryCapabilities(), false));
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
return true; return true;
} }
...@@ -709,6 +720,9 @@ class DriveTestVolume : public TestVolume { ...@@ -709,6 +720,9 @@ class DriveTestVolume : public TestVolume {
case AddEntriesMessage::TEAM_DRIVE: case AddEntriesMessage::TEAM_DRIVE:
CreateTeamDrive(entry.team_drive_name, team_drive_capabilities); CreateTeamDrive(entry.team_drive_name, team_drive_capabilities);
break; break;
case AddEntriesMessage::COMPUTER:
NOTREACHED() << "Can't create a computer in a drive test volume: "
<< entry.computer_name;
} }
// Any file or directory created above, will only appear in Drive after // Any file or directory created above, will only appear in Drive after
...@@ -902,6 +916,9 @@ class DriveFsTestVolume : public DriveTestVolume { ...@@ -902,6 +916,9 @@ class DriveFsTestVolume : public DriveTestVolume {
ASSERT_TRUE(base::CreateDirectory(target_path)) ASSERT_TRUE(base::CreateDirectory(target_path))
<< "Failed to create a team drive: " << target_path.value(); << "Failed to create a team drive: " << target_path.value();
break; break;
case AddEntriesMessage::COMPUTER:
ASSERT_TRUE(base::CreateDirectory(target_path))
<< "Failed to create a computer: " << target_path.value();
} }
ASSERT_TRUE(UpdateModifiedTime(entry)); ASSERT_TRUE(UpdateModifiedTime(entry));
...@@ -913,6 +930,7 @@ class DriveFsTestVolume : public DriveTestVolume { ...@@ -913,6 +930,7 @@ class DriveFsTestVolume : public DriveTestVolume {
CreateDriveFsConnectionDelegate() override { CreateDriveFsConnectionDelegate() override {
CHECK(base::CreateDirectory(GetMyDrivePath())); CHECK(base::CreateDirectory(GetMyDrivePath()));
CHECK(base::CreateDirectory(GetTeamDriveGrandRoot())); CHECK(base::CreateDirectory(GetTeamDriveGrandRoot()));
CHECK(base::CreateDirectory(GetComputerGrandRoot()));
if (!fake_drivefs_helper_) { if (!fake_drivefs_helper_) {
fake_drivefs_helper_ = fake_drivefs_helper_ =
...@@ -934,6 +952,7 @@ class DriveFsTestVolume : public DriveTestVolume { ...@@ -934,6 +952,7 @@ class DriveFsTestVolume : public DriveTestVolume {
// Update the modified time of parent directories because they may be // Update the modified time of parent directories because they may be
// also affected by the update of child items. // also affected by the update of child items.
if (path.DirName() != GetTeamDriveGrandRoot() && if (path.DirName() != GetTeamDriveGrandRoot() &&
path.DirName() != GetComputerGrandRoot() &&
path.DirName() != GetMyDrivePath() && path.DirName() != GetMyDrivePath() &&
path.DirName() != GetSharedWithMePath()) { path.DirName() != GetSharedWithMePath()) {
const auto it = entries_.find(path.DirName()); const auto it = entries_.find(path.DirName());
...@@ -963,6 +982,9 @@ class DriveFsTestVolume : public DriveTestVolume { ...@@ -963,6 +982,9 @@ class DriveFsTestVolume : public DriveTestVolume {
if (!entry.team_drive_name.empty()) { if (!entry.team_drive_name.empty()) {
return GetTeamDrivePath(entry.team_drive_name); return GetTeamDrivePath(entry.team_drive_name);
} }
if (!entry.computer_name.empty()) {
return GetComputerPath(entry.computer_name);
}
return GetMyDrivePath(); return GetMyDrivePath();
} }
...@@ -982,6 +1004,10 @@ class DriveFsTestVolume : public DriveTestVolume { ...@@ -982,6 +1004,10 @@ class DriveFsTestVolume : public DriveTestVolume {
return mount_path().Append("team_drives"); return mount_path().Append("team_drives");
} }
base::FilePath GetComputerGrandRoot() {
return mount_path().Append("Computers");
}
base::FilePath GetSharedWithMePath() { base::FilePath GetSharedWithMePath() {
return mount_path().Append(".files-by-id/123"); return mount_path().Append(".files-by-id/123");
} }
...@@ -990,6 +1016,10 @@ class DriveFsTestVolume : public DriveTestVolume { ...@@ -990,6 +1016,10 @@ class DriveFsTestVolume : public DriveTestVolume {
return GetTeamDriveGrandRoot().Append(team_drive_name); return GetTeamDriveGrandRoot().Append(team_drive_name);
} }
base::FilePath GetComputerPath(const std::string& computer_name) {
return GetComputerGrandRoot().Append(computer_name);
}
Profile* const profile_; Profile* const profile_;
std::map<base::FilePath, const AddEntriesMessage::TestEntryInfo> entries_; std::map<base::FilePath, const AddEntriesMessage::TestEntryInfo> entries_;
std::unique_ptr<drive::FakeDriveFsHelper> fake_drivefs_helper_; std::unique_ptr<drive::FakeDriveFsHelper> fake_drivefs_helper_;
......
...@@ -108,6 +108,11 @@ LocationLine.prototype.getComponents_ = function(entry) { ...@@ -108,6 +108,11 @@ LocationLine.prototype.getComponents_ = function(entry) {
displayRootUrl, VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH); displayRootUrl, VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH);
components.push(new LocationLine.PathComponent( components.push(new LocationLine.PathComponent(
util.getRootTypeLabel(locationInfo), displayRootUrl)); util.getRootTypeLabel(locationInfo), displayRootUrl));
} else if (locationInfo.rootType === VolumeManagerCommon.RootType.COMPUTER) {
displayRootUrl = this.replaceRootName_(
displayRootUrl, VolumeManagerCommon.COMPUTERS_DIRECTORY_PATH);
components.push(new LocationLine.PathComponent(
util.getRootTypeLabel(locationInfo), displayRootUrl));
} else { } else {
components.push(new LocationLine.PathComponent( components.push(new LocationLine.PathComponent(
util.getRootTypeLabel(locationInfo), displayRootUrl)); util.getRootTypeLabel(locationInfo), displayRootUrl));
...@@ -119,6 +124,10 @@ LocationLine.prototype.getComponents_ = function(entry) { ...@@ -119,6 +124,10 @@ LocationLine.prototype.getComponents_ = function(entry) {
VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH)) { VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH)) {
relativePath = entry.fullPath.slice( relativePath = entry.fullPath.slice(
VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH.length); VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH.length);
} else if (entry.fullPath.startsWith(
VolumeManagerCommon.COMPUTERS_DIRECTORY_PATH)) {
relativePath = entry.fullPath.slice(
VolumeManagerCommon.COMPUTERS_DIRECTORY_PATH.length);
} }
if (relativePath.indexOf('/') === 0) { if (relativePath.indexOf('/') === 0) {
relativePath = relativePath.slice(1); relativePath = relativePath.slice(1);
......
...@@ -425,7 +425,7 @@ test.addEntries = function(downloads, drive, crostini) { ...@@ -425,7 +425,7 @@ test.addEntries = function(downloads, drive, crostini) {
.fileSystem); .fileSystem);
fsDrive.populate( fsDrive.populate(
test.TestEntryInfo.getMockFileSystemPopulateRows(drive, '/root/'), true); test.TestEntryInfo.getMockFileSystemPopulateRows(drive, '/root/'), true);
fsDrive.populate(['/team_drives/']); fsDrive.populate(['/team_drives/', '/Computers/']);
const fsCrostini = /** @type {MockFileSystem} */ ( const fsCrostini = /** @type {MockFileSystem} */ (
mockVolumeManager mockVolumeManager
......
...@@ -251,6 +251,17 @@ var TEAM_DRIVE_ENTRY_SET = [ ...@@ -251,6 +251,17 @@ var TEAM_DRIVE_ENTRY_SET = [
ENTRIES.teamDriveBFile, ENTRIES.teamDriveBFile,
]; ];
/**
* Entry set for Drive that includes Computers, including nested computers with
* files and nested "USB and External Devices" with nested devices.
*/
let COMPUTERS_ENTRY_SET = [
ENTRIES.hello,
ENTRIES.computerA,
ENTRIES.computerAFile,
ENTRIES.computerAdirectoryA,
];
/** /**
* Opens a Files app's main window. * Opens a Files app's main window.
* *
......
...@@ -159,6 +159,48 @@ testcase.fileDisplayDriveOnline = function() { ...@@ -159,6 +159,48 @@ testcase.fileDisplayDriveOnline = function() {
]); ]);
}; };
/**
* Tests files display in the "Computers" section of Google Drive. Testing that
* we can navigate to folders inside /Computers also has the side effect of
* testing that the breadcrumbs are working.
*/
testcase.fileDisplayComputers = function() {
let appId;
StepsRunner.run([
// Open Files app on Drive with Computers registered.
function() {
setupAndWaitUntilReady(
null, RootPath.DRIVE, this.next, [], COMPUTERS_ENTRY_SET);
},
function(result) {
appId = result.windowId;
// Navigate to Comuter Grand Root.
return remoteCall
.navigateWithDirectoryTree(appId, '/Computers', 'Computers')
.then(this.next);
},
function() {
// Navigiate to a Computer Root.
return remoteCall
.navigateWithDirectoryTree(
appId, '/Computers/Computer A', 'Computers')
.then(this.next);
},
function() {
// Navigiate to a subdirectory under a Computer Root.
return remoteCall
.navigateWithDirectoryTree(
appId, '/Computers/Computer A/A', 'Computers')
.then(this.next);
},
function() {
checkIfNoErrorsOccured(this.next);
},
]);
};
/** /**
* Tests files display in an MTP volume. * Tests files display in an MTP volume.
*/ */
......
...@@ -552,7 +552,9 @@ RemoteCallFilesApp.prototype.navigateWithDirectoryTree = function( ...@@ -552,7 +552,9 @@ RemoteCallFilesApp.prototype.navigateWithDirectoryTree = function(
.then(() => { .then(() => {
// Entries within Drive starts with /root/ but it isn't displayed in the // Entries within Drive starts with /root/ but it isn't displayed in the
// breadcrubms used by waitUntilCurrentDirectoryIsChanged. // breadcrubms used by waitUntilCurrentDirectoryIsChanged.
path = path.replace(/^\/root/, '').replace(/^\/team_drives/, ''); path = path.replace(/^\/root/, '')
.replace(/^\/team_drives/, '')
.replace(/^\/Computers/, '');
// Wait until the Files app is navigated to the path. // Wait until the Files app is navigated to the path.
return this.waitUntilCurrentDirectoryIsChanged( return this.waitUntilCurrentDirectoryIsChanged(
......
...@@ -228,6 +228,7 @@ var EntryType = Object.freeze({ ...@@ -228,6 +228,7 @@ var EntryType = Object.freeze({
FILE: 'file', FILE: 'file',
DIRECTORY: 'directory', DIRECTORY: 'directory',
TEAM_DRIVE: 'team_drive', TEAM_DRIVE: 'team_drive',
COMPUTER: 'Computer'
}); });
/** /**
...@@ -314,6 +315,11 @@ TestEntryInfoOptions.prototype.targetPath; ...@@ -314,6 +315,11 @@ TestEntryInfoOptions.prototype.targetPath;
* string (no team drive). Team Drive names must be unique. * string (no team drive). Team Drive names must be unique.
*/ */
TestEntryInfoOptions.prototype.teamDriveName; TestEntryInfoOptions.prototype.teamDriveName;
/**
* @type {string} Name of the computer this entry is in. Defaults to a blank
* string (no computer). Computer names must be unique.
*/
TestEntryInfoOptions.prototype.computerName;
/** /**
* @type {string|undefined} Mime type. * @type {string|undefined} Mime type.
*/ */
...@@ -358,6 +364,7 @@ function TestEntryInfo(options) { ...@@ -358,6 +364,7 @@ function TestEntryInfo(options) {
this.sourceFileName = options.sourceFileName || ''; this.sourceFileName = options.sourceFileName || '';
this.targetPath = options.targetPath; this.targetPath = options.targetPath;
this.teamDriveName = options.teamDriveName || ''; this.teamDriveName = options.teamDriveName || '';
this.computerName = options.computerName || '';
this.mimeType = options.mimeType || ''; this.mimeType = options.mimeType || '';
this.sharedOption = options.sharedOption || SharedOption.NONE; this.sharedOption = options.sharedOption || SharedOption.NONE;
this.lastModifiedTime = options.lastModifiedTime; this.lastModifiedTime = options.lastModifiedTime;
...@@ -750,6 +757,41 @@ var ENTRIES = { ...@@ -750,6 +757,41 @@ var ENTRIES = {
}, },
}), }),
// Computer entries.
computerA: new TestEntryInfo({
type: EntryType.COMPUTER,
computerName: 'Computer A',
}),
computerAFile: new TestEntryInfo({
type: EntryType.FILE,
sourceFileName: 'text.txt',
targetPath: 'computerAFile.txt',
mimeType: 'text/plain',
lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
nameText: 'computerAFile.txt',
sizeText: '51 bytes',
typeText: 'Plain text',
computerName: 'Computer A',
capabilities: {
canCopy: true,
canDelete: true,
canRename: true,
canAddChildren: false,
canShare: true,
},
}),
computerAdirectoryA: new TestEntryInfo({
type: EntryType.DIRECTORY,
targetPath: 'A',
lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
computerName: 'Computer A',
nameText: 'A',
sizeText: '--',
typeText: 'Folder'
}),
// Read-only and write-restricted entries. // Read-only and write-restricted entries.
// TODO(sashab): Generate all combinations of capabilities inside the test, to // TODO(sashab): Generate all combinations of capabilities inside the test, to
// ensure maximum coverage. // ensure maximum coverage.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment