diff --git a/SaxoniteCoreUnitTests/SaxoniteCoreUnitTests.csproj b/SaxoniteCoreUnitTests/SaxoniteCoreUnitTests.csproj index f9569c5..b77ebe3 100644 --- a/SaxoniteCoreUnitTests/SaxoniteCoreUnitTests.csproj +++ b/SaxoniteCoreUnitTests/SaxoniteCoreUnitTests.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SaxoniteCoreUnitTestsOnline/SaxoniteCoreUnitTestsOnline.csproj b/SaxoniteCoreUnitTestsOnline/SaxoniteCoreUnitTestsOnline.csproj index 2ba9a0b..8f9798f 100644 --- a/SaxoniteCoreUnitTestsOnline/SaxoniteCoreUnitTestsOnline.csproj +++ b/SaxoniteCoreUnitTestsOnline/SaxoniteCoreUnitTestsOnline.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SaxoniteCoreWeb/Base/Middleware/SaxModelBinder.cs b/SaxoniteCoreWeb/Base/Middleware/SaxModelBinder.cs index 84abc99..b77af81 100644 --- a/SaxoniteCoreWeb/Base/Middleware/SaxModelBinder.cs +++ b/SaxoniteCoreWeb/Base/Middleware/SaxModelBinder.cs @@ -90,7 +90,12 @@ namespace Saxonite.Net.Core.Web.Base.Middleware result = ModelBindingResult.Success(value); } } - if (result.Model != null && !result.Model.ToString().Equals(result.Model.GetType().FullName) && SaxSystem.DebugMode) + // Log property details. + if (result.Model != null && SaxSystem.DebugMode && + // Ignore items that correspond with objects in the model rather than values. + !result.Model.ToString().Equals(result.Model.GetType().FullName) && + // Ignore form files. + !result.Model.ToString().ContainsCI("Microsoft.AspNetCore.Http.IFormFile")) SaLogger.Debug(SysUtil.Fn + " Form property " + modelName + ": '" + result.Model.ToString() + "'." + (originalValue != null && !originalValue.Equals(result.Model) ? " Original value: '" + originalValue.ToString() + "'" : "")); base.SetProperty(bindingContext, modelName, propertyMetadata, result); } diff --git a/SaxoniteCoreWebAdmin/Admin/Controllers/OrganisationController-Ajax-Upload.cs b/SaxoniteCoreWebAdmin/Admin/Controllers/OrganisationController-Ajax-Upload.cs index 1c7ce5c..56ee45f 100644 --- a/SaxoniteCoreWebAdmin/Admin/Controllers/OrganisationController-Ajax-Upload.cs +++ b/SaxoniteCoreWebAdmin/Admin/Controllers/OrganisationController-Ajax-Upload.cs @@ -120,6 +120,8 @@ namespace Saxonite.Net.Core.Web.Admin.Controllers // Create the upload process. SaxOrganisationUploader.Config processConfig = new SaxOrganisationUploader.Config(SessionContext, model.OrganisationDataFile[0]); processConfig.SheetNameOrNumber = model.SheetNameOrNumber; + processConfig.HeaderRowPos = (uint)StringUtil.ToInt(model.HeaderRow); + processConfig.FirstDataRowPos = (uint)StringUtil.ToInt(model.FirstDataRow); SaxOrganisationUploader process = new SaxOrganisationUploader(processConfig); // Store the process and run. diff --git a/SaxoniteCoreWebAdmin/Admin/Models/Model_OrganisationUpload.cs b/SaxoniteCoreWebAdmin/Admin/Models/Model_OrganisationUpload.cs index a03fadb..c3a6b56 100644 --- a/SaxoniteCoreWebAdmin/Admin/Models/Model_OrganisationUpload.cs +++ b/SaxoniteCoreWebAdmin/Admin/Models/Model_OrganisationUpload.cs @@ -36,7 +36,9 @@ namespace Saxonite.Net.Core.Web.Admin.Models // Organisation data files - only one allowed per request. public List OrganisationDataFile { get; set; } // Data file properties. - public String SheetNameOrNumber { get; set; } + public String SheetNameOrNumber { get; set; } = "1"; + public String HeaderRow { get; set; } = "1"; + public String FirstDataRow { get; set; } = "2"; //------------------------------------------------------------------- // Validation. @@ -63,6 +65,24 @@ namespace Saxonite.Net.Core.Web.Admin.Models responseData.SetControlError(controlName, Localiser["msg.error.sheet.name.invalid"]); } + // Header row position (from 1). + { + String controlName = nameof(HeaderRow); + if (StringUtil.IsBlank(HeaderRow)) + responseData.SetControlError(controlName, Localiser["msg.validation.mandatory.value"]); + else if (!StringUtil.IsNumeric(HeaderRow) || StringUtil.ToInt(HeaderRow) < 1) + responseData.SetControlError(controlName, Localiser["msg.error.spreadsheet.row.invalid"]); + } + + // First data row position (from 1). + { + String controlName = nameof(FirstDataRow); + if (StringUtil.IsBlank(FirstDataRow)) + responseData.SetControlError(controlName, Localiser["msg.validation.mandatory.value"]); + else if (!StringUtil.IsNumeric(FirstDataRow) || StringUtil.ToInt(FirstDataRow) < 1) + responseData.SetControlError(controlName, Localiser["msg.error.spreadsheet.row.invalid"]); + } + return !responseData.HasControlErrors(); } diff --git a/SaxoniteCoreWebAdmin/Admin/Procs/SaxOrganisationUploader.cs b/SaxoniteCoreWebAdmin/Admin/Procs/SaxOrganisationUploader.cs index 7ad103e..974f678 100644 --- a/SaxoniteCoreWebAdmin/Admin/Procs/SaxOrganisationUploader.cs +++ b/SaxoniteCoreWebAdmin/Admin/Procs/SaxOrganisationUploader.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Http; +using EnumsNET; +using Microsoft.AspNetCore.Http; using Sarand.Net.Standard.Base.App; +using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Helpers; using Sarand.Net.Standard.Base.Utils; using Sarand.Net.Standard.Proc.Excel; @@ -34,6 +36,7 @@ namespace Saxonite.Net.Core.Web.Admin.Procs public UploadFileData DataFile { get; } public String SheetNameOrNumber { get; set; } public uint HeaderRowPos { get; set; } + public uint FirstDataRowPos { get; set; } public bool ValidateOnly { get; set; } public bool AllowCreateOrganisation { get; set; } public bool RequireOrganisationIdForUpdate { get; set; } @@ -53,28 +56,38 @@ namespace Saxonite.Net.Core.Web.Admin.Procs // Organisation data profile. private enum OrganisationProperty { - // Property columns. - OrganisationId, OrganisationName, Sport, Region, + // Organisation. + OrganisationId, OrganisationName, OrganisationType, OrganisationCategory, Sport, Sports, Region, // Address. - AddressLine1, AddressLine2, AddressTown, AddressPostcode, + AddressLine1, AddressLine2, AddressTown, AddressProvince, AddressPostcode, // Contact personal. - ContactName, ContactRole, ContactEmail, + ContactName, ContactRole, ContactNameRole, ContactEmail, // Contact. ContactPhone, ContactWeb, ContactMap, ContactWiki, ContactFacebook, ContactInstagram, ContactTwitter, } - private class OrganisationPropertyData + private class OrganisationPropertyDfn { - public OrganisationPropertyData(OrganisationProperty property, String columnHeaders) + public OrganisationPropertyDfn(OrganisationProperty property, String columnHeaders) { this.Property = property; - foreach (String value in Regex.Split(columnHeaders, "[\\s,]+", RegexOptions.None)) - if (!StringUtil.IsBlank(value)) this.ColumnHeaders.Add(value); + foreach (String value in Regex.Split(columnHeaders, "(\\s*,\\s*)+", RegexOptions.None)) + if (!StringUtil.IsBlank(value)) this.ColumnHeaders.Add(StringUtil.TrimWhitespace(value)); } public OrganisationProperty Property { get; } public HashSet ColumnHeaders { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); public uint ColumnPos { get; set; } public bool IsValid { get { return ColumnPos > 0; } } } + private class OrganisationPropertyData + { + public OrganisationPropertyData(OrganisationProperty property, DataValue value) + { + this.Property = property; + this.Value = value; + } + public OrganisationProperty Property { get; } + public DataValue Value { get; } + } private class OrganisationDataProfile { public OrganisationDataProfile() @@ -82,17 +95,22 @@ namespace Saxonite.Net.Core.Web.Admin.Procs // Property columns. AddProperty(OrganisationProperty.OrganisationId, "Id, Organisation Id"); AddProperty(OrganisationProperty.OrganisationName, "Name, Organisation Name"); + AddProperty(OrganisationProperty.OrganisationType, "Type, Organisation Type"); + AddProperty(OrganisationProperty.OrganisationCategory, "Category, Organisation Category"); AddProperty(OrganisationProperty.Sport, "Sport"); + AddProperty(OrganisationProperty.Sports, "Sports"); AddProperty(OrganisationProperty.Region, "Region"); // Address. AddProperty(OrganisationProperty.AddressLine1, "Address Line 1, Line 1, Street"); AddProperty(OrganisationProperty.AddressLine2, "Address Line 2, Line 2"); AddProperty(OrganisationProperty.AddressTown, "Town,City,Address Town,Address City"); - AddProperty(OrganisationProperty.AddressPostcode, "Postcode"); + AddProperty(OrganisationProperty.AddressProvince, "Province, County, Address Province, Address County"); + AddProperty(OrganisationProperty.AddressPostcode, "Postcode, Post code, Postal code, Zip code"); // Contact personal. - AddProperty(OrganisationProperty.ContactName, "Contact Name"); - AddProperty(OrganisationProperty.ContactRole, "Contact Role"); - AddProperty(OrganisationProperty.ContactEmail, "Contatc Email"); + AddProperty(OrganisationProperty.ContactName, "Contact, Contact Name"); + AddProperty(OrganisationProperty.ContactRole, "Role, Contact Role"); + AddProperty(OrganisationProperty.ContactNameRole, "Contact & Role, Contact Name & Role"); + AddProperty(OrganisationProperty.ContactEmail, "Contact Email"); // Contact. AddProperty(OrganisationProperty.ContactPhone, "Phone"); AddProperty(OrganisationProperty.ContactWeb, "Web, Web Link, Website"); @@ -102,35 +120,42 @@ namespace Saxonite.Net.Core.Web.Admin.Procs AddProperty(OrganisationProperty.ContactInstagram, "Instagram"); AddProperty(OrganisationProperty.ContactTwitter, "X, Twitter"); } - public Dictionary Properties { get; } = new Dictionary(); + public Dictionary Properties { get; } = new Dictionary(); private void AddProperty(OrganisationProperty property, String columnHeaders) { - Properties.Add(property, new OrganisationPropertyData(property, columnHeaders)); + Properties.Add(property, new OrganisationPropertyDfn(property, columnHeaders)); } public bool IsValidProperty(OrganisationProperty property) { - OrganisationPropertyData propertyData = Properties[property]; - return propertyData != null && propertyData.IsValid; + OrganisationPropertyDfn propertyDfn = Properties[property]; + return propertyDfn != null && propertyDfn.IsValid; } + public uint FirstDataColPos { get { return Properties.Values.Where(item => item.ColumnPos > 0).Any() ? Properties.Values.Where(item => item.ColumnPos > 0).Select(item => item.ColumnPos).Min() : 1; } } + public uint LastDataColPos { get { return Properties.Values.Where(item => item.ColumnPos > 0).Any() ? Properties.Values.Where(item => item.ColumnPos > 0).Select(item => item.ColumnPos).Max() : FirstDataColPos; } } } // Processing control data. private class ProcessControlData { - public Config config { get; set; } + public ProcessControlData(Config config) + { + this.config = config; + } + public Config config { get; } public SaMsSpreadsheet spreadsheet { get; set; } public OrganisationDataProfile dataProfile { get; } = new OrganisationDataProfile(); + public List organisationsData { get; } = new List(); } //------------------------------------------------------------------- // Data members. - private ProcessControlData control { get; } = new ProcessControlData(); + private ProcessControlData control { get; } //------------------------------------------------------------------- // Construction. public SaxOrganisationUploader(Config config) : base(config) { - control.config = config; + control = new ProcessControlData(config); } //------------------------------------------------------------------- @@ -142,19 +167,17 @@ namespace Saxonite.Net.Core.Web.Admin.Procs // Run the process. protected override bool Run() { - String fn = GetType().Name + ".Run"; - // Verify file. if (control.config.DataFile == null) { AddErrorMessage("Data file must be supplied."); - SaLogger.Warn(fn + " " + GetErrorMessage()); + SaLogger.Warn(SysUtil.Fn + " " + GetErrorMessage()); return OK; } else if (!validContentTypes.Contains(control.config.DataFile.ContentType)) { AddErrorMessage("Unsupported content type: " + control.config.DataFile.ContentType + "."); - SaLogger.Warn(fn + " " + GetErrorMessage()); + SaLogger.Warn(SysUtil.Fn + " " + GetErrorMessage()); } AddInfoMessage("Data file: " + control.config.DataFile.FileName); AddInfoMessage("Sheet: " + control.config.SheetNameOrNumber); @@ -173,14 +196,14 @@ namespace Saxonite.Net.Core.Web.Admin.Procs // Failed to open workbook. String msg = "Failed to open workbook - " + spreadsheet.ErrorStateMessage; AddErrorMessage(msg); - SaLogger.Warn(fn + " " + msg); + SaLogger.Warn(SysUtil.Fn + " " + msg); } if (OK && !spreadsheet.OpenWorksheet(1)) { // Failed to open workbook. String msg = "Failed to open worksheet - " + spreadsheet.ErrorStateMessage; AddErrorMessage(msg); - SaLogger.Warn(fn + " " + msg); + SaLogger.Warn(SysUtil.Fn + " " + msg); } if (OK) { @@ -188,16 +211,10 @@ namespace Saxonite.Net.Core.Web.Admin.Procs PopulateDataProfile(); } if (OK) - { - uint firstRowPos = spreadsheet.FirstRowPos; - for (uint rowPos = firstRowPos; rowPos <= firstRowPos; rowPos++) - { - for (uint colPos = 1; colPos <= 5; colPos++) - { - SaLogger.Info("Row: " + rowPos + ", colPos: " + colPos + ": '" + spreadsheet.GetCellText(colPos, rowPos) + "'"); - } - } - } + LoadOrganisationsData(); + if (OK) + ValidateOrganisationData(); + // Clean-up. control.spreadsheet = null; } @@ -207,44 +224,50 @@ namespace Saxonite.Net.Core.Web.Admin.Procs { String msg = exc.GetType().Name + " uploading organisation data."; AddErrorMessage(msg); - SaLogger.Warn(fn + " " + msg + " " + exc.Message + ExceptionUtil.GetStackEntries(exc, 6).Concat("", "\n\t")); + SaLogger.Warn(SysUtil.Fn + " " + msg + " " + exc.Message + ExceptionUtil.GetStackEntries(exc, 6).Concat("", "\n\t")); } return OK; } //------------------------------------------------------------------- + /// + /// Assign organisation properties to spreadsheet columns. + /// private bool PopulateDataProfile() { - String fn = GetType().Name + ".PopulateDataProfile"; - - // Identify header row. - uint headerRowPos = control.config.HeaderRowPos; - if (headerRowPos < 1) - headerRowPos = control.spreadsheet.FirstRowPos; - // Column headings. List columnHeadings = new List(); for (uint colPos = 1; colPos <= 100; colPos++) { - columnHeadings.Add(control.spreadsheet.GetCellText(colPos, headerRowPos)); + columnHeadings.Add(control.spreadsheet.GetCellText(colPos, control.config.HeaderRowPos)); } - SaLogger.Info(fn + " Column headings: " + columnHeadings.Concat(", ")); + columnHeadings.RemoveTrailingBlanks(); + SaLogger.Info(SysUtil.Fn + " Column headings: " + columnHeadings.Concat(", ")); // Populate header columns in profile. - foreach (OrganisationPropertyData propertyData in control.dataProfile.Properties.Values) + for (uint colPos = 1; colPos <= columnHeadings.Count; colPos++) { - for (uint colPos = 1; colPos <= columnHeadings.Count; colPos++) + String columnHeading = columnHeadings[(int)(colPos - 1)]; + if (StringUtil.IsBlank(columnHeading)) continue; + + List propertyDfns = control.dataProfile.Properties.Values.Where(item => item.ColumnHeaders.ContainsCI(columnHeading)).ToList(); + if (propertyDfns.Any()) { - String columnHeading = columnHeadings[(int)(colPos - 1)]; - if (propertyData.ColumnHeaders.ContainsCI(columnHeading)) + // Retrieve the first matching property. + OrganisationPropertyDfn propertyDfn = propertyDfns[0]; + if (propertyDfn.ColumnPos > 0) { - if (control.dataProfile.Properties.Values.ToList().Exists(item => item.ColumnPos == colPos)) - AddWarningMessage("Multiple properties associated with column '" + columnHeading + "'."); - propertyData.ColumnPos = colPos; - break; + AddWarningMessage("Property '" + propertyDfn.Property.GetName() + "' associated with multiple columns."); + continue; } + // Assign incoming data column to the first property matched by header. + propertyDfn.ColumnPos = colPos; + if (propertyDfns.Count > 1) + AddWarningMessage("Multiple properties associated with organisation column '" + columnHeading + "'."); } + else + AddWarningMessage("No properties associated with organisation column '" + columnHeading + "'."); } if (!control.dataProfile.IsValidProperty(OrganisationProperty.OrganisationId) && !control.dataProfile.IsValidProperty(OrganisationProperty.OrganisationName)) @@ -253,6 +276,35 @@ namespace Saxonite.Net.Core.Web.Admin.Procs return OK; } + //------------------------------------------------------------------- + /// + /// Load the data from the incoming spreadsheet. + /// + private bool LoadOrganisationsData() + { + // Reset organisation data. + control.organisationsData.Clear(); + + for (uint rowPos = control.config.FirstDataRowPos; rowPos <= control.spreadsheet.LastRowPos; rowPos++) + { + List values = control.spreadsheet.GetRowValues(rowPos, (int)control.dataProfile.LastDataColPos); + foreach (OrganisationPropertyDfn property in control.dataProfile.Properties.Values) + { + if (property.ColumnPos < 1) continue; + if (property.ColumnPos > values.Count) continue; + control.organisationsData.Add(new OrganisationPropertyData(property.Property, values[(int)(property.ColumnPos-1)])); + } + } + return OK; + } + + //------------------------------------------------------------------- + private bool ValidateOrganisationData() + { + List organisationProperties = new List(); + return OK; + } + //------------------------------------------------------------------- } //======================================================================= diff --git a/SaxoniteCoreWebAdmin/Localisation/SaxAdminProperties.resx b/SaxoniteCoreWebAdmin/Localisation/SaxAdminProperties.resx index 59f0582..26172b2 100644 --- a/SaxoniteCoreWebAdmin/Localisation/SaxAdminProperties.resx +++ b/SaxoniteCoreWebAdmin/Localisation/SaxAdminProperties.resx @@ -321,12 +321,18 @@ First + + First data row + First name Forgotten password? + + Header row + Image @@ -597,6 +603,9 @@ Invalid sheet name supplied. + + Spreadsheet row is identified by a number from 1. + System error encountered. diff --git a/SaxoniteCoreWebAdmin/Views/Organisation/Panel-Organisation-Upload.cshtml b/SaxoniteCoreWebAdmin/Views/Organisation/Panel-Organisation-Upload.cshtml index 0f6acec..51431a3 100644 --- a/SaxoniteCoreWebAdmin/Views/Organisation/Panel-Organisation-Upload.cshtml +++ b/SaxoniteCoreWebAdmin/Views/Organisation/Panel-Organisation-Upload.cshtml @@ -69,7 +69,41 @@
- + +
+
+
+ + + + @* Header row *@ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + @* First data row *@ +
+
+
+
+ +
+
+
+
+
diff --git a/SaxoniteStandardBase/Base/App/Sax-Version.cs b/SaxoniteStandardBase/Base/App/Sax-Version.cs index f297260..7d4a0d0 100644 --- a/SaxoniteStandardBase/Base/App/Sax-Version.cs +++ b/SaxoniteStandardBase/Base/App/Sax-Version.cs @@ -18,7 +18,7 @@ namespace Saxonite.Net.Standard.Base.App //------------------------------------------------------------------- public static class Version { - private static String version { get; } = "1.2.11.2"; + private static String version { get; } = "1.2.11.3"; // Retrieve version. public static String GetFullVersion () { return version; } public static String GetAppVersion () diff --git a/SaxoniteStandardBase/Base/App/Sax.cs b/SaxoniteStandardBase/Base/App/Sax.cs index ed5bc17..0367e86 100644 --- a/SaxoniteStandardBase/Base/App/Sax.cs +++ b/SaxoniteStandardBase/Base/App/Sax.cs @@ -374,6 +374,7 @@ namespace Saxonite.Net.Standard.Base.App Email = 100, Website = 200, Wikipedia = 210, + Location = 220, // Social media. LinkedIn = 300, Twitter = 310, diff --git a/SaxoniteStandardBase/Base/Helpers/SaxContactMethodHelper.cs b/SaxoniteStandardBase/Base/Helpers/SaxContactMethodHelper.cs index 585fe18..c75f655 100644 --- a/SaxoniteStandardBase/Base/Helpers/SaxContactMethodHelper.cs +++ b/SaxoniteStandardBase/Base/Helpers/SaxContactMethodHelper.cs @@ -25,6 +25,7 @@ namespace Saxonite.Net.Standard.Base.Helpers return Sax.ContactMethodType.Email; case Sax.ContactMethod.Website: case Sax.ContactMethod.Wikipedia: + case Sax.ContactMethod.Location: case Sax.ContactMethod.LinkedIn: case Sax.ContactMethod.Twitter: case Sax.ContactMethod.Facebook: @@ -58,6 +59,11 @@ namespace Saxonite.Net.Standard.Base.Helpers // Wikipedia. return SacsDataValidationHelper.IsValidWikipediaUrl(value); } + else if (method == Sax.ContactMethod.Location) + { + // Location URL. + return SacsDataValidationHelper.IsValidLocationUrl(value); + } else if (method == Sax.ContactMethod.LinkedIn) { // LinkedIn.