Skip to content

Commit

Permalink
Feature/add csv support (#126)
Browse files Browse the repository at this point in the history
* Added csv support

Added in csv import and export support. Utilised csvhelper for heavy lifting

* Fixed incorrect using directives

* More code clean up

* Code review fixes

---------

Co-authored-by: Jevgenij Puchko <jevgen.pucko@gmail.com>
  • Loading branch information
jaymarvels and jevgenijsp authored Oct 1, 2024
1 parent abd7d06 commit 05c047f
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
<div class="d-flex w-100 justify-content-between">
<div class="flex-fill">
<div class="d-flex w-100 align-items-center">
Import redirects from an XML-file
<input class="form-control ms-4 input-file" type="file" asp-for="ImportFile" accept=".xml">
Import redirects from a valid XML or CSV
<input class="form-control ms-4 input-file" type="file" asp-for="ImportFile" accept=".xml, .csv">
</div>
</div>
<button type="submit" class="btn btn-primary administer-button"
Expand Down Expand Up @@ -99,6 +99,19 @@
<span data-feather="download"></span> export
</button>
</div>
<input type="hidden" name="TypeId" value="xml">
</form>
</li>
<li class="list-group-item">
<form method="post">
<div class="d-flex w-100 justify-content-between align-items-center">
<div>Export redirects to an CSV-file</div>
<button type="submit" class="btn btn-primary administer-button"
asp-page-handler="ExportRedirects">
<span data-feather="download"></span> export
</button>
</div>
<input type="hidden" name="TypeId" value="csv">
</form>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using CsvHelper;
using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card;
using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Infrastructure;
using Geta.NotFoundHandler.Core.Redirects;
Expand All @@ -20,15 +23,21 @@ public class AdministerModel : PageModel
private readonly IRedirectsService _redirectsService;
private readonly ISuggestionService _suggestionService;
private readonly RedirectsXmlParser _redirectsXmlParser;
private readonly RedirectsCsvParser _redirectsCsvParser;
private readonly RedirectsTxtParser _redirectsTxtParser;

public AdministerModel(
IRedirectsService redirectsService,
ISuggestionService suggestionService,
RedirectsXmlParser redirectsXmlParser)
RedirectsXmlParser redirectsXmlParser,
RedirectsCsvParser redirectsCsvParser,
RedirectsTxtParser redirectsTxtParser)
{
_redirectsService = redirectsService;
_suggestionService = suggestionService;
_redirectsXmlParser = redirectsXmlParser;
_redirectsCsvParser = redirectsCsvParser;
_redirectsTxtParser = redirectsTxtParser;
}

[BindProperty(SupportsGet = true)]
Expand All @@ -46,162 +55,135 @@ public AdministerModel(
public IActionResult OnPostDeleteAllIgnoredSuggestions()
{
var count = _redirectsService.DeleteAllIgnored();
Message = $"All {count} ignored suggestions permanently removed";
CardType = CardType.Success;

return RedirectToPage(new {
Message,
CardType
});
return BuildResponse($"All {count} ignored suggestions permanently removed", CardType.Success);
}

public IActionResult OnPostDeleteAllSuggestions()
{
_suggestionService.DeleteAll();
Message = "Suggestions successfully deleted";
CardType = CardType.Success;

return RedirectToPage(new
{
Message,
CardType
});
return BuildResponse("Suggestions successfully deleted", CardType.Success);
}

public IActionResult OnPostDeleteAllRedirects()
{
_redirectsService.DeleteAll();
Message = "Redirects successfully deleted";
CardType = CardType.Success;

return RedirectToPage(new
{
Message,
CardType
});
return BuildResponse("Redirects successfully deleted", CardType.Success);
}

public IActionResult OnPostDeleteSuggestions()
{
if (!ModelState.IsValid) return Page();

_suggestionService.Delete(DeleteSuggestions.MaxErrors, DeleteSuggestions.MinimumDays);
Message = "Suggestions successfully deleted";
CardType = CardType.Success;

return RedirectToPage(new
{
Message,
CardType
});
return BuildResponse("Suggestions successfully deleted", CardType.Success);
}

public IActionResult OnPostImportRedirects()
{
if (ImportFile == null || !ImportFile.IsXml())
if (ImportFile == null)
{
Message = "The uploaded file is not a valid XML file. Please upload a valid XML file.";
CardType = CardType.Warning;

return RedirectToPage(new
{
Message,
CardType
});
return BuildResponse("The uploaded file is not a valid XML or CSV file. Please upload a valid XML/CSV file.",
CardType.Warning);
}

var redirects = _redirectsXmlParser.LoadFromStream(ImportFile.OpenReadStream());

if (redirects.Any())
if (ImportFile.IsXml())
{
_redirectsService.AddOrUpdate(redirects);
Message = $"{redirects.Count()} urls successfully imported.";
CardType = CardType.Success;
return ProcessFile(_redirectsXmlParser);
}
else

if (ImportFile.IsCsv())
{
Message = "No redirects could be imported";
CardType = CardType.Warning;
return ProcessFile(_redirectsCsvParser);
}

return RedirectToPage(new
{
Message,
CardType
});
return BuildResponse("The uploaded file is not a valid. Please upload a file.",
CardType.Warning);
}

public IActionResult OnPostImportDeletedRedirects()
{
if (ImportFile == null || !ImportFile.IsTxt())
{
Message = "The uploaded file is not a valid TXT file. Please upload a valid TXT file.";
CardType = CardType.Warning;

return RedirectToPage(new
{
Message,
CardType
});
return BuildResponse("The uploaded file is not a valid TXT file. Please upload a valid TXT file.", CardType.Warning);
}

var redirects = ReadDeletedRedirectsFromImportFile();
return ProcessFile(_redirectsTxtParser);
}

if (redirects.Any())
public IActionResult OnPostExportRedirects(string typeId)
{
if (string.IsNullOrEmpty(typeId))
{
_redirectsService.AddOrUpdate(redirects);
Message = $"{redirects.Count()} urls successfully imported.";
CardType = CardType.Success;
return BuildResponse("Failed to Export", CardType.Warning);
}
else

var redirects = _redirectsService.GetSaved().ToList();

if (!redirects.Any())
{
Message = "No redirects could be imported";
CardType = CardType.Warning;
return BuildResponse("Nothing to Export", CardType.Warning);
}

return RedirectToPage(new
return typeId.ToLower() switch
{
Message,
CardType
});
"xml" => ExportAsXml(redirects),
"csv" => ExportAsCsv(redirects),
_ => BuildResponse("Failed to export. Unsupported export type", CardType.Warning)
};
}

public IActionResult OnPostExportRedirects()
private IActionResult ExportAsXml(List<CustomRedirect> redirects)
{
var redirects = _redirectsService.GetSaved().ToList();
var document = _redirectsXmlParser.Export(redirects);

var memoryStream = new MemoryStream();
var writer = new XmlTextWriter(memoryStream, Encoding.UTF8)
{
Formatting = Formatting.Indented
};
using var memoryStream = new MemoryStream();
using var writer = new XmlTextWriter(memoryStream, Encoding.UTF8) { Formatting = Formatting.Indented };
document.WriteTo(writer);
writer.Flush();
memoryStream.Seek(0, SeekOrigin.Begin);

return File(memoryStream, "text/xml", "customRedirects.xml");
return File(memoryStream.ToArray(), "text/xml", "customRedirects.xml");
}

private CustomRedirectCollection ReadDeletedRedirectsFromImportFile()
private IActionResult ExportAsCsv(List<CustomRedirect> redirects)
{
var redirects = new CustomRedirectCollection();
using var streamReader = new StreamReader(ImportFile.OpenReadStream());
while (streamReader.Peek() >= 0)
var documents = _redirectsCsvParser.Export(redirects);

using var memoryStream = new MemoryStream();
using (var tw = new StreamWriter(memoryStream, leaveOpen: true))
using (var csv = new CsvWriter(tw, CultureInfo.InvariantCulture))
{
var url = streamReader.ReadLine();
if (!string.IsNullOrEmpty(url))
{
redirects.Add(new CustomRedirect
{
OldUrl = url,
NewUrl = string.Empty,
State = (int)RedirectState.Deleted
});
}
csv.WriteRecords(documents);
}

memoryStream.Seek(0, SeekOrigin.Begin);
return File(memoryStream.ToArray(), "text/csv", "customRedirects.csv");
}

private IActionResult BuildResponse(string message, CardType cardType)
{
Message = message;
CardType = cardType;

return RedirectToPage(new { Message, CardType });
}

private IActionResult ProcessFile(IRedirectsParser parser)
{
var redirects = parser.LoadFromStream(ImportFile.OpenReadStream());

if (redirects.Any())
{
_redirectsService.AddOrUpdate(redirects);

return BuildResponse($"{redirects.Count()} urls successfully imported.", CardType.Success);
}

return redirects;
return BuildResponse("No redirects could be imported", CardType.Warning);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public static bool IsXml(this IFormFile file)
return FileIsOfType(file, new[] { "text/xml", "application/xml" }, new[] { "xml" });
}

public static bool IsCsv(this IFormFile file)
{
return FileIsOfType(file, new[] { "text/csv" }, new[] { "csv" });
}

public static bool IsTxt(this IFormFile file)
{
return FileIsOfType(file, new[] { "text/plain" }, new[] { "txt" });
Expand Down
8 changes: 8 additions & 0 deletions src/Geta.NotFoundHandler/Core/Redirects/IRedirectsParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.IO;

namespace Geta.NotFoundHandler.Core.Redirects;

public interface IRedirectsParser
{
CustomRedirectCollection LoadFromStream(Stream xmlContent);
}
Loading

0 comments on commit 05c047f

Please sign in to comment.