diff --git a/CHANGELOG.md b/CHANGELOG.md index 570c3b22f2..80362bac23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added 'Set Description' command to [AggregateConfiguration] context menu - Template cohort builder aggregates can be dragged onto extraction datasets to import the container tree [#1307](https://github.com/HicServices/RDMP/issues/1307) - Having a JoinInfo between 2 columns that have different collations is now flagged by ProblemProvider [#1288](https://github.com/HicServices/RDMP/issues/1288) +- Added command `SetExtractionPrimaryKeys` for controlling which columns (if any) will make the primary key when extracting to database [#1335](https://github.com/HicServices/RDMP/issues/1335) - Added ability to pop out tooltips/problems into modal popup [#1334](https://github.com/HicServices/RDMP/issues/1334) ### Changed diff --git a/Rdmp.Core/CommandExecution/AtomicCommandFactory.cs b/Rdmp.Core/CommandExecution/AtomicCommandFactory.cs index edef8d1738..71ad65400b 100644 --- a/Rdmp.Core/CommandExecution/AtomicCommandFactory.cs +++ b/Rdmp.Core/CommandExecution/AtomicCommandFactory.cs @@ -130,9 +130,15 @@ public IEnumerable CreateCommands(object o) yield return new ExecuteCommandMakeCatalogueProjectSpecific(_activator, c, null) { Weight = -99.0009f, SuggestedCategory = Extraction }; } - + yield return new ExecuteCommandSetExtractionIdentifier(_activator, c, null, null) { Weight = -99.0008f, SuggestedCategory = Extraction }; + + yield return new ExecuteCommandSetExtractionPrimaryKeys(_activator, c, null, null) + { + Weight = -99.0007f, + SuggestedCategory = Extraction + }; } yield return new ExecuteCommandExportObjectsToFile(_activator, new[] {c}) { diff --git a/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetColumnSettingBase.cs b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetColumnSettingBase.cs new file mode 100644 index 0000000000..dfd214e5b4 --- /dev/null +++ b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetColumnSettingBase.cs @@ -0,0 +1,189 @@ +// Copyright (c) The University of Dundee 2018-2019 +// This file is part of the Research Data Management Platform (RDMP). +// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with RDMP. If not, see . + +using System; +using System.Linq; +using Rdmp.Core.Curation.Data; +using Rdmp.Core.DataExport.Data; + +namespace Rdmp.Core.CommandExecution.AtomicCommands +{ + public abstract class ExecuteCommandSetColumnSettingBase : BasicCommandExecution, IAtomicCommand + { + private ICatalogue _catalogue; + private ExtractionInformation[] _extractionInformations; + private ExtractionInformation[] _alreadyMarked; + + private readonly IExtractionConfiguration _inConfiguration; + private readonly string _commandName; + private ConcreteColumn[] _selectedDataSetColumns; + private ConcreteColumn[] _alreadyMarkedInConfiguration; + + /// + /// Explicit columns to pick rather than prompting to choose at runtime + /// + private string[] toPick; + private readonly string _commandProperty; + + /// + /// + /// + /// + /// The dataset you want to change the setting for + /// Optional - If setting should only be applied to a specific extraction or Null for the Catalogue itself (will affect all future extractions) + /// "Optional - The Column name(s) you want to select as the new selection(s). Comma seperate multiple entries if needed" + /// Describe what is being changed from user perspective e.g. "Set IsExtractionIdentifier" + /// Name of property being changed by this command e.g "Extraction Identifier" + public ExecuteCommandSetColumnSettingBase( + IBasicActivateItems activator,ICatalogue catalogue,IExtractionConfiguration inConfiguration,string column, + string commandName, string commandProperty + ) : base(activator) + { + _catalogue = catalogue; + _inConfiguration = inConfiguration; + _commandName = commandName; + _catalogue.ClearAllInjections(); + _commandProperty = commandProperty; + + if (inConfiguration != null) + { + SetImpossibleIfReadonly(_inConfiguration); + + var allEds = inConfiguration.GetAllExtractableDataSets(); + var eds = allEds.FirstOrDefault(sds => sds.Catalogue_ID == _catalogue.ID); + if (eds == null) + { + SetImpossible($"Catalogue '{_catalogue}' is not part of ExtractionConfiguration '{inConfiguration}'"); + return; + } + + _selectedDataSetColumns = inConfiguration.GetAllExtractableColumnsFor(eds); + + if (_selectedDataSetColumns.Length == 0) + { + SetImpossible($"Catalogue '{_catalogue}' in '{inConfiguration}' does not have any extractable columns"); + return; + } + + _alreadyMarkedInConfiguration = _selectedDataSetColumns.Where(c => Getter(c)).ToArray(); + } + else + { + _extractionInformations = _catalogue.GetAllExtractionInformation(ExtractionCategory.Any); + + if (_extractionInformations.Length == 0) + { + SetImpossible("Catalogue does not have any extractable columns"); + return; + } + + _alreadyMarked = _extractionInformations.Where(c=>Getter(c)).ToArray(); + } + + if (!string.IsNullOrWhiteSpace(column)) + { + toPick = column.Split(',', StringSplitOptions.RemoveEmptyEntries); + } + } + + + public override string GetCommandName() + { + if (!string.IsNullOrWhiteSpace(OverrideCommandName)) + return OverrideCommandName; + + var cols = _alreadyMarked ?? _alreadyMarkedInConfiguration; + + if (cols == null || cols.Length == 0) + return _commandName; + + return _commandName + " (" + string.Join(",", cols.Select(e => e.GetRuntimeName())) + ")"; + } + public override void Execute() + { + base.Execute(); + + var oldCols = _alreadyMarked ?? _alreadyMarkedInConfiguration; + + string initialSearchText = oldCols.Length == 1 ? oldCols[0].GetRuntimeName() : null; + + if (_inConfiguration != null) + { + ChangeFor(initialSearchText, _selectedDataSetColumns); + Publish(_inConfiguration); + } + else + { + ChangeFor(initialSearchText, _extractionInformations); + Publish(_catalogue); + } + } + private void ChangeFor(string initialSearchText, ConcreteColumn[] allColumns) + { + ConcreteColumn[] selected = null; + + if (toPick != null && toPick.Length > 0) + { + selected = allColumns.Where(a => toPick.Contains(a.GetRuntimeName())).ToArray(); + + if (selected.Length != toPick.Length) + { + throw new Exception($"Could not find column(s) {string.Join(',', toPick)} amongst available columns ({string.Join(',', allColumns.Select(c => c.GetRuntimeName()))})"); + } + } + else + { + if (SelectMany(new DialogArgs { + InitialObjectSelection = _alreadyMarked ?? _alreadyMarkedInConfiguration, + AllowSelectingNull = true, + WindowTitle = $"Set {_commandProperty}", + TaskDescription = $"Choose which columns will make up the new {_commandProperty}. Or select null to clear" + }, allColumns, out selected)) + { + if (selected == null || selected.Length == 0) + if (!YesNo($"Do you want to clear the {_commandProperty}?", $"Clear {_commandProperty}?")) + return; + if(!IsValidSelection(selected)) + return; + } + else + return; + } + + foreach (var ec in allColumns) + { + bool newValue = selected != null && selected.Contains(ec); + + if (Getter(ec) != newValue) + { + Setter(ec,newValue); + ec.SaveToDatabase(); + } + } + } + + /// + /// Value getter to determine if a given is included in the current selection for your setting + /// + /// + /// + protected abstract bool Getter(ConcreteColumn c); + + /// + /// Value setter to assign new inclusion/exclusion status for the column (if command is executed and a new selection confirmed) + /// + /// + /// New status, true = include in selection, false = exclude + protected abstract void Setter(ConcreteColumn c, bool newValue); + + /// + /// Show any warnings if applicable and then return false if user changes their mind about + /// + /// + /// + protected abstract bool IsValidSelection(ConcreteColumn[] selected); + } +} \ No newline at end of file diff --git a/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionIdentifier.cs b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionIdentifier.cs index 743fee18bf..7dce73fab8 100644 --- a/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionIdentifier.cs +++ b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionIdentifier.cs @@ -4,9 +4,7 @@ // RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // You should have received a copy of the GNU General Public License along with RDMP. If not, see . -using System; using System.Drawing; -using System.Linq; using Rdmp.Core.Curation.Data; using Rdmp.Core.DataExport.Data; using Rdmp.Core.Icons.IconProvision; @@ -14,26 +12,12 @@ namespace Rdmp.Core.CommandExecution.AtomicCommands { - /// /// Change which column is used to perform linkage against a cohort. This command supports both changing the global setting on a /// or changing it only for a specific /// - public class ExecuteCommandSetExtractionIdentifier : BasicCommandExecution, IAtomicCommand + public class ExecuteCommandSetExtractionIdentifier : ExecuteCommandSetColumnSettingBase, IAtomicCommand { - private ICatalogue _catalogue; - private ExtractionInformation[] _extractionInformations; - private ExtractionInformation[] _alreadyMarked; - - private readonly IExtractionConfiguration _inConfiguration; - private ConcreteColumn[] _selectedDataSetColumns; - private ConcreteColumn[] _alreadyMarkedInConfiguration; - - /// - /// Explicit columns to pick rather than prompting to choose at runtime - /// - private string[] toPick; - /// /// Change which column is the linkage identifier in a either at a global level or for a specific /// @@ -49,62 +33,14 @@ public ExecuteCommandSetExtractionIdentifier(IBasicActivateItems activator, IExtractionConfiguration inConfiguration, [DemandsInitialization("Optional - The Column name(s) you want to select as the new linkage identifier(s). Comma seperate multiple entries if needed")] - string column) :base(activator) + string column) + // base class args + :base(activator, catalogue, inConfiguration,column, + "Set Extraction Identifier", + "Extraction Identifier") { - _catalogue = catalogue; - _inConfiguration = inConfiguration; - _catalogue.ClearAllInjections(); - - if(inConfiguration != null) - { - SetImpossibleIfReadonly(_inConfiguration); - - var allEds = inConfiguration.GetAllExtractableDataSets(); - var eds = allEds.FirstOrDefault(sds => sds.Catalogue_ID == _catalogue.ID); - if(eds == null) - { - SetImpossible($"Catalogue '{_catalogue}' is not part of ExtractionConfiguration '{inConfiguration}'"); - return; - } - - _selectedDataSetColumns = inConfiguration.GetAllExtractableColumnsFor(eds); - - if (_selectedDataSetColumns.Length == 0) - { - SetImpossible($"Catalogue '{_catalogue}' in '{inConfiguration}' does not have any extractable columns"); - return; - } - - _alreadyMarkedInConfiguration = _selectedDataSetColumns.Where(ei => ei.IsExtractionIdentifier).ToArray(); - } - else - { - _extractionInformations = _catalogue.GetAllExtractionInformation(ExtractionCategory.Any); - - if (_extractionInformations.Length == 0) - { - SetImpossible("Catalogue does not have any extractable columns"); - return; - } - - _alreadyMarked = _extractionInformations.Where(ei => ei.IsExtractionIdentifier).ToArray(); - } - - if(!string.IsNullOrWhiteSpace(column)) - { - toPick = column.Split(',', StringSplitOptions.RemoveEmptyEntries); - } - - } - - public override string GetCommandName() - { - var cols = _alreadyMarked ?? _alreadyMarkedInConfiguration; - - if (cols == null || cols.Length == 0) - return base.GetCommandName(); - - return base.GetCommandName() + " (" + string.Join(",", cols.Select(e=>e.GetRuntimeName())) + ")"; + + } public override Image GetImage(IIconProvider iconProvider) @@ -117,66 +53,25 @@ public override string GetCommandHelp() return "Change which column(s) contain the patient id / linkage column e.g. CHI"; } - public override void Execute() + protected override bool IsValidSelection(ConcreteColumn[] selected) { - base.Execute(); + if (selected == null) + return true; - var oldCols = _alreadyMarked ?? _alreadyMarkedInConfiguration; + // if multiple selected warn user + if (selected.Length > 1) + return YesNo("Are you sure you want multiple linkable extraction identifier columns (most datasets only have 1 person ID column in them)?", "Multiple IsExtractionIdentifier columns?"); - string initialSearchText = oldCols.Length == 1 ? oldCols[0].GetRuntimeName():null; - - if(_inConfiguration != null) - { - ChangeFor(initialSearchText, _selectedDataSetColumns); - Publish(_inConfiguration); - } - else - { - ChangeFor(initialSearchText, _extractionInformations); - Publish(_catalogue); - } + return true; + } + protected override bool Getter(ConcreteColumn c) + { + return c.IsExtractionIdentifier; } - private void ChangeFor(string initialSearchText,ConcreteColumn[] allColumns) + protected override void Setter(ConcreteColumn c, bool newValue) { - ConcreteColumn[] selected = null; - - if (toPick != null && toPick.Length > 0) - { - selected = allColumns.Where(a => toPick.Contains(a.GetRuntimeName())).ToArray(); - - if(selected.Length != toPick.Length) - { - throw new Exception($"Could not find column(s) {string.Join(',', toPick)} amongst available columns ({string.Join(',',allColumns.Select(c=>c.GetRuntimeName()))})"); - } - } - else - { - if (SelectMany(allColumns, out selected, initialSearchText)) - { - if (selected.Length == 0) - if (!YesNo("Do you want to clear the Extraction Identifier?", "Clear Extraction Identifier?")) - return; - - if (selected.Length > 1) - if (!YesNo("Are you sure you want multiple linkable extraction identifier columns (most datasets only have 1 person ID column in them)?", "Multiple IsExtractionIdentifier columns?")) - return; - } - else - return; - } - - foreach (var ec in allColumns) - { - bool newValue = selected.Contains(ec); - - if (ec.IsExtractionIdentifier != newValue) - { - ec.IsExtractionIdentifier = newValue; - ec.SaveToDatabase(); - } - } - + c.IsExtractionIdentifier = newValue; } } } \ No newline at end of file diff --git a/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionPrimaryKeys.cs b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionPrimaryKeys.cs new file mode 100644 index 0000000000..97f17f9c5f --- /dev/null +++ b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandSetExtractionPrimaryKeys.cs @@ -0,0 +1,57 @@ +// Copyright (c) The University of Dundee 2018-2019 +// This file is part of the Research Data Management Platform (RDMP). +// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// You should have received a copy of the GNU General Public License along with RDMP. If not, see . + +using Rdmp.Core.Curation.Data; +using Rdmp.Core.DataExport.Data; + +namespace Rdmp.Core.CommandExecution.AtomicCommands +{ + public class ExecuteCommandSetExtractionPrimaryKeys : ExecuteCommandSetColumnSettingBase, IAtomicCommand + { + /// + /// Change which column(s) should be marked as primary key on extraction to a destination that supports this feature (e.g. to database). + /// Operation can be applied either at global level or for a specific + /// + /// + /// + /// + /// + public ExecuteCommandSetExtractionPrimaryKeys(IBasicActivateItems activator, + [DemandsInitialization("The dataset you want to change the extraction primary keys for")] + ICatalogue catalogue, + + [DemandsInitialization("Optional - The specific extraction you want the change made in or Null for the Catalogue itself (will affect all future extractions)")] + IExtractionConfiguration inConfiguration, + + [DemandsInitialization("Optional - The Column name(s) you want to select as the new extraction primary keys. Comma seperate multiple entries if needed")] + string column) + // base class args + : base(activator, catalogue, inConfiguration, column, + "Set Extraction Primary Key", + "Extraction Primary Key") + { + + + } + public override string GetCommandHelp() + { + return "Change which column(s) should be marked as primary key if extracting to database"; + } + + protected override bool IsValidSelection(ConcreteColumn[] selected) + { + return true; + } + protected override bool Getter(ConcreteColumn c) + { + return c.IsPrimaryKey; + } + protected override void Setter(ConcreteColumn ec, bool newValue) + { + ec.IsPrimaryKey = newValue; + } + } +} \ No newline at end of file diff --git a/Rdmp.Core/CommandExecution/BasicCommandExecution.cs b/Rdmp.Core/CommandExecution/BasicCommandExecution.cs index 3cf73ed5b0..5a5f310c34 100644 --- a/Rdmp.Core/CommandExecution/BasicCommandExecution.cs +++ b/Rdmp.Core/CommandExecution/BasicCommandExecution.cs @@ -403,7 +403,7 @@ protected bool SelectMany(T[] available, out T[] selected, string initialSear protected bool SelectMany(DialogArgs dialogArgs, T[] available, out T[] selected) where T : DatabaseEntity { selected = BasicActivator.SelectMany(dialogArgs, typeof(T), available)?.Cast()?.ToArray(); - return selected != null && selected.Any(); + return selected != null && (dialogArgs.AllowSelectingNull || selected.Any()); } protected void Wait(string title, Task task, CancellationTokenSource cts) diff --git a/Rdmp.UI/SimpleDialogs/SelectDialog.cs b/Rdmp.UI/SimpleDialogs/SelectDialog.cs index c760af0d1a..b620cfe429 100644 --- a/Rdmp.UI/SimpleDialogs/SelectDialog.cs +++ b/Rdmp.UI/SimpleDialogs/SelectDialog.cs @@ -832,7 +832,7 @@ private void btnSelect_Click(object sender, EventArgs e) private void btnSelectNULL_Click(object sender, EventArgs e) { Selected = default(T); - MultiSelected = null; + MultiSelected = new HashSet(); DialogResult = DialogResult.OK; this.Close(); } @@ -840,7 +840,7 @@ private void btnSelectNULL_Click(object sender, EventArgs e) private void btnCancel_Click(object sender, EventArgs e) { Selected = default(T); - MultiSelected = null; + MultiSelected = new HashSet(); DialogResult = DialogResult.Cancel; this.Close(); }