From d474d9105f5f921c4b23bf17cd00737ee6229ddd Mon Sep 17 00:00:00 2001 From: Jelle Maas Date: Fri, 3 Jun 2022 12:58:31 +0200 Subject: [PATCH 1/3] Restructure code and add support for multiple exporters simultaneously --- .../Export/ICanvasModuleCollectionExporter.cs | 8 --- ...leExporter.cs => ICanvasModuleExporter.cs} | 2 +- Epsilon.Abstractions/Export/IExporter.cs | 8 +++ Epsilon.Abstractions/Export/IFileExporter.cs | 8 --- .../Export/IModuleExporterCollection.cs | 6 +++ Epsilon.Cli/Startup.cs | 40 ++++++++++----- .../Export/CanvasModuleCollectionExporter.cs | 28 ----------- .../Export/ConsoleCanvasModuleFileExporter.cs | 35 ------------- .../Exceptions/NoExportersFoundException.cs | 13 +++++ Epsilon/Export/ExportOptions.cs | 11 ++++ Epsilon/Export/ExportSettings.cs | 6 --- .../Export/Exporters/ConsoleModuleExporter.cs | 50 +++++++++++++++++++ .../CsvModuleExporter.cs} | 18 +++++-- Epsilon/Export/ModuleExporterCollection.cs | 38 ++++++++++++++ .../CoreServiceCollectionExtensions.cs | 11 ++-- 15 files changed, 174 insertions(+), 108 deletions(-) delete mode 100644 Epsilon.Abstractions/Export/ICanvasModuleCollectionExporter.cs rename Epsilon.Abstractions/Export/{ICanvasModuleFileExporter.cs => ICanvasModuleExporter.cs} (53%) create mode 100644 Epsilon.Abstractions/Export/IExporter.cs delete mode 100644 Epsilon.Abstractions/Export/IFileExporter.cs create mode 100644 Epsilon.Abstractions/Export/IModuleExporterCollection.cs delete mode 100644 Epsilon/Export/CanvasModuleCollectionExporter.cs delete mode 100644 Epsilon/Export/ConsoleCanvasModuleFileExporter.cs create mode 100644 Epsilon/Export/Exceptions/NoExportersFoundException.cs create mode 100644 Epsilon/Export/ExportOptions.cs delete mode 100644 Epsilon/Export/ExportSettings.cs create mode 100644 Epsilon/Export/Exporters/ConsoleModuleExporter.cs rename Epsilon/Export/{CsvCanvasModuleFileExporter.cs => Exporters/CsvModuleExporter.cs} (82%) create mode 100644 Epsilon/Export/ModuleExporterCollection.cs diff --git a/Epsilon.Abstractions/Export/ICanvasModuleCollectionExporter.cs b/Epsilon.Abstractions/Export/ICanvasModuleCollectionExporter.cs deleted file mode 100644 index 172ab2e5..00000000 --- a/Epsilon.Abstractions/Export/ICanvasModuleCollectionExporter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Epsilon.Canvas.Abstractions.Data; - -namespace Epsilon.Abstractions.Export; - -public interface ICanvasModuleCollectionExporter -{ - public void Export(IEnumerable modules, string format); -} \ No newline at end of file diff --git a/Epsilon.Abstractions/Export/ICanvasModuleFileExporter.cs b/Epsilon.Abstractions/Export/ICanvasModuleExporter.cs similarity index 53% rename from Epsilon.Abstractions/Export/ICanvasModuleFileExporter.cs rename to Epsilon.Abstractions/Export/ICanvasModuleExporter.cs index b2ca2049..c26a3848 100644 --- a/Epsilon.Abstractions/Export/ICanvasModuleFileExporter.cs +++ b/Epsilon.Abstractions/Export/ICanvasModuleExporter.cs @@ -2,7 +2,7 @@ namespace Epsilon.Abstractions.Export; -public interface ICanvasModuleFileExporter : IFileExporter> +public interface ICanvasModuleExporter : IExporter> { } \ No newline at end of file diff --git a/Epsilon.Abstractions/Export/IExporter.cs b/Epsilon.Abstractions/Export/IExporter.cs new file mode 100644 index 00000000..fd463eff --- /dev/null +++ b/Epsilon.Abstractions/Export/IExporter.cs @@ -0,0 +1,8 @@ +namespace Epsilon.Abstractions.Export; + +public interface IExporter +{ + public IEnumerable Formats { get; } + + void Export(T data, string format); +} \ No newline at end of file diff --git a/Epsilon.Abstractions/Export/IFileExporter.cs b/Epsilon.Abstractions/Export/IFileExporter.cs deleted file mode 100644 index 9e9e3391..00000000 --- a/Epsilon.Abstractions/Export/IFileExporter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Epsilon.Abstractions.Export; - -public interface IFileExporter -{ - public bool CanExport(string format); - - void Export(T data, string path); -} \ No newline at end of file diff --git a/Epsilon.Abstractions/Export/IModuleExporterCollection.cs b/Epsilon.Abstractions/Export/IModuleExporterCollection.cs new file mode 100644 index 00000000..881200d9 --- /dev/null +++ b/Epsilon.Abstractions/Export/IModuleExporterCollection.cs @@ -0,0 +1,6 @@ +namespace Epsilon.Abstractions.Export; + +public interface IModuleExporterCollection +{ + public IDictionary DetermineExporters(IEnumerable formats); +} \ No newline at end of file diff --git a/Epsilon.Cli/Startup.cs b/Epsilon.Cli/Startup.cs index 9bebc149..6dedd892 100644 --- a/Epsilon.Cli/Startup.cs +++ b/Epsilon.Cli/Startup.cs @@ -2,6 +2,7 @@ using Epsilon.Canvas; using Epsilon.Canvas.Abstractions; using Epsilon.Export; +using Epsilon.Export.Exceptions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,32 +13,29 @@ public class Startup : IHostedService { private readonly ILogger _logger; private readonly IHostApplicationLifetime _lifetime; - private readonly ExportSettings _exportSettings; + private readonly ExportOptions _exportOptions; private readonly CanvasSettings _canvasSettings; private readonly ICanvasModuleCollectionFetcher _collectionFetcher; - private readonly ICanvasModuleCollectionExporter _collectionExporter; + private readonly IModuleExporterCollection _exporterCollection; public Startup( ILogger logger, IHostApplicationLifetime lifetime, IOptions canvasSettings, - IOptions exportSettings, + IOptions exportSettings, ICanvasModuleCollectionFetcher collectionFetcher, - ICanvasModuleCollectionExporter collectionExporter) + IModuleExporterCollection exporterCollection) { _logger = logger; _canvasSettings = canvasSettings.Value; - _exportSettings = exportSettings.Value; + _exportOptions = exportSettings.Value; _lifetime = lifetime; _collectionFetcher = collectionFetcher; - _collectionExporter = collectionExporter; + _exporterCollection = exporterCollection; } public Task StartAsync(CancellationToken cancellationToken) { - _logger.LogInformation("Starting Epsilon, targeting course: {CourseId}", _canvasSettings.CourseId); - _logger.LogInformation("Using export format: {Format}", _exportSettings.Format); - _lifetime.ApplicationStarted.Register(() => Task.Run(ExecuteAsync, cancellationToken)); return Task.CompletedTask; @@ -50,11 +48,27 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task ExecuteAsync() { - var modules = await _collectionFetcher.Fetch(_canvasSettings.CourseId); + _logger.LogInformation("Targeting Canvas course: {CourseId}, at {Url}", _canvasSettings.CourseId, _canvasSettings.ApiUrl); + _logger.LogInformation("Using export formats: {Formats}", string.Join(",", _exportOptions.Formats)); - var format = _exportSettings.Format.ToLower(); - _collectionExporter.Export(modules, format); + try + { + var exporters = _exporterCollection.DetermineExporters(_exportOptions.Formats).ToArray(); + var modules = (await _collectionFetcher.Fetch(_canvasSettings.CourseId)).ToArray(); - _lifetime.StopApplication(); + foreach (var (format, exporter) in exporters) + { + _logger.LogInformation("Exporting to {Format} using {Exporter}...", format, exporter.GetType().Name); + exporter.Export(modules, format); + } + } + catch (NoExportersFoundException e) + { + _logger.LogCritical("An error occured: {Message}", e.Message); + } + finally + { + _lifetime.StopApplication(); + } } } \ No newline at end of file diff --git a/Epsilon/Export/CanvasModuleCollectionExporter.cs b/Epsilon/Export/CanvasModuleCollectionExporter.cs deleted file mode 100644 index 3beb48a0..00000000 --- a/Epsilon/Export/CanvasModuleCollectionExporter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Epsilon.Abstractions.Export; -using Epsilon.Canvas.Abstractions.Data; - -namespace Epsilon.Export; - -public class CanvasModuleCollectionExporter : ICanvasModuleCollectionExporter -{ - private readonly IEnumerable _fileExporters; - - public CanvasModuleCollectionExporter(IEnumerable fileExporters) - { - _fileExporters = fileExporters; - } - - public void Export(IEnumerable modules, string format) - { - var filename = "Epsilon-Export-" + DateTime.Now.ToString("ddMMyyyyHHmmss"); - - foreach (var fileExporter in _fileExporters) - { - if (fileExporter.CanExport(format)) - { - fileExporter.Export(modules, filename); - break; - } - } - } -} \ No newline at end of file diff --git a/Epsilon/Export/ConsoleCanvasModuleFileExporter.cs b/Epsilon/Export/ConsoleCanvasModuleFileExporter.cs deleted file mode 100644 index 30c94915..00000000 --- a/Epsilon/Export/ConsoleCanvasModuleFileExporter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Epsilon.Abstractions.Export; -using Epsilon.Canvas.Abstractions.Data; -using Microsoft.Extensions.Logging; - -namespace Epsilon.Export; - -public class ConsoleCanvasModuleFileExporter : ICanvasModuleFileExporter -{ - private readonly ILogger _logger; - - public ConsoleCanvasModuleFileExporter(ILogger logger) - { - _logger = logger; - } - - public bool CanExport(string format) => format is "console" or ""; - - public void Export(IEnumerable data, string path) - { - foreach (var module in data) - { - _logger.LogInformation("================ {ModuleName} ================", module.Name); - - foreach (var assignment in module.Assignments) - { - _logger.LogInformation("{AssignmentName}", assignment.Name); - - foreach (var outcomeResult in assignment.OutcomeResults) - { - _logger.LogInformation("\t- {OutcomeTitle}", outcomeResult.Outcome?.Title); - } - } - } - } -} \ No newline at end of file diff --git a/Epsilon/Export/Exceptions/NoExportersFoundException.cs b/Epsilon/Export/Exceptions/NoExportersFoundException.cs new file mode 100644 index 00000000..f1e498e0 --- /dev/null +++ b/Epsilon/Export/Exceptions/NoExportersFoundException.cs @@ -0,0 +1,13 @@ +namespace Epsilon.Export.Exceptions; + +[Serializable] +public class NoExportersFoundException : Exception +{ + public NoExportersFoundException() + { + } + + public NoExportersFoundException(IEnumerable formats) : base($"No exporters could be found with the given formats {string.Join(",", formats)}") + { + } +} \ No newline at end of file diff --git a/Epsilon/Export/ExportOptions.cs b/Epsilon/Export/ExportOptions.cs new file mode 100644 index 00000000..c0226793 --- /dev/null +++ b/Epsilon/Export/ExportOptions.cs @@ -0,0 +1,11 @@ +namespace Epsilon.Export; + +public class ExportOptions +{ + public string OutputName { get; set; } = "Epsilon-Export-{DateTime}"; + + public List Formats { get; } = new(); + + public string FormattedOutputName => OutputName + .Replace("{DateTime}", DateTime.Now.ToString("ddMMyyyyHHmmss")); +} \ No newline at end of file diff --git a/Epsilon/Export/ExportSettings.cs b/Epsilon/Export/ExportSettings.cs deleted file mode 100644 index 86825aab..00000000 --- a/Epsilon/Export/ExportSettings.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Epsilon.Export; - -public record ExportSettings -{ - public string Format { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/Epsilon/Export/Exporters/ConsoleModuleExporter.cs b/Epsilon/Export/Exporters/ConsoleModuleExporter.cs new file mode 100644 index 00000000..b7d41b0d --- /dev/null +++ b/Epsilon/Export/Exporters/ConsoleModuleExporter.cs @@ -0,0 +1,50 @@ +using Epsilon.Abstractions.Export; +using Epsilon.Canvas.Abstractions.Data; +using Microsoft.Extensions.Logging; + +namespace Epsilon.Export.Exporters; + +public class ConsoleModuleExporter : ICanvasModuleExporter +{ + private readonly ILogger _logger; + + public ConsoleModuleExporter(ILogger logger) + { + _logger = logger; + } + + public IEnumerable Formats { get; } = new[] { "console", "logs" }; + + public void Export(IEnumerable data, string format) + { + LogModule(data); + } + + private void LogModule(IEnumerable modules) + { + foreach (var module in modules) + { + _logger.LogInformation("================ {ModuleName} ================", module.Name); + + LogAssignments(module.Assignments); + } + } + + private void LogAssignments(IEnumerable assignments) + { + foreach (var assignment in assignments) + { + _logger.LogInformation("{AssignmentName}", assignment.Name); + + LogOutcomeResults(assignment.OutcomeResults); + } + } + + private void LogOutcomeResults(IEnumerable results) + { + foreach (var outcomeResult in results) + { + _logger.LogInformation("\t- {OutcomeTitle}", outcomeResult.Outcome?.Title); + } + } +} \ No newline at end of file diff --git a/Epsilon/Export/CsvCanvasModuleFileExporter.cs b/Epsilon/Export/Exporters/CsvModuleExporter.cs similarity index 82% rename from Epsilon/Export/CsvCanvasModuleFileExporter.cs rename to Epsilon/Export/Exporters/CsvModuleExporter.cs index 3691603f..e26c2a96 100644 --- a/Epsilon/Export/CsvCanvasModuleFileExporter.cs +++ b/Epsilon/Export/Exporters/CsvModuleExporter.cs @@ -1,18 +1,26 @@ using System.Data; using Epsilon.Abstractions.Export; using Epsilon.Canvas.Abstractions.Data; +using Microsoft.Extensions.Options; -namespace Epsilon.Export; +namespace Epsilon.Export.Exporters; -public class CsvCanvasModuleFileExporter : ICanvasModuleFileExporter +public class CsvModuleExporter : ICanvasModuleExporter { - public bool CanExport(string format) => format == "csv"; + private readonly ExportOptions _options; - public void Export(IEnumerable data, string path) + public CsvModuleExporter(IOptions options) + { + _options = options.Value; + } + + public IEnumerable Formats { get; } = new[] { "csv" }; + + public void Export(IEnumerable data, string format) { var dt = CreateDataTable(data); - var stream = new StreamWriter(path + ".csv", false); + var stream = new StreamWriter($"{_options.FormattedOutputName}.{format}", false); WriteHeader(stream, dt); WriteRows(stream, dt); diff --git a/Epsilon/Export/ModuleExporterCollection.cs b/Epsilon/Export/ModuleExporterCollection.cs new file mode 100644 index 00000000..b76c2feb --- /dev/null +++ b/Epsilon/Export/ModuleExporterCollection.cs @@ -0,0 +1,38 @@ +using Epsilon.Abstractions.Export; +using Epsilon.Export.Exceptions; + +namespace Epsilon.Export; + +public class ModuleExporterCollection : IModuleExporterCollection +{ + private readonly IEnumerable _exporters; + + public ModuleExporterCollection(IEnumerable exporters) + { + _exporters = exporters; + } + + public IDictionary DetermineExporters(IEnumerable formats) + { + var formatsArray = formats as string[] ?? formats.ToArray(); // To prevent multiple enumeration + var foundExporters = new Dictionary(); + + foreach (var exporter in _exporters) + { + foreach (var format in formatsArray) + { + if (exporter.Formats.Contains(format)) + { + foundExporters.Add(format, exporter); + } + } + } + + if (!foundExporters.Any()) + { + throw new NoExportersFoundException(formatsArray); + } + + return foundExporters; + } +} \ No newline at end of file diff --git a/Epsilon/Extensions/CoreServiceCollectionExtensions.cs b/Epsilon/Extensions/CoreServiceCollectionExtensions.cs index 9143e3a1..1b5afb09 100644 --- a/Epsilon/Extensions/CoreServiceCollectionExtensions.cs +++ b/Epsilon/Extensions/CoreServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Epsilon.Abstractions.Export; using Epsilon.Canvas; using Epsilon.Export; +using Epsilon.Export.Exporters; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -18,10 +19,12 @@ public static IServiceCollection AddCore(this IServiceCollection services, IConf private static IServiceCollection AddExport(this IServiceCollection services, IConfiguration config) { - services.Configure(config); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.Configure(config); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); return services; } From 83c6fe1200b4a0e70c8c4c50a1a3102b9c686590 Mon Sep 17 00:00:00 2001 From: Jelle Maas Date: Fri, 3 Jun 2022 12:59:39 +0200 Subject: [PATCH 2/3] Use semicolon instead of comma to allow Excel to automatically recognize CSV format --- Epsilon/Export/Exporters/CsvModuleExporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Epsilon/Export/Exporters/CsvModuleExporter.cs b/Epsilon/Export/Exporters/CsvModuleExporter.cs index e26c2a96..fac254e0 100644 --- a/Epsilon/Export/Exporters/CsvModuleExporter.cs +++ b/Epsilon/Export/Exporters/CsvModuleExporter.cs @@ -58,7 +58,7 @@ private static void WriteHeader(TextWriter writer, DataTable dt) writer.Write(dt.Columns[i]); if (i < dt.Columns.Count - 1) { - writer.Write(","); + writer.Write(";"); } } @@ -77,7 +77,7 @@ private static void WriteRows(TextWriter writer, DataTable dt) var value = dr[i].ToString(); if (value != null) { - if (value.Contains(',')) + if (value.Contains(';')) { value = $"\"{value}\""; writer.Write(value); @@ -91,7 +91,7 @@ private static void WriteRows(TextWriter writer, DataTable dt) if (i < dt.Columns.Count - 1) { - writer.Write(","); + writer.Write(";"); } } From 5ebb16a7b9a8a787cfd5b3582fa756dbd696b63f Mon Sep 17 00:00:00 2001 From: Jelle Maas Date: Fri, 3 Jun 2022 13:38:47 +0200 Subject: [PATCH 3/3] Incorporate argument null check from #13 --- Epsilon.Cli/Startup.cs | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Epsilon.Cli/Startup.cs b/Epsilon.Cli/Startup.cs index b5743fe6..dfde87ad 100644 --- a/Epsilon.Cli/Startup.cs +++ b/Epsilon.Cli/Startup.cs @@ -36,18 +36,6 @@ public Startup( public Task StartAsync(CancellationToken cancellationToken) { - if (_canvasSettings.CourseId <= 0) - { - _logger.LogError("No course id has been given"); - return Task.FromException(new Exception("No course id has been given")); - } - - if (_canvasSettings.AccessToken.Length <= 0) - { - _logger.LogError("No Access token has been given"); - return Task.FromException(new Exception("No Access token has been given")); - } - _lifetime.ApplicationStarted.Register(() => Task.Run(ExecuteAsync, cancellationToken)); return Task.CompletedTask; @@ -60,11 +48,13 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task ExecuteAsync() { - _logger.LogInformation("Targeting Canvas course: {CourseId}, at {Url}", _canvasSettings.CourseId, _canvasSettings.ApiUrl); - _logger.LogInformation("Using export formats: {Formats}", string.Join(",", _exportOptions.Formats)); - try { + ValidateOptions(); + + _logger.LogInformation("Targeting Canvas course: {CourseId}, at {Url}", _canvasSettings.CourseId, _canvasSettings.ApiUrl); + _logger.LogInformation("Using export formats: {Formats}", string.Join(",", _exportOptions.Formats)); + var exporters = _exporterCollection.DetermineExporters(_exportOptions.Formats).ToArray(); var modules = (await _collectionFetcher.Fetch(_canvasSettings.CourseId)).ToArray(); @@ -74,6 +64,10 @@ private async Task ExecuteAsync() exporter.Export(modules, format); } } + catch (ArgumentNullException e) + { + _logger.LogCritical("Argument is required: {ParamName}", e.ParamName); + } catch (NoExportersFoundException e) { _logger.LogCritical("An error occured: {Message}", e.Message); @@ -83,4 +77,17 @@ private async Task ExecuteAsync() _lifetime.StopApplication(); } } + + private void ValidateOptions() + { + if (_canvasSettings.CourseId <= 0) + { + throw new ArgumentNullException(nameof(_canvasSettings.CourseId)); + } + + if (string.IsNullOrEmpty(_canvasSettings.AccessToken)) + { + throw new ArgumentNullException(nameof(_canvasSettings.AccessToken)); + } + } } \ No newline at end of file