Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SPAL-15: Add NavigationItemBuilder.SiteSettings extension #200

Merged
merged 16 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Html;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;

namespace Microsoft.AspNetCore.Mvc.Localization;
Expand Down Expand Up @@ -29,4 +31,29 @@ public static string Html(this IHtmlContent htmlContent)
htmlContent.WriteTo(stringWriter, HtmlEncoder.Default);
return stringWriter.ToString();
}

/// <summary>
/// Concatenates the <paramref name="first"/> localized HTML string with the <paramref name="other"/> provided HTML
/// content. This is suitable for joining individually localizable HTML strings.
/// </summary>
public static LocalizedHtmlString Concat(this LocalizedHtmlString first, params IHtmlContent[] other)
{
if (other.Length == 0) return first;

var builder = new StringBuilder(first.Html());
foreach (var content in other) builder.Append(content.Html());
var html = builder.ToString();

return new LocalizedHtmlString(html, html);
}

public static LocalizedHtmlString Join(this IHtmlContent separator, params LocalizedHtmlString[] items)
{
if (items.Length == 0) return null;

var first = items[0];
var other = items.Skip(1).SelectMany(item => new[] { separator, item }).ToArray();

return first.Concat(other);
}
}
98 changes: 98 additions & 0 deletions Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;

Expand Down Expand Up @@ -165,4 +166,101 @@ public static void AddToList<TKey, TValue>(
/// </summary>
public static IReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) =>
dictionary as IReadOnlyDictionary<TKey, TValue> ?? new Dictionary<TKey, TValue>(dictionary);

/// <summary>
/// Creates a new dictionary based on <paramref name="dictionary"/> by taking each entry's value, and if it's not
/// <see langword="null"/> and has at least one not <see langword="null"/> item then that item is added under the
/// same key.
/// </summary>
[SuppressMessage(
"Design",
"MA0016:Prefer returning collection abstraction instead of implementation",
Justification = "Better compatibility.")]
public static Dictionary<TKey, TValue> WithFirstValues<TKey, TValue, TValues>(
this IEnumerable<KeyValuePair<TKey, TValues>> dictionary,
Func<TValues, TValue> select = null)
where TValues : IEnumerable<TValue>
{
var result = new Dictionary<TKey, TValue>();

foreach (var (key, values) in dictionary)
{
if (select == null)
{
if (values is not null && values.FirstOrDefault() is { } value)
{
result[key] = value;
}
}
else
{
if (select(values) is { } value)
{
result[key] = value;
}
}
}

return result;
}

/// <summary>
/// Creates a new dictionary based on <paramref name="dictionary"/> where each value is a new instance of
/// <typeparamref name="TValues"/> which is an <see cref="IList{T}"/> of <typeparamref name="TValue"/>. If the value
/// is not <see langword="null"/> then it's added to the list.
/// </summary>
[SuppressMessage(
"Design",
"MA0016:Prefer returning collection abstraction instead of implementation",
Justification = "Better compatibility.")]
public static Dictionary<TKey, TValues> ToListValuedDictionary<TKey, TValue, TValues>(
this IEnumerable<KeyValuePair<TKey, TValue>> dictionary,
Func<TValue, TValues> select = null)
where TValues : IList<TValue>, new()
{
var result = new Dictionary<TKey, TValues>();

foreach (var (key, value) in dictionary)
{
if (select != null)
{
result[key] = select(value);
continue;
}

var list = new TValues();
if (value is not null) list.Add(value);
result[key] = list;
}

return result;
}

/// <summary>
/// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner.
/// </summary>
public static IDictionary<string, TValue> ToDictionaryIgnoreCase<TValue>(
this IEnumerable<KeyValuePair<string, TValue>> source) =>
new Dictionary<string, TValue>(source, StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner.
/// </summary>
public static IDictionary<string, TSource> ToDictionaryIgnoreCase<TSource>(
this IEnumerable<TSource> source,
Func<TSource, string> keySelector) =>
new Dictionary<string, TSource>(
source.Select(item => new KeyValuePair<string, TSource>(keySelector(item), item)),
StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner.
/// </summary>
public static IDictionary<string, TValue> ToDictionaryIgnoreCase<TSource, TValue>(
this IEnumerable<TSource> source,
Func<TSource, string> keySelector,
Func<TSource, TValue> valueSelector) =>
new Dictionary<string, TValue>(
source.Select(item => new KeyValuePair<string, TValue>(keySelector(item), valueSelector(item))),
StringComparer.OrdinalIgnoreCase);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public static class CommonContentDisplayTypes
public const string Detail = nameof(Detail);
public const string Summary = nameof(Summary);
public const string SummaryAdmin = nameof(SummaryAdmin);
public const string Thumbnail = nameof(Thumbnail);
}
5 changes: 5 additions & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Lombiq Helpful Libraries - Orchard Core Libraries - Validation for Orchard Core

## `IEmailAndPasswordValidator`

A service for returning validation errors for email of password. Use its `ValidateEmailAsync` method to get email format validation errors, and its `ValidatePasswordAsync` to get validation errors from the registered `IPasswordValidator<IUser>` instances.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Fluid.Values;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Implementation;
using System;
using System.Threading.Tasks;

namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid;

/// <summary>
/// Service that displays shapes as HTML in a <see cref="FluidValue"/>.
/// </summary>
public interface ILiquidContentDisplayService
{
/// <summary>
/// Creates new instances of a dynamic shape object and renders it to HTML in a <see cref="FluidValue"/>.
/// </summary>
ValueTask<FluidValue> DisplayShapeAsync(IShape shape);

/// <summary>
/// Creates new instances of a typed shape object and renders it to HTML in a <see cref="FluidValue"/>.
/// </summary>
/// <typeparam name="TModel">The type to instantiate.</typeparam>
ValueTask<FluidValue> DisplayNewAsync<TModel>(string shapeType, Action<TModel> initialize);

/// <summary>
/// Displays an already instantiated <see cref="IShape"/> as a <see cref="FluidValue"/>.
/// </summary>
ValueTask<FluidValue> DisplayNewAsync(
string shapeType,
Func<ValueTask<IShape>> shapeFactory,
Action<ShapeCreatingContext> creating = null,
Action<ShapeCreatedContext> created = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Fluid.Values;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Implementation;
using OrchardCore.DisplayManagement.Liquid;
using System;
using System.Threading.Tasks;

namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid;

public class LiquidContentDisplayService : ILiquidContentDisplayService
{
private readonly IDisplayHelper _displayHelper;
private readonly IShapeFactory _shapeFactory;
public LiquidContentDisplayService(IDisplayHelper displayHelper, IShapeFactory shapeFactory)
{
_displayHelper = displayHelper;
_shapeFactory = shapeFactory;
}

public async ValueTask<FluidValue> DisplayNewAsync(
string shapeType,
Func<ValueTask<IShape>> shapeFactory,
Action<ShapeCreatingContext> creating = null,
Action<ShapeCreatedContext> created = null)
{
var shape = await _shapeFactory.CreateAsync(shapeType, shapeFactory, creating, created);
return await DisplayShapeAsync(shape);
}

public async ValueTask<FluidValue> DisplayNewAsync<TModel>(string shapeType, Action<TModel> initialize)
{
var shape = await _shapeFactory.CreateAsync(shapeType, initialize);
return await DisplayShapeAsync(shape);
}

public async ValueTask<FluidValue> DisplayShapeAsync(IShape shape)
{
var content = await _displayHelper.ShapeExecuteAsync(shape);
return new HtmlContentValue(content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,10 @@ public static NavigationBuilder AddSeparator(this NavigationBuilder builder, ISt
/// </remarks>
public static NavigationBuilder AddLabel(this NavigationBuilder builder, LocalizedString label) =>
builder.Add(label, subMenu => subMenu.Url("#").AddClass("disabled menuWidget__link_label"));

/// <summary>
/// Adds a link to the menu item pointing to the site settings page identified by the <paramref name="groupId"/>.
/// </summary>
public static NavigationItemBuilder SiteSettings(this NavigationItemBuilder builder, string groupId) =>
builder.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Localization;
using OrchardCore.Email;
using OrchardCore.Users;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.HelpfulLibraries.OrchardCore.Validation;

public class EmailAndPasswordValidator : IEmailAndPasswordValidator
{
private readonly IEmailAddressValidator _emailAddressValidator;
private readonly UserManager<IUser> _userManager;
private readonly IStringLocalizer<EmailAndPasswordValidator> T;
public EmailAndPasswordValidator(
IEmailAddressValidator emailAddressValidator,
UserManager<IUser> userManager,
IStringLocalizer<EmailAndPasswordValidator> stringLocalizer)
{
_emailAddressValidator = emailAddressValidator;
_userManager = userManager;
T = stringLocalizer;
}

public Task<IEnumerable<LocalizedString>> ValidateEmailAsync(string email) =>
Task.FromResult(_emailAddressValidator.Validate(email)
? Enumerable.Empty<LocalizedString>()
: new[] { T["Invalid email address."] });

public async Task<IEnumerable<LocalizedString>> ValidatePasswordAsync(string password)
{
var errors = new List<LocalizedString>();
if (password == null) return errors;

foreach (var passwordValidator in _userManager.PasswordValidators)
{
var result = await passwordValidator.ValidateAsync(_userManager, user: null, password);

if (result.Succeeded) continue;
errors.AddRange(result.Errors.Select(error => T[error.Description]));
}

return errors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.Localization;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Lombiq.HelpfulLibraries.OrchardCore.Validation;

/// <summary>
/// A service for returning validation errors for email of password.
/// </summary>
public interface IEmailAndPasswordValidator
{
/// <summary>
/// Validates the provided <paramref name="email"/> and returns any validation errors as a localized string.
/// </summary>
Task<IEnumerable<LocalizedString>> ValidateEmailAsync(string email);

/// <summary>
/// Validates the provided <paramref name="password"/> and returns any validation errors as a localized string.
/// </summary>
Task<IEnumerable<LocalizedString>> ValidatePasswordAsync(string password);
}