Skip to content

Commit

Permalink
Merge pull request #31 from ninameghan/feature/issue-2-import-export
Browse files Browse the repository at this point in the history
Feature/issue 2 import export
  • Loading branch information
patrickdemooij9 authored Feb 4, 2023
2 parents 03645a0 + 7748570 commit 2c9881b
Show file tree
Hide file tree
Showing 37 changed files with 1,157 additions and 917 deletions.
7 changes: 7 additions & 0 deletions source/SimpleRedirects.Core/Enums/DataRecordProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SimpleRedirects.Core.Enums;

public enum DataRecordProvider
{
Csv,
Excel
}
30 changes: 30 additions & 0 deletions source/SimpleRedirects.Core/Extensions/FormFileExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Http;
using SimpleRedirects.Core.Enums;

namespace SimpleRedirects.Core.Extensions;

internal static class FormFileExtensions
{
/// <summary>
/// Attempts to read a file's extension and translate this to a DataRecordProvider to be able to determine the correct service to use to handle the import file
/// </summary>
/// <param name="file">The file which will be used to determine the data record provider</param>
/// <param name="dataRecordProvider"></param>
/// <returns>True and a DataRecordProvider as out var when filetype can be matched to one, false and null when the file's type cannot be mapped to a DataRecordProvider</returns>
internal static bool CanGetDataRecordProviderFromFile(this IFormFile file, out DataRecordProvider dataRecordProvider)
{
var fileType = System.IO.Path.GetExtension(file?.FileName);
switch (fileType)
{
case ".csv":
dataRecordProvider = DataRecordProvider.Csv;
return true;
case ".xlsx":
dataRecordProvider = DataRecordProvider.Excel;
return true;
default:
dataRecordProvider = DataRecordProvider.Csv;
return false;
}
}
}
9 changes: 1 addition & 8 deletions source/SimpleRedirects.Core/Models/AddRedirectResponse.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace SimpleRedirects.Core.Models
{
public class AddRedirectResponse
public class AddRedirectResponse : BaseResponse
{
[JsonProperty("newRedirect")]
public Redirect NewRedirect { get; set; }

[JsonProperty("success")]
public bool Success { get; set; }

[JsonProperty("message")]
public string Message { get; set; }
}
}
12 changes: 12 additions & 0 deletions source/SimpleRedirects.Core/Models/BaseResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace SimpleRedirects.Core.Models;

public abstract class BaseResponse
{
[JsonProperty("success")]
public bool Success { get; set; }

[JsonProperty("message")]
public string Message { get; set; }
}
23 changes: 23 additions & 0 deletions source/SimpleRedirects.Core/Models/DataRecordCollectionFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using SimpleRedirects.Core.Enums;

namespace SimpleRedirects.Core.Models;

public class DataRecordCollectionFile
{
public DataRecordProvider DataRecordProvider { get; set; }
public byte[] File { get; set; }
public string ContentType => DataRecordProvider == DataRecordProvider.Csv ? "text/csv" : "application/vnd.ms-excel";
public string FileExtension => DataRecordProvider == DataRecordProvider.Csv ? ".csv" : ".xlsx";
public string FileName => $"SimpleRedirects-{DataRecordProvider}-Export-{DateTimeOffset.Now.ToString("dd-M-yyyy", CultureInfo.InvariantCulture)}{FileExtension}";

public DataRecordCollectionFile(DataRecordProvider dataRecordProvider, byte[] file)
{
DataRecordProvider = dataRecordProvider;
File = file;
}

public FileContentResult AsFileContentResult() => new(File, ContentType) { FileDownloadName = FileName };
}
7 changes: 1 addition & 6 deletions source/SimpleRedirects.Core/Models/DeleteRedirectResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@

namespace SimpleRedirects.Core.Models
{
public class DeleteRedirectResponse
public class DeleteRedirectResponse : BaseResponse
{
[JsonProperty("success")]
public bool Success { get; set; }

[JsonProperty("message")]
public string Message { get; set; }
}
}
45 changes: 45 additions & 0 deletions source/SimpleRedirects.Core/Models/ImportRedirectsResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Linq;
using Newtonsoft.Json;

namespace SimpleRedirects.Core.Models;

public class ImportRedirectsResponse : BaseResponse
{
[JsonProperty("addedRedirects")]
public int AddedRedirects { get; set; }

[JsonProperty("updatedRedirects")]
public int UpdatedRedirects { get; set; }

[JsonProperty("existingRedirects")]
public int ExistingRedirects { get; set; }

[JsonProperty("errorRedirects")]
public Redirect[] ErrorRedirects { get; set; }

public static ImportRedirectsResponse FromImport(int addedRedirects, int updatedRedirects, int existingRedirects, Redirect[] errorRedirects)
=> new ImportRedirectsResponse
{
Success = !errorRedirects.Any(),
Message = $"Redirect import completed {(!errorRedirects.Any() ? "without errors" : $"with {errorRedirects.Length} error{FormatSingularOrPlural(errorRedirects.Length)}")},{(existingRedirects > 0 ? $" ignored {existingRedirects} redirect{FormatSingularOrPlural(existingRedirects)} because they already existed," : string.Empty)} added {addedRedirects} redirect{FormatSingularOrPlural(addedRedirects)}{(updatedRedirects > 0 ? $" and updated {updatedRedirects} redirect{FormatSingularOrPlural(updatedRedirects)}" : string.Empty)}.",
AddedRedirects = addedRedirects,
UpdatedRedirects = updatedRedirects,
ExistingRedirects = existingRedirects,
ErrorRedirects = errorRedirects
};

public static ImportRedirectsResponse EmptyImportRecordResponse(string message = "No valid redirects could be processed.")
=> new ImportRedirectsResponse
{
Success = false,
Message = message,
AddedRedirects = 0,
UpdatedRedirects = 0,
ExistingRedirects = 0,
ErrorRedirects = Array.Empty<Redirect>()
};

private static string FormatSingularOrPlural(int length)
=> length == 1 ? string.Empty : "s";
}
8 changes: 7 additions & 1 deletion source/SimpleRedirects.Core/Models/Redirect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using CsvHelper.Configuration.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using NPoco;
Expand All @@ -21,6 +22,7 @@ public class Redirect

[Column("IsRegex")]
[JsonProperty("isRegex")]
[Default("False")]
public bool IsRegex { get; set; }

[Column("OldUrl")]
Expand All @@ -29,18 +31,22 @@ public class Redirect

[Column("NewUrl")]
[JsonProperty("newUrl")]
[Default("")]
public string NewUrl { get; set; }

[Column("RedirectCode")]
[JsonProperty("redirectCode")]
[Default(301)]
public int RedirectCode { get; set; }

[Column("LastUpdated")]
[JsonProperty("lastUpdated")]
public DateTime LastUpdated { get; set; }
[Default("")]
public DateTime? LastUpdated { get; set; }

[Column("Notes")]
[JsonProperty("notes")]
[Default("")]
public string Notes { get; set; }

public string GetNewUrl(Uri uri, bool preserveQueryString)
Expand Down
13 changes: 13 additions & 0 deletions source/SimpleRedirects.Core/Models/RedirectMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Globalization;
using CsvHelper.Configuration;

namespace SimpleRedirects.Core.Models;

public sealed class RedirectMap : ClassMap<Redirect>
{
public RedirectMap()
{
AutoMap(CultureInfo.InvariantCulture);
Map(m => m.Id).Ignore();
}
}
8 changes: 1 addition & 7 deletions source/SimpleRedirects.Core/Models/UpdateRedirectResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@

namespace SimpleRedirects.Core.Models
{
public class UpdateRedirectResponse
public class UpdateRedirectResponse : BaseResponse
{
[JsonProperty("updatedRedirect")]
public Redirect UpdatedRedirect { get; set; }

[JsonProperty("success")]
public bool Success { get; set; }

[JsonProperty("message")]
public string Message { get; set; }
}
}
84 changes: 72 additions & 12 deletions source/SimpleRedirects.Core/RedirectApiController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using SimpleRedirects.Core.Enums;
using SimpleRedirects.Core.Extensions;
using SimpleRedirects.Core.Models;
using SimpleRedirects.Core.Services;
using Umbraco.Cms.Web.BackOffice.Controllers;
using Umbraco.Cms.Web.Common.Attributes;

Expand All @@ -11,10 +15,12 @@ namespace SimpleRedirects.Core
public class RedirectApiController : UmbracoAuthorizedApiController
{
private readonly RedirectRepository _redirectRepository;
private readonly ImportExportFactory _importExportFactory;

public RedirectApiController(RedirectRepository redirectRepository)
public RedirectApiController(RedirectRepository redirectRepository, ImportExportFactory importExportFactory)
{
_redirectRepository = redirectRepository;
_importExportFactory = importExportFactory;
}

/// <summary>
Expand All @@ -36,18 +42,20 @@ public IEnumerable<Redirect> GetAll()
public AddRedirectResponse Add(AddRedirectRequest request)
{
if (request == null) return new AddRedirectResponse() { Success = false, Message = "Request was empty" };
if (!ModelState.IsValid) return new AddRedirectResponse() { Success = false, Message = "Missing required attributes" };
if (!ModelState.IsValid)
return new AddRedirectResponse() { Success = false, Message = "Missing required attributes" };

try
{
var redirect = _redirectRepository.AddRedirect(request.IsRegex, request.OldUrl, request.NewUrl, request.RedirectCode, request.Notes);
var redirect = _redirectRepository.AddRedirect(request.IsRegex, request.OldUrl, request.NewUrl,
request.RedirectCode, request.Notes);
return new AddRedirectResponse() { Success = true, NewRedirect = redirect };
}
catch(Exception e)
catch (Exception e)
{
return new AddRedirectResponse() { Success = false, Message = "There was an error adding the redirect : "+ e.Message };
return new AddRedirectResponse()
{ Success = false, Message = "There was an error adding the redirect : " + e.Message };
}

}

/// <summary>
Expand All @@ -58,9 +66,9 @@ public AddRedirectResponse Add(AddRedirectRequest request)
[HttpPost]
public UpdateRedirectResponse Update(UpdateRedirectRequest request)
{

if (request == null) return new UpdateRedirectResponse() { Success = false, Message = "Request was empty" };
if (!ModelState.IsValid) return new UpdateRedirectResponse() { Success = false, Message = "Missing required attributes" };
if (!ModelState.IsValid)
return new UpdateRedirectResponse() { Success = false, Message = "Missing required attributes" };

try
{
Expand All @@ -69,7 +77,8 @@ public UpdateRedirectResponse Update(UpdateRedirectRequest request)
}
catch (Exception e)
{
return new UpdateRedirectResponse() { Success = false, Message = "There was an error updating the redirect : "+e.Message };
return new UpdateRedirectResponse()
{ Success = false, Message = "There was an error updating the redirect : " + e.Message };
}
}

Expand All @@ -81,16 +90,38 @@ public UpdateRedirectResponse Update(UpdateRedirectRequest request)
[HttpDelete]
public DeleteRedirectResponse Delete(int id)
{
if (id == 0) return new DeleteRedirectResponse() { Success = false, Message = "Invalid ID passed for redirect to delete" };
if (id == 0)
return new DeleteRedirectResponse()
{ Success = false, Message = "Invalid ID passed for redirect to delete" };

try
{
_redirectRepository.DeleteRedirect(id);
return new DeleteRedirectResponse() { Success = true };
}
catch(Exception e)
catch (Exception e)
{
return new DeleteRedirectResponse() { Success = false, Message = "There was an error deleting the redirect : " + e.Message };
return new DeleteRedirectResponse()
{ Success = false, Message = "There was an error deleting the redirect : " + e.Message };
}
}

/// <summary>
/// DELETE to delete all redirects
/// </summary>
/// <returns>Response object detailing success or failure</returns>
[HttpDelete]
public DeleteRedirectResponse DeleteAll()
{
try
{
_redirectRepository.DeleteAllRedirects();
return new DeleteRedirectResponse() { Success = true };
}
catch (Exception e)
{
return new DeleteRedirectResponse()
{ Success = false, Message = "There was an error deleting the redirects : " + e.Message };
}
}

Expand All @@ -102,5 +133,34 @@ public void ClearCache()
{
_redirectRepository.ClearCache();
}

/// <summary>
/// GET to export simple redirects to CSV
/// </summary>
[HttpGet]
public ActionResult ExportRedirects(DataRecordProvider dataRecordProvider)
{
var dataRecordCollectionFile = _importExportFactory.GetDataRecordProvider(dataRecordProvider)
.ExportDataRecordCollection();

return dataRecordCollectionFile.AsFileContentResult();
}

/// <summary>
/// Import redirects from CSV
/// </summary>
[HttpPost]
public ImportRedirectsResponse ImportRedirects(bool overwriteMatches)
{
var file = HttpContext.Request.Form.Files.Any() ? HttpContext.Request.Form.Files[0] : null;
if (file is null) return ImportRedirectsResponse.EmptyImportRecordResponse();
if (!file.CanGetDataRecordProviderFromFile(out var provider))
return ImportRedirectsResponse.EmptyImportRecordResponse(
"No redirects imported, provided file is not supported by the import process. Please provide a .csv or .xlsx file.");

var response = _importExportFactory.GetDataRecordProvider(provider)
.ImportRedirectsFromCollection(file, overwriteMatches);
return response;
}
}
}
Loading

0 comments on commit 2c9881b

Please sign in to comment.