From eb0448341732fc211c5f44e210522c58169ec595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Mila=C5=A1inovi=C4=87?= Date: Sat, 25 Apr 2026 19:52:11 +0200 Subject: [PATCH] Simple MVC CRUD. Remove NOT NULL on some attributes in Person --- DataAccess/EF_Demo/Models/Person.cs | 18 +- .../Events.EF/Data/MSSQL/EventsContext.cs | 143 +++++++++ .../Events.EF/Data/Postgres/EventsContext.cs | 165 ++++++++++ MVC-SimpleCRUD/Events.EF/Events.EF.csproj | 14 + MVC-SimpleCRUD/Events.EF/Models/Country.cs | 19 ++ MVC-SimpleCRUD/Events.EF/Models/Event.cs | 17 + MVC-SimpleCRUD/Events.EF/Models/Person.cs | 41 +++ .../Events.EF/Models/Registration.cs | 25 ++ MVC-SimpleCRUD/Events.EF/Models/Sport.cs | 15 + .../Events.EF/efpt.mssql.config.json | 70 ++++ .../Events.EF/efpt.postgres.config.json | 70 ++++ MVC-SimpleCRUD/MVC-SimpleCRUD.slnx | 4 + MVC-SimpleCRUD/MVC-SimpleCRUD/Constants.cs | 33 ++ .../Controllers/HomeController.cs | 11 + .../Controllers/PeopleController.cs | 302 ++++++++++++++++++ .../Controllers/PeopleSimpleController.cs | 38 +++ .../MVC-SimpleCRUD/MVC-SimpleCRUD.csproj | 29 ++ .../MVC-SimpleCRUD/Models/PagedList.cs | 3 + .../MVC-SimpleCRUD/Models/PagingInfo.cs | 37 +++ .../MVC-SimpleCRUD/Models/PagingSettings.cs | 10 + .../Models/People/PersonForm.cs | 77 +++++ .../Models/People/PersonInfo.cs | 30 ++ MVC-SimpleCRUD/MVC-SimpleCRUD/Program.cs | 59 ++++ .../Properties/launchSettings.json | 14 + .../Util/Extensions/StringExtensions.cs | 9 + .../Util/TagHelpers/PagerTagHelper.cs | 128 ++++++++ .../MVC-SimpleCRUD/Views/Home/Index.cshtml | 10 + .../MVC-SimpleCRUD/Views/People/Create.cshtml | 16 + .../MVC-SimpleCRUD/Views/People/Edit.cshtml | 16 + .../MVC-SimpleCRUD/Views/People/Index.cshtml | 138 ++++++++ .../Views/People/_PersonForm.cshtml | 101 ++++++ .../Views/People/_PersonRow.cshtml | 43 +++ .../Views/PeopleSimple/Index.cshtml | 44 +++ .../Views/Shared/_Layout.cshtml | 69 ++++ .../Shared/_ValidationScriptsPartial.cshtml | 2 + .../MVC-SimpleCRUD/Views/_ViewImports.cshtml | 4 + .../MVC-SimpleCRUD/Views/_ViewStart.cshtml | 3 + .../appsettings.Development.json | 8 + .../MVC-SimpleCRUD/appsettings.json | 17 + MVC-SimpleCRUD/MVC-SimpleCRUD/libman.json | 32 ++ .../MVC-SimpleCRUD/wwwroot/css/site.css | 16 + .../MVC-SimpleCRUD/wwwroot/js/pager.js | 48 +++ .../MVC-SimpleCRUD/wwwroot/js/site.js | 30 ++ .../PeopleDataGenerator/Program.cs | 5 +- .../mssql-eventsdb/init/02-schema.sql | 16 +- .../postgres-eventsdb/init/02-schema.sql | 16 +- 46 files changed, 1989 insertions(+), 26 deletions(-) create mode 100644 MVC-SimpleCRUD/Events.EF/Data/MSSQL/EventsContext.cs create mode 100644 MVC-SimpleCRUD/Events.EF/Data/Postgres/EventsContext.cs create mode 100644 MVC-SimpleCRUD/Events.EF/Events.EF.csproj create mode 100644 MVC-SimpleCRUD/Events.EF/Models/Country.cs create mode 100644 MVC-SimpleCRUD/Events.EF/Models/Event.cs create mode 100644 MVC-SimpleCRUD/Events.EF/Models/Person.cs create mode 100644 MVC-SimpleCRUD/Events.EF/Models/Registration.cs create mode 100644 MVC-SimpleCRUD/Events.EF/Models/Sport.cs create mode 100644 MVC-SimpleCRUD/Events.EF/efpt.mssql.config.json create mode 100644 MVC-SimpleCRUD/Events.EF/efpt.postgres.config.json create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD.slnx create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Constants.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/HomeController.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleController.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleSimpleController.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/MVC-SimpleCRUD.csproj create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagedList.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingInfo.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingSettings.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonForm.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonInfo.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Program.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Properties/launchSettings.json create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Util/Extensions/StringExtensions.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Util/TagHelpers/PagerTagHelper.cs create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Home/Index.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Create.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Edit.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Index.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonForm.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonRow.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/PeopleSimple/Index.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_Layout.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_ValidationScriptsPartial.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewImports.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewStart.cshtml create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.Development.json create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.json create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/libman.json create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/css/site.css create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/pager.js create mode 100644 MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/site.js diff --git a/DataAccess/EF_Demo/Models/Person.cs b/DataAccess/EF_Demo/Models/Person.cs index 266f448..515394e 100644 --- a/DataAccess/EF_Demo/Models/Person.cs +++ b/DataAccess/EF_Demo/Models/Person.cs @@ -9,25 +9,25 @@ public partial class Person { public int Id { get; set; } - public string FirstName { get; set; } = null!; + public string? FirstName { get; set; } - public string LastName { get; set; } = null!; + public string? LastName { get; set; } public string FirstNameTranscription { get; set; } = null!; public string LastNameTranscription { get; set; } = null!; - public string AddressLine { get; set; } = null!; + public string? AddressLine { get; set; } - public string PostalCode { get; set; } = null!; + public string? PostalCode { get; set; } - public string City { get; set; } = null!; + public string? City { get; set; } - public string AddressCountry { get; set; } = null!; + public string? AddressCountry { get; set; } - public string Email { get; set; } = null!; + public string? Email { get; set; } - public string ContactPhone { get; set; } = null!; + public string? ContactPhone { get; set; } public DateOnly BirthDate { get; set; } @@ -38,4 +38,4 @@ public partial class Person public virtual Country CountryCodeNavigation { get; set; } = null!; public virtual ICollection Registrations { get; set; } = new List(); -} \ No newline at end of file +} diff --git a/MVC-SimpleCRUD/Events.EF/Data/MSSQL/EventsContext.cs b/MVC-SimpleCRUD/Events.EF/Data/MSSQL/EventsContext.cs new file mode 100644 index 0000000..df5e7a7 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Data/MSSQL/EventsContext.cs @@ -0,0 +1,143 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; +using Events.EF.Models; +using Microsoft.EntityFrameworkCore; + +namespace Events.EF.Data.MSSQL; + +public partial class EventsContext : DbContext +{ + public EventsContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Countries { get; set; } + + public virtual DbSet Events { get; set; } + + public virtual DbSet People { get; set; } + + public virtual DbSet Registrations { get; set; } + + public virtual DbSet Sports { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Code); + + entity.ToTable("Country"); + + entity.HasIndex(e => e.Name, "UQ_Country_Name").IsUnique(); + + entity.Property(e => e.Code) + .HasMaxLength(3) + .IsUnicode(false); + entity.Property(e => e.Alpha3) + .HasMaxLength(3) + .IsUnicode(false) + .IsFixedLength(); + entity.Property(e => e.Name) + .HasMaxLength(100) + .IsUnicode(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Event"); + + entity.Property(e => e.Name) + .HasMaxLength(150) + .IsUnicode(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Person"); + + entity.HasIndex(e => new { e.DocumentNumber, e.CountryCode }, "UQ_Person_DocumentNumber_CountryCode").IsUnique(); + + entity.Property(e => e.AddressCountry) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.AddressLine) + .HasMaxLength(200) + .IsUnicode(false); + entity.Property(e => e.City) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.ContactPhone) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.CountryCode) + .HasMaxLength(3) + .IsUnicode(false); + entity.Property(e => e.DocumentNumber) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Email) + .HasMaxLength(255) + .IsUnicode(false); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.FirstNameTranscription) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.LastNameTranscription) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.PostalCode) + .HasMaxLength(20) + .IsUnicode(false); + + entity.HasOne(d => d.CountryCodeNavigation).WithMany(p => p.People) + .HasForeignKey(d => d.CountryCode) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_Person_Country"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Registration"); + + entity.HasIndex(e => new { e.PersonId, e.SportId, e.EventId }, "UQ_Registration_PersonId_SportId_EventId").IsUnique(); + + entity.Property(e => e.RegisteredAt).HasDefaultValueSql("(sysutcdatetime())", "DF_Registration_RegisteredAt"); + + entity.HasOne(d => d.Event).WithMany(p => p.Registrations) + .HasForeignKey(d => d.EventId) + .HasConstraintName("FK_Registration_Event"); + + entity.HasOne(d => d.Person).WithMany(p => p.Registrations) + .HasForeignKey(d => d.PersonId) + .HasConstraintName("FK_Registration_Person"); + + entity.HasOne(d => d.Sport).WithMany(p => p.Registrations) + .HasForeignKey(d => d.SportId) + .HasConstraintName("FK_Registration_Sport"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Sport"); + + entity.HasIndex(e => e.Name, "UQ_Sport_Name").IsUnique(); + + entity.Property(e => e.Name) + .HasMaxLength(100) + .IsUnicode(false); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/MVC-SimpleCRUD/Events.EF/Data/Postgres/EventsContext.cs b/MVC-SimpleCRUD/Events.EF/Data/Postgres/EventsContext.cs new file mode 100644 index 0000000..1e66779 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Data/Postgres/EventsContext.cs @@ -0,0 +1,165 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; +using Events.EF.Models; +using Microsoft.EntityFrameworkCore; + +namespace Events.EF.Data.Postgres; + +public partial class EventsContext : DbContext +{ + public EventsContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Countries { get; set; } + + public virtual DbSet Events { get; set; } + + public virtual DbSet People { get; set; } + + public virtual DbSet Registrations { get; set; } + + public virtual DbSet Sports { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Code).HasName("country_pkey"); + + entity.ToTable("country"); + + entity.HasIndex(e => e.Name, "country_name_key").IsUnique(); + + entity.Property(e => e.Code) + .HasMaxLength(3) + .HasColumnName("code"); + entity.Property(e => e.Alpha3) + .HasMaxLength(3) + .IsFixedLength() + .HasColumnName("alpha3"); + entity.Property(e => e.Name) + .HasMaxLength(100) + .HasColumnName("name"); + entity.Property(e => e.Translations) + .HasColumnType("jsonb") + .HasColumnName("translations"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("event_pkey"); + + entity.ToTable("event"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.EventDate).HasColumnName("event_date"); + entity.Property(e => e.Name) + .HasMaxLength(150) + .HasColumnName("name"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("person_pkey"); + + entity.ToTable("person"); + + entity.HasIndex(e => new { e.DocumentNumber, e.CountryCode }, "person_document_number_country_code_key").IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.AddressCountry) + .HasMaxLength(100) + .HasColumnName("address_country"); + entity.Property(e => e.AddressLine) + .HasMaxLength(200) + .HasColumnName("address_line"); + entity.Property(e => e.BirthDate).HasColumnName("birth_date"); + entity.Property(e => e.City) + .HasMaxLength(100) + .HasColumnName("city"); + entity.Property(e => e.ContactPhone) + .HasMaxLength(50) + .HasColumnName("contact_phone"); + entity.Property(e => e.CountryCode) + .HasMaxLength(3) + .HasColumnName("country_code"); + entity.Property(e => e.DocumentNumber) + .HasMaxLength(50) + .HasColumnName("document_number"); + entity.Property(e => e.Email) + .HasMaxLength(255) + .HasColumnName("email"); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .HasColumnName("first_name"); + entity.Property(e => e.FirstNameTranscription) + .HasMaxLength(100) + .HasColumnName("first_name_transcription"); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .HasColumnName("last_name"); + entity.Property(e => e.LastNameTranscription) + .HasMaxLength(100) + .HasColumnName("last_name_transcription"); + entity.Property(e => e.PostalCode) + .HasMaxLength(20) + .HasColumnName("postal_code"); + + entity.HasOne(d => d.CountryCodeNavigation).WithMany(p => p.People) + .HasForeignKey(d => d.CountryCode) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("person_country_code_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("registration_pkey"); + + entity.ToTable("registration"); + + entity.HasIndex(e => new { e.PersonId, e.SportId, e.EventId }, "registration_person_id_sport_id_event_id_key").IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.EventId).HasColumnName("event_id"); + entity.Property(e => e.PersonId).HasColumnName("person_id"); + entity.Property(e => e.RegisteredAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasColumnName("registered_at"); + entity.Property(e => e.SportId).HasColumnName("sport_id"); + + entity.HasOne(d => d.Event).WithMany(p => p.Registrations) + .HasForeignKey(d => d.EventId) + .HasConstraintName("registration_event_id_fkey"); + + entity.HasOne(d => d.Person).WithMany(p => p.Registrations) + .HasForeignKey(d => d.PersonId) + .HasConstraintName("registration_person_id_fkey"); + + entity.HasOne(d => d.Sport).WithMany(p => p.Registrations) + .HasForeignKey(d => d.SportId) + .HasConstraintName("registration_sport_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("sport_pkey"); + + entity.ToTable("sport"); + + entity.HasIndex(e => e.Name, "sport_name_key").IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Name) + .HasMaxLength(100) + .HasColumnName("name"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/Events.EF/Events.EF.csproj b/MVC-SimpleCRUD/Events.EF/Events.EF.csproj new file mode 100644 index 0000000..87692b8 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Events.EF.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/MVC-SimpleCRUD/Events.EF/Models/Country.cs b/MVC-SimpleCRUD/Events.EF/Models/Country.cs new file mode 100644 index 0000000..3203a07 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Models/Country.cs @@ -0,0 +1,19 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace Events.EF.Models; + +public partial class Country +{ + public string Code { get; set; } = null!; + + public string Alpha3 { get; set; } = null!; + + public string Name { get; set; } = null!; + + public string? Translations { get; set; } + + public virtual ICollection People { get; set; } = new List(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/Events.EF/Models/Event.cs b/MVC-SimpleCRUD/Events.EF/Models/Event.cs new file mode 100644 index 0000000..13b99f6 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Models/Event.cs @@ -0,0 +1,17 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace Events.EF.Models; + +public partial class Event +{ + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public DateOnly EventDate { get; set; } + + public virtual ICollection Registrations { get; set; } = new List(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/Events.EF/Models/Person.cs b/MVC-SimpleCRUD/Events.EF/Models/Person.cs new file mode 100644 index 0000000..c6df0ab --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Models/Person.cs @@ -0,0 +1,41 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace Events.EF.Models; + +public partial class Person +{ + public int Id { get; set; } + + public string? FirstName { get; set; } + + public string? LastName { get; set; } + + public string FirstNameTranscription { get; set; } = null!; + + public string LastNameTranscription { get; set; } = null!; + + public string? AddressLine { get; set; } + + public string? PostalCode { get; set; } + + public string? City { get; set; } + + public string? AddressCountry { get; set; } + + public string? Email { get; set; } + + public string? ContactPhone { get; set; } + + public DateOnly BirthDate { get; set; } + + public string DocumentNumber { get; set; } = null!; + + public string CountryCode { get; set; } = null!; + + public virtual Country CountryCodeNavigation { get; set; } = null!; + + public virtual ICollection Registrations { get; set; } = new List(); +} diff --git a/MVC-SimpleCRUD/Events.EF/Models/Registration.cs b/MVC-SimpleCRUD/Events.EF/Models/Registration.cs new file mode 100644 index 0000000..460d2b1 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Models/Registration.cs @@ -0,0 +1,25 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace Events.EF.Models; + +public partial class Registration +{ + public int Id { get; set; } + + public int PersonId { get; set; } + + public int SportId { get; set; } + + public int EventId { get; set; } + + public DateTime RegisteredAt { get; set; } + + public virtual Event Event { get; set; } = null!; + + public virtual Person Person { get; set; } = null!; + + public virtual Sport Sport { get; set; } = null!; +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/Events.EF/Models/Sport.cs b/MVC-SimpleCRUD/Events.EF/Models/Sport.cs new file mode 100644 index 0000000..8b126d6 --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/Models/Sport.cs @@ -0,0 +1,15 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable enable +using System; +using System.Collections.Generic; + +namespace Events.EF.Models; + +public partial class Sport +{ + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public virtual ICollection Registrations { get; set; } = new List(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/Events.EF/efpt.mssql.config.json b/MVC-SimpleCRUD/Events.EF/efpt.mssql.config.json new file mode 100644 index 0000000..6df4a7a --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/efpt.mssql.config.json @@ -0,0 +1,70 @@ +{ + "CodeGenerationMode": 6, + "ContextClassName": "EventsContext", + "ContextNamespace": null, + "FilterSchemas": false, + "IncludeConnectionString": false, + "IrregularWords": null, + "MinimumProductVersion": "2.6.1465", + "ModelNamespace": null, + "OutputContextPath": "Data\/MSSQL", + "OutputPath": "Models", + "PluralRules": null, + "PreserveCasingWithRegex": true, + "ProjectRootNamespace": "Events.EF", + "Schemas": null, + "SelectedHandlebarsLanguage": 2, + "SelectedToBeGenerated": 0, + "SingularRules": null, + "T4TemplatePath": null, + "Tables": [ + { + "Name": "[dbo].[Country]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Event]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Person]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Registration]", + "ObjectType": 0 + }, + { + "Name": "[dbo].[Sport]", + "ObjectType": 0 + } + ], + "UiHint": null, + "UncountableWords": null, + "UseAsyncStoredProcedureCalls": true, + "UseBoolPropertiesWithoutDefaultSql": false, + "UseDatabaseNames": false, + "UseDatabaseNamesForRoutines": true, + "UseDateOnlyTimeOnly": true, + "UseDbContextSplitting": false, + "UseDecimalDataAnnotationForSprocResult": true, + "UseFluentApiOnly": true, + "UseHandleBars": false, + "UseHierarchyId": false, + "UseInflector": true, + "UseInternalAccessModifiersForSprocsAndFunctions": false, + "UseLegacyPluralizer": false, + "UseManyToManyEntity": false, + "UseNoDefaultConstructor": true, + "UseNoNavigations": false, + "UseNoObjectFilter": false, + "UseNodaTime": false, + "UseNullableReferences": true, + "UsePrefixNavigationNaming": false, + "UseSchemaFolders": false, + "UseSchemaNamespaces": false, + "UseSpatial": false, + "UseT4": false, + "UseT4Split": false, + "UseTypedTvpParameters": true +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/Events.EF/efpt.postgres.config.json b/MVC-SimpleCRUD/Events.EF/efpt.postgres.config.json new file mode 100644 index 0000000..38b602d --- /dev/null +++ b/MVC-SimpleCRUD/Events.EF/efpt.postgres.config.json @@ -0,0 +1,70 @@ +{ + "CodeGenerationMode": 6, + "ContextClassName": "EventsContext", + "ContextNamespace": null, + "FilterSchemas": false, + "IncludeConnectionString": false, + "IrregularWords": null, + "MinimumProductVersion": "2.6.1465", + "ModelNamespace": null, + "OutputContextPath": "Data\/Postgres", + "OutputPath": "Models", + "PluralRules": null, + "PreserveCasingWithRegex": true, + "ProjectRootNamespace": "Events.EF", + "Schemas": null, + "SelectedHandlebarsLanguage": 2, + "SelectedToBeGenerated": 0, + "SingularRules": null, + "T4TemplatePath": null, + "Tables": [ + { + "Name": "public.country", + "ObjectType": 0 + }, + { + "Name": "public.event", + "ObjectType": 0 + }, + { + "Name": "public.person", + "ObjectType": 0 + }, + { + "Name": "public.registration", + "ObjectType": 0 + }, + { + "Name": "public.sport", + "ObjectType": 0 + } + ], + "UiHint": null, + "UncountableWords": null, + "UseAsyncStoredProcedureCalls": true, + "UseBoolPropertiesWithoutDefaultSql": false, + "UseDatabaseNames": false, + "UseDatabaseNamesForRoutines": true, + "UseDateOnlyTimeOnly": true, + "UseDbContextSplitting": false, + "UseDecimalDataAnnotationForSprocResult": true, + "UseFluentApiOnly": true, + "UseHandleBars": false, + "UseHierarchyId": false, + "UseInflector": true, + "UseInternalAccessModifiersForSprocsAndFunctions": false, + "UseLegacyPluralizer": false, + "UseManyToManyEntity": false, + "UseNoDefaultConstructor": true, + "UseNoNavigations": false, + "UseNoObjectFilter": false, + "UseNodaTime": false, + "UseNullableReferences": true, + "UsePrefixNavigationNaming": false, + "UseSchemaFolders": false, + "UseSchemaNamespaces": false, + "UseSpatial": false, + "UseT4": false, + "UseT4Split": false, + "UseTypedTvpParameters": true +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD.slnx b/MVC-SimpleCRUD/MVC-SimpleCRUD.slnx new file mode 100644 index 0000000..244ea5f --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Constants.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Constants.cs new file mode 100644 index 0000000..757aaef --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Constants.cs @@ -0,0 +1,33 @@ +namespace MVC_SimpleCRUD; + +public static class Constants +{ + public static class TempDataKeys + { + public const string ToastMessage = "ToastMessage"; + public const string ToastVariant = "ToastVariant"; + public const string ToastTitle = "ToastTitle"; + } + + public static class ToastVariants + { + public const string Success = "success"; + public const string Danger = "danger"; + } + + public static class ToastTitles + { + public const string Notification = "Notification"; + public const string Error = "Error"; + } + + public static class ViewDataKeys + { + public const string PagingInfo = "PagingInfo"; + } + + public static class ValidationMessages + { + public const string EmailOrContactPhoneRequired = "Email or contact phone is required."; + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/HomeController.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/HomeController.cs new file mode 100644 index 0000000..4db7154 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace MVC_SimpleCRUD.Controllers; + +public class HomeController : Controller +{ + public IActionResult Index() + { + return View(); + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleController.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleController.cs new file mode 100644 index 0000000..6e8f04c --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleController.cs @@ -0,0 +1,302 @@ +#if POSTGRES +using Events.EF.Data.Postgres; +#else +using Events.EF.Data.MSSQL; +#endif +using Events.EF.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using MVC_SimpleCRUD.Models; +using MVC_SimpleCRUD.People; +using MVC_SimpleCRUD.Util.Extensions; +using Sieve.Models; +using Sieve.Services; + +namespace MVC_SimpleCRUD.Controllers; + +public class PeopleController : Controller +{ + private readonly EventsContext ctx; + private readonly ISieveProcessor sieveProcessor; + private readonly PagingSettings pagingSettings; + + public PeopleController(EventsContext ctx, IOptionsSnapshot pagingSettings, ISieveProcessor sieveProcessor) + { + this.ctx = ctx; + this.sieveProcessor = sieveProcessor; + this.pagingSettings = pagingSettings.Value; + } + + public async Task Index(int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + if (page < 1) + { + page = 1; + } + + if (pageSize <= 0) + { + pageSize = pagingSettings.PageSize; + } + + sorts = sorts.NullIfWhiteSpace() ?? nameof(PersonInfo.LastNameTranscription); + + var sieveModel = new SieveModel + { + Page = page, + PageSize = pageSize, + Sorts = sorts + }; + + int totalCount = await ctx.People.CountAsync(); + + var query = ctx.People + .Select(p => new PersonInfo + { + Id = p.Id, + FirstName = p.FirstName, + LastName = p.LastName, + FirstNameTranscription = p.FirstNameTranscription, + LastNameTranscription = p.LastNameTranscription, + BirthDate = p.BirthDate, + CountryName = p.CountryCodeNavigation.Name, + }); + + int filteredCount; + searchText = searchText.NullIfWhiteSpace(); + if (searchText is not null) + { + sieveModel.Filters = $"({nameof(PersonInfo.FirstNameTranscription)}|{nameof(PersonInfo.LastNameTranscription)}|{nameof(PersonInfo.CountryName)})@=*{searchText}"; + filteredCount = await sieveProcessor.Apply(sieveModel, query, applyPagination: false, applySorting: false) + .CountAsync(); + } + else + { + filteredCount = totalCount; + } + + var pagingInfo = new PagingInfo + { + FilteredItemsCount = filteredCount, + TotalItemsCount = totalCount, + ItemsPerPage = pageSize, + CurrentPage = page, + Sorts = sorts, + SearchText = searchText + }; + + if (pagingInfo.CurrentPage > pagingInfo.TotalPages) + { + page = pagingInfo.CurrentPage = pagingInfo.TotalPages; + sieveModel.Page = page; + } + + if (filteredCount > 0) + { + query = sieveProcessor.Apply(sieveModel, query); + var people = await query.ToListAsync(); + var model = new PagedList(people, pagingInfo); + return View(model); + } + else + { + return View(new PagedList(new(), pagingInfo)); + } + } + + [HttpGet] + public async Task Create(int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + var model = new PersonForm + { + Page = page, + PageSize = pageSize, + Sorts = sorts, + SearchText = searchText + }; + + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PersonForm model) + { + if (!ModelState.IsValid) + { + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + var person = new Person(); + CopyToPerson(model, person); + ctx.People.Add(person); + + try + { + await ctx.SaveChangesAsync(); + ShowToast($"Person {person.FirstNameTranscription} {person.LastNameTranscription} added."); + return RedirectToIndex(model); + } + catch (DbUpdateException exc) + { + ModelState.AddModelError(string.Empty, exc.InnerException?.Message ?? exc.Message); + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + } + + [HttpGet] + public async Task Edit(int id, int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + var person = await ctx.People.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id); + if (person is null) + { + ShowToast($"Person with id {id} was not found.", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + return RedirectToAction(nameof(Index), new { page, pageSize, sorts, searchText }); + } + + var model = CreateForm(person, page, pageSize, sorts, searchText); + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, PersonForm model) + { + if (model.Id != id) + { + return BadRequest(); + } + + if (!ModelState.IsValid) + { + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + + var person = await ctx.People.FindAsync(id); + if (person is null) + { + ShowToast($"Person with id {id} was not found.", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + return RedirectToIndex(model); + } + + CopyToPerson(model, person); + + try + { + await ctx.SaveChangesAsync(); + ShowToast($"Person {person.FirstNameTranscription} {person.LastNameTranscription} updated."); + return RedirectToIndex(model); + } + catch (DbUpdateException exc) + { + ModelState.AddModelError(string.Empty, exc.InnerException?.Message ?? exc.Message); + await PrepareDropDownLists(model.CountryCode); + return View(model); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(int id, int page = 1, int pageSize = 0, string? sorts = null, string? searchText = null) + { + var person = await ctx.People.FindAsync(id); + if (person is null) + { + ShowToast($"Person with id {id} was not found.", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + return RedirectToAction(nameof(Index), new { page, pageSize, sorts, searchText }); + } + + string personName = $"{person.FirstNameTranscription} {person.LastNameTranscription}"; + ctx.People.Remove(person); + + try + { + await ctx.SaveChangesAsync(); + ShowToast($"Person {personName} deleted."); + } + catch (DbUpdateException exc) + { + ShowToast($"Person {personName} could not be deleted. {exc.InnerException?.Message ?? exc.Message}", Constants.ToastVariants.Danger, Constants.ToastTitles.Error); + } + + return RedirectToAction(nameof(Index), new { page, pageSize, sorts, searchText }); + } + + private async Task PrepareDropDownLists(string? selectedCountryCode = null) + { + var countries = await ctx.Countries + .AsNoTracking() + .OrderBy(d => d.Name) + .Select(d => new { d.Name, d.Code }) + .ToListAsync(); + + ViewBag.Countries = new SelectList(countries, nameof(Country.Code), nameof(Country.Name), selectedCountryCode); + } + + private static PersonForm CreateForm(Person person, int page, int pageSize, string? sorts, string? searchText) + { + return new PersonForm + { + Id = person.Id, + FirstName = person.FirstName, + LastName = person.LastName, + FirstNameTranscription = person.FirstNameTranscription, + LastNameTranscription = person.LastNameTranscription, + AddressLine = person.AddressLine, + PostalCode = person.PostalCode, + City = person.City, + AddressCountry = person.AddressCountry, + Email = person.Email, + ContactPhone = person.ContactPhone, + BirthDate = person.BirthDate, + DocumentNumber = person.DocumentNumber, + CountryCode = person.CountryCode, + Page = page, + PageSize = pageSize, + Sorts = sorts, + SearchText = searchText + }; + } + + private static void CopyToPerson(PersonForm model, Person person) + { + person.FirstName = model.FirstName.NullIfWhiteSpace(); + person.LastName = model.LastName.NullIfWhiteSpace(); + person.FirstNameTranscription = model.FirstNameTranscription; + person.LastNameTranscription = model.LastNameTranscription; + person.AddressLine = model.AddressLine.NullIfWhiteSpace(); + person.PostalCode = model.PostalCode.NullIfWhiteSpace(); + person.City = model.City.NullIfWhiteSpace(); + person.AddressCountry = model.AddressCountry.NullIfWhiteSpace(); + person.Email = model.Email.NullIfWhiteSpace(); + person.ContactPhone = model.ContactPhone.NullIfWhiteSpace(); + person.BirthDate = model.BirthDate!.Value; + person.DocumentNumber = model.DocumentNumber; + person.CountryCode = model.CountryCode; + } + + private RedirectToActionResult RedirectToIndex(PersonForm model) + { + return RedirectToAction(nameof(Index), new + { + page = model.Page, + pageSize = model.PageSize, + sorts = model.Sorts, + searchText = model.SearchText + }); + } + + private void ShowToast(string message, string variant = Constants.ToastVariants.Success, string title = Constants.ToastTitles.Notification) + { + TempData[Constants.TempDataKeys.ToastMessage] = message; + TempData[Constants.TempDataKeys.ToastVariant] = variant; + TempData[Constants.TempDataKeys.ToastTitle] = title; + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleSimpleController.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleSimpleController.cs new file mode 100644 index 0000000..da29e0c --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Controllers/PeopleSimpleController.cs @@ -0,0 +1,38 @@ +#if POSTGRES +using Events.EF.Data.Postgres; +#else +using Events.EF.Data.MSSQL; +#endif +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MVC_SimpleCRUD.People; + +namespace MVC_SimpleCRUD.Controllers; + +public class PeopleSimpleController : Controller +{ + private readonly EventsContext ctx; + + public PeopleSimpleController(EventsContext ctx) + { + this.ctx = ctx; + } + + public IActionResult Index() + { + var people = ctx.People + .OrderBy(p => p.LastNameTranscription) + .Select(p => new PersonInfo + { + Id = p.Id, + FirstName = p.FirstName, + LastName = p.LastName, + FirstNameTranscription = p.FirstNameTranscription, + LastNameTranscription = p.LastNameTranscription, + BirthDate = p.BirthDate, + CountryName = p.CountryCodeNavigation.Name, + }) + .ToList(); + return View(people); + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/MVC-SimpleCRUD.csproj b/MVC-SimpleCRUD/MVC-SimpleCRUD/MVC-SimpleCRUD.csproj new file mode 100644 index 0000000..5213e49 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/MVC-SimpleCRUD.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + MVC_SimpleCRUD + + + + PI + + + + $(DefineConstants);MSSQL + + + + + + + + + + + + + + diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagedList.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagedList.cs new file mode 100644 index 0000000..408c246 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagedList.cs @@ -0,0 +1,3 @@ +namespace MVC_SimpleCRUD.Models; + +public record PagedList(List Data, PagingInfo PagingInfo); diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingInfo.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingInfo.cs new file mode 100644 index 0000000..bdb16cb --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingInfo.cs @@ -0,0 +1,37 @@ +namespace MVC_SimpleCRUD.Models; + +public class PagingInfo +{ + public int TotalItemsCount { get; set; } + + public int FilteredItemsCount { get; set; } + + public int ItemsPerPage { get; set; } + + public int CurrentPage { get; set; } + + public string? Sorts { get; set; } + + public string? SearchText { get; set; } + + public int TotalPages => Math.Max(1, (int)Math.Ceiling((decimal)FilteredItemsCount / ItemsPerPage)); + + public bool IsFiltered => !string.IsNullOrWhiteSpace(SearchText); + + public string ToggleSort(string propertyName) + { + return string.Equals(Sorts, propertyName, StringComparison.OrdinalIgnoreCase) + ? $"-{propertyName}" + : propertyName; + } + + public bool IsSortedBy(string propertyName) + { + return string.Equals(Sorts?.TrimStart('-'), propertyName, StringComparison.OrdinalIgnoreCase); + } + + public bool IsDescending() + { + return Sorts?.StartsWith("-", StringComparison.Ordinal) == true; + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingSettings.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingSettings.cs new file mode 100644 index 0000000..af938b1 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/PagingSettings.cs @@ -0,0 +1,10 @@ +namespace MVC_SimpleCRUD.Models; + +public class PagingSettings +{ + public const string SectionName = "Paging"; + + public int PageSize { get; set; } = 20; + + public int PageOffset { get; set; } = 5; +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonForm.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonForm.cs new file mode 100644 index 0000000..53f45be --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonForm.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.DataAnnotations; +using MVC_SimpleCRUD; + +namespace MVC_SimpleCRUD.People; + +public class PersonForm : IValidatableObject +{ + public int? Id { get; set; } + + [StringLength(100)] + [Display(Name = "First name (native)")] + public string? FirstName { get; set; } + + [StringLength(100)] + [Display(Name = "Last name (native)")] + public string? LastName { get; set; } + + [Required, StringLength(100)] + [Display(Name = "First name (English)")] + public string FirstNameTranscription { get; set; } = string.Empty; + + [Required, StringLength(100)] + [Display(Name = "Last name (English)")] + public string LastNameTranscription { get; set; } = string.Empty; + + [StringLength(200)] + [Display(Name = "Address")] + public string? AddressLine { get; set; } + + [StringLength(20)] + [Display(Name = "Postal Code")] + public string? PostalCode { get; set; } + + [StringLength(100)] + public string? City { get; set; } + + [StringLength(100)] + [Display(Name = "Address Country")] + public string? AddressCountry { get; set; } + + [StringLength(255), EmailAddress] + public string? Email { get; set; } + + [StringLength(50), Phone] + [Display(Name = "Contact Phone")] + public string? ContactPhone { get; set; } + + [Required] + [Display(Name = "Birth Date")] + public DateOnly? BirthDate { get; set; } + + [Required, StringLength(50)] + [Display(Name = "Document Number")] + public string DocumentNumber { get; set; } = string.Empty; + + [Required, StringLength(3)] + [Display(Name = "Country")] + public string CountryCode { get; set; } = string.Empty; + + public int Page { get; set; } = 1; + + public int PageSize { get; set; } + + public string? Sorts { get; set; } + + public string? SearchText { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(ContactPhone)) + { + yield return new ValidationResult( + Constants.ValidationMessages.EmailOrContactPhoneRequired, + new[] { nameof(Email), nameof(ContactPhone) }); + } + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonInfo.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonInfo.cs new file mode 100644 index 0000000..a978a08 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Models/People/PersonInfo.cs @@ -0,0 +1,30 @@ +using Sieve.Attributes; + +namespace MVC_SimpleCRUD.People; + +public class PersonInfo +{ + [Sieve(CanFilter = true, CanSort = true)] + public int Id { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public string? FirstName { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public string? LastName { get; set; } + + public string OriginalName => ((FirstName ?? string.Empty) + " " + (LastName ?? string.Empty)).Trim(); + + [Sieve(CanFilter = true, CanSort = true)] + public required string FirstNameTranscription { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public required string LastNameTranscription { get; set; } + + + [Sieve(CanFilter = true, CanSort = true)] + public DateOnly BirthDate { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public required string CountryName { get; set; } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Program.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Program.cs new file mode 100644 index 0000000..0834502 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Program.cs @@ -0,0 +1,59 @@ +#if POSTGRES +using Events.EF.Data.Postgres; +#else +using Events.EF.Data.MSSQL; +#endif +using Microsoft.EntityFrameworkCore; +using MVC_SimpleCRUD.Models; +using NLog; +using NLog.Web; +using Sieve.Services; + +var logger = NLog.LogManager.Setup().GetCurrentClassLogger(); +logger.Debug("init main"); +try +{ + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseNLog(new NLogAspNetCoreOptions() { RemoveLoggerFactoryFilter = false }); + + #region Configure services + builder.Services.AddControllersWithViews(); + builder.Services.Configure(builder.Configuration.GetSection(PagingSettings.SectionName)); +#if POSTGRES + builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("EventsPostgres"))); +#else + builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("EventsMssql"))); +#endif + builder.Services.AddScoped(); + #endregion + + var app = builder.Build(); + + #region configure middleware pipeline + //middleware order https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/#middleware-order + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseStaticFiles(); + + app.UseRouting(); + + app.MapDefaultControllerRoute(); + + #endregion + app.Run(); +} +catch (Exception exception) +{ + // NLog: catch setup errors + logger.Error(exception, "Stopped program because of exception"); + throw; +} +finally +{ + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + NLog.LogManager.Shutdown(); +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Properties/launchSettings.json b/MVC-SimpleCRUD/MVC-SimpleCRUD/Properties/launchSettings.json new file mode 100644 index 0000000..9ab727c --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Events.MVC": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7210", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Util/Extensions/StringExtensions.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Util/Extensions/StringExtensions.cs new file mode 100644 index 0000000..4930235 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Util/Extensions/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace MVC_SimpleCRUD.Util.Extensions; + +public static class StringExtensions +{ + public static string? NullIfWhiteSpace(this string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Util/TagHelpers/PagerTagHelper.cs b/MVC-SimpleCRUD/MVC-SimpleCRUD/Util/TagHelpers/PagerTagHelper.cs new file mode 100644 index 0000000..3a51c40 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Util/TagHelpers/PagerTagHelper.cs @@ -0,0 +1,128 @@ + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; +using MVC_SimpleCRUD.Models; + +namespace MVC_SimpleCRUD.Util.TagHelpers; + +[HtmlTargetElement("pager", Attributes = "page-info,page-action")] +public class PagerTagHelper : TagHelper +{ + private readonly IUrlHelperFactory urlHelperFactory; + private readonly PagingSettings pagingSettings; + + public PagerTagHelper(IUrlHelperFactory urlHelperFactory, IOptions pagingSettings) + { + this.urlHelperFactory = urlHelperFactory; + this.pagingSettings = pagingSettings.Value; + } + + [ViewContext] + [HtmlAttributeNotBound] + public ViewContext ViewContext { get; set; } = null!; + + public PagingInfo PageInfo { get; set; } = new(); + + public string PageAction { get; set; } = string.Empty; + + public string PageTitle { get; set; } = "Enter page number"; + + [HtmlAttributeName(DictionaryAttributePrefix = "page-route-")] + public Dictionary PageRouteValues { get; set; } = []; + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "nav"; + output.Attributes.SetAttribute("aria-label", "Pager"); + + var paginationList = new TagBuilder("ul"); + paginationList.AddCssClass("pagination"); + paginationList.AddCssClass("mb-0"); + + var firstPageInRange = Math.Max(1, PageInfo.CurrentPage - pagingSettings.PageOffset); + var lastPageInRange = Math.Min(PageInfo.TotalPages, PageInfo.CurrentPage + pagingSettings.PageOffset); + + if (firstPageInRange > 1) + { + paginationList.InnerHtml.AppendHtml(BuildListItemForPage(1, "1..")); + } + + for (var page = firstPageInRange; page <= lastPageInRange; page++) + { + paginationList.InnerHtml.AppendHtml( + page == PageInfo.CurrentPage + ? BuildListItemForCurrentPage(page) + : BuildListItemForPage(page)); + } + + if (lastPageInRange < PageInfo.TotalPages) + { + paginationList.InnerHtml.AppendHtml(BuildListItemForPage(PageInfo.TotalPages, $"..{PageInfo.TotalPages}")); + } + + output.Content.AppendHtml(paginationList); + } + + private TagBuilder BuildListItemForPage(int page) + { + return BuildListItemForPage(page, page.ToString()); + } + + private TagBuilder BuildListItemForPage(int page, string text) + { + var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); + var url = urlHelper.Action(PageAction, BuildRouteValues(page)) ?? string.Empty; + + var anchor = new TagBuilder("a"); + anchor.InnerHtml.Append(text); + anchor.Attributes["href"] = url; + anchor.AddCssClass("page-link"); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("page-item"); + listItem.InnerHtml.AppendHtml(anchor); + return listItem; + } + + private TagBuilder BuildListItemForCurrentPage(int page) + { + var urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); + var urlTemplate = urlHelper.Action(PageAction, BuildRouteValues("__page__")) ?? string.Empty; + + var input = new TagBuilder("input"); + input.Attributes["type"] = "text"; + input.Attributes["value"] = page.ToString(); + input.Attributes["data-current"] = page.ToString(); + input.Attributes["data-min"] = "1"; + input.Attributes["data-max"] = PageInfo.TotalPages.ToString(); + input.Attributes["data-url-template"] = urlTemplate; + input.Attributes["title"] = PageTitle; + input.AddCssClass("page-link"); + input.AddCssClass("pagebox"); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("page-item"); + listItem.AddCssClass("active"); + listItem.InnerHtml.AppendHtml(input); + + return listItem; + } + + private RouteValueDictionary BuildRouteValues(object pageValue) + { + var routeValues = new RouteValueDictionary(PageRouteValues.ToDictionary(kvp => kvp.Key, kvp => (object?)kvp.Value)); + routeValues["page"] = pageValue; + routeValues["pageSize"] = PageInfo.ItemsPerPage; + routeValues["sorts"] = PageInfo.Sorts; + if (!string.IsNullOrWhiteSpace(PageInfo.SearchText)) + { + routeValues["searchText"] = PageInfo.SearchText; + } + + return routeValues; + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Home/Index.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Home/Index.cshtml new file mode 100644 index 0000000..5abaa28 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "Home"; +} + +
+
+

Events

+

This sample demonstrates how to create a simple CRUD ASP.NET Core MVC application using data from Person table

+
+
diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Create.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Create.cshtml new file mode 100644 index 0000000..eb31971 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Create.cshtml @@ -0,0 +1,16 @@ +@using MVC_SimpleCRUD.People +@model PersonForm +@{ + ViewData["Title"] = "Add person"; + ViewData["FormAction"] = "Create"; +} + +
+

Add person

+
+ + + +@section Scripts { + +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Edit.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Edit.cshtml new file mode 100644 index 0000000..0da67bb --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Edit.cshtml @@ -0,0 +1,16 @@ +@using MVC_SimpleCRUD.People +@model PersonForm +@{ + ViewData["Title"] = "Edit person"; + ViewData["FormAction"] = "Edit"; +} + +
+

Edit person

+
+ + + +@section Scripts { + +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Index.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Index.cshtml new file mode 100644 index 0000000..379be7c --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/Index.cshtml @@ -0,0 +1,138 @@ +@using MVC_SimpleCRUD.People +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@model PagedList +@{ + ViewData["Title"] = "People"; +} +

People list

+ +@{ + (string PropertyName, string Title, bool Sortable)[] columns = + [ + (nameof(PersonInfo.FirstNameTranscription), "First Name", true), + (nameof(PersonInfo.LastNameTranscription), "Last Name", true), + (nameof(PersonInfo.OriginalName), "Original Name", false), + (nameof(PersonInfo.BirthDate), "Birth Date", true), + (nameof(PersonInfo.CountryName), "Country Name", true) + ]; + + var personRowViewData = new ViewDataDictionary(ViewData) + { + { Constants.ViewDataKeys.PagingInfo, Model.PagingInfo } + }; +} + + +
+
+
+ + + + +
+

People list

+ @(Model.PagingInfo.IsFiltered ? $"{Model.PagingInfo.FilteredItemsCount} / {Model.PagingInfo.TotalItemsCount}" : Model.PagingInfo.TotalItemsCount.ToString()) + + Add + +
+ +
+ + + + @if (Model.PagingInfo.IsFiltered) + { + + Clear + + } +
+
+ +
+ + + + @foreach (var column in columns) + { + + } + + + + + @if (Model.Data.Count == 0) + { + + + + } + else + { + @foreach (var person in Model.Data) + { + + } + } + +
+ @if (column.Sortable) + { + + @column.Title@(Model.PagingInfo.IsSortedBy(column.PropertyName) ? (Model.PagingInfo.IsDescending() ? " ↓" : " ↑") : "") + + } + else + { + @column.Title + } +
No data to display.
+
+ +
+
+ Page @Model.PagingInfo.CurrentPage of @Model.PagingInfo.TotalPages +
+ + + + +
+
+ + +
+
+
diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonForm.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonForm.cshtml new file mode 100644 index 0000000..883d756 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonForm.cshtml @@ -0,0 +1,101 @@ +@using Microsoft.AspNetCore.Mvc.Rendering +@using MVC_SimpleCRUD.People +@model PersonForm + +@{ + var formAction = ViewData["FormAction"]?.ToString() ?? "Create"; + var countries = ViewBag.Countries as SelectList ?? new SelectList(Array.Empty()); +} + +
+
+
+ + + + + + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonRow.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonRow.cshtml new file mode 100644 index 0000000..caa569f --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/People/_PersonRow.cshtml @@ -0,0 +1,43 @@ +@using MVC_SimpleCRUD.People +@model PersonInfo +@{ + var pagingInfo = ViewData[Constants.ViewDataKeys.PagingInfo] as PagingInfo ?? new PagingInfo(); +} + + + @Model.FirstNameTranscription + @Model.LastNameTranscription + @Model.OriginalName + @Model.BirthDate.ToString("dd.MM.yyyy.") + @Model.CountryName + +
+ + Edit + + +
+ + + + + +
+
+ + diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/PeopleSimple/Index.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/PeopleSimple/Index.cshtml new file mode 100644 index 0000000..5468a60 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/PeopleSimple/Index.cshtml @@ -0,0 +1,44 @@ +@using MVC_SimpleCRUD.People +@model IEnumerable +@{ + ViewData["Title"] = "People"; +} +

People list

+ + + + + + + + + + + + + @foreach (var person in Model) + { + + + + + + + + } + +
First nameLast nameOriginal nameBirth dateCountry
@person.FirstNameTranscription@person.LastNameTranscription@person.OriginalName@person.BirthDate.ToString("yyy-MM-dd")@person.CountryName
+ +@section styles{ + +} + +@section scripts{ + + + +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_Layout.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..5c6a9a8 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_Layout.cshtml @@ -0,0 +1,69 @@ +@using System.Text.Json + + + + + + @ViewData["Title"] - Events + + + @RenderSection("Styles", required: false) + + +
+ +
+ +
+ @RenderBody() +
+ +
+
+
+ Notification + +
+
+
+
+ + + + + + + @RenderSection("Scripts", required: false) + + diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_ValidationScriptsPartial.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..d72a309 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewImports.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewImports.cshtml new file mode 100644 index 0000000..1c4483f --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using MVC_SimpleCRUD.Models +@using MVC_SimpleCRUD +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, MVC-SimpleCRUD diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewStart.cshtml b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.Development.json b/MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.json b/MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.json new file mode 100644 index 0000000..2168225 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Paging": { + "PageSize": 10, + "PageOffset": 5 + }, + "ConnectionStrings": { + "EventsMssql": "Data Source=.,3030;Initial Catalog=Events;User Id=************;Password=**********;TrustServerCertificate=True", + "EventsPostgres": "Host=localhost;Port=5432;Database=events;Username=***********;Password=**************;Persist Security Info=True" + } +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/libman.json b/MVC-SimpleCRUD/MVC-SimpleCRUD/libman.json new file mode 100644 index 0000000..b5acc37 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/libman.json @@ -0,0 +1,32 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.8", + "destination": "wwwroot/lib/bootstrap/", + "files": [ + "css/bootstrap.min.css", + "js/bootstrap.bundle.min.js" + ] + }, + { + "library": "jquery@3.7.1", + "destination": "wwwroot/lib/jquery/" + }, + { + "library": "jquery-validate@1.21.0", + "destination": "wwwroot/lib/jquery-validate/", + "files": [ + "jquery.validate.min.js" + ] + }, + { + "library": "jquery-validation-unobtrusive@4.0.0", + "destination": "wwwroot/lib/jquery-validation-unobtrusive/", + "files": [ + "jquery.validate.unobtrusive.min.js" + ] + } + ] +} diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/css/site.css b/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/css/site.css new file mode 100644 index 0000000..ccce651 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/css/site.css @@ -0,0 +1,16 @@ +main { + padding-left: 50px; + padding-right: 50px; +} + +.validation-summary-errors { font-weight: bold; color:#a94442;} +.validation-summary-valid { display: none;} +.input-validation-error{ + border-color:red; +} +.pagebox { + width: 45px; +} +.pagebox::selection{ + background-color:grey; +} \ No newline at end of file diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/pager.js b/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/pager.js new file mode 100644 index 0000000..bc89b89 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/pager.js @@ -0,0 +1,48 @@ +(function () { + function validRange(value, min, max) { + if (!/^\d+$/.test(value)) { + return false; + } + + var page = parseInt(value, 10); + return page >= min && page <= max; + } + + function goToPage(input) { + var value = input.value.trim(); + var min = parseInt(input.dataset.min, 10); + var max = parseInt(input.dataset.max, 10); + + if (!validRange(value, min, max)) { + input.value = input.dataset.current || ""; + return; + } + + var url = (input.dataset.urlTemplate || "").replace("__page__", value); + if (!url) { + return; + } + + window.location.href = url; + } + + document.addEventListener("focusin", function (event) { + if (event.target.matches(".pagebox")) { + event.target.select(); + } + }); + + document.addEventListener("keydown", function (event) { + if (!event.target.matches(".pagebox")) { + return; + } + + if (event.key === "Enter") { + event.preventDefault(); + goToPage(event.target); + } + else if (event.key === "Escape") { + event.target.value = event.target.dataset.current || ""; + } + }); +})(); diff --git a/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/site.js b/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/site.js new file mode 100644 index 0000000..43b0cc6 --- /dev/null +++ b/MVC-SimpleCRUD/MVC-SimpleCRUD/wwwroot/js/site.js @@ -0,0 +1,30 @@ +$(function () { + $(document).on('click', '.delete', function (event) { + if (!confirm("Delete entry?")) { + event.preventDefault(); + } + }); +}); + +function showAppToast(options) { + var toastOptions = options || {}; + var variant = toastOptions.variant || "success"; + var title = toastOptions.title || "Notification"; + var message = toastOptions.message || ""; + var toast = document.getElementById("app-toast"); + var header = document.getElementById("app-toast-header"); + var titleElement = document.getElementById("app-toast-title"); + var body = document.getElementById("app-toast-body"); + + if (!toast || !header || !titleElement || !body) { + return; + } + + toast.className = "toast border-0 shadow-sm"; + header.className = "toast-header text-bg-" + variant; + titleElement.textContent = title; + body.textContent = message; + + var bootstrapToast = new bootstrap.Toast(toast); + bootstrapToast.show(); +} diff --git a/docker-definitions/PeopleDataGenerator/Program.cs b/docker-definitions/PeopleDataGenerator/Program.cs index 614f28b..11693ce 100644 --- a/docker-definitions/PeopleDataGenerator/Program.cs +++ b/docker-definitions/PeopleDataGenerator/Program.cs @@ -148,6 +148,7 @@ static List BuildPersonInsertStatements(DatabaseTarget targetDatabase) var faker = new Faker(locale); var transliterationLanguage = locale.Split('_')[0]; var useTranscriptionForAddress = nonLatinLocales.Contains(transliterationLanguage); + var addressCountry = GetEnglishCountryName(countryCode); for (var i = 0; i < peoplePerCountry; i++) { @@ -158,7 +159,6 @@ static List BuildPersonInsertStatements(DatabaseTarget targetDatabase) var city = NormalizeForStorage(faker.Address.City(), transliterationLanguage, useTranscriptionForAddress); var addressLine = NormalizeForStorage(faker.Address.StreetAddress(), transliterationLanguage, useTranscriptionForAddress); var postalCode = NormalizeForStorage(faker.Address.ZipCode(), transliterationLanguage, useTranscriptionForAddress); - var addressCountry = NormalizeForStorage(faker.Address.Country(), transliterationLanguage, useTranscriptionForAddress); var email = faker.Internet.Email(firstNameTranscription, lastNameTranscription); var contactPhone = NormalizePhoneNumber(faker.Phone.PhoneNumber()); var birthDate = faker.Date.BetweenDateOnly(new DateOnly(1950, 1, 1), new DateOnly(2010, 12, 31)); @@ -211,6 +211,9 @@ static string CreateInsertStatement( static string EscapeSql(string value) => value.Replace("'", "''"); +static string GetEnglishCountryName(string countryCode) => + new System.Globalization.RegionInfo(countryCode).EnglishName; + static string NormalizeForStorage(string value, string transliterationLanguage, bool useTranscriptionForAddress) => useTranscriptionForAddress ? value.Transliterate(transliterationLanguage) : value; diff --git a/docker-definitions/mssql-eventsdb/init/02-schema.sql b/docker-definitions/mssql-eventsdb/init/02-schema.sql index 3a98d7c..4762ff0 100644 --- a/docker-definitions/mssql-eventsdb/init/02-schema.sql +++ b/docker-definitions/mssql-eventsdb/init/02-schema.sql @@ -14,16 +14,16 @@ GO CREATE TABLE dbo.Person ( Id int IDENTITY(1,1) NOT NULL, - FirstName varchar(100) NOT NULL, - LastName varchar(100) NOT NULL, + FirstName varchar(100) NULL, + LastName varchar(100) NULL, FirstNameTranscription varchar(100) NOT NULL, LastNameTranscription varchar(100) NOT NULL, - AddressLine varchar(200) NOT NULL, - PostalCode varchar(20) NOT NULL, - City varchar(100) NOT NULL, - AddressCountry varchar(100) NOT NULL, - Email varchar(255) NOT NULL, - ContactPhone varchar(50) NOT NULL, + AddressLine varchar(200) NULL, + PostalCode varchar(20) NULL, + City varchar(100) NULL, + AddressCountry varchar(100) NULL, + Email varchar(255) NULL, + ContactPhone varchar(50) NULL, BirthDate date NOT NULL, DocumentNumber varchar(50) NOT NULL, CountryCode varchar(3) NOT NULL, diff --git a/docker-definitions/postgres-eventsdb/init/02-schema.sql b/docker-definitions/postgres-eventsdb/init/02-schema.sql index 0ffecf9..9e102d1 100644 --- a/docker-definitions/postgres-eventsdb/init/02-schema.sql +++ b/docker-definitions/postgres-eventsdb/init/02-schema.sql @@ -11,16 +11,16 @@ CREATE TABLE country ( -- PERSONS CREATE TABLE person ( id SERIAL PRIMARY KEY, - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), first_name_transcription VARCHAR(100) NOT NULL, last_name_transcription VARCHAR(100) NOT NULL, - address_line VARCHAR(200) NOT NULL, - postal_code VARCHAR(20) NOT NULL, - city VARCHAR(100) NOT NULL, - address_country VARCHAR(100) NOT NULL, - email VARCHAR(255) NOT NULL, - contact_phone VARCHAR(50) NOT NULL, + address_line VARCHAR(200), + postal_code VARCHAR(20), + city VARCHAR(100), + address_country VARCHAR(100), + email VARCHAR(255), + contact_phone VARCHAR(50), birth_date DATE NOT NULL, document_number VARCHAR(50) NOT NULL, country_code VARCHAR(3) NOT NULL,