Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions code/+nansen/+manage/detectNewSessions.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
% look for session folders based on the current datalocation model
% (i.e current project) and make a list of session objects based on
% folders. Session objects for all sessions that are not present in the
% table is returned.
% table is returned. The session objects are of the class of the
% metatable (its MetaTableClass).
%
% INPUTS:
% metaTable : a session metatable
Expand Down Expand Up @@ -36,8 +37,9 @@
return
end

% Todo: Get schema based on selection
sessionSchema = @nansen.metadata.type.Session;
% New sessions are of the table's class, so that their columns match
% the rows of the table
sessionSchema = str2func(metaTable.MetaTableClass);
args = {'DataLocationModel', dataLocationModel};

% Create a list of session metadata objects
Expand Down
21 changes: 15 additions & 6 deletions code/+nansen/+manage/updateSubjectTable.m
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
function updateSubjectTable(metatableCatalog, subjectSchema)

if nargin < 2
subjectSchema = 'nansen.metadata.type.Subject';
end
%updateSubjectTable Add the subjects of the session table that the subject table lacks
%
% updateSubjectTable(metatableCatalog) adds a row to the master subject
% table for each subject ID in the master session table that the subject
% table does not have. The new subjects are of the subject table's class,
% so that their columns match the existing rows.
%
% updateSubjectTable(metatableCatalog, subjectSchema) creates the new
% subjects with the class named by subjectSchema.

% Find master session table from metatable catalog
sessionTable = metatableCatalog.getMasterMetaTable('session');
subjectTable = metatableCatalog.getMasterMetaTable('subject');

if nargin < 2
subjectSchema = subjectTable.MetaTableClass;
end

try
uniqueSubjectIds = unique( sessionTable.entries.subjectID );

Expand All @@ -20,8 +29,8 @@ function updateSubjectTable(metatableCatalog, subjectSchema)

% Create subjects.
subjectArray(numSubjects) = feval(subjectSchema); %#ok<FVAL>
for i = 1:numel(uniqueSubjectIds)
subjectArray(i).SubjectID = uniqueSubjectIds{i};
for i = 1:numSubjects
subjectArray(i).SubjectID = newSubjectIds{i};
end
catch
return
Expand Down
6 changes: 4 additions & 2 deletions code/+nansen/+metadata/+type/Session.m
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,12 @@ function refreshDataLocations(obj)
end

rootUid = obj(iObj).DataLocation(jDl).RootUid;
if isempty(rootUid)
if isempty(rootUid) && ~isempty(S(jDl).RootPath)
% If the rootUid is empty, use the last available
% rootpath from the model, using this as the
% automatic location.
% automatic location. A data location that has no
% root path yet (a new project's default location
% before it is configured) leaves it empty.
rootUid = S(jDl).RootPath(end).Key;
obj(iObj).DataLocation(jDl).RootUid = rootUid;
end
Expand Down
13 changes: 13 additions & 0 deletions code/+nansen/+metadata/@MetaTable/MetaTable.m
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,19 @@ function fromFileStruct(obj, S)
error('NANSEN:MetaTable:InvalidFileType', ...
'The file "%s" does not contain a MetaTable', fileName)
end

% A table saved without the name of its ID column takes it from
% its class (SchemaIdName), so the class must be on the path
className = char(S.MetaTableClass);
hasIdVarname = isfield(S, 'MetaTableIdVarname') && ~isempty(S.MetaTableIdVarname);
if ~hasIdVarname && ~isempty(className) && ~strcmp(className, 'table') ...
&& isempty(meta.class.fromName(className))
[~, fileName] = fileparts(obj.filepath);
error('NANSEN:MetaTable:ClassNotFound', ...
['The MetaTable "%s" holds entries of class %s, which is not on the MATLAB path. ', ...
'Add the folder that defines %s to the path and open the table again.'], ...
fileName, className, className)
end
obj.fromStruct(S);
end

Expand Down
22 changes: 22 additions & 0 deletions code/+nansen/+metadata/@MetaTableCatalog/MetaTableCatalog.m
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,22 @@ function removeSavePathFromTable(obj)
end
end

function tf = hasMasterMetaTable(obj, typeName)
%hasMasterMetaTable Check if the catalog has a master MetaTable of a type
%
% tf = hasMasterMetaTable(obj, typeName) is true when the class
% name of a master MetaTable contains typeName, ignoring case.
% This is the rule getMasterMetaTable uses to find the table, so
% a table of a project's own class, such as
% myproject.metadata.type.Subject, counts as a subject table.
if isempty(obj.Table)
tf = false;
return
end
tf = any(obj.Table.IsMaster & ...
contains(obj.Table.MetaTableClass, typeName, 'IgnoreCase', true));
end

function metaTable = getMasterMetaTable(obj, typeName)
% Todo: merge with method below (getMasterTable)
isMatch = obj.Table.IsMaster & ...
Expand Down Expand Up @@ -342,6 +358,12 @@ function registerMetaTable(obj, metaTable, options)
end

metaTable.save(true)

% A table opened earlier from the same file may still be in the
% cache, and version numbers count saves of one file, so its
% number can equal the new file's and open would return it.
% The registered table replaces it.
nansen.metadata.MetaTableCache.instance().add(metaTable.filepath, metaTable)
end

function setDefaultMetaTable(obj, metaTable)
Expand Down
7 changes: 4 additions & 3 deletions code/apps/+nansen/@App/App.m
Original file line number Diff line number Diff line change
Expand Up @@ -3699,9 +3699,10 @@ function setRequestFocusOnNextMetaTableRefresh(app, requestFocus)

function tf = checkIfSubjectTableExists(~, metaTableCatalog)
% Todo: should not be a nansen.App method, project level...
existingClasses = unique( metaTableCatalog.Table.MetaTableClass );
% Todo: generalize, i.e are there subclasses (project specific subject definitions?)
tf = any(strcmp(existingClasses, 'nansen.metadata.type.Subject'));
% A project may define its own subject class. Such a table is
% the project's subject table, and creating another one would
% overwrite its file, which has the same default name.
tf = metaTableCatalog.hasMasterMetaTable('subject');
end
end

Expand Down
17 changes: 17 additions & 0 deletions tests/+nansen/+unittest/+manage/+helper/LabeledSession.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
classdef LabeledSession < nansen.metadata.type.Session
%LabeledSession - A session type with one property that nansen.metadata.type.Session lacks
%
% Tests use it as the class of a session table, to check that functions
% which create sessions for a table create them with the class of the
% table.

properties (SetObservable)
Label char
end

methods
function obj = LabeledSession(varargin)
obj@nansen.metadata.type.Session(varargin{:})
end
end
end
10 changes: 10 additions & 0 deletions tests/+nansen/+unittest/+manage/+helper/LabeledSubject.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
classdef LabeledSubject < nansen.metadata.type.Subject
%LabeledSubject - A subject type with one property that nansen.metadata.type.Subject lacks
%
% Tests use it as the class of a subject table, to check that functions
% which add subjects create them with the class of the table.

properties (SetObservable)
Label char
end
end
80 changes: 80 additions & 0 deletions tests/+nansen/+unittest/+manage/DetectNewSessionsTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
classdef DetectNewSessionsTest < matlab.unittest.TestCase
%DetectNewSessionsTest - Tests for nansen.manage.detectNewSessions
%
% The data location of the mock project holds several sessions. Each
% test creates the master session table with a session class, removes
% one session from it, and checks what detectNewSessions finds.
%
% Run tests:
% runtests('nansen.unittest.manage.DetectNewSessionsTest')

properties
Project
end

methods (TestClassSetup)
function setupProject(testCase)
testCase.applyFixture(nansen.fixture.ProjectFixture)
testCase.Project = nansen.getCurrentProject();
end
end

methods (Test)
function testDetectsSessionMissingFromTable(testCase)
[sessionTable, removedSessionId] = testCase.createSessionTableWithoutFirstSession( ...
@nansen.metadata.type.Session);

newSessions = nansen.manage.detectNewSessions(sessionTable, 'all');

testCase.verifyEqual(string({newSessions.sessionID}), removedSessionId)
end

function testNewSessionsHaveClassOfSessionTable(testCase)
sessionTable = testCase.createSessionTableWithoutFirstSession( ...
@nansen.unittest.manage.helper.LabeledSession);

newSessions = nansen.manage.detectNewSessions(sessionTable, 'all');

testCase.verifyClass(newSessions, 'nansen.unittest.manage.helper.LabeledSession')
end

function testDetectedSessionsCanBeAddedToTable(testCase)
% The steps the app takes when the user detects new sessions
[sessionTable, removedSessionId] = testCase.createSessionTableWithoutFirstSession( ...
@nansen.unittest.manage.helper.LabeledSession);

newSessions = nansen.manage.detectNewSessions(sessionTable, 'all');
newSessionTable = nansen.metadata.MetaTable.new(newSessions);
testCase.Project.synchronizeMetaTableVariables(newSessionTable);
sessionTable.addTable(newSessionTable.entries)

testCase.verifyTrue(ismember(removedSessionId, string(sessionTable.entries.sessionID)))
end
end

methods (Access = private)
function [sessionTable, removedSessionId] = createSessionTableWithoutFirstSession(testCase, sessionConstructor)
%createSessionTableWithoutFirstSession - Replace the master session table with one that lacks a session
catalog = testCase.Project.MetaTableCatalog;
isSessionTable = catalog.Table.IsMaster & ...
contains(string(catalog.Table.MetaTableClass), "session", 'IgnoreCase', true);
for name = reshape(string(catalog.Table.MetaTableName(isSessionTable)), 1, [])
filePath = catalog.getMetaTableFilePath(char(name));
catalog.removeEntry(char(name))
if isfile(filePath)
nansen.metadata.MetaTableCache.instance().remove(filePath)
delete(filePath)
end
end

% initializeSessionTable registers the table in a catalog it
% reads from disk, so the catalog is read again here
nansen.config.initializeSessionTable(testCase.Project.DataLocationModel, ...
sessionConstructor, SkipInteractiveSteps=true);
sessionTable = testCase.Project.MetaTableCatalog.getMasterMetaTable('session');
removedSessionId = string(sessionTable.entries.sessionID{1});
sessionTable.removeEntries(char(removedSessionId))
sessionTable.save()
end
end
end
73 changes: 73 additions & 0 deletions tests/+nansen/+unittest/+manage/UpdateSubjectTableTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
classdef UpdateSubjectTableTest < matlab.unittest.TestCase
%UpdateSubjectTableTest - Tests for nansen.manage.updateSubjectTable
%
% The mock project has a session table whose sessions name several
% subjects. Each test creates the master subject table from those
% sessions, removes one subject from it, and checks what
% updateSubjectTable adds back.
%
% Run tests:
% runtests('nansen.unittest.manage.UpdateSubjectTableTest')

properties
Catalog
SessionSubjectIds string
end

methods (TestClassSetup)
function setupProject(testCase)
testCase.applyFixture(nansen.fixture.ProjectFixture)
testCase.Catalog = nansen.getCurrentProject().MetaTableCatalog;
sessionTable = testCase.Catalog.getMasterMetaTable('session');
testCase.SessionSubjectIds = unique(string(sessionTable.entries.subjectID));
testCase.assumeGreaterThan(numel(testCase.SessionSubjectIds), 1, ...
'The mock project must have sessions of more than one subject.')
end
end

methods (Test)
function testAddsEachMissingSubjectOnce(testCase)
subjectTable = testCase.createSubjectTableWithoutFirstSubject( ...
'nansen.metadata.type.Subject');

nansen.manage.updateSubjectTable(testCase.Catalog)

testCase.verifyEqual(sort(string(subjectTable.entries.SubjectID)), ...
testCase.SessionSubjectIds)
end

function testNewSubjectsHaveClassOfSubjectTable(testCase)
subjectClassName = 'nansen.unittest.manage.helper.LabeledSubject';
subjectTable = testCase.createSubjectTableWithoutFirstSubject(subjectClassName);

nansen.manage.updateSubjectTable(testCase.Catalog)

testCase.verifyEqual(sort(string(subjectTable.entries.SubjectID)), ...
testCase.SessionSubjectIds)
testCase.verifyTrue(ismember('Label', subjectTable.entries.Properties.VariableNames))
testCase.verifyEqual(subjectTable.MetaTableClass, subjectClassName)
end
end

methods (Access = private)
function subjectTable = createSubjectTableWithoutFirstSubject(testCase, subjectClassName)
%createSubjectTableWithoutFirstSubject - Replace the master subject table with one that lacks a subject
catalog = testCase.Catalog;
isSubjectTable = catalog.Table.IsMaster & ...
contains(string(catalog.Table.MetaTableClass), "subject", 'IgnoreCase', true);
for name = reshape(string(catalog.Table.MetaTableName(isSubjectTable)), 1, [])
filePath = catalog.getMetaTableFilePath(char(name));
catalog.removeEntry(char(name))
if isfile(filePath)
nansen.metadata.MetaTableCache.instance().remove(filePath)
delete(filePath)
end
end

nansen.config.initializeSubjectTable(catalog, subjectClassName)
subjectTable = catalog.getMasterMetaTable('subject');
subjectTable.removeEntries(char(testCase.SessionSubjectIds(1)))
subjectTable.save()
end
end
end
Loading
Loading