diff --git a/Gibbon.Git.Server.Tests/Models/UserModelTest.cs b/Gibbon.Git.Server.Tests/Models/UserModelTest.cs index caf330a3..cebedfb4 100644 --- a/Gibbon.Git.Server.Tests/Models/UserModelTest.cs +++ b/Gibbon.Git.Server.Tests/Models/UserModelTest.cs @@ -1,4 +1,5 @@ -using Gibbon.Git.Server.Models; +using Gibbon.Git.Server.Data.Entities; +using Gibbon.Git.Server.Models; namespace Gibbon.Git.Server.Tests.Models; @@ -27,4 +28,100 @@ public void SortNameFormation() Assert.AreEqual("Smith", new UserModel { GivenName = "", Surname = "Smith" }.SortName); Assert.AreEqual("JohnSmith", new UserModel { Username = "JohnSmith" }.SortName); } + + [TestMethod] + public void DisplayNameFormation_FirstLast() + { + var user = new UserModel { GivenName = "John", Surname = "Smith" }; + Assert.AreEqual("John Smith", user.GetDisplayName(NameFormat.FirstLast)); + + var userOnlyGiven = new UserModel { GivenName = "John", Surname = null }; + Assert.AreEqual("John", userOnlyGiven.GetDisplayName(NameFormat.FirstLast)); + + var userOnlySurname = new UserModel { GivenName = null, Surname = "Smith" }; + Assert.AreEqual("Smith", userOnlySurname.GetDisplayName(NameFormat.FirstLast)); + + var userNoNames = new UserModel { Username = "JohnSmith" }; + Assert.AreEqual("JohnSmith", userNoNames.GetDisplayName(NameFormat.FirstLast)); + } + + [TestMethod] + public void DisplayNameFormation_LastCommaFirst() + { + var user = new UserModel { GivenName = "John", Surname = "Smith" }; + Assert.AreEqual("Smith, John", user.GetDisplayName(NameFormat.LastCommaFirst)); + + var userOnlyGiven = new UserModel { GivenName = "John", Surname = null }; + Assert.AreEqual("John", userOnlyGiven.GetDisplayName(NameFormat.LastCommaFirst)); + + var userOnlySurname = new UserModel { GivenName = null, Surname = "Smith" }; + Assert.AreEqual("Smith", userOnlySurname.GetDisplayName(NameFormat.LastCommaFirst)); + + var userNoNames = new UserModel { Username = "JohnSmith" }; + Assert.AreEqual("JohnSmith", userNoNames.GetDisplayName(NameFormat.LastCommaFirst)); + } + + [TestMethod] + public void DisplayNameFormation_LastFirst() + { + var user = new UserModel { GivenName = "John", Surname = "Smith" }; + Assert.AreEqual("Smith John", user.GetDisplayName(NameFormat.LastFirst)); + + var userOnlyGiven = new UserModel { GivenName = "John", Surname = null }; + Assert.AreEqual("John", userOnlyGiven.GetDisplayName(NameFormat.LastFirst)); + + var userOnlySurname = new UserModel { GivenName = null, Surname = "Smith" }; + Assert.AreEqual("Smith", userOnlySurname.GetDisplayName(NameFormat.LastFirst)); + + var userNoNames = new UserModel { Username = "JohnSmith" }; + Assert.AreEqual("JohnSmith", userNoNames.GetDisplayName(NameFormat.LastFirst)); + } + + [TestMethod] + public void SortNameFormation_FirstLast() + { + var user = new UserModel { GivenName = "John", Surname = "Smith" }; + Assert.AreEqual("JohnSmith", user.GetSortName(NameFormat.FirstLast)); + + var userOnlyGiven = new UserModel { GivenName = "John", Surname = null }; + Assert.AreEqual("John", userOnlyGiven.GetSortName(NameFormat.FirstLast)); + + var userOnlySurname = new UserModel { GivenName = null, Surname = "Smith" }; + Assert.AreEqual("Smith", userOnlySurname.GetSortName(NameFormat.FirstLast)); + + var userNoNames = new UserModel { Username = "JohnSmith" }; + Assert.AreEqual("JohnSmith", userNoNames.GetSortName(NameFormat.FirstLast)); + } + + [TestMethod] + public void SortNameFormation_LastCommaFirst() + { + var user = new UserModel { GivenName = "John", Surname = "Smith" }; + Assert.AreEqual("SmithJohn", user.GetSortName(NameFormat.LastCommaFirst)); + + var userOnlyGiven = new UserModel { GivenName = "John", Surname = null }; + Assert.AreEqual("John", userOnlyGiven.GetSortName(NameFormat.LastCommaFirst)); + + var userOnlySurname = new UserModel { GivenName = null, Surname = "Smith" }; + Assert.AreEqual("Smith", userOnlySurname.GetSortName(NameFormat.LastCommaFirst)); + + var userNoNames = new UserModel { Username = "JohnSmith" }; + Assert.AreEqual("JohnSmith", userNoNames.GetSortName(NameFormat.LastCommaFirst)); + } + + [TestMethod] + public void SortNameFormation_LastFirst() + { + var user = new UserModel { GivenName = "John", Surname = "Smith" }; + Assert.AreEqual("SmithJohn", user.GetSortName(NameFormat.LastFirst)); + + var userOnlyGiven = new UserModel { GivenName = "John", Surname = null }; + Assert.AreEqual("John", userOnlyGiven.GetSortName(NameFormat.LastFirst)); + + var userOnlySurname = new UserModel { GivenName = null, Surname = "Smith" }; + Assert.AreEqual("Smith", userOnlySurname.GetSortName(NameFormat.LastFirst)); + + var userNoNames = new UserModel { Username = "JohnSmith" }; + Assert.AreEqual("JohnSmith", userNoNames.GetSortName(NameFormat.LastFirst)); + } } diff --git a/Gibbon.Git.Server/App_Resources/Resources.Designer.cs b/Gibbon.Git.Server/App_Resources/Resources.Designer.cs index 3fe6c029..7eaf1d50 100644 --- a/Gibbon.Git.Server/App_Resources/Resources.Designer.cs +++ b/Gibbon.Git.Server/App_Resources/Resources.Designer.cs @@ -2366,6 +2366,51 @@ public static string Settings_Global_DefaultLanguage_Hint { } } + /// + /// Looks up a localized string similar to Name Format. + /// + public static string Settings_NameFormat { + get { + return ResourceManager.GetString("Settings_NameFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose how names are displayed throughout the application.. + /// + public static string Settings_NameFormat_Hint { + get { + return ResourceManager.GetString("Settings_NameFormat_Hint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First Last (e.g., John Smith). + /// + public static string Settings_NameFormat_FirstLast { + get { + return ResourceManager.GetString("Settings_NameFormat_FirstLast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last, First (e.g., Smith, John). + /// + public static string Settings_NameFormat_LastCommaFirst { + get { + return ResourceManager.GetString("Settings_NameFormat_LastCommaFirst", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last First (e.g., Smith John). + /// + public static string Settings_NameFormat_LastFirst { + get { + return ResourceManager.GetString("Settings_NameFormat_LastFirst", resourceCulture); + } + } + /// /// Looks up a localized string similar to All repositories are stored in this directory.. /// diff --git a/Gibbon.Git.Server/App_Resources/Resources.resx b/Gibbon.Git.Server/App_Resources/Resources.resx index 9e0434f5..531358c1 100644 --- a/Gibbon.Git.Server/App_Resources/Resources.resx +++ b/Gibbon.Git.Server/App_Resources/Resources.resx @@ -700,6 +700,21 @@ Language set by default in the application. + + Name Format + + + Choose how names are displayed throughout the application. + + + First Last (e.g., John Smith) + + + Last, First (e.g., Smith, John) + + + Last First (e.g., Smith John) + Status diff --git a/Gibbon.Git.Server/Configuration/UserSettings.cs b/Gibbon.Git.Server/Configuration/UserSettings.cs index 5655c649..e39a6bf9 100644 --- a/Gibbon.Git.Server/Configuration/UserSettings.cs +++ b/Gibbon.Git.Server/Configuration/UserSettings.cs @@ -1,7 +1,9 @@ -namespace Gibbon.Git.Server.Configuration; +using Gibbon.Git.Server.Data.Entities; + +namespace Gibbon.Git.Server.Configuration; public sealed class UserSettings { public string PreferredLanguage { get; set; } - + public NameFormat PreferredNameFormat { get; set; } } diff --git a/Gibbon.Git.Server/Configuration/UserSettingsService.cs b/Gibbon.Git.Server/Configuration/UserSettingsService.cs index 6867db9b..cf739842 100644 --- a/Gibbon.Git.Server/Configuration/UserSettingsService.cs +++ b/Gibbon.Git.Server/Configuration/UserSettingsService.cs @@ -24,6 +24,7 @@ public async Task SaveSettings(int userId, UserSettings settings) } entity.PreferredLanguage = settings.PreferredLanguage; + entity.PreferredNameFormat = settings.PreferredNameFormat; await _context.SaveChangesAsync(); @@ -43,7 +44,8 @@ public async Task GetSettings(int userId) .Where(u => u.UserId == userId) .Select(entity => new UserSettings { - PreferredLanguage = entity.PreferredLanguage + PreferredLanguage = entity.PreferredLanguage, + PreferredNameFormat = entity.PreferredNameFormat }) .SingleOrDefaultAsync(); @@ -58,7 +60,8 @@ public UserSettings GetDefaultSettings() { return new UserSettings { - PreferredLanguage = null + PreferredLanguage = null, + PreferredNameFormat = NameFormat.LastCommaFirst }; } } diff --git a/Gibbon.Git.Server/Controllers/AccountController.cs b/Gibbon.Git.Server/Controllers/AccountController.cs index 5397d35a..022a966f 100644 --- a/Gibbon.Git.Server/Controllers/AccountController.cs +++ b/Gibbon.Git.Server/Controllers/AccountController.cs @@ -136,12 +136,33 @@ public async Task Settings() Value = "" }); + var nameFormatItems = new List + { + new SelectListItem + { + Text = Resources.Settings_NameFormat_FirstLast, + Value = ((int)Data.Entities.NameFormat.FirstLast).ToString() + }, + new SelectListItem + { + Text = Resources.Settings_NameFormat_LastCommaFirst, + Value = ((int)Data.Entities.NameFormat.LastCommaFirst).ToString() + }, + new SelectListItem + { + Text = Resources.Settings_NameFormat_LastFirst, + Value = ((int)Data.Entities.NameFormat.LastFirst).ToString() + } + }; + var settings = await _userSettingsService.GetSettings(UserModel.Id); return View(new MeSettingsModel { PreferredLanguage = settings.PreferredLanguage, - AvailableLanguages = cultureItems + PreferredNameFormat = settings.PreferredNameFormat, + AvailableLanguages = cultureItems, + AvailableNameFormats = nameFormatItems }); } @@ -160,12 +181,32 @@ public async Task Settings(MeSettingsModel settings) }) .ToList(); + settings.AvailableNameFormats = new List + { + new SelectListItem + { + Text = Resources.Settings_NameFormat_FirstLast, + Value = ((int)Data.Entities.NameFormat.FirstLast).ToString() + }, + new SelectListItem + { + Text = Resources.Settings_NameFormat_LastCommaFirst, + Value = ((int)Data.Entities.NameFormat.LastCommaFirst).ToString() + }, + new SelectListItem + { + Text = Resources.Settings_NameFormat_LastFirst, + Value = ((int)Data.Entities.NameFormat.LastFirst).ToString() + } + }; + return View(settings); } await _userSettingsService.SaveSettings(UserModel.Id, new UserSettings { - PreferredLanguage = settings.PreferredLanguage + PreferredLanguage = settings.PreferredLanguage, + PreferredNameFormat = settings.PreferredNameFormat }); return RedirectToAction("Settings"); diff --git a/Gibbon.Git.Server/Data/Entities/NameFormat.cs b/Gibbon.Git.Server/Data/Entities/NameFormat.cs new file mode 100644 index 00000000..83e6b07d --- /dev/null +++ b/Gibbon.Git.Server/Data/Entities/NameFormat.cs @@ -0,0 +1,8 @@ +namespace Gibbon.Git.Server.Data.Entities; + +public enum NameFormat +{ + FirstLast = 0, + LastCommaFirst = 1, + LastFirst = 2 +} diff --git a/Gibbon.Git.Server/Data/Entities/UserSettingsEntity.cs b/Gibbon.Git.Server/Data/Entities/UserSettingsEntity.cs index 1b4f83e0..73cbad23 100644 --- a/Gibbon.Git.Server/Data/Entities/UserSettingsEntity.cs +++ b/Gibbon.Git.Server/Data/Entities/UserSettingsEntity.cs @@ -9,4 +9,5 @@ public class UserSettingsEntity public string TimeZone { get; set; } public string DateFormat { get; set; } public string DefaultHomePage { get; set; } + public NameFormat PreferredNameFormat { get; set; } } diff --git a/Gibbon.Git.Server/Data/EntityConfiguration/UserSettingsEntityConfiguration.cs b/Gibbon.Git.Server/Data/EntityConfiguration/UserSettingsEntityConfiguration.cs index 7485a9bd..bd4c6091 100644 --- a/Gibbon.Git.Server/Data/EntityConfiguration/UserSettingsEntityConfiguration.cs +++ b/Gibbon.Git.Server/Data/EntityConfiguration/UserSettingsEntityConfiguration.cs @@ -33,5 +33,8 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.DefaultHomePage) .HasMaxLength(100); + + builder.Property(e => e.PreferredNameFormat) + .HasConversion(); } } diff --git a/Gibbon.Git.Server/Migrations/SqlServerMigrations/20250930133205_AddPreferredNameFormat.cs b/Gibbon.Git.Server/Migrations/SqlServerMigrations/20250930133205_AddPreferredNameFormat.cs new file mode 100644 index 00000000..2db34cdc --- /dev/null +++ b/Gibbon.Git.Server/Migrations/SqlServerMigrations/20250930133205_AddPreferredNameFormat.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Gibbon.Git.Server.Migrations.SqlServerMigrations +{ + /// + public partial class AddPreferredNameFormat : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PreferredNameFormat", + table: "UserSettings", + type: "int", + nullable: false, + defaultValue: 1); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PreferredNameFormat", + table: "UserSettings"); + } + } +} diff --git a/Gibbon.Git.Server/Migrations/SqliteMigrations/20250930133205_AddPreferredNameFormat.cs b/Gibbon.Git.Server/Migrations/SqliteMigrations/20250930133205_AddPreferredNameFormat.cs new file mode 100644 index 00000000..0f1affd2 --- /dev/null +++ b/Gibbon.Git.Server/Migrations/SqliteMigrations/20250930133205_AddPreferredNameFormat.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Gibbon.Git.Server.Migrations.SqliteMigrations +{ + /// + public partial class AddPreferredNameFormat : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PreferredNameFormat", + table: "UserSettings", + type: "INTEGER", + nullable: false, + defaultValue: 1); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PreferredNameFormat", + table: "UserSettings"); + } + } +} diff --git a/Gibbon.Git.Server/Models/MeSettingsModel.cs b/Gibbon.Git.Server/Models/MeSettingsModel.cs index 7fe0276f..56c0b006 100644 --- a/Gibbon.Git.Server/Models/MeSettingsModel.cs +++ b/Gibbon.Git.Server/Models/MeSettingsModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Gibbon.Git.Server.Data.Entities; using Microsoft.AspNetCore.Mvc.Rendering; namespace Gibbon.Git.Server.Models; @@ -8,6 +9,9 @@ public class MeSettingsModel [Display(ResourceType = typeof(Resources), Name = "Settings_Global_DefaultLanguage")] public string PreferredLanguage { get; set; } + [Display(ResourceType = typeof(Resources), Name = "Settings_NameFormat")] + public NameFormat PreferredNameFormat { get; set; } + /// /// This is the list of available languages for the user to choose from. /// @@ -15,4 +19,9 @@ public class MeSettingsModel /// This is just for the user to choose from, why we don't need a display attribute. /// internal List AvailableLanguages { get; set; } + + /// + /// This is the list of available name formats for the user to choose from. + /// + internal List AvailableNameFormats { get; set; } } diff --git a/Gibbon.Git.Server/Models/UserModel.cs b/Gibbon.Git.Server/Models/UserModel.cs index 578c60f5..b0517d13 100644 --- a/Gibbon.Git.Server/Models/UserModel.cs +++ b/Gibbon.Git.Server/Models/UserModel.cs @@ -1,4 +1,6 @@ -namespace Gibbon.Git.Server.Models; +using Gibbon.Git.Server.Data.Entities; + +namespace Gibbon.Git.Server.Models; public class UserModel { @@ -8,14 +10,43 @@ public class UserModel public string Surname { get; set; } public string Email { get; set; } - public string DisplayName => !string.IsNullOrWhiteSpace(GivenName) || !string.IsNullOrWhiteSpace(Surname) - ? $"{Surname}, {GivenName}".Trim(' ', ',') - : Username; + public string DisplayName => GetDisplayName(NameFormat.LastCommaFirst); /// /// This is the name we'd sort users by /// - public string SortName => !string.IsNullOrWhiteSpace(Surname) || !string.IsNullOrWhiteSpace(GivenName) - ? $"{Surname}{GivenName}" - : Username; + public string SortName => GetSortName(NameFormat.LastCommaFirst); + + public string GetDisplayName(NameFormat format) + { + if (string.IsNullOrWhiteSpace(GivenName) && string.IsNullOrWhiteSpace(Surname)) + { + return Username; + } + + return format switch + { + NameFormat.FirstLast => $"{GivenName} {Surname}".Trim(), + NameFormat.LastCommaFirst => $"{Surname}, {GivenName}".Trim(' ', ','), + NameFormat.LastFirst => $"{Surname} {GivenName}".Trim(), + _ => $"{Surname}, {GivenName}".Trim(' ', ',') + }; + } + + public string GetSortName(NameFormat format) + { + if (string.IsNullOrWhiteSpace(Surname) && string.IsNullOrWhiteSpace(GivenName)) + { + return Username; + } + + return format switch + { + NameFormat.FirstLast => $"{GivenName}{Surname}", + NameFormat.LastCommaFirst => $"{Surname}{GivenName}", + NameFormat.LastFirst => $"{Surname}{GivenName}", + _ => $"{Surname}{GivenName}" + }; + } } + diff --git a/Gibbon.Git.Server/Views/Account/Settings.cshtml b/Gibbon.Git.Server/Views/Account/Settings.cshtml index 03806402..ac32e7f1 100644 --- a/Gibbon.Git.Server/Views/Account/Settings.cshtml +++ b/Gibbon.Git.Server/Views/Account/Settings.cshtml @@ -20,6 +20,12 @@ +
+ @Html.LabelFor(m => m.PreferredNameFormat) + @Html.DropDownListFor(m => m.PreferredNameFormat, Model.AvailableNameFormats, new { @class = "medium" }) + +
+