Added extended Email subscriber metrics

This commit is contained in:
Mia Rose Winter 2024-03-07 13:41:49 +01:00
parent e9de881850
commit 204ea4987e
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
6 changed files with 742 additions and 1 deletions

View file

@ -108,7 +108,7 @@
subscriber ??= new EmailSubscriber {
Email = Model.Email.Trim(),
Unsubscribed = true
Unsubscribed = true, Language = "en-US" // TODO
};
subscriber.Name = Model.Name;
context.Update(subscriber);

View file

@ -103,6 +103,11 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
subscriber.Property(s => s.Name).IsRequired(false).HasMaxLength(128);
subscriber.Property(s => s.Email).IsRequired().HasMaxLength(256).UseCollation("default-case-insensitive");
subscriber.HasIndex(s => s.Email).IsUnique();
subscriber.Property(s => s.Language).IsRequired().HasMaxLength(8).HasDefaultValue("en-US");
subscriber.Property(s => s.UnsubscribeReason).HasMaxLength(256);
subscriber.Property(s => s.LastMailReceived).HasConversion(dateTimeOffsetUtcConverter);
subscriber.Property(s => s.LastMailOpened).HasConversion(dateTimeOffsetUtcConverter);
subscriber.HasIndex(s => s.Unsubscribed);

View file

@ -10,6 +10,13 @@ public class EmailSubscriber {
public string? Name { get; set; }
[EmailAddress, MaxLength(256)]
public required string Email { get; set; }
[MaxLength(8)]
public required string Language { get; set; } = "en-US";
[MaxLength(256)]
public string? UnsubscribeReason { get; set; }
public DateTimeOffset? LastMailReceived { get; set; }
public DateTimeOffset? LastMailOpened { get; set; }
public bool Unsubscribed { get; set; }
}

View file

@ -0,0 +1,654 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Wave.Data;
#nullable disable
namespace Wave.Data.Migrations.postgres
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240307122238_EmailSubscriberMetrics")]
partial class EmailSubscriberMetrics
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:CollationDefinition:default-case-insensitive", "und-u-kf-upper-ks-level1,und-u-kf-upper-ks-level1,icu,False")
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Wave.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("AboutTheAuthor")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("Biography")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<string>("BiographyHtml")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("ContactEmail")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ContactPhone")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ContactPhoneBusiness")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ContactWebsite")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<string>("FullName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Wave.Data.Article", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AuthorId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BodyHtml")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BodyPlain")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("");
b.Property<DateTimeOffset>("CreationDate")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("LastModified")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<DateTimeOffset>("PublishDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReviewerId")
.HasColumnType("text");
b.Property<string>("Slug")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasDefaultValue("");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("ReviewerId");
b.ToTable("Articles", (string)null);
});
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid>("ArticleId")
.HasColumnType("uuid");
b.Property<Guid>("CategoryId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ArticleId");
b.HasIndex("CategoryId");
b.ToTable("ArticleCategories", (string)null);
});
modelBuilder.Entity("Wave.Data.ArticleImage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("ArticleId")
.HasColumnType("uuid");
b.Property<string>("ImageDescription")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Id");
b.HasIndex("ArticleId");
b.ToTable("Images", (string)null);
});
modelBuilder.Entity("Wave.Data.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("Color")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(25);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.UseCollation("default-case-insensitive");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Categories", (string)null);
});
modelBuilder.Entity("Wave.Data.EmailNewsletter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid>("ArticleId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("DistributionDateTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSend")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ArticleId")
.IsUnique();
b.ToTable("Newsletter", (string)null);
});
modelBuilder.Entity("Wave.Data.EmailSubscriber", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.UseCollation("default-case-insensitive");
b.Property<string>("Language")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(8)
.HasColumnType("character varying(8)")
.HasDefaultValue("en-US");
b.Property<DateTimeOffset?>("LastMailOpened")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("LastMailReceived")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("UnsubscribeReason")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("Unsubscribed")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Unsubscribed");
b.ToTable("NewsletterSubscribers", (string)null);
});
modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApplicationUserId")
.HasColumnType("text");
b.Property<Guid>("ImageId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ProfilePictures", (string)null);
});
modelBuilder.Entity("Wave.Data.UserLink", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApplicationUserId")
.HasColumnType("text");
b.Property<string>("UrlString")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("UserLink");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Wave.Data.Article", b =>
{
b.HasOne("Wave.Data.ApplicationUser", "Author")
.WithMany("Articles")
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Wave.Data.ApplicationUser", "Reviewer")
.WithMany()
.HasForeignKey("ReviewerId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Author");
b.Navigation("Reviewer");
});
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
{
b.HasOne("Wave.Data.Article", "Article")
.WithMany()
.HasForeignKey("ArticleId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.HasOne("Wave.Data.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("Article");
b.Navigation("Category");
});
modelBuilder.Entity("Wave.Data.ArticleImage", b =>
{
b.HasOne("Wave.Data.Article", null)
.WithMany("Images")
.HasForeignKey("ArticleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Wave.Data.EmailNewsletter", b =>
{
b.HasOne("Wave.Data.Article", "Article")
.WithOne()
.HasForeignKey("Wave.Data.EmailNewsletter", "ArticleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Article");
});
modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithOne("ProfilePicture")
.HasForeignKey("Wave.Data.ProfilePicture", "ApplicationUserId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Wave.Data.UserLink", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany("Links")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Wave.Data.ApplicationUser", b =>
{
b.Navigation("Articles");
b.Navigation("Links");
b.Navigation("ProfilePicture");
});
modelBuilder.Entity("Wave.Data.Article", b =>
{
b.Navigation("Images");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Wave.Data.Migrations.postgres;
/// <inheritdoc />
public partial class EmailSubscriberMetrics : Migration {
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) {
migrationBuilder.AddColumn<string>(
name: "Language",
table: "NewsletterSubscribers",
type: "character varying(8)",
maxLength: 8,
nullable: false,
defaultValue: "en-US");
migrationBuilder.AddColumn<DateTimeOffset>(
name: "LastMailOpened",
table: "NewsletterSubscribers",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "LastMailReceived",
table: "NewsletterSubscribers",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "UnsubscribeReason",
table: "NewsletterSubscribers",
type: "character varying(256)",
maxLength: 256,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) {
migrationBuilder.DropColumn(
name: "Language",
table: "NewsletterSubscribers");
migrationBuilder.DropColumn(
name: "LastMailOpened",
table: "NewsletterSubscribers");
migrationBuilder.DropColumn(
name: "LastMailReceived",
table: "NewsletterSubscribers");
migrationBuilder.DropColumn(
name: "UnsubscribeReason",
table: "NewsletterSubscribers");
}
}

View file

@ -430,10 +430,27 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("character varying(256)")
.UseCollation("default-case-insensitive");
b.Property<string>("Language")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(8)
.HasColumnType("character varying(8)")
.HasDefaultValue("en-US");
b.Property<DateTimeOffset?>("LastMailOpened")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("LastMailReceived")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("UnsubscribeReason")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("Unsubscribed")
.HasColumnType("boolean");