diff --git a/PhoneToolMX.Models/AllowNullAttribute.cs b/PhoneToolMX.Models/AllowNullAttribute.cs new file mode 100644 index 0000000..5181d0d --- /dev/null +++ b/PhoneToolMX.Models/AllowNullAttribute.cs @@ -0,0 +1,7 @@ +namespace PhoneToolMX.Models +{ + public class AllowNullAttribute : Attribute + { + + } +} diff --git a/PhoneToolMX.Models/Data/PTMXContext.cs b/PhoneToolMX.Models/Data/PTMXContext.cs index c7401b9..8bcea1b 100644 --- a/PhoneToolMX.Models/Data/PTMXContext.cs +++ b/PhoneToolMX.Models/Data/PTMXContext.cs @@ -38,6 +38,20 @@ namespace PhoneToolMX.Data { var entry = await AddAsync(entity); return entry; } + + /// + /// Gets a DbSet with all required relationships + /// + /// IOwnedModel + /// DbSet<TEntity> + private IQueryable GetFullSet() where TEntity: class, IModel + { + var set = Set().AsQueryable(); + return typeof(TEntity).GetProperties() + .Where(p => p.GetCustomAttributes(typeof(AlwaysIncludeAttribute), true) + .Length != 0) + .Aggregate(set, (current, prop) => current.Include(prop.Name)); + } /// /// Gets all entities of a certain model owned by the . @@ -48,14 +62,11 @@ namespace PhoneToolMX.Data { public ICollection GetOwned(User owner) where TEntity : class, IOwnedModel { if (owner == null) return null; - var entity = Set().Where(x => x.Owners.Any(o => o.Id == owner.Id)); + var entity = GetFullSet().Where(x => x.Owners.Any(o => o.Id == owner.Id)); // eager load all w/ AlwaysInclude - entity = typeof(TEntity).GetProperties() - .Where(p => p.GetCustomAttributes(typeof(AlwaysIncludeAttribute), true) - .Length != 0) - .Aggregate(entity, (current, prop) => current.Include(prop.Name)); return entity.ToList(); } + /// /// Gets a model entity by its Id @@ -67,7 +78,7 @@ namespace PhoneToolMX.Data { /// public TEntity GetEntityById(int? id) where TEntity : class, IModel { - return id == null ? null : Set().FirstOrDefault(o => o.Id == id); + return id == null ? null : GetFullSet().FirstOrDefault(o => o.Id == id); } #endregion @@ -79,6 +90,7 @@ namespace PhoneToolMX.Data { ext.HasKey(x => x.Id); ext.Property(x => x.Id).UseIdentityColumn(); ext.Property(x => x.ExtId).HasComputedColumnSql("\"Id\" + 1000", stored: true); + // Randomly generates a 24-char password ext .Property(p => p.Password) .HasDefaultValueSql("encode(gen_random_bytes(18), 'base64')"); @@ -89,7 +101,7 @@ namespace PhoneToolMX.Data { .WithMany(x => x.Phones); phone.HasKey(p => p.Id); phone.Property(p => p.Id).UseIdentityColumn(); - // Randomly generates a 24-char password + phone.HasOne(p => p.PrimaryExtension).WithMany(); // Wallpapers, ringtones, etc var cd = modelBuilder.Entity(); diff --git a/PhoneToolMX.Models/Migrations/20231020205952_PrimaryExtension.Designer.cs b/PhoneToolMX.Models/Migrations/20231020205952_PrimaryExtension.Designer.cs new file mode 100644 index 0000000..005038a --- /dev/null +++ b/PhoneToolMX.Models/Migrations/20231020205952_PrimaryExtension.Designer.cs @@ -0,0 +1,361 @@ +// +using System; +using System.Net.NetworkInformation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PhoneToolMX.Data; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + [DbContext(typeof(PTMXContext))] + [Migration("20231020205952_PrimaryExtension")] + partial class PrimaryExtension + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("ExtensionsId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("ExtensionPhone"); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("OwnersId") + .HasColumnType("text"); + + b.HasKey("ExtensionsId", "OwnersId"); + + b.HasIndex("OwnersId"); + + b.ToTable("ExtensionUser"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .HasColumnType("bytea"); + + b.Property("DataType") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("PhoneId") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PhoneId"); + + b.ToTable("CustomData"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtId") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("integer") + .HasComputedColumnSql("\"Id\" + 1000", true); + + b.Property("HoldMusicId") + .HasColumnType("integer"); + + b.Property("Listed") + .HasColumnType("boolean"); + + b.Property("Password") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValueSql("encode(gen_random_bytes(18), 'base64')"); + + b.HasKey("Id"); + + b.HasIndex("HoldMusicId"); + + b.ToTable("Extensions"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BackgroundId") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MacAddress") + .IsRequired() + .HasColumnType("macaddr"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("PrimaryExtension") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BackgroundId"); + + b.HasIndex("ModelId"); + + b.ToTable("Phones"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.PhoneModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxExtensions") + .HasColumnType("bigint"); + + b.Property("ModelName") + .HasColumnType("text"); + + b.Property("PreVvxPolycom") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("PhoneModels"); + + b.HasData( + new + { + Id = 0, + MaxExtensions = 6L, + ModelName = "Polycom VVX300/310", + PreVvxPolycom = false + }); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.Property("OwnersId") + .HasColumnType("text"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("OwnersId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("PhoneUser"); + }); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany("Ringtones") + .HasForeignKey("PhoneId"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "HoldMusic") + .WithMany() + .HasForeignKey("HoldMusicId"); + + b.Navigation("HoldMusic"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "Background") + .WithMany() + .HasForeignKey("BackgroundId"); + + b.HasOne("PhoneToolMX.Models.PhoneModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Background"); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Navigation("Ringtones"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhoneToolMX.Models/Migrations/20231020205952_PrimaryExtension.cs b/PhoneToolMX.Models/Migrations/20231020205952_PrimaryExtension.cs new file mode 100644 index 0000000..cb677d5 --- /dev/null +++ b/PhoneToolMX.Models/Migrations/20231020205952_PrimaryExtension.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + public partial class PrimaryExtension : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PrimaryExtension", + table: "Phones", + type: "integer", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PrimaryExtension", + table: "Phones"); + } + } +} diff --git a/PhoneToolMX.Models/Migrations/20231020213049_PrimaryExtensionAsExtension.Designer.cs b/PhoneToolMX.Models/Migrations/20231020213049_PrimaryExtensionAsExtension.Designer.cs new file mode 100644 index 0000000..492ce36 --- /dev/null +++ b/PhoneToolMX.Models/Migrations/20231020213049_PrimaryExtensionAsExtension.Designer.cs @@ -0,0 +1,369 @@ +// +using System; +using System.Net.NetworkInformation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PhoneToolMX.Data; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + [DbContext(typeof(PTMXContext))] + [Migration("20231020213049_PrimaryExtensionAsExtension")] + partial class PrimaryExtensionAsExtension + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("ExtensionsId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("ExtensionPhone"); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("OwnersId") + .HasColumnType("text"); + + b.HasKey("ExtensionsId", "OwnersId"); + + b.HasIndex("OwnersId"); + + b.ToTable("ExtensionUser"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .HasColumnType("bytea"); + + b.Property("DataType") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("PhoneId") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PhoneId"); + + b.ToTable("CustomData"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtId") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("integer") + .HasComputedColumnSql("\"Id\" + 1000", true); + + b.Property("HoldMusicId") + .HasColumnType("integer"); + + b.Property("Listed") + .HasColumnType("boolean"); + + b.Property("Password") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValueSql("encode(gen_random_bytes(18), 'base64')"); + + b.HasKey("Id"); + + b.HasIndex("HoldMusicId"); + + b.ToTable("Extensions"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BackgroundId") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MacAddress") + .IsRequired() + .HasColumnType("macaddr"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("PrimaryExtensionId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BackgroundId"); + + b.HasIndex("ModelId"); + + b.HasIndex("PrimaryExtensionId"); + + b.ToTable("Phones"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.PhoneModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxExtensions") + .HasColumnType("bigint"); + + b.Property("ModelName") + .HasColumnType("text"); + + b.Property("PreVvxPolycom") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("PhoneModels"); + + b.HasData( + new + { + Id = 0, + MaxExtensions = 6L, + ModelName = "Polycom VVX300/310", + PreVvxPolycom = false + }); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.Property("OwnersId") + .HasColumnType("text"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("OwnersId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("PhoneUser"); + }); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany("Ringtones") + .HasForeignKey("PhoneId"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "HoldMusic") + .WithMany() + .HasForeignKey("HoldMusicId"); + + b.Navigation("HoldMusic"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "Background") + .WithMany() + .HasForeignKey("BackgroundId"); + + b.HasOne("PhoneToolMX.Models.PhoneModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Extension", "PrimaryExtension") + .WithMany() + .HasForeignKey("PrimaryExtensionId"); + + b.Navigation("Background"); + + b.Navigation("Model"); + + b.Navigation("PrimaryExtension"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Navigation("Ringtones"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhoneToolMX.Models/Migrations/20231020213049_PrimaryExtensionAsExtension.cs b/PhoneToolMX.Models/Migrations/20231020213049_PrimaryExtensionAsExtension.cs new file mode 100644 index 0000000..81d4d29 --- /dev/null +++ b/PhoneToolMX.Models/Migrations/20231020213049_PrimaryExtensionAsExtension.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + public partial class PrimaryExtensionAsExtension : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "PrimaryExtension", + table: "Phones", + newName: "PrimaryExtensionId"); + + migrationBuilder.CreateIndex( + name: "IX_Phones_PrimaryExtensionId", + table: "Phones", + column: "PrimaryExtensionId"); + + migrationBuilder.AddForeignKey( + name: "FK_Phones_Extensions_PrimaryExtensionId", + table: "Phones", + column: "PrimaryExtensionId", + principalTable: "Extensions", + principalColumn: "Id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Phones_Extensions_PrimaryExtensionId", + table: "Phones"); + + migrationBuilder.DropIndex( + name: "IX_Phones_PrimaryExtensionId", + table: "Phones"); + + migrationBuilder.RenameColumn( + name: "PrimaryExtensionId", + table: "Phones", + newName: "PrimaryExtension"); + } + } +} diff --git a/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs b/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs index 47245c4..dceadaa 100644 --- a/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs +++ b/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs @@ -141,12 +141,17 @@ namespace PhoneToolMX.Models.Migrations b.Property("ModelId") .HasColumnType("integer"); + b.Property("PrimaryExtensionId") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("BackgroundId"); b.HasIndex("ModelId"); + b.HasIndex("PrimaryExtensionId"); + b.ToTable("Phones"); }); @@ -326,9 +331,15 @@ namespace PhoneToolMX.Models.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("PhoneToolMX.Models.Extension", "PrimaryExtension") + .WithMany() + .HasForeignKey("PrimaryExtensionId"); + b.Navigation("Background"); b.Navigation("Model"); + + b.Navigation("PrimaryExtension"); }); modelBuilder.Entity("PhoneUser", b => diff --git a/PhoneToolMX.Models/Models/IModel.cs b/PhoneToolMX.Models/Models/IModel.cs index 49335f8..a17077c 100644 --- a/PhoneToolMX.Models/Models/IModel.cs +++ b/PhoneToolMX.Models/Models/IModel.cs @@ -12,6 +12,8 @@ namespace PhoneToolMX.Models foreach (var prop in GetType().GetProperties().Where(p => p.CanWrite)) { if (prop.GetValue(obj, null) is {} propVal) { prop.SetValue(this, propVal, null); + } else if (prop.GetCustomAttributes(typeof(AllowNullAttribute)).FirstOrDefault() != null) { + prop.SetValue(this, null, null); } } } diff --git a/PhoneToolMX.Models/Models/Phone.cs b/PhoneToolMX.Models/Models/Phone.cs index c396439..8e34c2c 100644 --- a/PhoneToolMX.Models/Models/Phone.cs +++ b/PhoneToolMX.Models/Models/Phone.cs @@ -21,7 +21,10 @@ namespace PhoneToolMX.Models { [AlwaysInclude] public ICollection Extensions { get; set; } + public CustomData Background { get; set; } public ICollection Ringtones { get; set; } + + public Extension PrimaryExtension { get; set; } } } \ No newline at end of file diff --git a/PhoneToolMX.Models/ViewModels/PhoneVM.cs b/PhoneToolMX.Models/ViewModels/PhoneVM.cs index 68db836..67df3b9 100644 --- a/PhoneToolMX.Models/ViewModels/PhoneVM.cs +++ b/PhoneToolMX.Models/ViewModels/PhoneVM.cs @@ -1,6 +1,4 @@ using PhoneToolMX.Data; -using PhoneToolMX.Models; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Net.NetworkInformation; @@ -25,18 +23,25 @@ namespace PhoneToolMX.Models.ViewModels public ICollection Extensions { get; set; } + [AllowNull] + public int? PrimaryExtension { get; set; } + public int MaxExtensions { get; set; } public IOwnedModel ToEntity(PTMXContext ctx) { + var exts = Extensions?.Select(x => ctx.Extensions.FirstOrDefault(e => e.Id == x)).ToList(); return new Phone { Id = Id, MacAddress = PhysicalAddress.Parse(MacAddress), FriendlyName = FriendlyName, Model = ctx.PhoneModels.FirstOrDefault(p => p.Id == Model)!, - Extensions = Extensions?.Select(x => ctx.Extensions.FirstOrDefault(e => e.Id == x)).ToList(), + Extensions = exts, Owners = new List(), + PrimaryExtension = PrimaryExtension != null + ? exts?.FirstOrDefault(x => x.Id == PrimaryExtension) + : null, }; } @@ -58,6 +63,7 @@ namespace PhoneToolMX.Models.ViewModels Model = phoneEnt.Model!.Id, Extensions = phoneEnt.Extensions?.Select(x => x.Id).ToList(), MaxExtensions = (int)phoneEnt.Model!.MaxExtensions, + PrimaryExtension = phoneEnt.PrimaryExtension?.Id, }; } } diff --git a/PhoneToolMX/Controllers/PhoneController.cs b/PhoneToolMX/Controllers/PhoneController.cs index 70b0fd0..19586fa 100644 --- a/PhoneToolMX/Controllers/PhoneController.cs +++ b/PhoneToolMX/Controllers/PhoneController.cs @@ -17,8 +17,10 @@ namespace PhoneToolMX.Controllers { var myExts = _context.GetOwned(await CurrentUser()); var phoneModels = _context.PhoneModels.ToList(); + var selectedExts = pvm?.Extensions == null ? null : myExts.Where(x => pvm.Extensions.Contains(x.Id)).ToList(); ViewBag.MyExtensions = myExts; - ViewBag.SelectedExtensions = pvm?.Extensions == null ? null : myExts.Where(x => pvm.Extensions.Contains(x.Id)).ToList(); + ViewBag.SelectedExtensions = selectedExts; + ViewBag.PrimaryExtension = selectedExts?.FirstOrDefault(x => x.Id == (pvm.PrimaryExtension ?? -1)); ViewBag.ModelNumbers = phoneModels; ViewBag.CurrentModel = pvm?.Model == null ? null : phoneModels.Where(m => m.Id == pvm.Model); } diff --git a/PhoneToolMX/Views/Phone/_Form.cshtml b/PhoneToolMX/Views/Phone/_Form.cshtml index aa5e92c..bbc0858 100644 --- a/PhoneToolMX/Views/Phone/_Form.cshtml +++ b/PhoneToolMX/Views/Phone/_Form.cshtml @@ -24,6 +24,14 @@ @Html.LabelFor(m => m.Extensions, "Extensions:") @Html.ListBoxFor(m => m.Extensions, new MultiSelectList(ViewBag.MyExtensions, "Id", "ListViewName", ViewBag.SelectedExtensions)) + + @Html.LabelFor(m => m.PrimaryExtension, "Primary Extension:") + @if (ViewBag.SelectedExtensions == null || ((ICollection)ViewBag.SelectedExtensions).Count == 0) { + @Html.DropDownListFor(m => m.PrimaryExtension, new List(), "-- No Extensions Selected --", new { @disabled=true }) + } else { + @Html.DropDownListFor(m => m.PrimaryExtension, new SelectList(ViewBag.SelectedExtensions, "Id", "ListViewName", ViewBag.PrimaryExtension), "None") + } + diff --git a/PolyProv/Controllers/ConfigController.cs b/PolyProv/Controllers/ConfigController.cs index 60c2a16..977009c 100644 --- a/PolyProv/Controllers/ConfigController.cs +++ b/PolyProv/Controllers/ConfigController.cs @@ -54,6 +54,14 @@ namespace PolyProv.Controllers public IActionResult PhoneSettings(string addr) { var phone = GetByMacStr(addr); + + // ReSharper disable once InvertIf + if (phone?.PrimaryExtension is {} primary && phone.Extensions.FirstOrDefault() != primary) { + // primary extension exists and is not first, reorder so it's first + phone.Extensions.Remove(primary); + phone.Extensions = phone.Extensions.Prepend(primary).ToList(); + } + return phone != null ? View("Phone", phone) : NotFound(); }